diff --git a/docs/plan/NewEditor_帧调度与UI合成性能重构计划_2026-04-21.md b/docs/plan/NewEditor_帧调度与UI合成性能重构计划_2026-04-21.md new file mode 100644 index 00000000..6440aaf5 --- /dev/null +++ b/docs/plan/NewEditor_帧调度与UI合成性能重构计划_2026-04-21.md @@ -0,0 +1,112 @@ +# NewEditor 帧调度与 UI 合成性能重构计划 2026-04-21 + +## 说明 + +- 本计划只覆盖 `new_editor` 当前最关键的底层性能问题,不扩散到一般性 UI 绘制优化。 +- 当前判断的主因不是单个控件或某个面板,而是 `D3D12 viewport -> D3D11On12/D2D UI 合成 -> frame fence wait` 这条热路径的帧调度设计。 +- 本计划的目标不是先做零散 patch,而是先把帧上下文复用、swapchain/backbuffer 复用、UI 合成提交三者的边界理顺。 + +## 当前问题 + +### 1. 帧上下文复用与 backbuffer 复用被错误耦合 + +- `D3D12WindowRenderer::BeginFrame()` 当前按 backbuffer index 驱动 `D3D12HostDevice::BeginFrame(frameIndex)`。 +- `D3D12HostDevice::BeginFrame()` 会在帧入口直接 `WaitForFrame(frameIndex)`。 +- 当前 frame fence 记录的不是“命令列表/allocator 可以复用”的完成点,而是整轮 viewport 渲染加 UI 合成之后的完成点。 + +这会导致: + +- CPU 录制下一帧命令时,被迫等待上一轮同 backbuffer 的整帧工作完成。 +- 帧上下文复用不能独立于 swapchain buffer 轮转。 +- 多帧在飞被压缩成接近串行执行。 + +### 2. UI 合成处于 D3D11On12 热路径并且每帧强制提交 + +- `NativeRenderer::RenderToWindowRenderer()` 会在 viewport D3D12 提交后进入 D3D11On12/D2D 合成。 +- 合成路径每帧会 `AcquireWrappedResources` / `ReleaseWrappedResources`,并且最终 `d3d11DeviceContext->Flush()`。 +- frame completion fence 在这轮 interop flush 之后才发出。 + +这意味着: + +- UI 合成不是异步附属工作,而是当前帧主路径的一部分。 +- 下一帧等待会覆盖 UI interop 的全部提交成本。 +- 当前架构天然放大 D3D11On12 互操作开销。 + +### 3. swapchain 延迟策略过于激进 + +- `D3D12WindowSwapChainPresenter::ConfigureFrameLatency()` 当前固定 `SetMaximumFrameLatency(1u)`。 +- `D3D12WindowSwapChainPresenter` 当前仅使用 2-buffer swapchain。 +- `D3D12HostDevice` 实际已经准备了 3 份 frame context,但没有被正确利用。 + +这会进一步压缩: + +- CPU/GPU 重叠空间 +- queue 深度 +- backbuffer 可回旋余量 + +## 重构目标 + +### Goal 1. 拆开 frame slot 与 backbuffer index + +- 引入独立的 frame slot 轮转,不再直接把 backbuffer index 作为 host frame context index。 +- frame fence 仅用于 frame slot 级别的命令列表/allocator 复用。 +- backbuffer 仍由 swapchain 当前 index 决定,但不再承担 frame slot 生命周期管理职责。 + +### Goal 2. 放宽当前帧流水深度 + +- 把 swapchain buffer count 与 host frame context 数量对齐到 3。 +- 去掉当前 `maximum frame latency = 1` 的极端限制,恢复合理的 frame queue 深度。 +- 让 CPU 可以在不破坏正确性的前提下,至少维持 2 到 3 帧的录制/执行重叠。 + +### Goal 3. 保持现有 UI backend 可运行前提下,降低热路径同步强度 + +- 不在本轮直接重写成 D3D12-native UI renderer。 +- 先保证当前 D3D11On12 合成链不再把 frame slot 复用错误地锁死在 backbuffer 完成点上。 +- 如有必要,额外清理 interop 资源生命周期中明显的每帧重复初始化行为。 + +## 执行阶段 + +### Phase 1. 帧调度语义重构 + +1. 审查 `D3D12WindowRenderer` 当前 active frame index 与 active backbuffer index 的耦合点。 +2. 引入独立的 frame slot index。 +3. 修改 `GetRenderContext()`、`BeginFrame()`、`SignalFrameCompletion()` 等调用链,使其按 frame slot 工作。 +4. 确保 frame fence 的含义收敛为“该 frame slot 可安全复用”。 + +### Phase 2. swapchain 策略重构 + +1. 将 swapchain buffer count 调整为 3。 +2. 调整 `SetMaximumFrameLatency(...)` 策略,不再固定 1。 +3. 检查 resize、backbuffer target rebuild、present 路径是否仍然成立。 + +### Phase 3. interop 热路径复核 + +1. 复核 `PrepareSourceTextures()` 是否存在每帧重复创建包装资源的重活。 +2. 若重构 frame slot 后仍存在显著不合理同步,再做最小范围 interop 资源缓存。 +3. 不在本轮扩展为完整 D3D12-native UI 渲染器,避免范围失控。 + +## 验收标准 + +- `XCUIEditorAppLib` 与 `XCUIEditorApp` 能重新编译通过。 +- `new_editor` 主窗口仍可正常显示 viewport 与 shell UI。 +- 渲染路径中不再出现“frame slot 等待直接绑定 backbuffer index”的旧语义。 +- swapchain/backbuffer 数量与 frame slot 配置一致,frame latency 不再固定为 1。 +- 本轮修改不引入新的窗口 resize、present、viewport 显示回退问题。 + +## 提交策略 + +### Commit A. 当前 new_editor 工作树快照 + +- 仅提交当前 `new_editor/**` 改动与本计划文档。 +- 目标是先固化当前正在进行的大重构工作,避免后续性能重构混入历史改动。 + +### Commit B. 帧调度/同步链重构 + +- 只提交本次性能问题修复直接相关的帧调度与 present/interop 变更。 +- 提交前至少完成 `XCUIEditorAppLib` 或 `XCUIEditorApp` 构建验证。 + +## 风险 + +- 当前仓库存在大量 `new_editor` 以外的脏改动,不能被这轮误带入提交。 +- `XCUIEditor.exe` 若在运行,会阻塞最终链接验证。 +- 即便本轮重构完成,也不能提前承诺最终帧率一定达到某个绝对数字;本轮目标是拆除最明显的底层串行瓶颈。 diff --git a/new_editor/CMakeLists.txt b/new_editor/CMakeLists.txt index 15e300a8..e8673e72 100644 --- a/new_editor/CMakeLists.txt +++ b/new_editor/CMakeLists.txt @@ -35,6 +35,7 @@ set(XCUI_EDITOR_FIELD_SOURCES src/Fields/UIEditorColorField.cpp src/Fields/UIEditorColorFieldInteraction.cpp src/Fields/ColorFieldRendering.cpp + src/Fields/UIEditorEditableFieldCore.cpp src/Fields/UIEditorEnumField.cpp src/Fields/UIEditorEnumFieldInteraction.cpp src/Fields/UIEditorFieldStyle.cpp @@ -44,13 +45,6 @@ set(XCUI_EDITOR_FIELD_SOURCES src/Fields/UIEditorObjectFieldInteraction.cpp src/Fields/UIEditorPropertyGrid.cpp src/Fields/UIEditorPropertyGridInteraction.cpp - src/Fields/PropertyGridInteractionAsset.cpp - src/Fields/PropertyGridInteractionColor.cpp - src/Fields/PropertyGridInteractionEdit.cpp - src/Fields/PropertyGridInteractionHelpers.cpp - src/Fields/PropertyGridInteractionPopup.cpp - src/Fields/PropertyGridInteractionVector.cpp - src/Fields/PropertyGridRendering.cpp src/Fields/UIEditorTextField.cpp src/Fields/UIEditorTextFieldInteraction.cpp src/Fields/UIEditorVector2Field.cpp @@ -79,6 +73,7 @@ set(XCUI_EDITOR_DOCKING_SOURCES src/Docking/UIEditorDockHost.cpp src/Docking/DockHostHitTest.cpp src/Docking/DockHostRendering.cpp + src/Docking/UIEditorDockHostTransfer.cpp src/Docking/UIEditorDockHostInteraction.cpp src/Docking/DockHostInteractionHelpers.cpp ) @@ -103,8 +98,6 @@ set(XCUI_EDITOR_SHELL_SOURCES src/Shell/UIEditorShellCompose.cpp src/Shell/UIEditorShellInteraction.cpp src/Shell/UIEditorStructuredShell.cpp - src/Shell/ShellInteractionRequest.cpp - src/Shell/ShellInteractionRendering.cpp src/Shell/UIEditorStatusBar.cpp ) @@ -117,17 +110,12 @@ set(XCUI_EDITOR_VIEWPORT_SOURCES set(XCUI_EDITOR_WORKSPACE_SOURCES src/Workspace/UIEditorWorkspaceCompose.cpp src/Workspace/UIEditorWorkspaceController.cpp - src/Workspace/WorkspaceControllerDispatch.cpp - src/Workspace/WorkspaceControllerLayoutOps.cpp src/Workspace/UIEditorWorkspaceInteraction.cpp src/Workspace/UIEditorWorkspaceInputOwner.cpp src/Workspace/UIEditorWorkspaceLayoutPersistence.cpp src/Workspace/UIEditorWorkspaceModel.cpp - src/Workspace/WorkspaceModelMutation.cpp - src/Workspace/WorkspaceModelQueries.cpp src/Workspace/SplitterDragCorrection/Chain.cpp src/Workspace/SplitterDragCorrection/Correction.cpp - src/Workspace/WorkspaceModelValidation.cpp src/Workspace/UIEditorWorkspaceSession.cpp src/Workspace/UIEditorWorkspaceTransfer.cpp src/Workspace/UIEditorWindowWorkspaceController.cpp @@ -158,6 +146,7 @@ target_include_directories(XCUIEditorLib ${CMAKE_CURRENT_SOURCE_DIR}/include ${CMAKE_SOURCE_DIR}/engine/include PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/app ${CMAKE_CURRENT_SOURCE_DIR}/src ) @@ -173,38 +162,18 @@ target_link_libraries(XCUIEditorLib PRIVATE set(XCUI_EDITOR_HOST_PLATFORM_SOURCES app/Platform/Win32/BorderlessWindowChrome.cpp - app/Platform/Win32/BorderlessWindowChromeRendering.cpp - app/Platform/Win32/BorderlessWindowChromeDwm.cpp app/Platform/Win32/BorderlessWindowFrame.cpp ) set(XCUI_EDITOR_HOST_RENDERING_SOURCES app/Rendering/Native/AutoScreenshot.cpp - app/Rendering/D3D12/D3D12HostDeviceFence.cpp - app/Rendering/D3D12/D3D12HostDeviceFrame.cpp - app/Rendering/D3D12/D3D12HostDeviceLifecycle.cpp + app/Rendering/D3D12/D3D12HostDevice.cpp app/Rendering/D3D12/D3D12ShaderResourceDescriptorAllocator.cpp app/Rendering/D3D12/D3D12WindowInteropContext.cpp - app/Rendering/D3D12/D3D12WindowInteropDevice.cpp - app/Rendering/D3D12/D3D12WindowInteropResources.cpp - app/Rendering/D3D12/D3D12WindowInteropSourceTextures.cpp app/Rendering/D3D12/D3D12WindowRenderer.cpp - app/Rendering/D3D12/SwapChainPresenter/BackBuffers.cpp - app/Rendering/D3D12/SwapChainPresenter/Lifecycle.cpp - app/Rendering/D3D12/SwapChainPresenter/Presentation.cpp - app/Rendering/D3D12/SwapChainPresenter/Resize.cpp + app/Rendering/D3D12/D3D12WindowSwapChainPresenter.cpp app/Rendering/D3D12/D3D12WindowRenderLoop.cpp - app/Rendering/Native/NativeRendererCapture.cpp - app/Rendering/Native/NativeRendererDraw.cpp - app/Rendering/Native/NativeRendererDrawContent.cpp - app/Rendering/Native/NativeRendererDrawGeometry.cpp - app/Rendering/Native/NativeRendererLifecycle.cpp - app/Rendering/Native/NativeRendererRenderTarget.cpp - app/Rendering/Native/NativeRendererRendering.cpp - app/Rendering/Native/NativeRendererText.cpp - app/Rendering/Native/NativeRendererTextureDecoding.cpp - app/Rendering/Native/NativeRendererTextures.cpp - app/Rendering/Native/NativeRendererWindowInterop.cpp + app/Rendering/Native/NativeRenderer.cpp ) add_library(XCUIEditorHost STATIC @@ -236,14 +205,11 @@ if(XCENGINE_BUILD_XCUI_EDITOR_APP) set(XCUI_EDITOR_APP_BOOTSTRAP_SOURCES app/Bootstrap/EditorApp.rc app/main.cpp - app/Bootstrap/ApplicationBootstrap.cpp - app/Bootstrap/ApplicationBootstrapInternal.cpp - app/Bootstrap/ApplicationLifecycle.cpp - app/Bootstrap/ApplicationRunLoop.cpp - app/Bootstrap/ApplicationWindowClass.cpp + app/Bootstrap/Application.cpp ) set(XCUI_EDITOR_APP_STATE_SOURCES + app/State/EditorColorPickerToolState.cpp app/State/EditorSession.cpp ) @@ -253,38 +219,28 @@ if(XCENGINE_BUILD_XCUI_EDITOR_APP) set(XCUI_EDITOR_APP_COMPOSITION_SOURCES app/Composition/EditorShellAssetBuilder.cpp - app/Composition/EditorShellAssetCommands.cpp - app/Composition/EditorShellAssetDefinition.cpp - app/Composition/EditorShellAssetLayout.cpp - app/Composition/EditorShellAssetMenu.cpp app/Composition/EditorContext.cpp - app/Composition/EditorContextStatus.cpp app/Composition/EditorShellRuntime.cpp - app/Composition/EditorShellRuntimeRendering.cpp - app/Composition/EditorShellRuntimeUpdate.cpp - app/Composition/EditorShellRuntimeViewport.cpp app/Composition/EditorWindowWorkspaceStore.cpp app/Composition/WorkspaceEventSync.cpp ) set(XCUI_EDITOR_APP_FEATURE_SOURCES app/Features/Console/ConsolePanel.cpp + app/Features/ColorPicker/ColorPickerPanel.cpp app/Features/Hierarchy/HierarchyModel.cpp app/Features/Hierarchy/HierarchyPanel.cpp - app/Features/Hierarchy/HierarchyPanelInternal.cpp app/Features/Inspector/InspectorPanel.cpp app/Features/Inspector/InspectorPresentationModel.cpp app/Features/Inspector/InspectorSubject.cpp + app/Features/Inspector/Components/InspectorBindingComponentEditor.cpp app/Features/Inspector/Components/InspectorComponentEditorRegistry.cpp app/Features/Inspector/Components/TransformInspectorComponentEditor.cpp app/Features/Project/ProjectPanel.cpp - app/Features/Project/ProjectPanelInternal.cpp app/Features/Project/ProjectBrowserModel.cpp - app/Features/Project/ProjectBrowserModelAssets.cpp - app/Features/Project/ProjectBrowserModelFolders.cpp - app/Features/Project/ProjectBrowserModelInternal.cpp app/Features/Scene/SceneViewportTransformGizmo.cpp app/Features/Scene/SceneViewportTransformGizmoSupport.cpp + app/Features/Scene/SceneViewportSceneOverlay.cpp app/Features/Scene/SceneEditCommandRoute.cpp app/Features/Scene/SceneViewportFeature.cpp app/Features/Scene/SceneViewportToolOverlay.cpp @@ -295,38 +251,32 @@ if(XCENGINE_BUILD_XCUI_EDITOR_APP) app/Rendering/Assets/BuiltInIcons.cpp app/Rendering/Viewport/Passes/SceneViewportGridPass.cpp app/Rendering/Viewport/Passes/SceneViewportSelectionOutlinePass.cpp + app/Rendering/Viewport/Passes/SceneViewportSelectedHelpersPass.cpp app/Rendering/Viewport/SceneViewportRenderPassBundle.cpp app/Rendering/Viewport/SceneViewportRenderService.cpp app/Rendering/Viewport/ViewportHostService.cpp app/Rendering/Viewport/ViewportRenderTargets.cpp - app/Rendering/Viewport/ViewportRenderTargetInternal.cpp + app/Rendering/Viewport/ViewportRenderTargetUtils.cpp ) set(XCUI_EDITOR_APP_SUPPORT_SOURCES app/Project/EditorProjectRuntime.cpp app/Scene/EditorSceneRuntime.cpp - app/Internal/EmbeddedPngLoader.cpp + app/Support/EmbeddedPngLoader.cpp app/Scene/EditorSceneBridge.cpp ) set(XCUI_EDITOR_APP_PLATFORM_SOURCES - app/Platform/Win32/EditorWindowBorderlessPlacement.cpp - app/Platform/Win32/EditorWindowBorderlessResize.cpp + app/Platform/Win32/EditorWindow.cpp app/Platform/Win32/EditorWindowChromeController.cpp - app/Platform/Win32/EditorWindowFrame.cpp app/Platform/Win32/EditorWindowFrameOrchestrator.cpp app/Platform/Win32/EditorWindowInputController.cpp - app/Platform/Win32/EditorWindowLifecycle.cpp - app/Platform/Win32/EditorWindowInput.cpp app/Platform/Win32/EditorWindowRuntimeController.cpp - app/Platform/Win32/EditorWindowTitleBarInteraction.cpp - app/Platform/Win32/EditorWindowTitleBarRendering.cpp app/Platform/Win32/Win32SystemInteractionHost.cpp + app/Platform/Win32/WindowManager/EditorWindowHostRuntime.cpp + app/Platform/Win32/WindowManager/EditorWindowManager.cpp app/Platform/Win32/WindowManager/EditorWindowMessageDispatcher.cpp - app/Platform/Win32/WindowManager/Lifecycle.cpp - app/Platform/Win32/WindowManager/TabDragDropTarget.cpp - app/Platform/Win32/WindowManager/WindowSync.cpp - app/Platform/Win32/WindowManager/TabDrag.cpp + app/Platform/Win32/WindowManager/EditorWindowWorkspaceCoordinator.cpp ) set(XCUI_EDITOR_APP_CORE_SOURCES @@ -346,6 +296,7 @@ if(XCENGINE_BUILD_XCUI_EDITOR_APP) ${CMAKE_CURRENT_SOURCE_DIR}/app ${CMAKE_CURRENT_SOURCE_DIR}/include ${CMAKE_SOURCE_DIR}/engine/include + ${CMAKE_SOURCE_DIR}/engine/third_party/stb ) target_compile_definitions(XCUIEditorAppCore PRIVATE diff --git a/new_editor/app/Bootstrap/Application.cpp b/new_editor/app/Bootstrap/Application.cpp new file mode 100644 index 00000000..46a544a1 --- /dev/null +++ b/new_editor/app/Bootstrap/Application.cpp @@ -0,0 +1,346 @@ +#include "Bootstrap/Application.h" +#include "Bootstrap/EditorResources.h" +#include "Ports/SystemInteractionPort.h" +#include "Composition/EditorContext.h" +#include "Platform/Win32/EditorWindowManager.h" +#include "Platform/Win32/EditorWindow.h" +#include "Platform/Win32/Win32SystemInteractionHost.h" +#include "Support/ExecutablePath.h" + +#include +#include + +#include +#include + +namespace XCEngine::UI::Editor { + +namespace { + +constexpr const wchar_t* kWindowClassName = L"XCEditorShellHost"; +constexpr const wchar_t* kWindowTitle = L"Main Scene * - Main.xx - XCEngine Editor"; +constexpr DWORD kBorderlessWindowStyle = WS_POPUP | WS_THICKFRAME; + +void EnableDpiAwareness() { + const HMODULE user32 = GetModuleHandleW(L"user32.dll"); + if (user32 != nullptr) { + using SetProcessDpiAwarenessContextFn = BOOL(WINAPI*)(DPI_AWARENESS_CONTEXT); + const auto setProcessDpiAwarenessContext = + reinterpret_cast( + GetProcAddress(user32, "SetProcessDpiAwarenessContext")); + if (setProcessDpiAwarenessContext != nullptr) { + if (setProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)) { + return; + } + if (GetLastError() == ERROR_ACCESS_DENIED) { + return; + } + if (setProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE)) { + return; + } + if (GetLastError() == ERROR_ACCESS_DENIED) { + return; + } + } + } + + const HMODULE shcore = LoadLibraryW(L"shcore.dll"); + if (shcore != nullptr) { + using SetProcessDpiAwarenessFn = HRESULT(WINAPI*)(PROCESS_DPI_AWARENESS); + const auto setProcessDpiAwareness = + reinterpret_cast(GetProcAddress(shcore, "SetProcessDpiAwareness")); + if (setProcessDpiAwareness != nullptr) { + const HRESULT hr = setProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE); + FreeLibrary(shcore); + if (SUCCEEDED(hr) || hr == E_ACCESSDENIED) { + return; + } + } else { + FreeLibrary(shcore); + } + } + + if (user32 != nullptr) { + using SetProcessDPIAwareFn = BOOL(WINAPI*)(); + const auto setProcessDPIAware = + reinterpret_cast(GetProcAddress(user32, "SetProcessDPIAware")); + if (setProcessDPIAware != nullptr) { + setProcessDPIAware(); + } + } +} + +} // namespace + +Application::Application() + : m_editorContext(std::make_unique()) {} + +Application::~Application() = default; + +int RunXCUIEditorApp(HINSTANCE hInstance, int nCmdShow) { + Application application; + return application.Run(hInstance, nCmdShow); +} + +} // namespace XCEngine::UI::Editor + +#ifndef XCUIEDITOR_REPO_ROOT +#define XCUIEDITOR_REPO_ROOT "." +#endif + +namespace XCEngine::UI::Editor { + +using App::GetExecutableDirectory; + +bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) { + m_hInstance = hInstance; + m_repoRoot = ResolveRepoRootPath(); + EnableDpiAwareness(); + + const std::filesystem::path logRoot = GetExecutableDirectory() / "logs"; + InitializeUIEditorRuntimeTrace(logRoot); + SetUnhandledExceptionFilter(&Application::HandleUnhandledException); + AppendUIEditorRuntimeTrace("app", "initialize begin"); + + if (!m_editorContext->Initialize(m_repoRoot)) { + AppendUIEditorRuntimeTrace( + "app", + "shell asset validation failed: " + m_editorContext->GetValidationMessage()); + return false; + } + if (!RegisterWindowClass()) { + return false; + } + + m_systemInteractionHost = std::make_unique(); + m_editorContext->SetSystemInteractionHost(m_systemInteractionHost.get()); + + App::EditorWindowHostConfig hostConfig = {}; + hostConfig.hInstance = m_hInstance; + hostConfig.windowClassName = kWindowClassName; + hostConfig.windowStyle = kBorderlessWindowStyle; + hostConfig.primaryWindowTitle = kWindowTitle; + hostConfig.windowUserData = this; + m_windowManager = std::make_unique( + hostConfig, + m_repoRoot, + *m_editorContext); + + m_editorContext->SetExitRequestHandler([this]() { + if (m_windowManager == nullptr) { + return; + } + + if (App::EditorWindow* primaryWindow = m_windowManager->FindPrimaryWindow(); + primaryWindow != nullptr && + primaryWindow->GetHwnd() != nullptr) { + PostMessageW(primaryWindow->GetHwnd(), WM_CLOSE, 0, 0); + } + }); + m_editorContext->SetReadyStatus(); + + App::EditorWindowManager::CreateParams createParams = {}; + createParams.windowId = "main"; + createParams.title = kWindowTitle; + createParams.showCommand = nCmdShow; + createParams.primary = true; + createParams.autoCaptureOnStartup = true; + if (m_windowManager->CreateEditorWindow( + m_editorContext->BuildWorkspaceController(), + createParams) == nullptr) { + AppendUIEditorRuntimeTrace("app", "primary window creation failed"); + return false; + } + + AppendUIEditorRuntimeTrace("app", "initialize end"); + return true; +} + +void Application::Shutdown() { + AppendUIEditorRuntimeTrace("app", "shutdown begin"); + + if (m_windowManager != nullptr) { + m_windowManager->Shutdown(); + m_windowManager.reset(); + } + + if (m_editorContext != nullptr) { + m_editorContext->SetSystemInteractionHost(nullptr); + } + m_systemInteractionHost.reset(); + + ::XCEngine::Resources::ResourceManager::Get().Shutdown(); + + if (m_windowClassAtom != 0 && m_hInstance != nullptr) { + UnregisterClassW(kWindowClassName, m_hInstance); + m_windowClassAtom = 0; + } + + AppendUIEditorRuntimeTrace("app", "shutdown end"); + ShutdownUIEditorRuntimeTrace(); +} + +std::filesystem::path Application::ResolveRepoRootPath() { + std::string root = XCUIEDITOR_REPO_ROOT; + if (root.size() >= 2u && root.front() == '"' && root.back() == '"') { + root = root.substr(1u, root.size() - 2u); + } + return std::filesystem::path(root).lexically_normal(); +} + +LONG WINAPI Application::HandleUnhandledException(EXCEPTION_POINTERS* exceptionInfo) { + if (exceptionInfo != nullptr && + exceptionInfo->ExceptionRecord != nullptr) { + AppendUIEditorCrashTrace( + exceptionInfo->ExceptionRecord->ExceptionCode, + exceptionInfo->ExceptionRecord->ExceptionAddress); + } else { + AppendUIEditorCrashTrace(0u, nullptr); + } + + return EXCEPTION_EXECUTE_HANDLER; +} + +} // namespace XCEngine::UI::Editor + +namespace XCEngine::UI::Editor { + +int Application::Run(HINSTANCE hInstance, int nCmdShow) { + if (!Initialize(hInstance, nCmdShow)) { + Shutdown(); + return 1; + } + + constexpr int kMaxMessagesPerTick = 64; + MSG message = {}; + while (true) { + int processedMessageCount = 0; + while (processedMessageCount < kMaxMessagesPerTick && + PeekMessageW(&message, nullptr, 0U, 0U, PM_REMOVE)) { + if (message.message == WM_QUIT) { + Shutdown(); + return static_cast(message.wParam); + } + + TranslateMessage(&message); + DispatchMessageW(&message); + ++processedMessageCount; + } + + if (m_windowManager != nullptr) { + m_windowManager->DestroyClosedWindows(); + if (!m_windowManager->HasWindows()) { + break; + } + + m_windowManager->RenderAllWindows(); + } else { + break; + } + } + + Shutdown(); + return 0; +} + +} // namespace XCEngine::UI::Editor + +namespace XCEngine::UI::Editor { + +namespace { + +void TryEnableNonClientDpiScaling(HWND hwnd) { + if (hwnd == nullptr) { + return; + } + + const HMODULE user32 = GetModuleHandleW(L"user32.dll"); + if (user32 == nullptr) { + return; + } + + using EnableNonClientDpiScalingFn = BOOL(WINAPI*)(HWND); + const auto enableNonClientDpiScaling = + reinterpret_cast( + GetProcAddress(user32, "EnableNonClientDpiScaling")); + if (enableNonClientDpiScaling != nullptr) { + enableNonClientDpiScaling(hwnd); + } +} + +Application* GetApplicationFromWindowUserData(HWND hwnd) { + return reinterpret_cast(GetWindowLongPtrW(hwnd, GWLP_USERDATA)); +} + +} // namespace + +bool Application::RegisterWindowClass() { + WNDCLASSEXW windowClass = {}; + windowClass.cbSize = sizeof(windowClass); + windowClass.style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS; + windowClass.lpfnWndProc = &Application::WndProc; + windowClass.cbClsExtra = 0; + windowClass.cbWndExtra = 0; + windowClass.hInstance = m_hInstance; + windowClass.hCursor = LoadCursorW(nullptr, IDC_ARROW); + windowClass.hbrBackground = nullptr; + windowClass.lpszMenuName = nullptr; + windowClass.hIcon = static_cast( + LoadImageW( + m_hInstance, + MAKEINTRESOURCEW(IDI_APP_ICON), + IMAGE_ICON, + 0, + 0, + LR_DEFAULTSIZE)); + windowClass.hIconSm = static_cast( + LoadImageW( + m_hInstance, + MAKEINTRESOURCEW(IDI_APP_ICON_SMALL), + IMAGE_ICON, + GetSystemMetrics(SM_CXSMICON), + GetSystemMetrics(SM_CYSMICON), + LR_DEFAULTCOLOR)); + windowClass.lpszClassName = kWindowClassName; + m_windowClassAtom = RegisterClassExW(&windowClass); + if (m_windowClassAtom == 0) { + AppendUIEditorRuntimeTrace("app", "window class registration failed"); + return false; + } + + return true; +} + +LRESULT CALLBACK Application::WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { + if (message == WM_NCCREATE) { + TryEnableNonClientDpiScaling(hwnd); + const auto* createStruct = reinterpret_cast(lParam); + Application* application = + createStruct != nullptr + ? reinterpret_cast(createStruct->lpCreateParams) + : nullptr; + SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast(application)); + if (application != nullptr && application->m_windowManager != nullptr) { + application->m_windowManager->HandlePendingNativeWindowCreated(hwnd); + } + return TRUE; + } + + Application* application = GetApplicationFromWindowUserData(hwnd); + LRESULT dispatcherResult = 0; + if (application != nullptr && + application->m_windowManager != nullptr && + application->m_windowManager->TryDispatchWindowMessage( + hwnd, + message, + wParam, + lParam, + dispatcherResult)) { + return dispatcherResult; + } + + return DefWindowProcW(hwnd, message, wParam, lParam); +} + +} // namespace XCEngine::UI::Editor + diff --git a/new_editor/app/Bootstrap/ApplicationBootstrap.cpp b/new_editor/app/Bootstrap/ApplicationBootstrap.cpp deleted file mode 100644 index e549b69f..00000000 --- a/new_editor/app/Bootstrap/ApplicationBootstrap.cpp +++ /dev/null @@ -1,133 +0,0 @@ -#include "Bootstrap/Application.h" -#include "Bootstrap/ApplicationBootstrapInternal.h" - -#include - -#include - -#include "Ports/SystemInteractionPort.h" -#include "Composition/EditorContext.h" -#include "Platform/Win32/EditorWindow.h" -#include "Platform/Win32/EditorWindowManager.h" -#include "Platform/Win32/Win32SystemInteractionHost.h" -#include "Internal/ExecutablePath.h" - -#ifndef XCUIEDITOR_REPO_ROOT -#define XCUIEDITOR_REPO_ROOT "." -#endif - -namespace XCEngine::UI::Editor { - -using namespace BootstrapInternal; -using App::Internal::GetExecutableDirectory; - -bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) { - m_hInstance = hInstance; - m_repoRoot = ResolveRepoRootPath(); - EnableDpiAwareness(); - - const std::filesystem::path logRoot = GetExecutableDirectory() / "logs"; - InitializeUIEditorRuntimeTrace(logRoot); - SetUnhandledExceptionFilter(&Application::HandleUnhandledException); - AppendUIEditorRuntimeTrace("app", "initialize begin"); - - if (!m_editorContext->Initialize(m_repoRoot)) { - AppendUIEditorRuntimeTrace( - "app", - "shell asset validation failed: " + m_editorContext->GetValidationMessage()); - return false; - } - if (!RegisterWindowClass()) { - return false; - } - - m_systemInteractionHost = std::make_unique(); - m_editorContext->SetSystemInteractionHost(m_systemInteractionHost.get()); - - App::EditorWindowHostConfig hostConfig = {}; - hostConfig.hInstance = m_hInstance; - hostConfig.windowClassName = kWindowClassName; - hostConfig.windowStyle = kBorderlessWindowStyle; - hostConfig.primaryWindowTitle = kWindowTitle; - hostConfig.windowUserData = this; - m_windowManager = std::make_unique( - hostConfig, - m_repoRoot, - *m_editorContext); - - m_editorContext->SetExitRequestHandler([this]() { - if (m_windowManager == nullptr) { - return; - } - - if (App::EditorWindow* primaryWindow = m_windowManager->FindPrimaryWindow(); - primaryWindow != nullptr && - primaryWindow->GetHwnd() != nullptr) { - PostMessageW(primaryWindow->GetHwnd(), WM_CLOSE, 0, 0); - } - }); - m_editorContext->SetReadyStatus(); - - App::EditorWindowManager::CreateParams createParams = {}; - createParams.windowId = "main"; - createParams.title = kWindowTitle; - createParams.showCommand = nCmdShow; - createParams.primary = true; - createParams.autoCaptureOnStartup = true; - if (m_windowManager->CreateEditorWindow( - m_editorContext->BuildWorkspaceController(), - createParams) == nullptr) { - AppendUIEditorRuntimeTrace("app", "primary window creation failed"); - return false; - } - - AppendUIEditorRuntimeTrace("app", "initialize end"); - return true; -} - -void Application::Shutdown() { - AppendUIEditorRuntimeTrace("app", "shutdown begin"); - - if (m_windowManager != nullptr) { - m_windowManager->Shutdown(); - m_windowManager.reset(); - } - - if (m_editorContext != nullptr) { - m_editorContext->SetSystemInteractionHost(nullptr); - } - m_systemInteractionHost.reset(); - - ::XCEngine::Resources::ResourceManager::Get().Shutdown(); - - if (m_windowClassAtom != 0 && m_hInstance != nullptr) { - UnregisterClassW(kWindowClassName, m_hInstance); - m_windowClassAtom = 0; - } - - AppendUIEditorRuntimeTrace("app", "shutdown end"); - ShutdownUIEditorRuntimeTrace(); -} - -std::filesystem::path Application::ResolveRepoRootPath() { - std::string root = XCUIEDITOR_REPO_ROOT; - if (root.size() >= 2u && root.front() == '"' && root.back() == '"') { - root = root.substr(1u, root.size() - 2u); - } - return std::filesystem::path(root).lexically_normal(); -} - -LONG WINAPI Application::HandleUnhandledException(EXCEPTION_POINTERS* exceptionInfo) { - if (exceptionInfo != nullptr && - exceptionInfo->ExceptionRecord != nullptr) { - AppendUIEditorCrashTrace( - exceptionInfo->ExceptionRecord->ExceptionCode, - exceptionInfo->ExceptionRecord->ExceptionAddress); - } else { - AppendUIEditorCrashTrace(0u, nullptr); - } - - return EXCEPTION_EXECUTE_HANDLER; -} - -} // namespace XCEngine::UI::Editor diff --git a/new_editor/app/Bootstrap/ApplicationBootstrapInternal.cpp b/new_editor/app/Bootstrap/ApplicationBootstrapInternal.cpp deleted file mode 100644 index 5474f9c3..00000000 --- a/new_editor/app/Bootstrap/ApplicationBootstrapInternal.cpp +++ /dev/null @@ -1,55 +0,0 @@ -#include "Bootstrap/ApplicationBootstrapInternal.h" - -namespace XCEngine::UI::Editor::BootstrapInternal { - -void EnableDpiAwareness() { - const HMODULE user32 = GetModuleHandleW(L"user32.dll"); - if (user32 != nullptr) { - using SetProcessDpiAwarenessContextFn = BOOL(WINAPI*)(DPI_AWARENESS_CONTEXT); - const auto setProcessDpiAwarenessContext = - reinterpret_cast( - GetProcAddress(user32, "SetProcessDpiAwarenessContext")); - if (setProcessDpiAwarenessContext != nullptr) { - if (setProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)) { - return; - } - if (GetLastError() == ERROR_ACCESS_DENIED) { - return; - } - if (setProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE)) { - return; - } - if (GetLastError() == ERROR_ACCESS_DENIED) { - return; - } - } - } - - const HMODULE shcore = LoadLibraryW(L"shcore.dll"); - if (shcore != nullptr) { - using SetProcessDpiAwarenessFn = HRESULT(WINAPI*)(PROCESS_DPI_AWARENESS); - const auto setProcessDpiAwareness = - reinterpret_cast( - GetProcAddress(shcore, "SetProcessDpiAwareness")); - if (setProcessDpiAwareness != nullptr) { - const HRESULT hr = setProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE); - FreeLibrary(shcore); - if (SUCCEEDED(hr) || hr == E_ACCESSDENIED) { - return; - } - } else { - FreeLibrary(shcore); - } - } - - if (user32 != nullptr) { - using SetProcessDPIAwareFn = BOOL(WINAPI*)(); - const auto setProcessDPIAware = - reinterpret_cast(GetProcAddress(user32, "SetProcessDPIAware")); - if (setProcessDPIAware != nullptr) { - setProcessDPIAware(); - } - } -} - -} // namespace XCEngine::UI::Editor::BootstrapInternal diff --git a/new_editor/app/Bootstrap/ApplicationBootstrapInternal.h b/new_editor/app/Bootstrap/ApplicationBootstrapInternal.h deleted file mode 100644 index 9478e9c8..00000000 --- a/new_editor/app/Bootstrap/ApplicationBootstrapInternal.h +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once - -#include "Bootstrap/EditorResources.h" - -#include - -namespace XCEngine::UI::Editor::BootstrapInternal { - -inline constexpr const wchar_t* kWindowClassName = L"XCEditorShellHost"; -inline constexpr const wchar_t* kWindowTitle = L"Main Scene * - Main.xx - XCEngine Editor"; -inline constexpr DWORD kBorderlessWindowStyle = WS_POPUP | WS_THICKFRAME; - -void EnableDpiAwareness(); - -} // namespace XCEngine::UI::Editor::BootstrapInternal diff --git a/new_editor/app/Bootstrap/ApplicationLifecycle.cpp b/new_editor/app/Bootstrap/ApplicationLifecycle.cpp deleted file mode 100644 index 34844bff..00000000 --- a/new_editor/app/Bootstrap/ApplicationLifecycle.cpp +++ /dev/null @@ -1,21 +0,0 @@ -#include "Bootstrap/Application.h" - -#include "Ports/SystemInteractionPort.h" -#include "Composition/EditorContext.h" -#include "Platform/Win32/EditorWindowManager.h" - -#include - -namespace XCEngine::UI::Editor { - -Application::Application() - : m_editorContext(std::make_unique()) {} - -Application::~Application() = default; - -int RunXCUIEditorApp(HINSTANCE hInstance, int nCmdShow) { - Application application; - return application.Run(hInstance, nCmdShow); -} - -} // namespace XCEngine::UI::Editor diff --git a/new_editor/app/Bootstrap/ApplicationRunLoop.cpp b/new_editor/app/Bootstrap/ApplicationRunLoop.cpp deleted file mode 100644 index 77cd0a86..00000000 --- a/new_editor/app/Bootstrap/ApplicationRunLoop.cpp +++ /dev/null @@ -1,45 +0,0 @@ -#include "Bootstrap/Application.h" - -#include "Platform/Win32/EditorWindowManager.h" - -namespace XCEngine::UI::Editor { - -int Application::Run(HINSTANCE hInstance, int nCmdShow) { - if (!Initialize(hInstance, nCmdShow)) { - Shutdown(); - return 1; - } - - constexpr int kMaxMessagesPerTick = 64; - MSG message = {}; - while (true) { - int processedMessageCount = 0; - while (processedMessageCount < kMaxMessagesPerTick && - PeekMessageW(&message, nullptr, 0U, 0U, PM_REMOVE)) { - if (message.message == WM_QUIT) { - Shutdown(); - return static_cast(message.wParam); - } - - TranslateMessage(&message); - DispatchMessageW(&message); - ++processedMessageCount; - } - - if (m_windowManager != nullptr) { - m_windowManager->DestroyClosedWindows(); - if (!m_windowManager->HasWindows()) { - break; - } - - m_windowManager->RenderAllWindows(); - } else { - break; - } - } - - Shutdown(); - return 0; -} - -} // namespace XCEngine::UI::Editor diff --git a/new_editor/app/Bootstrap/ApplicationWindowClass.cpp b/new_editor/app/Bootstrap/ApplicationWindowClass.cpp deleted file mode 100644 index 8448ae6e..00000000 --- a/new_editor/app/Bootstrap/ApplicationWindowClass.cpp +++ /dev/null @@ -1,107 +0,0 @@ -#include "Bootstrap/Application.h" -#include "Bootstrap/ApplicationBootstrapInternal.h" - -#include - -#include "Platform/Win32/EditorWindowManager.h" - -namespace XCEngine::UI::Editor { - -using namespace BootstrapInternal; - -namespace { - -void TryEnableNonClientDpiScaling(HWND hwnd) { - if (hwnd == nullptr) { - return; - } - - const HMODULE user32 = GetModuleHandleW(L"user32.dll"); - if (user32 == nullptr) { - return; - } - - using EnableNonClientDpiScalingFn = BOOL(WINAPI*)(HWND); - const auto enableNonClientDpiScaling = - reinterpret_cast( - GetProcAddress(user32, "EnableNonClientDpiScaling")); - if (enableNonClientDpiScaling != nullptr) { - enableNonClientDpiScaling(hwnd); - } -} - -Application* GetApplicationFromWindowUserData(HWND hwnd) { - return reinterpret_cast(GetWindowLongPtrW(hwnd, GWLP_USERDATA)); -} - -} // namespace - -bool Application::RegisterWindowClass() { - WNDCLASSEXW windowClass = {}; - windowClass.cbSize = sizeof(windowClass); - windowClass.style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS; - windowClass.lpfnWndProc = &Application::WndProc; - windowClass.cbClsExtra = 0; - windowClass.cbWndExtra = 0; - windowClass.hInstance = m_hInstance; - windowClass.hCursor = LoadCursorW(nullptr, IDC_ARROW); - windowClass.hbrBackground = nullptr; - windowClass.lpszMenuName = nullptr; - windowClass.hIcon = static_cast( - LoadImageW( - m_hInstance, - MAKEINTRESOURCEW(IDI_APP_ICON), - IMAGE_ICON, - 0, - 0, - LR_DEFAULTSIZE)); - windowClass.hIconSm = static_cast( - LoadImageW( - m_hInstance, - MAKEINTRESOURCEW(IDI_APP_ICON_SMALL), - IMAGE_ICON, - GetSystemMetrics(SM_CXSMICON), - GetSystemMetrics(SM_CYSMICON), - LR_DEFAULTCOLOR)); - windowClass.lpszClassName = kWindowClassName; - m_windowClassAtom = RegisterClassExW(&windowClass); - if (m_windowClassAtom == 0) { - AppendUIEditorRuntimeTrace("app", "window class registration failed"); - return false; - } - - return true; -} - -LRESULT CALLBACK Application::WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { - if (message == WM_NCCREATE) { - TryEnableNonClientDpiScaling(hwnd); - const auto* createStruct = reinterpret_cast(lParam); - Application* application = - createStruct != nullptr - ? reinterpret_cast(createStruct->lpCreateParams) - : nullptr; - SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast(application)); - if (application != nullptr && application->m_windowManager != nullptr) { - application->m_windowManager->HandlePendingNativeWindowCreated(hwnd); - } - return TRUE; - } - - Application* application = GetApplicationFromWindowUserData(hwnd); - LRESULT dispatcherResult = 0; - if (application != nullptr && - application->m_windowManager != nullptr && - application->m_windowManager->TryDispatchWindowMessage( - hwnd, - message, - wParam, - lParam, - dispatcherResult)) { - return dispatcherResult; - } - - return DefWindowProcW(hwnd, message, wParam, lParam); -} - -} // namespace XCEngine::UI::Editor diff --git a/new_editor/app/Bootstrap/EditorApp.rc b/new_editor/app/Bootstrap/EditorApp.rc index 6525da89..e3b7c9a3 100644 --- a/new_editor/app/Bootstrap/EditorApp.rc +++ b/new_editor/app/Bootstrap/EditorApp.rc @@ -5,5 +5,12 @@ IDI_APP_ICON_SMALL ICON "../resources/Icons/logo_icon.ico" IDR_PNG_FOLDER_ICON PNG "../resources/Icons/folder_icon.png" IDR_PNG_GAMEOBJECT_ICON PNG "../resources/Icons/gameobject_icon.png" IDR_PNG_SCENE_ICON PNG "../resources/Icons/scene_icon.png" +IDR_PNG_CAMERA_GIZMO_ICON PNG "../resources/Icons/camera_gizmo.png" +IDR_PNG_DIRECTIONAL_LIGHT_GIZMO_ICON PNG "../resources/Icons/directional_light_gizmo.png" +IDR_PNG_POINT_LIGHT_GIZMO_ICON PNG "../resources/Icons/point_light_gizmo.png" +IDR_PNG_SPOT_LIGHT_GIZMO_ICON PNG "../resources/Icons/spot_light_gizmo.png" IDR_PNG_LOGO PNG "../resources/Icons/logo.png" IDR_PNG_LOGO_ICON PNG "../resources/Icons/logo_icon.png" +IDR_PNG_PLAY_BUTTON_ICON PNG "../resources/Icons/play_button.png" +IDR_PNG_PAUSE_BUTTON_ICON PNG "../resources/Icons/pause_button.png" +IDR_PNG_STEP_BUTTON_ICON PNG "../resources/Icons/step_button.png" diff --git a/new_editor/app/Bootstrap/EditorResources.h b/new_editor/app/Bootstrap/EditorResources.h index 46e81b2b..83adce4b 100644 --- a/new_editor/app/Bootstrap/EditorResources.h +++ b/new_editor/app/Bootstrap/EditorResources.h @@ -7,3 +7,10 @@ #define IDR_PNG_SCENE_ICON 203 #define IDR_PNG_LOGO 204 #define IDR_PNG_LOGO_ICON 205 +#define IDR_PNG_CAMERA_GIZMO_ICON 206 +#define IDR_PNG_DIRECTIONAL_LIGHT_GIZMO_ICON 207 +#define IDR_PNG_POINT_LIGHT_GIZMO_ICON 208 +#define IDR_PNG_SPOT_LIGHT_GIZMO_ICON 209 +#define IDR_PNG_PLAY_BUTTON_ICON 210 +#define IDR_PNG_PAUSE_BUTTON_ICON 211 +#define IDR_PNG_STEP_BUTTON_ICON 212 diff --git a/new_editor/app/Composition/EditorContext.cpp b/new_editor/app/Composition/EditorContext.cpp index f409fd2b..23e99ae0 100644 --- a/new_editor/app/Composition/EditorContext.cpp +++ b/new_editor/app/Composition/EditorContext.cpp @@ -1,10 +1,7 @@ #include "EditorContext.h" - #include "Composition/EditorShellAssetBuilder.h" #include "Scene/EditorSceneRuntime.h" - #include "Composition/EditorPanelIds.h" - #include #include @@ -61,6 +58,7 @@ bool EditorContext::Initialize(const std::filesystem::path& repoRoot) { m_sceneRuntime = {}; m_sceneRuntime.Initialize(m_session.projectRoot); m_sceneRuntime.BindSelectionService(&m_selectionService); + ResetEditorColorPickerToolState(m_colorPickerToolState); SyncSessionFromSelectionService(); m_hostCommandBridge.BindSession(m_session); m_hostCommandBridge.BindCommandFocusService(m_commandFocusService); @@ -145,6 +143,14 @@ const EditorSceneRuntime& EditorContext::GetSceneRuntime() const { return m_sceneRuntime; } +EditorColorPickerToolState& EditorContext::GetColorPickerToolState() { + return m_colorPickerToolState; +} + +const EditorColorPickerToolState& EditorContext::GetColorPickerToolState() const { + return m_colorPickerToolState; +} + void EditorContext::SetSelection(EditorSelectionState selection) { m_selectionService.SetSelection(std::move(selection)); SyncSessionFromSelectionService(); @@ -198,7 +204,8 @@ UIEditorShellInteractionDefinition EditorContext::BuildShellDefinition( if (UIEditorWorkspacePanelPresentationModel* scenePresentation = FindMutablePresentation(definition.workspacePresentations, kScenePanelId); scenePresentation != nullptr) { - scenePresentation->viewportShellModel.spec.chrome.showTopBar = false; + scenePresentation->viewportShellModel.spec.chrome.showTopBar = true; + scenePresentation->viewportShellModel.spec.chrome.topBarHeight = 24.0f; scenePresentation->viewportShellModel.spec.chrome.title = {}; scenePresentation->viewportShellModel.spec.chrome.subtitle = {}; scenePresentation->viewportShellModel.spec.toolItems.clear(); @@ -240,3 +247,115 @@ std::string EditorContext::DescribeWorkspaceState( } } // namespace XCEngine::UI::Editor::App + +namespace XCEngine::UI::Editor::App { + +namespace { + +constexpr std::size_t kMaxConsoleEntries = 256u; + +std::string ResolveViewportStatusMessage( + const UIEditorShellInteractionResult& result) { + if (result.viewportInputFrame.captureStarted) { + return "Viewport capture started."; + } + if (result.viewportInputFrame.captureEnded) { + return "Viewport capture ended."; + } + if (result.viewportInputFrame.focusGained) { + return "Viewport focused."; + } + if (result.viewportInputFrame.focusLost) { + return "Viewport focus lost."; + } + if (result.viewportInputFrame.pointerPressedInside) { + return "Viewport pointer down."; + } + if (result.viewportInputFrame.pointerReleasedInside) { + return "Viewport pointer up."; + } + if (result.viewportInputFrame.pointerMoved) { + return "Viewport pointer move."; + } + if (result.viewportInputFrame.wheelDelta != 0.0f) { + return "Viewport wheel."; + } + + return {}; +} + +} // namespace + +void EditorContext::SetReadyStatus() { + SetStatus("Ready", "Application shell loaded."); +} + +void EditorContext::SetStatus( + std::string status, + std::string message) { + if (m_lastStatus != status || m_lastMessage != message) { + AppendConsoleEntry(status, message); + } + m_lastStatus = std::move(status); + m_lastMessage = std::move(message); +} + +void EditorContext::UpdateStatusFromShellResult( + const UIEditorWorkspaceController& workspaceController, + const UIEditorShellInteractionResult& result) { + (void)workspaceController; + + if (result.commandDispatched) { + SetStatus( + std::string(GetUIEditorCommandDispatchStatusName(result.commandDispatchResult.status)), + result.commandDispatchResult.message.empty() + ? result.commandDispatchResult.displayName + : result.commandDispatchResult.message); + return; + } + + if (result.workspaceResult.dockHostResult.layoutChanged) { + SetStatus("Layout", result.workspaceResult.dockHostResult.layoutResult.message); + return; + } + + if (result.workspaceResult.dockHostResult.commandExecuted) { + SetStatus("Workspace", result.workspaceResult.dockHostResult.commandResult.message); + return; + } + + if (!result.viewportPanelId.empty()) { + std::string message = ResolveViewportStatusMessage(result); + if (!message.empty()) { + SetStatus(result.viewportPanelId, std::move(message)); + } + return; + } + + if (!result.menuMutation.changed) { + return; + } + + if (!result.itemId.empty() && !result.menuMutation.openedPopupId.empty()) { + SetStatus("Menu", result.itemId + " opened child popup."); + } else if (!result.menuId.empty() && !result.menuMutation.openedPopupId.empty()) { + SetStatus("Menu", result.menuId + " opened."); + } else { + SetStatus("Menu", "Popup chain dismissed."); + } +} + +void EditorContext::AppendConsoleEntry( + std::string channel, + std::string message) { + EditorConsoleEntry entry = {}; + entry.channel = std::move(channel); + entry.message = std::move(message); + m_session.consoleEntries.push_back(std::move(entry)); + if (m_session.consoleEntries.size() > kMaxConsoleEntries) { + m_session.consoleEntries.erase(m_session.consoleEntries.begin()); + } +} + +} // namespace XCEngine::UI::Editor::App + diff --git a/new_editor/app/Composition/EditorContext.h b/new_editor/app/Composition/EditorContext.h index 3193c152..fb1f1679 100644 --- a/new_editor/app/Composition/EditorContext.h +++ b/new_editor/app/Composition/EditorContext.h @@ -5,6 +5,7 @@ #include "Scene/EditorSceneRuntime.h" #include "Commands/EditorHostCommandBridge.h" +#include "State/EditorColorPickerToolState.h" #include "State/EditorCommandFocusService.h" #include "State/EditorSelectionService.h" #include "State/EditorSession.h" @@ -49,6 +50,8 @@ public: const EditorProjectRuntime& GetProjectRuntime() const; EditorSceneRuntime& GetSceneRuntime(); const EditorSceneRuntime& GetSceneRuntime() const; + EditorColorPickerToolState& GetColorPickerToolState(); + const EditorColorPickerToolState& GetColorPickerToolState() const; void SetSelection(EditorSelectionState selection); void ClearSelection(); void SyncSessionFromSelectionService(); @@ -85,6 +88,7 @@ private: EditorSelectionService m_selectionService = {}; EditorProjectRuntime m_projectRuntime = {}; EditorSceneRuntime m_sceneRuntime = {}; + EditorColorPickerToolState m_colorPickerToolState = {}; EditorHostCommandBridge m_hostCommandBridge = {}; Ports::SystemInteractionPort* m_systemInteractionHost = nullptr; std::string m_lastStatus = {}; diff --git a/new_editor/app/Composition/EditorContextStatus.cpp b/new_editor/app/Composition/EditorContextStatus.cpp deleted file mode 100644 index fd1d9553..00000000 --- a/new_editor/app/Composition/EditorContextStatus.cpp +++ /dev/null @@ -1,115 +0,0 @@ -#include "EditorContext.h" - -#include -#include - -namespace XCEngine::UI::Editor::App { - -namespace { - -constexpr std::size_t kMaxConsoleEntries = 256u; - -std::string ResolveViewportStatusMessage( - const UIEditorShellInteractionResult& result) { - if (result.viewportInputFrame.captureStarted) { - return "Viewport capture started."; - } - if (result.viewportInputFrame.captureEnded) { - return "Viewport capture ended."; - } - if (result.viewportInputFrame.focusGained) { - return "Viewport focused."; - } - if (result.viewportInputFrame.focusLost) { - return "Viewport focus lost."; - } - if (result.viewportInputFrame.pointerPressedInside) { - return "Viewport pointer down."; - } - if (result.viewportInputFrame.pointerReleasedInside) { - return "Viewport pointer up."; - } - if (result.viewportInputFrame.pointerMoved) { - return "Viewport pointer move."; - } - if (result.viewportInputFrame.wheelDelta != 0.0f) { - return "Viewport wheel."; - } - - return {}; -} - -} // namespace - -void EditorContext::SetReadyStatus() { - SetStatus("Ready", "Application shell loaded."); -} - -void EditorContext::SetStatus( - std::string status, - std::string message) { - if (m_lastStatus != status || m_lastMessage != message) { - AppendConsoleEntry(status, message); - } - m_lastStatus = std::move(status); - m_lastMessage = std::move(message); -} - -void EditorContext::UpdateStatusFromShellResult( - const UIEditorWorkspaceController& workspaceController, - const UIEditorShellInteractionResult& result) { - (void)workspaceController; - - if (result.commandDispatched) { - SetStatus( - std::string(GetUIEditorCommandDispatchStatusName(result.commandDispatchResult.status)), - result.commandDispatchResult.message.empty() - ? result.commandDispatchResult.displayName - : result.commandDispatchResult.message); - return; - } - - if (result.workspaceResult.dockHostResult.layoutChanged) { - SetStatus("Layout", result.workspaceResult.dockHostResult.layoutResult.message); - return; - } - - if (result.workspaceResult.dockHostResult.commandExecuted) { - SetStatus("Workspace", result.workspaceResult.dockHostResult.commandResult.message); - return; - } - - if (!result.viewportPanelId.empty()) { - std::string message = ResolveViewportStatusMessage(result); - if (!message.empty()) { - SetStatus(result.viewportPanelId, std::move(message)); - } - return; - } - - if (!result.menuMutation.changed) { - return; - } - - if (!result.itemId.empty() && !result.menuMutation.openedPopupId.empty()) { - SetStatus("Menu", result.itemId + " opened child popup."); - } else if (!result.menuId.empty() && !result.menuMutation.openedPopupId.empty()) { - SetStatus("Menu", result.menuId + " opened."); - } else { - SetStatus("Menu", "Popup chain dismissed."); - } -} - -void EditorContext::AppendConsoleEntry( - std::string channel, - std::string message) { - EditorConsoleEntry entry = {}; - entry.channel = std::move(channel); - entry.message = std::move(message); - m_session.consoleEntries.push_back(std::move(entry)); - if (m_session.consoleEntries.size() > kMaxConsoleEntries) { - m_session.consoleEntries.erase(m_session.consoleEntries.begin()); - } -} - -} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Composition/EditorPanelIds.h b/new_editor/app/Composition/EditorPanelIds.h index ca608769..c54b9793 100644 --- a/new_editor/app/Composition/EditorPanelIds.h +++ b/new_editor/app/Composition/EditorPanelIds.h @@ -10,6 +10,7 @@ inline constexpr std::string_view kGamePanelId = "game"; inline constexpr std::string_view kInspectorPanelId = "inspector"; inline constexpr std::string_view kConsolePanelId = "console"; inline constexpr std::string_view kProjectPanelId = "project"; +inline constexpr std::string_view kColorPickerPanelId = "color-picker"; inline constexpr std::string_view kHierarchyPanelTitle = "Hierarchy"; inline constexpr std::string_view kScenePanelTitle = "Scene"; @@ -17,6 +18,7 @@ inline constexpr std::string_view kGamePanelTitle = "Game"; inline constexpr std::string_view kInspectorPanelTitle = "Inspector"; inline constexpr std::string_view kConsolePanelTitle = "Console"; inline constexpr std::string_view kProjectPanelTitle = "Project"; +inline constexpr std::string_view kColorPickerPanelTitle = "Color Picker"; [[nodiscard]] constexpr bool IsEditorViewportPanelId(std::string_view panelId) { return panelId == kScenePanelId || panelId == kGamePanelId; diff --git a/new_editor/app/Composition/EditorShellAssetBuilder.cpp b/new_editor/app/Composition/EditorShellAssetBuilder.cpp index f610c90b..154d0847 100644 --- a/new_editor/app/Composition/EditorShellAssetBuilder.cpp +++ b/new_editor/app/Composition/EditorShellAssetBuilder.cpp @@ -1,10 +1,594 @@ #include "EditorShellAssetBuilder.h" - -#include "Composition/EditorShellAssetBuilderInternal.h" +#include +#include +#include +#include +#include +#include "Composition/EditorPanelIds.h" +#include +#include + +namespace XCEngine::UI::Editor::App { + +UIEditorPanelRegistry BuildEditorPanelRegistry(); +UIEditorWorkspaceModel BuildEditorWorkspaceModel( + const UIEditorPanelRegistry& panelRegistry); +UIEditorCommandRegistry BuildEditorCommandRegistry(); +UIEditorMenuModel BuildEditorMenuModel(); +std::vector<::XCEngine::UI::UIShortcutBinding> BuildEditorShortcutBindings(); +UIEditorShellInteractionDefinition BuildBaseEditorShellDefinition( + const UIEditorPanelRegistry& panelRegistry); +std::string ResolveEditorPanelTitle( + const UIEditorPanelRegistry& registry, + std::string_view panelId); + +} // namespace XCEngine::UI::Editor::App + +namespace XCEngine::UI::Editor::App { + +namespace { + +UIEditorShellToolbarButton BuildToolbarButton( + std::string buttonId, + BuiltInIconKind iconKind) { + UIEditorShellToolbarButton button = {}; + button.buttonId = std::move(buttonId); + button.iconKind = static_cast(iconKind); + button.enabled = true; + return button; +} + +UIEditorWorkspacePanelPresentationModel BuildViewportPresentation( + const UIEditorPanelDescriptor& descriptor) { + UIEditorWorkspacePanelPresentationModel presentation = {}; + presentation.panelId = descriptor.panelId; + presentation.kind = descriptor.presentationKind; + presentation.viewportShellModel.spec = descriptor.viewportShellSpec; + if (presentation.viewportShellModel.spec.chrome.title.empty()) { + presentation.viewportShellModel.spec.chrome.title = descriptor.defaultTitle; + } + return presentation; +} + +UIEditorWorkspacePanelPresentationModel BuildPanelPresentation( + const UIEditorPanelDescriptor& descriptor) { + if (descriptor.presentationKind == UIEditorPanelPresentationKind::ViewportShell) { + return BuildViewportPresentation(descriptor); + } + + UIEditorWorkspacePanelPresentationModel presentation = {}; + presentation.panelId = descriptor.panelId; + presentation.kind = descriptor.presentationKind; + return presentation; +} + +} // namespace + +UIEditorShellInteractionDefinition BuildBaseEditorShellDefinition( + const UIEditorPanelRegistry& panelRegistry) { + UIEditorShellInteractionDefinition definition = {}; + definition.menuModel = BuildEditorMenuModel(); + definition.toolbarButtons = { + BuildToolbarButton("run.play", BuiltInIconKind::PlayButton), + BuildToolbarButton("run.pause", BuiltInIconKind::PauseButton), + BuildToolbarButton("run.step", BuiltInIconKind::StepButton) + }; + definition.statusSegments = {}; + definition.workspacePresentations.reserve(panelRegistry.panels.size()); + for (const UIEditorPanelDescriptor& descriptor : panelRegistry.panels) { + definition.workspacePresentations.push_back(BuildPanelPresentation(descriptor)); + } + return definition; +} + +std::string ResolveEditorPanelTitle( + 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 XCEngine::UI::Editor::App + +namespace XCEngine::UI::Editor::App { + +namespace { + +using XCEngine::Input::KeyCode; +using XCEngine::UI::UIInputEventType; +using XCEngine::UI::UIShortcutBinding; +using XCEngine::UI::UIShortcutScope; + +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; +} + +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; +} + +} // namespace + +UIEditorCommandRegistry BuildEditorCommandRegistry() { + 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.create_folder", "Create Folder"), + BuildHostCommand("assets.create_material", "Create Material"), + BuildHostCommand("assets.copy_path", "Copy Path"), + BuildHostCommand("assets.show_in_explorer", "Show in Explorer"), + 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, + std::string(kHierarchyPanelId)), + BuildWorkspaceCommand( + "view.activate_scene", + "Scene", + UIEditorWorkspaceCommandKind::ActivatePanel, + std::string(kScenePanelId)), + BuildWorkspaceCommand( + "view.activate_game", + "Game", + UIEditorWorkspaceCommandKind::ActivatePanel, + std::string(kGamePanelId)), + BuildWorkspaceCommand( + "view.activate_inspector", + "Inspector", + UIEditorWorkspaceCommandKind::ActivatePanel, + std::string(kInspectorPanelId)), + BuildWorkspaceCommand( + "view.activate_console", + "Console", + UIEditorWorkspaceCommandKind::ActivatePanel, + std::string(kConsolePanelId)), + BuildWorkspaceCommand( + "view.activate_project", + "Project", + UIEditorWorkspaceCommandKind::ActivatePanel, + std::string(kProjectPanelId)) + }; + return registry; +} + +std::vector BuildEditorShortcutBindings() { + 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("assets.create_folder", static_cast(KeyCode::N), true, true), + 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) + }; +} + +} // namespace XCEngine::UI::Editor::App + +namespace XCEngine::UI::Editor::App { + +namespace { + +UIEditorPanelDescriptor BuildHostedContentPanelDescriptor( + std::string_view panelId, + std::string_view title, + bool placeholder, + bool canHide, + bool canClose) { + UIEditorPanelDescriptor descriptor = {}; + descriptor.panelId = std::string(panelId); + descriptor.defaultTitle = std::string(title); + descriptor.presentationKind = UIEditorPanelPresentationKind::HostedContent; + descriptor.placeholder = placeholder; + descriptor.canHide = canHide; + descriptor.canClose = canClose; + return descriptor; +} + +UIEditorPanelDescriptor BuildToolWindowHostedContentPanelDescriptor( + std::string_view panelId, + std::string_view title, + float preferredWidth, + float preferredHeight, + float minimumWidth, + float minimumHeight) { + UIEditorPanelDescriptor descriptor = + BuildHostedContentPanelDescriptor(panelId, title, false, true, true); + descriptor.toolWindow = true; + descriptor.preferredDetachedWindowSize = + ::XCEngine::UI::UISize(preferredWidth, preferredHeight); + descriptor.minimumDetachedWindowSize = + ::XCEngine::UI::UISize(minimumWidth, minimumHeight); + return descriptor; +} + +UIEditorPanelDescriptor BuildViewportPanelDescriptor( + std::string_view panelId, + std::string_view title, + bool canHide, + bool canClose, + bool showTopBar, + bool showBottomBar) { + UIEditorPanelDescriptor descriptor = {}; + descriptor.panelId = std::string(panelId); + descriptor.defaultTitle = std::string(title); + descriptor.presentationKind = UIEditorPanelPresentationKind::ViewportShell; + descriptor.placeholder = false; + descriptor.canHide = canHide; + descriptor.canClose = canClose; + descriptor.viewportShellSpec.chrome.title = descriptor.defaultTitle; + descriptor.viewportShellSpec.chrome.showTopBar = showTopBar; + descriptor.viewportShellSpec.chrome.showBottomBar = showBottomBar; + return descriptor; +} + +const UIEditorPanelDescriptor& RequirePanelDescriptor( + const UIEditorPanelRegistry& registry, + std::string_view panelId) { + if (const UIEditorPanelDescriptor* descriptor = + FindUIEditorPanelDescriptor(registry, panelId); + descriptor != nullptr) { + return *descriptor; + } + + static const UIEditorPanelDescriptor fallback = {}; + return fallback; +} + +} // namespace + +UIEditorPanelRegistry BuildEditorPanelRegistry() { + UIEditorPanelRegistry registry = {}; + registry.panels = { + BuildHostedContentPanelDescriptor(kHierarchyPanelId, kHierarchyPanelTitle, true, false, false), + BuildViewportPanelDescriptor(kScenePanelId, kScenePanelTitle, false, false, false, false), + BuildViewportPanelDescriptor(kGamePanelId, kGamePanelTitle, false, false, false, false), + BuildHostedContentPanelDescriptor(kInspectorPanelId, kInspectorPanelTitle, true, false, false), + BuildHostedContentPanelDescriptor(kConsolePanelId, kConsolePanelTitle, true, false, false), + BuildHostedContentPanelDescriptor(kProjectPanelId, kProjectPanelTitle, false, false, false), + BuildToolWindowHostedContentPanelDescriptor( + kColorPickerPanelId, + kColorPickerPanelTitle, + 352.0f, + 500.0f, + 320.0f, + 460.0f) + }; + return registry; +} + +UIEditorWorkspaceModel BuildEditorWorkspaceModel( + const UIEditorPanelRegistry& panelRegistry) { + const UIEditorPanelDescriptor& hierarchy = + RequirePanelDescriptor(panelRegistry, kHierarchyPanelId); + const UIEditorPanelDescriptor& scene = + RequirePanelDescriptor(panelRegistry, kScenePanelId); + const UIEditorPanelDescriptor& game = + RequirePanelDescriptor(panelRegistry, kGamePanelId); + const UIEditorPanelDescriptor& inspector = + RequirePanelDescriptor(panelRegistry, kInspectorPanelId); + const UIEditorPanelDescriptor& console = + RequirePanelDescriptor(panelRegistry, kConsolePanelId); + const UIEditorPanelDescriptor& project = + RequirePanelDescriptor(panelRegistry, kProjectPanelId); + + UIEditorWorkspaceModel workspace = {}; + workspace.root = BuildUIEditorWorkspaceSplit( + "workspace-root", + UIEditorWorkspaceSplitAxis::Vertical, + 0.75f, + BuildUIEditorWorkspaceSplit( + "workspace-top", + UIEditorWorkspaceSplitAxis::Horizontal, + 0.7875f, + BuildUIEditorWorkspaceSplit( + "workspace-main", + UIEditorWorkspaceSplitAxis::Horizontal, + 0.19047619f, + BuildUIEditorWorkspaceSingleTabStack( + "hierarchy-panel", + std::string(kHierarchyPanelId), + hierarchy.defaultTitle, + hierarchy.placeholder), + BuildUIEditorWorkspaceTabStack( + "center-tabs", + { + BuildUIEditorWorkspacePanel( + "scene-panel", + std::string(kScenePanelId), + scene.defaultTitle, + scene.placeholder), + BuildUIEditorWorkspacePanel( + "game-panel", + std::string(kGamePanelId), + game.defaultTitle, + game.placeholder) + }, + 0u)), + BuildUIEditorWorkspaceSingleTabStack( + "inspector-panel", + std::string(kInspectorPanelId), + inspector.defaultTitle, + inspector.placeholder)), + BuildUIEditorWorkspaceTabStack( + "bottom-tabs", + { + BuildUIEditorWorkspacePanel( + "console-panel", + std::string(kConsolePanelId), + console.defaultTitle, + console.placeholder), + BuildUIEditorWorkspacePanel( + "project-panel", + std::string(kProjectPanelId), + project.defaultTitle, + project.placeholder) + }, + 1u)); + workspace.activePanelId = std::string(kScenePanelId); + return workspace; +} + +} // namespace XCEngine::UI::Editor::App + +namespace XCEngine::UI::Editor::App { + +namespace { + +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; +} + +} // namespace + +UIEditorMenuModel BuildEditorMenuModel() { + 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 = { + BuildSubmenuItem( + "assets-create", + "Create", + { + BuildCommandItem("assets-create-folder", "Folder", "assets.create_folder"), + BuildCommandItem("assets-create-material", "Material", "assets.create_material") + }), + BuildSeparatorItem("assets-separator-create"), + BuildCommandItem("assets-show-in-explorer", "Show in Explorer", "assets.show_in_explorer"), + BuildCommandItem("assets-copy-path", "Copy Path", "assets.copy_path"), + BuildSeparatorItem("assets-separator-utility"), + 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, + std::string(kHierarchyPanelId) + }; + UIEditorMenuCheckedStateBinding sceneActive = { + UIEditorMenuCheckedStateSource::PanelActive, + std::string(kScenePanelId) + }; + UIEditorMenuCheckedStateBinding gameActive = { + UIEditorMenuCheckedStateSource::PanelActive, + std::string(kGamePanelId) + }; + UIEditorMenuCheckedStateBinding inspectorActive = { + UIEditorMenuCheckedStateSource::PanelActive, + std::string(kInspectorPanelId) + }; + UIEditorMenuCheckedStateBinding consoleActive = { + UIEditorMenuCheckedStateSource::PanelActive, + std::string(kConsolePanelId) + }; + UIEditorMenuCheckedStateBinding projectActive = { + UIEditorMenuCheckedStateSource::PanelActive, + std::string(kProjectPanelId) + }; + + 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; +} + +} // namespace XCEngine::UI::Editor::App namespace XCEngine::UI::Editor::App { -using namespace CompositionInternal; EditorShellAsset BuildEditorApplicationShellAsset(const std::filesystem::path& repoRoot) { EditorShellAsset asset = {}; @@ -53,3 +637,6 @@ UIEditorShellInteractionDefinition BuildEditorApplicationShellInteractionDefinit } } // namespace XCEngine::UI::Editor::App + + + diff --git a/new_editor/app/Composition/EditorShellAssetBuilderInternal.h b/new_editor/app/Composition/EditorShellAssetBuilderInternal.h deleted file mode 100644 index 98d564ba..00000000 --- a/new_editor/app/Composition/EditorShellAssetBuilderInternal.h +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#include "EditorShellAssetBuilder.h" - -#include - -#include -#include -#include - -namespace XCEngine::UI::Editor::App::CompositionInternal { - -UIEditorPanelRegistry BuildEditorPanelRegistry(); -UIEditorWorkspaceModel BuildEditorWorkspaceModel( - const UIEditorPanelRegistry& panelRegistry); -UIEditorCommandRegistry BuildEditorCommandRegistry(); -UIEditorMenuModel BuildEditorMenuModel(); -std::vector<::XCEngine::UI::UIShortcutBinding> BuildEditorShortcutBindings(); -UIEditorShellInteractionDefinition BuildBaseEditorShellDefinition( - const UIEditorPanelRegistry& panelRegistry); -std::string ResolveEditorPanelTitle( - const UIEditorPanelRegistry& registry, - std::string_view panelId); - -} // namespace XCEngine::UI::Editor::App::CompositionInternal diff --git a/new_editor/app/Composition/EditorShellAssetCommands.cpp b/new_editor/app/Composition/EditorShellAssetCommands.cpp deleted file mode 100644 index e3848f12..00000000 --- a/new_editor/app/Composition/EditorShellAssetCommands.cpp +++ /dev/null @@ -1,157 +0,0 @@ -#include "EditorShellAssetBuilderInternal.h" - -#include "Composition/EditorPanelIds.h" -#include - -#include - -namespace XCEngine::UI::Editor::App::CompositionInternal { - -namespace { - -using XCEngine::Input::KeyCode; -using XCEngine::UI::UIInputEventType; -using XCEngine::UI::UIShortcutBinding; -using XCEngine::UI::UIShortcutScope; - -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; -} - -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; -} - -} // namespace - -UIEditorCommandRegistry BuildEditorCommandRegistry() { - 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.create_folder", "Create Folder"), - BuildHostCommand("assets.create_material", "Create Material"), - BuildHostCommand("assets.copy_path", "Copy Path"), - BuildHostCommand("assets.show_in_explorer", "Show in Explorer"), - 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, - std::string(kHierarchyPanelId)), - BuildWorkspaceCommand( - "view.activate_scene", - "Scene", - UIEditorWorkspaceCommandKind::ActivatePanel, - std::string(kScenePanelId)), - BuildWorkspaceCommand( - "view.activate_game", - "Game", - UIEditorWorkspaceCommandKind::ActivatePanel, - std::string(kGamePanelId)), - BuildWorkspaceCommand( - "view.activate_inspector", - "Inspector", - UIEditorWorkspaceCommandKind::ActivatePanel, - std::string(kInspectorPanelId)), - BuildWorkspaceCommand( - "view.activate_console", - "Console", - UIEditorWorkspaceCommandKind::ActivatePanel, - std::string(kConsolePanelId)), - BuildWorkspaceCommand( - "view.activate_project", - "Project", - UIEditorWorkspaceCommandKind::ActivatePanel, - std::string(kProjectPanelId)) - }; - return registry; -} - -std::vector BuildEditorShortcutBindings() { - 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("assets.create_folder", static_cast(KeyCode::N), true, true), - 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) - }; -} - -} // namespace XCEngine::UI::Editor::App::CompositionInternal diff --git a/new_editor/app/Composition/EditorShellAssetDefinition.cpp b/new_editor/app/Composition/EditorShellAssetDefinition.cpp deleted file mode 100644 index 06a6831e..00000000 --- a/new_editor/app/Composition/EditorShellAssetDefinition.cpp +++ /dev/null @@ -1,74 +0,0 @@ -#include "EditorShellAssetBuilderInternal.h" - -#include - -namespace XCEngine::UI::Editor::App::CompositionInternal { - -namespace { - -UIEditorShellToolbarButton BuildToolbarButton( - std::string buttonId, - UIEditorShellToolbarGlyph glyph) { - UIEditorShellToolbarButton button = {}; - button.buttonId = std::move(buttonId); - button.glyph = glyph; - button.enabled = true; - return button; -} - -UIEditorWorkspacePanelPresentationModel BuildViewportPresentation( - const UIEditorPanelDescriptor& descriptor) { - UIEditorWorkspacePanelPresentationModel presentation = {}; - presentation.panelId = descriptor.panelId; - presentation.kind = descriptor.presentationKind; - presentation.viewportShellModel.spec = descriptor.viewportShellSpec; - if (presentation.viewportShellModel.spec.chrome.title.empty()) { - presentation.viewportShellModel.spec.chrome.title = descriptor.defaultTitle; - } - return presentation; -} - -UIEditorWorkspacePanelPresentationModel BuildPanelPresentation( - const UIEditorPanelDescriptor& descriptor) { - if (descriptor.presentationKind == UIEditorPanelPresentationKind::ViewportShell) { - return BuildViewportPresentation(descriptor); - } - - UIEditorWorkspacePanelPresentationModel presentation = {}; - presentation.panelId = descriptor.panelId; - presentation.kind = descriptor.presentationKind; - return presentation; -} - -} // namespace - -UIEditorShellInteractionDefinition BuildBaseEditorShellDefinition( - const UIEditorPanelRegistry& panelRegistry) { - UIEditorShellInteractionDefinition definition = {}; - definition.menuModel = BuildEditorMenuModel(); - definition.toolbarButtons = { - BuildToolbarButton("run.play", UIEditorShellToolbarGlyph::Play), - BuildToolbarButton("run.pause", UIEditorShellToolbarGlyph::Pause), - BuildToolbarButton("run.step", UIEditorShellToolbarGlyph::Step) - }; - definition.statusSegments = {}; - definition.workspacePresentations.reserve(panelRegistry.panels.size()); - for (const UIEditorPanelDescriptor& descriptor : panelRegistry.panels) { - definition.workspacePresentations.push_back(BuildPanelPresentation(descriptor)); - } - return definition; -} - -std::string ResolveEditorPanelTitle( - 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 XCEngine::UI::Editor::App::CompositionInternal diff --git a/new_editor/app/Composition/EditorShellAssetLayout.cpp b/new_editor/app/Composition/EditorShellAssetLayout.cpp deleted file mode 100644 index c828b770..00000000 --- a/new_editor/app/Composition/EditorShellAssetLayout.cpp +++ /dev/null @@ -1,145 +0,0 @@ -#include "EditorShellAssetBuilderInternal.h" - -#include "Composition/EditorPanelIds.h" - -namespace XCEngine::UI::Editor::App::CompositionInternal { - -namespace { - -UIEditorPanelDescriptor BuildHostedContentPanelDescriptor( - std::string_view panelId, - std::string_view title, - bool placeholder, - bool canHide, - bool canClose) { - UIEditorPanelDescriptor descriptor = {}; - descriptor.panelId = std::string(panelId); - descriptor.defaultTitle = std::string(title); - descriptor.presentationKind = UIEditorPanelPresentationKind::HostedContent; - descriptor.placeholder = placeholder; - descriptor.canHide = canHide; - descriptor.canClose = canClose; - return descriptor; -} - -UIEditorPanelDescriptor BuildViewportPanelDescriptor( - std::string_view panelId, - std::string_view title, - bool canHide, - bool canClose, - bool showTopBar, - bool showBottomBar) { - UIEditorPanelDescriptor descriptor = {}; - descriptor.panelId = std::string(panelId); - descriptor.defaultTitle = std::string(title); - descriptor.presentationKind = UIEditorPanelPresentationKind::ViewportShell; - descriptor.placeholder = false; - descriptor.canHide = canHide; - descriptor.canClose = canClose; - descriptor.viewportShellSpec.chrome.title = descriptor.defaultTitle; - descriptor.viewportShellSpec.chrome.showTopBar = showTopBar; - descriptor.viewportShellSpec.chrome.showBottomBar = showBottomBar; - return descriptor; -} - -const UIEditorPanelDescriptor& RequirePanelDescriptor( - const UIEditorPanelRegistry& registry, - std::string_view panelId) { - if (const UIEditorPanelDescriptor* descriptor = - FindUIEditorPanelDescriptor(registry, panelId); - descriptor != nullptr) { - return *descriptor; - } - - static const UIEditorPanelDescriptor fallback = {}; - return fallback; -} - -} // namespace - -UIEditorPanelRegistry BuildEditorPanelRegistry() { - UIEditorPanelRegistry registry = {}; - registry.panels = { - BuildHostedContentPanelDescriptor(kHierarchyPanelId, kHierarchyPanelTitle, true, false, false), - BuildViewportPanelDescriptor(kScenePanelId, kScenePanelTitle, false, false, false, false), - BuildViewportPanelDescriptor(kGamePanelId, kGamePanelTitle, false, false, false, false), - BuildHostedContentPanelDescriptor(kInspectorPanelId, kInspectorPanelTitle, true, false, false), - BuildHostedContentPanelDescriptor(kConsolePanelId, kConsolePanelTitle, true, false, false), - BuildHostedContentPanelDescriptor(kProjectPanelId, kProjectPanelTitle, false, false, false) - }; - return registry; -} - -UIEditorWorkspaceModel BuildEditorWorkspaceModel( - const UIEditorPanelRegistry& panelRegistry) { - const UIEditorPanelDescriptor& hierarchy = - RequirePanelDescriptor(panelRegistry, kHierarchyPanelId); - const UIEditorPanelDescriptor& scene = - RequirePanelDescriptor(panelRegistry, kScenePanelId); - const UIEditorPanelDescriptor& game = - RequirePanelDescriptor(panelRegistry, kGamePanelId); - const UIEditorPanelDescriptor& inspector = - RequirePanelDescriptor(panelRegistry, kInspectorPanelId); - const UIEditorPanelDescriptor& console = - RequirePanelDescriptor(panelRegistry, kConsolePanelId); - const UIEditorPanelDescriptor& project = - RequirePanelDescriptor(panelRegistry, kProjectPanelId); - - UIEditorWorkspaceModel workspace = {}; - workspace.root = BuildUIEditorWorkspaceSplit( - "workspace-root", - UIEditorWorkspaceSplitAxis::Vertical, - 0.75f, - BuildUIEditorWorkspaceSplit( - "workspace-top", - UIEditorWorkspaceSplitAxis::Horizontal, - 0.7875f, - BuildUIEditorWorkspaceSplit( - "workspace-main", - UIEditorWorkspaceSplitAxis::Horizontal, - 0.19047619f, - BuildUIEditorWorkspaceSingleTabStack( - "hierarchy-panel", - std::string(kHierarchyPanelId), - hierarchy.defaultTitle, - hierarchy.placeholder), - BuildUIEditorWorkspaceTabStack( - "center-tabs", - { - BuildUIEditorWorkspacePanel( - "scene-panel", - std::string(kScenePanelId), - scene.defaultTitle, - scene.placeholder), - BuildUIEditorWorkspacePanel( - "game-panel", - std::string(kGamePanelId), - game.defaultTitle, - game.placeholder) - }, - 0u)), - BuildUIEditorWorkspaceSingleTabStack( - "inspector-panel", - std::string(kInspectorPanelId), - inspector.defaultTitle, - inspector.placeholder)), - BuildUIEditorWorkspaceTabStack( - "bottom-tabs", - { - BuildUIEditorWorkspacePanel( - "console-panel", - std::string(kConsolePanelId), - console.defaultTitle, - console.placeholder), - BuildUIEditorWorkspacePanel( - "project-panel", - std::string(kProjectPanelId), - project.defaultTitle, - project.placeholder) - }, - 1u)); - workspace.activePanelId = std::string(kScenePanelId); - return workspace; -} - -} // namespace XCEngine::UI::Editor::App::CompositionInternal diff --git a/new_editor/app/Composition/EditorShellAssetMenu.cpp b/new_editor/app/Composition/EditorShellAssetMenu.cpp deleted file mode 100644 index 7e022ec0..00000000 --- a/new_editor/app/Composition/EditorShellAssetMenu.cpp +++ /dev/null @@ -1,180 +0,0 @@ -#include "EditorShellAssetBuilderInternal.h" - -#include "Composition/EditorPanelIds.h" - -#include - -namespace XCEngine::UI::Editor::App::CompositionInternal { - -namespace { - -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; -} - -} // namespace - -UIEditorMenuModel BuildEditorMenuModel() { - 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 = { - BuildSubmenuItem( - "assets-create", - "Create", - { - BuildCommandItem("assets-create-folder", "Folder", "assets.create_folder"), - BuildCommandItem("assets-create-material", "Material", "assets.create_material") - }), - BuildSeparatorItem("assets-separator-create"), - BuildCommandItem("assets-show-in-explorer", "Show in Explorer", "assets.show_in_explorer"), - BuildCommandItem("assets-copy-path", "Copy Path", "assets.copy_path"), - BuildSeparatorItem("assets-separator-utility"), - 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, - std::string(kHierarchyPanelId) - }; - UIEditorMenuCheckedStateBinding sceneActive = { - UIEditorMenuCheckedStateSource::PanelActive, - std::string(kScenePanelId) - }; - UIEditorMenuCheckedStateBinding gameActive = { - UIEditorMenuCheckedStateSource::PanelActive, - std::string(kGamePanelId) - }; - UIEditorMenuCheckedStateBinding inspectorActive = { - UIEditorMenuCheckedStateSource::PanelActive, - std::string(kInspectorPanelId) - }; - UIEditorMenuCheckedStateBinding consoleActive = { - UIEditorMenuCheckedStateSource::PanelActive, - std::string(kConsolePanelId) - }; - UIEditorMenuCheckedStateBinding projectActive = { - UIEditorMenuCheckedStateSource::PanelActive, - std::string(kProjectPanelId) - }; - - 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; -} - -} // namespace XCEngine::UI::Editor::App::CompositionInternal diff --git a/new_editor/app/Composition/EditorShellRuntime.cpp b/new_editor/app/Composition/EditorShellRuntime.cpp index 682da3f9..62e57f21 100644 --- a/new_editor/app/Composition/EditorShellRuntime.cpp +++ b/new_editor/app/Composition/EditorShellRuntime.cpp @@ -1,22 +1,50 @@ -#include "Composition/EditorShellRuntime.h" - +#include "Composition/EditorShellRuntime.h" #include "Ports/TexturePort.h" #include "Ports/ViewportRenderPort.h" #include "Composition/EditorContext.h" - #include "Composition/EditorPanelIds.h" - #include +#include +#include +#include "Features/PanelInputContext.h" namespace XCEngine::UI::Editor::App { +void ApplyViewportFramesToShellFrame( + UIEditorShellInteractionFrame& shellFrame, + ViewportHostService& viewportHostService); + +std::vector<::XCEngine::UI::UIInputEvent> FilterShellInputEventsForHostedContentCapture( + const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents); + +bool ShouldHostedContentYieldPointerStream( + const UIEditorShellInteractionFrame& shellFrame, + bool shellInteractiveCaptureActive); + +std::vector<::XCEngine::UI::UIInputEvent> FilterHostedContentInputEventsForShellOwnership( + const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents, + bool shellOwnsPointerStream); + +UIEditorShellComposeModel BuildShellComposeModelFromFrame( + const UIEditorShellInteractionFrame& frame); + +void AppendShellPopups( + ::XCEngine::UI::UIDrawList& drawList, + const UIEditorShellInteractionFrame& frame, + const UIEditorShellInteractionPalette& palette, + const UIEditorShellInteractionMetrics& metrics); + void EditorShellRuntime::Initialize( const std::filesystem::path& repoRoot, Ports::TexturePort& textureHost, UIEditorTextMeasurer& textMeasurer) { m_textureHost = &textureHost; m_builtInIcons.Initialize(textureHost); - m_sceneViewportFeature.Initialize(repoRoot, textureHost, m_viewportHostService); + m_sceneViewportFeature.Initialize( + repoRoot, + textureHost, + &m_builtInIcons, + m_viewportHostService); m_hierarchyPanel.SetBuiltInIcons(&m_builtInIcons); m_projectPanel.SetBuiltInIcons(&m_builtInIcons); m_projectPanel.SetTextMeasurer(&textMeasurer); @@ -59,6 +87,7 @@ void EditorShellRuntime::ResetInteractionState() { m_traceEntries.clear(); m_sceneViewportFeature.ResetInteractionState(); m_hierarchyPanel.ResetInteractionState(); + m_colorPickerPanel.ResetInteractionState(); m_projectPanel.ResetInteractionState(); } @@ -106,6 +135,26 @@ Widgets::UIEditorDockHostCursorKind EditorShellRuntime::GetDockCursorKind() cons m_shellFrame.workspaceInteractionFrame.dockHostFrame.layout); } +bool EditorShellRuntime::TryResolveDockTabDragHotspot( + std::string_view nodeId, + std::string_view panelId, + const ::XCEngine::UI::UIPoint& point, + ::XCEngine::UI::UIPoint& outHotspot) const { + return TryResolveUIEditorDockHostTabDragHotspot( + m_shellFrame.workspaceInteractionFrame.dockHostFrame.layout, + nodeId, + panelId, + point, + outHotspot); +} + +UIEditorDockHostTabDropTarget EditorShellRuntime::ResolveDockTabDropTarget( + const ::XCEngine::UI::UIPoint& point) const { + return ResolveUIEditorDockHostTabDropTarget( + m_shellFrame.workspaceInteractionFrame.dockHostFrame.layout, + point); +} + EditorShellPointerOwner EditorShellRuntime::GetPointerOwner() const { const auto& dockHostInteractionState = m_shellInteractionState.workspaceInteractionState.dockHostInteractionState; @@ -159,3 +208,465 @@ bool EditorShellRuntime::HasInteractiveCapture() const { } } // namespace XCEngine::UI::Editor::App + +namespace XCEngine::UI::Editor::App { + +using ::XCEngine::UI::UIDrawList; + +UIEditorShellComposeModel BuildShellComposeModelFromFrame( + const UIEditorShellInteractionFrame& frame) { + UIEditorShellComposeModel model = {}; + model.menuBarItems = frame.request.menuBarItems; + model.toolbarButtons = frame.model.toolbarButtons; + model.statusSegments = frame.model.statusSegments; + model.workspacePresentations = frame.model.workspacePresentations; + return model; +} + +void AppendShellPopups( + UIDrawList& drawList, + const UIEditorShellInteractionFrame& frame, + const UIEditorShellInteractionPalette& palette, + const UIEditorShellInteractionMetrics& metrics) { + const std::size_t popupCount = + (std::min)(frame.request.popupRequests.size(), frame.popupFrames.size()); + for (std::size_t index = 0; index < popupCount; ++index) { + const UIEditorShellInteractionPopupRequest& popupRequest = + frame.request.popupRequests[index]; + const UIEditorShellInteractionPopupFrame& popupFrame = + frame.popupFrames[index]; + Widgets::AppendUIEditorMenuPopupBackground( + drawList, + popupRequest.layout, + popupRequest.widgetItems, + popupFrame.popupState, + palette.popupPalette, + metrics.popupMetrics); + Widgets::AppendUIEditorMenuPopupForeground( + drawList, + popupRequest.layout, + popupRequest.widgetItems, + popupFrame.popupState, + palette.popupPalette, + metrics.popupMetrics); + } +} + +} // namespace XCEngine::UI::Editor::App + +namespace XCEngine::UI::Editor::App { + +using ::XCEngine::UI::UIDrawList; +void EditorShellRuntime::RenderRequestedViewports( + const ::XCEngine::Rendering::RenderContext& renderContext) { + m_viewportHostService.RenderRequestedViewports(renderContext); +} + +void EditorShellRuntime::Append(UIDrawList& drawList) const { + const auto& metrics = ResolveUIEditorShellInteractionMetrics(); + const auto& palette = ResolveUIEditorShellInteractionPalette(); + const UIEditorShellComposeModel shellComposeModel = + BuildShellComposeModelFromFrame(m_shellFrame); + AppendUIEditorShellCompose( + drawList, + m_shellFrame.shellFrame, + shellComposeModel, + m_shellInteractionState.composeState, + palette.shellPalette, + metrics.shellMetrics, + &m_builtInIcons); + m_consolePanel.Append(drawList); + m_colorPickerPanel.Append(drawList); + m_hierarchyPanel.Append(drawList); + m_inspectorPanel.Append(drawList); + m_projectPanel.Append(drawList); + m_sceneViewportFeature.Append(drawList); + AppendUIEditorShellComposeOverlay( + drawList, + m_shellFrame.shellFrame, + palette.shellPalette, + metrics.shellMetrics); + AppendShellPopups(drawList, m_shellFrame, palette, metrics); +} + +} // namespace XCEngine::UI::Editor::App + +namespace XCEngine::UI::Editor::App { + +namespace { + +using ::XCEngine::UI::UIInputEvent; +using ::XCEngine::UI::UIInputEventType; +using Widgets::UIEditorDockHostHitTargetKind; + +bool IsPointerInputEventType(UIInputEventType type) { + switch (type) { + case UIInputEventType::PointerMove: + case UIInputEventType::PointerEnter: + case UIInputEventType::PointerLeave: + case UIInputEventType::PointerButtonDown: + case UIInputEventType::PointerButtonUp: + case UIInputEventType::PointerWheel: + return true; + default: + return false; + } +} + +const UIEditorPanelDescriptor* ResolveSingleVisibleToolWindowPanelDescriptor( + const UIEditorWorkspaceController& workspaceController) { + const UIEditorWorkspaceNode& root = workspaceController.GetWorkspace().root; + if (root.kind != UIEditorWorkspaceNodeKind::TabStack) { + return nullptr; + } + + const UIEditorWorkspaceSession& session = workspaceController.GetSession(); + const UIEditorPanelRegistry& panelRegistry = workspaceController.GetPanelRegistry(); + const UIEditorPanelDescriptor* visibleDescriptor = nullptr; + std::size_t visibleCount = 0u; + for (const UIEditorWorkspaceNode& child : root.children) { + if (child.kind != UIEditorWorkspaceNodeKind::Panel) { + continue; + } + + const UIEditorPanelSessionState* panelState = + FindUIEditorPanelSessionState(session, child.panel.panelId); + if (panelState == nullptr || !panelState->open || !panelState->visible) { + continue; + } + + const UIEditorPanelDescriptor* descriptor = + FindUIEditorPanelDescriptor(panelRegistry, child.panel.panelId); + if (descriptor == nullptr) { + return nullptr; + } + + ++visibleCount; + visibleDescriptor = descriptor; + if (visibleCount > 1u) { + return nullptr; + } + } + + return visibleDescriptor != nullptr && visibleDescriptor->toolWindow + ? visibleDescriptor + : nullptr; +} + +PanelInputContext BuildHostedPanelInputContext( + const UIEditorWorkspaceInteractionFrame& workspaceFrame, + bool allowInteraction, + std::string_view panelId) { + PanelInputContext inputContext = {}; + inputContext.allowInteraction = allowInteraction; + inputContext.hasInputFocus = + IsUIEditorWorkspaceHostedPanelInputOwner(workspaceFrame.inputOwner, panelId); + inputContext.focusGained = + !IsUIEditorWorkspaceHostedPanelInputOwner(workspaceFrame.previousInputOwner, panelId) && + inputContext.hasInputFocus; + inputContext.focusLost = + IsUIEditorWorkspaceHostedPanelInputOwner(workspaceFrame.previousInputOwner, panelId) && + !inputContext.hasInputFocus; + return inputContext; +} + +} // namespace + +std::vector FilterShellInputEventsForHostedContentCapture( + const std::vector& inputEvents) { + std::vector filteredEvents = {}; + filteredEvents.reserve(inputEvents.size()); + for (const UIInputEvent& event : inputEvents) { + switch (event.type) { + case UIInputEventType::PointerMove: + case UIInputEventType::PointerEnter: + case UIInputEventType::PointerLeave: + case UIInputEventType::PointerButtonDown: + case UIInputEventType::PointerButtonUp: + case UIInputEventType::PointerWheel: + break; + default: + filteredEvents.push_back(event); + break; + } + } + return filteredEvents; +} + +bool ShouldHostedContentYieldPointerStream( + const UIEditorShellInteractionFrame& shellFrame, + bool shellInteractiveCaptureActive) { + if (shellInteractiveCaptureActive || + shellFrame.result.requestPointerCapture || + shellFrame.result.releasePointerCapture) { + return true; + } + + return shellFrame.result.workspaceResult.dockHostResult.hitTarget.kind == + UIEditorDockHostHitTargetKind::SplitterHandle; +} + +std::vector FilterHostedContentInputEventsForShellOwnership( + const std::vector& inputEvents, + bool shellOwnsPointerStream) { + if (!shellOwnsPointerStream) { + return inputEvents; + } + + std::vector filteredEvents = {}; + filteredEvents.reserve(inputEvents.size() + 1u); + + bool strippedPointerInput = false; + UIInputEvent lastPointerEvent = {}; + for (const UIInputEvent& event : inputEvents) { + if (IsPointerInputEventType(event.type)) { + strippedPointerInput = true; + lastPointerEvent = event; + continue; + } + + filteredEvents.push_back(event); + } + + if (strippedPointerInput) { + UIInputEvent leaveEvent = {}; + leaveEvent.type = UIInputEventType::PointerLeave; + leaveEvent.position = lastPointerEvent.position; + leaveEvent.modifiers = lastPointerEvent.modifiers; + filteredEvents.push_back(leaveEvent); + } + + return filteredEvents; +} + +} // namespace XCEngine::UI::Editor::App + +namespace XCEngine::UI::Editor::App { + +void EditorShellRuntime::Update( + EditorContext& context, + UIEditorWorkspaceController& workspaceController, + const ::XCEngine::UI::UIRect& bounds, + const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents, + std::string_view captureText, + EditorShellVariant shellVariant, + bool useDetachedTitleBarTabStrip, + float detachedTitleBarTabHeight, + float detachedWindowChromeHeight) { + UIEditorShellInteractionMetrics metrics = ResolveUIEditorShellInteractionMetrics(); + if (const UIEditorPanelDescriptor* toolWindowDescriptor = + ResolveSingleVisibleToolWindowPanelDescriptor(workspaceController); + toolWindowDescriptor != nullptr) { + metrics.shellMetrics.dockHostMetrics.tabStripMetrics.layoutMetrics.headerHeight = 0.0f; + const float minimumContentWidth = + toolWindowDescriptor->minimumDetachedWindowSize.width > 0.0f + ? toolWindowDescriptor->minimumDetachedWindowSize.width + : metrics.shellMetrics.dockHostMetrics.minimumStandalonePanelBodySize.width; + const float minimumContentHeight = + toolWindowDescriptor->minimumDetachedWindowSize.height > detachedWindowChromeHeight + ? toolWindowDescriptor->minimumDetachedWindowSize.height - + detachedWindowChromeHeight + : metrics.shellMetrics.dockHostMetrics.minimumStandalonePanelBodySize.height; + metrics.shellMetrics.dockHostMetrics.minimumStandalonePanelBodySize = + ::XCEngine::UI::UISize(minimumContentWidth, minimumContentHeight); + metrics.shellMetrics.dockHostMetrics.minimumTabContentBodySize = + metrics.shellMetrics.dockHostMetrics.minimumStandalonePanelBodySize; + } + if (useDetachedTitleBarTabStrip && detachedTitleBarTabHeight > 0.0f) { + metrics.shellMetrics.dockHostMetrics.tabStripMetrics.layoutMetrics.headerHeight = + detachedTitleBarTabHeight; + } + + const UIEditorWorkspaceLayoutSnapshot preUpdateSnapshot = + workspaceController.CaptureLayoutSnapshot(); + const Widgets::UIEditorDockHostLayout preUpdateDockLayout = + m_shellFrame.workspaceInteractionFrame.dockHostFrame.layout; + + m_hierarchyPanel.SetSceneRuntime(&context.GetSceneRuntime()); + m_hierarchyPanel.SetCommandFocusService(&context.GetCommandFocusService()); + m_sceneEditCommandRoute.BindSceneRuntime(&context.GetSceneRuntime()); + m_sceneViewportFeature.SetCommandFocusService(&context.GetCommandFocusService()); + // Keep the previous render request available for readback-based picking during + // this update, then refresh it again after camera/navigation state changes. + m_sceneViewportFeature.SyncRenderRequest(context.GetSceneRuntime()); + context.BindEditCommandRoutes( + &m_hierarchyPanel, + &m_projectPanel, + &m_sceneEditCommandRoute, + &m_inspectorPanel); + context.SyncSessionFromWorkspace(workspaceController); + UIEditorShellInteractionDefinition definition = + context.BuildShellDefinition(workspaceController, captureText, shellVariant); + m_viewportHostService.BeginFrame(); + const std::vector<::XCEngine::UI::UIInputEvent> shellEvents = + HasHostedContentCapture() + ? FilterShellInputEventsForHostedContentCapture(inputEvents) + : inputEvents; + + m_shellFrame = UpdateUIEditorShellInteraction( + m_shellInteractionState, + workspaceController, + bounds, + definition, + shellEvents, + context.GetShellServices(), + metrics); + + if (TryApplyUIEditorWorkspaceSplitterDragCorrection( + m_splitterDragCorrectionState, + m_shellInteractionState.workspaceInteractionState.dockHostInteractionState, + preUpdateSnapshot, + preUpdateDockLayout, + workspaceController, + metrics.shellMetrics.dockHostMetrics)) { + context.SyncSessionFromWorkspace(workspaceController); + definition = + context.BuildShellDefinition(workspaceController, captureText, shellVariant); + m_shellFrame = UpdateUIEditorShellInteraction( + m_shellInteractionState, + workspaceController, + bounds, + definition, + {}, + context.GetShellServices(), + metrics); + } + + const bool shellOwnsHostedContentPointerStream = + ShouldHostedContentYieldPointerStream(m_shellFrame, HasShellInteractiveCapture()); + const std::vector<::XCEngine::UI::UIInputEvent> hostedContentEvents = + FilterHostedContentInputEventsForShellOwnership( + inputEvents, + shellOwnsHostedContentPointerStream); + m_sceneViewportFeature.Update( + context.GetSceneRuntime(), + m_shellInteractionState.workspaceInteractionState.composeState, + m_shellFrame.workspaceInteractionFrame.composeFrame); + ApplyViewportFramesToShellFrame(m_shellFrame, m_viewportHostService); + context.SyncSessionFromWorkspace(workspaceController); + context.UpdateStatusFromShellResult(workspaceController, m_shellFrame.result); + + const bool allowHostedInteraction = !m_shellFrame.result.workspaceInputSuppressed; + const PanelInputContext hierarchyInputContext = BuildHostedPanelInputContext( + m_shellFrame.workspaceInteractionFrame, + allowHostedInteraction, + kHierarchyPanelId); + const PanelInputContext projectInputContext = BuildHostedPanelInputContext( + m_shellFrame.workspaceInteractionFrame, + allowHostedInteraction, + kProjectPanelId); + const PanelInputContext inspectorInputContext = BuildHostedPanelInputContext( + m_shellFrame.workspaceInteractionFrame, + allowHostedInteraction, + kInspectorPanelId); + const PanelInputContext colorPickerInputContext = BuildHostedPanelInputContext( + m_shellFrame.workspaceInteractionFrame, + allowHostedInteraction, + kColorPickerPanelId); + m_projectPanel.SetProjectRuntime(&context.GetProjectRuntime()); + m_projectPanel.SetCommandFocusService(&context.GetCommandFocusService()); + m_projectPanel.SetSystemInteractionHost(context.GetSystemInteractionHost()); + m_inspectorPanel.SetCommandFocusService(&context.GetCommandFocusService()); + m_hierarchyPanel.Update( + m_shellFrame.workspaceInteractionFrame.composeFrame.contentHostFrame, + hostedContentEvents, + hierarchyInputContext); + m_projectPanel.Update( + m_shellFrame.workspaceInteractionFrame.composeFrame.contentHostFrame, + hostedContentEvents, + projectInputContext); + m_traceEntries = SyncWorkspaceEvents(context, *this); + m_colorPickerPanel.Update( + context, + m_shellFrame.workspaceInteractionFrame.composeFrame.contentHostFrame, + hostedContentEvents, + colorPickerInputContext); + m_inspectorPanel.Update( + context, + m_shellFrame.workspaceInteractionFrame.composeFrame.contentHostFrame, + hostedContentEvents, + inspectorInputContext); + context.SyncSessionFromCommandFocusService(); + m_consolePanel.Update( + context.GetSession(), + m_shellFrame.workspaceInteractionFrame.composeFrame.contentHostFrame); +} + +} // namespace XCEngine::UI::Editor::App + +namespace XCEngine::UI::Editor::App { + +namespace { + +bool IsViewportPanel(std::string_view panelId) { + return IsEditorViewportPanelId(panelId); +} + +void ApplyViewportFrameToPresentation( + const ViewportFrame& viewportFrame, + UIEditorWorkspacePanelPresentationModel& presentation) { + presentation.viewportShellModel.frame.texture = viewportFrame.texture; + presentation.viewportShellModel.frame.requestedSize = viewportFrame.requestedSize; + presentation.viewportShellModel.frame.presentedSize = viewportFrame.renderSize; + presentation.viewportShellModel.frame.hasTexture = viewportFrame.hasTexture; + presentation.viewportShellModel.frame.statusText = viewportFrame.statusText; +} + +void ApplyViewportFrameToShellModel( + const ViewportFrame& viewportFrame, + UIEditorViewportShellModel& shellModel) { + shellModel.frame.texture = viewportFrame.texture; + shellModel.frame.requestedSize = viewportFrame.requestedSize; + shellModel.frame.presentedSize = viewportFrame.renderSize; + shellModel.frame.hasTexture = viewportFrame.hasTexture; + shellModel.frame.statusText = viewportFrame.statusText; +} + +UIEditorWorkspacePanelPresentationModel* FindMutableWorkspacePresentation( + std::vector& presentations, + std::string_view panelId) { + for (UIEditorWorkspacePanelPresentationModel& presentation : presentations) { + if (presentation.panelId == panelId) { + return &presentation; + } + } + + return nullptr; +} + +} // namespace + +void ApplyViewportFramesToShellFrame( + UIEditorShellInteractionFrame& shellFrame, + ViewportHostService& viewportHostService) { + auto applyToViewportFrames = + [&](std::vector& viewportFrames) { + for (UIEditorWorkspaceViewportComposeFrame& viewportComposeFrame : viewportFrames) { + if (!IsViewportPanel(viewportComposeFrame.panelId)) { + continue; + } + + const ViewportFrame viewportFrame = + viewportHostService.RequestViewport( + viewportComposeFrame.panelId, + viewportComposeFrame.viewportShellFrame.requestedViewportSize); + ApplyViewportFrameToShellModel( + viewportFrame, + viewportComposeFrame.viewportShellModel); + if (UIEditorWorkspacePanelPresentationModel* presentation = + FindMutableWorkspacePresentation( + shellFrame.model.workspacePresentations, + viewportComposeFrame.panelId); + presentation != nullptr) { + ApplyViewportFrameToPresentation(viewportFrame, *presentation); + } + } + }; + + applyToViewportFrames(shellFrame.workspaceInteractionFrame.composeFrame.viewportFrames); + applyToViewportFrames(shellFrame.shellFrame.workspaceFrame.viewportFrames); +} + +} // namespace XCEngine::UI::Editor::App + + diff --git a/new_editor/app/Composition/EditorShellRuntime.h b/new_editor/app/Composition/EditorShellRuntime.h index c08e359e..e8de9bfa 100644 --- a/new_editor/app/Composition/EditorShellRuntime.h +++ b/new_editor/app/Composition/EditorShellRuntime.h @@ -3,6 +3,7 @@ #include "Composition/EditorShellPointerInteraction.h" #include "Composition/EditorShellVariant.h" #include "Features/Console/ConsolePanel.h" +#include "Features/ColorPicker/ColorPickerPanel.h" #include "Features/Hierarchy/HierarchyPanel.h" #include "Features/Inspector/InspectorPanel.h" #include "Features/Project/ProjectPanel.h" @@ -13,6 +14,7 @@ #include "Composition/WorkspaceEventSync.h" #include +#include #include #include #include @@ -64,7 +66,8 @@ public: std::string_view captureText, EditorShellVariant shellVariant = EditorShellVariant::Primary, bool useDetachedTitleBarTabStrip = false, - float detachedTitleBarTabHeight = 0.0f); + float detachedTitleBarTabHeight = 0.0f, + float detachedWindowChromeHeight = 0.0f); void RenderRequestedViewports( const ::XCEngine::Rendering::RenderContext& renderContext); void Append(::XCEngine::UI::UIDrawList& drawList) const; @@ -81,6 +84,13 @@ public: ProjectPanel::CursorKind GetHostedContentCursorKind() const; Widgets::UIEditorDockHostCursorKind GetDockCursorKind() const; + bool TryResolveDockTabDragHotspot( + std::string_view nodeId, + std::string_view panelId, + const ::XCEngine::UI::UIPoint& point, + ::XCEngine::UI::UIPoint& outHotspot) const; + UIEditorDockHostTabDropTarget ResolveDockTabDropTarget( + const ::XCEngine::UI::UIPoint& point) const; EditorShellPointerOwner GetPointerOwner() const; bool WantsHostPointerCapture() const; bool WantsHostPointerRelease() const; @@ -94,6 +104,7 @@ private: BuiltInIcons m_builtInIcons = {}; Ports::TexturePort* m_textureHost = nullptr; ConsolePanel m_consolePanel = {}; + ColorPickerPanel m_colorPickerPanel = {}; HierarchyPanel m_hierarchyPanel = {}; InspectorPanel m_inspectorPanel = {}; ProjectPanel m_projectPanel = {}; diff --git a/new_editor/app/Composition/EditorShellRuntimeInternal.h b/new_editor/app/Composition/EditorShellRuntimeInternal.h deleted file mode 100644 index 1a091039..00000000 --- a/new_editor/app/Composition/EditorShellRuntimeInternal.h +++ /dev/null @@ -1,33 +0,0 @@ -#pragma once - -#include "Composition/EditorShellRuntime.h" - -#include - -namespace XCEngine::UI::Editor::App::Internal { - -void ApplyViewportFramesToShellFrame( - UIEditorShellInteractionFrame& shellFrame, - ViewportHostService& viewportHostService); - -std::vector<::XCEngine::UI::UIInputEvent> FilterShellInputEventsForHostedContentCapture( - const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents); - -bool ShouldHostedContentYieldPointerStream( - const UIEditorShellInteractionFrame& shellFrame, - bool shellInteractiveCaptureActive); - -std::vector<::XCEngine::UI::UIInputEvent> FilterHostedContentInputEventsForShellOwnership( - const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents, - bool shellOwnsPointerStream); - -UIEditorShellComposeModel BuildShellComposeModelFromFrame( - const UIEditorShellInteractionFrame& frame); - -void AppendShellPopups( - ::XCEngine::UI::UIDrawList& drawList, - const UIEditorShellInteractionFrame& frame, - const UIEditorShellInteractionPalette& palette, - const UIEditorShellInteractionMetrics& metrics); - -} // namespace XCEngine::UI::Editor::App::Internal diff --git a/new_editor/app/Composition/EditorShellRuntimeRendering.cpp b/new_editor/app/Composition/EditorShellRuntimeRendering.cpp deleted file mode 100644 index f6c263cf..00000000 --- a/new_editor/app/Composition/EditorShellRuntimeRendering.cpp +++ /dev/null @@ -1,87 +0,0 @@ -#include "Composition/EditorShellRuntimeInternal.h" - -#include - -#include - -namespace XCEngine::UI::Editor::App::Internal { - -using ::XCEngine::UI::UIDrawList; - -UIEditorShellComposeModel BuildShellComposeModelFromFrame( - const UIEditorShellInteractionFrame& frame) { - UIEditorShellComposeModel model = {}; - model.menuBarItems = frame.request.menuBarItems; - model.toolbarButtons = frame.model.toolbarButtons; - model.statusSegments = frame.model.statusSegments; - model.workspacePresentations = frame.model.workspacePresentations; - return model; -} - -void AppendShellPopups( - UIDrawList& drawList, - const UIEditorShellInteractionFrame& frame, - const UIEditorShellInteractionPalette& palette, - const UIEditorShellInteractionMetrics& metrics) { - const std::size_t popupCount = - (std::min)(frame.request.popupRequests.size(), frame.popupFrames.size()); - for (std::size_t index = 0; index < popupCount; ++index) { - const UIEditorShellInteractionPopupRequest& popupRequest = - frame.request.popupRequests[index]; - const UIEditorShellInteractionPopupFrame& popupFrame = - frame.popupFrames[index]; - Widgets::AppendUIEditorMenuPopupBackground( - drawList, - popupRequest.layout, - popupRequest.widgetItems, - popupFrame.popupState, - palette.popupPalette, - metrics.popupMetrics); - Widgets::AppendUIEditorMenuPopupForeground( - drawList, - popupRequest.layout, - popupRequest.widgetItems, - popupFrame.popupState, - palette.popupPalette, - metrics.popupMetrics); - } -} - -} // namespace XCEngine::UI::Editor::App::Internal - -namespace XCEngine::UI::Editor::App { - -using ::XCEngine::UI::UIDrawList; -using namespace Internal; - -void EditorShellRuntime::RenderRequestedViewports( - const ::XCEngine::Rendering::RenderContext& renderContext) { - m_viewportHostService.RenderRequestedViewports(renderContext); -} - -void EditorShellRuntime::Append(UIDrawList& drawList) const { - const auto& metrics = ResolveUIEditorShellInteractionMetrics(); - const auto& palette = ResolveUIEditorShellInteractionPalette(); - const UIEditorShellComposeModel shellComposeModel = - BuildShellComposeModelFromFrame(m_shellFrame); - AppendUIEditorShellCompose( - drawList, - m_shellFrame.shellFrame, - shellComposeModel, - m_shellInteractionState.composeState, - palette.shellPalette, - metrics.shellMetrics); - m_consolePanel.Append(drawList); - m_hierarchyPanel.Append(drawList); - m_inspectorPanel.Append(drawList); - m_projectPanel.Append(drawList); - m_sceneViewportFeature.Append(drawList); - AppendUIEditorShellComposeOverlay( - drawList, - m_shellFrame.shellFrame, - palette.shellPalette, - metrics.shellMetrics); - AppendShellPopups(drawList, m_shellFrame, palette, metrics); -} - -} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Composition/EditorShellRuntimeUpdate.cpp b/new_editor/app/Composition/EditorShellRuntimeUpdate.cpp deleted file mode 100644 index b0af3905..00000000 --- a/new_editor/app/Composition/EditorShellRuntimeUpdate.cpp +++ /dev/null @@ -1,245 +0,0 @@ -#include "Composition/EditorShellRuntimeInternal.h" - -#include "Features/PanelInputContext.h" -#include "Composition/EditorContext.h" - -#include "Composition/EditorPanelIds.h" -#include - -namespace XCEngine::UI::Editor::App::Internal { - -namespace { - -using ::XCEngine::UI::UIInputEvent; -using ::XCEngine::UI::UIInputEventType; -using Widgets::UIEditorDockHostHitTargetKind; - -bool IsPointerInputEventType(UIInputEventType type) { - switch (type) { - case UIInputEventType::PointerMove: - case UIInputEventType::PointerEnter: - case UIInputEventType::PointerLeave: - case UIInputEventType::PointerButtonDown: - case UIInputEventType::PointerButtonUp: - case UIInputEventType::PointerWheel: - return true; - default: - return false; - } -} - -PanelInputContext BuildHostedPanelInputContext( - const UIEditorWorkspaceInteractionFrame& workspaceFrame, - bool allowInteraction, - std::string_view panelId) { - PanelInputContext inputContext = {}; - inputContext.allowInteraction = allowInteraction; - inputContext.hasInputFocus = - IsUIEditorWorkspaceHostedPanelInputOwner(workspaceFrame.inputOwner, panelId); - inputContext.focusGained = - !IsUIEditorWorkspaceHostedPanelInputOwner(workspaceFrame.previousInputOwner, panelId) && - inputContext.hasInputFocus; - inputContext.focusLost = - IsUIEditorWorkspaceHostedPanelInputOwner(workspaceFrame.previousInputOwner, panelId) && - !inputContext.hasInputFocus; - return inputContext; -} - -} // namespace - -std::vector FilterShellInputEventsForHostedContentCapture( - const std::vector& inputEvents) { - std::vector filteredEvents = {}; - filteredEvents.reserve(inputEvents.size()); - for (const UIInputEvent& event : inputEvents) { - switch (event.type) { - case UIInputEventType::PointerMove: - case UIInputEventType::PointerEnter: - case UIInputEventType::PointerLeave: - case UIInputEventType::PointerButtonDown: - case UIInputEventType::PointerButtonUp: - case UIInputEventType::PointerWheel: - break; - default: - filteredEvents.push_back(event); - break; - } - } - return filteredEvents; -} - -bool ShouldHostedContentYieldPointerStream( - const UIEditorShellInteractionFrame& shellFrame, - bool shellInteractiveCaptureActive) { - if (shellInteractiveCaptureActive || - shellFrame.result.requestPointerCapture || - shellFrame.result.releasePointerCapture) { - return true; - } - - return shellFrame.result.workspaceResult.dockHostResult.hitTarget.kind == - UIEditorDockHostHitTargetKind::SplitterHandle; -} - -std::vector FilterHostedContentInputEventsForShellOwnership( - const std::vector& inputEvents, - bool shellOwnsPointerStream) { - if (!shellOwnsPointerStream) { - return inputEvents; - } - - std::vector filteredEvents = {}; - filteredEvents.reserve(inputEvents.size() + 1u); - - bool strippedPointerInput = false; - UIInputEvent lastPointerEvent = {}; - for (const UIInputEvent& event : inputEvents) { - if (IsPointerInputEventType(event.type)) { - strippedPointerInput = true; - lastPointerEvent = event; - continue; - } - - filteredEvents.push_back(event); - } - - if (strippedPointerInput) { - UIInputEvent leaveEvent = {}; - leaveEvent.type = UIInputEventType::PointerLeave; - leaveEvent.position = lastPointerEvent.position; - leaveEvent.modifiers = lastPointerEvent.modifiers; - filteredEvents.push_back(leaveEvent); - } - - return filteredEvents; -} - -} // namespace XCEngine::UI::Editor::App::Internal - -namespace XCEngine::UI::Editor::App { - -using namespace Internal; - -void EditorShellRuntime::Update( - EditorContext& context, - UIEditorWorkspaceController& workspaceController, - const ::XCEngine::UI::UIRect& bounds, - const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents, - std::string_view captureText, - EditorShellVariant shellVariant, - bool useDetachedTitleBarTabStrip, - float detachedTitleBarTabHeight) { - UIEditorShellInteractionMetrics metrics = ResolveUIEditorShellInteractionMetrics(); - if (useDetachedTitleBarTabStrip && detachedTitleBarTabHeight > 0.0f) { - metrics.shellMetrics.dockHostMetrics.tabStripMetrics.layoutMetrics.headerHeight = - detachedTitleBarTabHeight; - } - - const UIEditorWorkspaceLayoutSnapshot preUpdateSnapshot = - workspaceController.CaptureLayoutSnapshot(); - const Widgets::UIEditorDockHostLayout preUpdateDockLayout = - m_shellFrame.workspaceInteractionFrame.dockHostFrame.layout; - - m_hierarchyPanel.SetSceneRuntime(&context.GetSceneRuntime()); - m_hierarchyPanel.SetCommandFocusService(&context.GetCommandFocusService()); - m_sceneEditCommandRoute.BindSceneRuntime(&context.GetSceneRuntime()); - m_sceneViewportFeature.SetCommandFocusService(&context.GetCommandFocusService()); - // Keep the previous render request available for readback-based picking during - // this update, then refresh it again after camera/navigation state changes. - m_sceneViewportFeature.SyncRenderRequest(context.GetSceneRuntime()); - context.BindEditCommandRoutes( - &m_hierarchyPanel, - &m_projectPanel, - &m_sceneEditCommandRoute, - &m_inspectorPanel); - context.SyncSessionFromWorkspace(workspaceController); - UIEditorShellInteractionDefinition definition = - context.BuildShellDefinition(workspaceController, captureText, shellVariant); - m_viewportHostService.BeginFrame(); - const std::vector<::XCEngine::UI::UIInputEvent> shellEvents = - HasHostedContentCapture() - ? FilterShellInputEventsForHostedContentCapture(inputEvents) - : inputEvents; - - m_shellFrame = UpdateUIEditorShellInteraction( - m_shellInteractionState, - workspaceController, - bounds, - definition, - shellEvents, - context.GetShellServices(), - metrics); - - if (TryApplyUIEditorWorkspaceSplitterDragCorrection( - m_splitterDragCorrectionState, - m_shellInteractionState.workspaceInteractionState.dockHostInteractionState, - preUpdateSnapshot, - preUpdateDockLayout, - workspaceController, - metrics.shellMetrics.dockHostMetrics)) { - context.SyncSessionFromWorkspace(workspaceController); - definition = - context.BuildShellDefinition(workspaceController, captureText, shellVariant); - m_shellFrame = UpdateUIEditorShellInteraction( - m_shellInteractionState, - workspaceController, - bounds, - definition, - {}, - context.GetShellServices(), - metrics); - } - - const bool shellOwnsHostedContentPointerStream = - ShouldHostedContentYieldPointerStream(m_shellFrame, HasShellInteractiveCapture()); - const std::vector<::XCEngine::UI::UIInputEvent> hostedContentEvents = - FilterHostedContentInputEventsForShellOwnership( - inputEvents, - shellOwnsHostedContentPointerStream); - m_sceneViewportFeature.Update( - context.GetSceneRuntime(), - m_shellInteractionState.workspaceInteractionState.composeState, - m_shellFrame.workspaceInteractionFrame.composeFrame); - ApplyViewportFramesToShellFrame(m_shellFrame, m_viewportHostService); - context.SyncSessionFromWorkspace(workspaceController); - context.UpdateStatusFromShellResult(workspaceController, m_shellFrame.result); - - const bool allowHostedInteraction = !m_shellFrame.result.workspaceInputSuppressed; - const PanelInputContext hierarchyInputContext = BuildHostedPanelInputContext( - m_shellFrame.workspaceInteractionFrame, - allowHostedInteraction, - kHierarchyPanelId); - const PanelInputContext projectInputContext = BuildHostedPanelInputContext( - m_shellFrame.workspaceInteractionFrame, - allowHostedInteraction, - kProjectPanelId); - const PanelInputContext inspectorInputContext = BuildHostedPanelInputContext( - m_shellFrame.workspaceInteractionFrame, - allowHostedInteraction, - kInspectorPanelId); - m_projectPanel.SetProjectRuntime(&context.GetProjectRuntime()); - m_projectPanel.SetCommandFocusService(&context.GetCommandFocusService()); - m_projectPanel.SetSystemInteractionHost(context.GetSystemInteractionHost()); - m_inspectorPanel.SetCommandFocusService(&context.GetCommandFocusService()); - m_hierarchyPanel.Update( - m_shellFrame.workspaceInteractionFrame.composeFrame.contentHostFrame, - hostedContentEvents, - hierarchyInputContext); - m_projectPanel.Update( - m_shellFrame.workspaceInteractionFrame.composeFrame.contentHostFrame, - hostedContentEvents, - projectInputContext); - m_traceEntries = SyncWorkspaceEvents(context, *this); - m_inspectorPanel.Update( - context.GetSession(), - context.GetSceneRuntime(), - m_shellFrame.workspaceInteractionFrame.composeFrame.contentHostFrame, - hostedContentEvents, - inspectorInputContext); - context.SyncSessionFromCommandFocusService(); - m_consolePanel.Update( - context.GetSession(), - m_shellFrame.workspaceInteractionFrame.composeFrame.contentHostFrame); -} - -} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Composition/EditorShellRuntimeViewport.cpp b/new_editor/app/Composition/EditorShellRuntimeViewport.cpp deleted file mode 100644 index 61487146..00000000 --- a/new_editor/app/Composition/EditorShellRuntimeViewport.cpp +++ /dev/null @@ -1,78 +0,0 @@ -#include "Composition/EditorShellRuntimeInternal.h" - -#include "Composition/EditorPanelIds.h" - -namespace XCEngine::UI::Editor::App::Internal { - -namespace { - -bool IsViewportPanel(std::string_view panelId) { - return IsEditorViewportPanelId(panelId); -} - -void ApplyViewportFrameToPresentation( - const ViewportFrame& viewportFrame, - UIEditorWorkspacePanelPresentationModel& presentation) { - presentation.viewportShellModel.frame.texture = viewportFrame.texture; - presentation.viewportShellModel.frame.requestedSize = viewportFrame.requestedSize; - presentation.viewportShellModel.frame.presentedSize = viewportFrame.renderSize; - presentation.viewportShellModel.frame.hasTexture = viewportFrame.hasTexture; - presentation.viewportShellModel.frame.statusText = viewportFrame.statusText; -} - -void ApplyViewportFrameToShellModel( - const ViewportFrame& viewportFrame, - UIEditorViewportShellModel& shellModel) { - shellModel.frame.texture = viewportFrame.texture; - shellModel.frame.requestedSize = viewportFrame.requestedSize; - shellModel.frame.presentedSize = viewportFrame.renderSize; - shellModel.frame.hasTexture = viewportFrame.hasTexture; - shellModel.frame.statusText = viewportFrame.statusText; -} - -UIEditorWorkspacePanelPresentationModel* FindMutableWorkspacePresentation( - std::vector& presentations, - std::string_view panelId) { - for (UIEditorWorkspacePanelPresentationModel& presentation : presentations) { - if (presentation.panelId == panelId) { - return &presentation; - } - } - - return nullptr; -} - -} // namespace - -void ApplyViewportFramesToShellFrame( - UIEditorShellInteractionFrame& shellFrame, - ViewportHostService& viewportHostService) { - auto applyToViewportFrames = - [&](std::vector& viewportFrames) { - for (UIEditorWorkspaceViewportComposeFrame& viewportComposeFrame : viewportFrames) { - if (!IsViewportPanel(viewportComposeFrame.panelId)) { - continue; - } - - const ViewportFrame viewportFrame = - viewportHostService.RequestViewport( - viewportComposeFrame.panelId, - viewportComposeFrame.viewportShellFrame.requestedViewportSize); - ApplyViewportFrameToShellModel( - viewportFrame, - viewportComposeFrame.viewportShellModel); - if (UIEditorWorkspacePanelPresentationModel* presentation = - FindMutableWorkspacePresentation( - shellFrame.model.workspacePresentations, - viewportComposeFrame.panelId); - presentation != nullptr) { - ApplyViewportFrameToPresentation(viewportFrame, *presentation); - } - } - }; - - applyToViewportFrames(shellFrame.workspaceInteractionFrame.composeFrame.viewportFrames); - applyToViewportFrames(shellFrame.shellFrame.workspaceFrame.viewportFrames); -} - -} // namespace XCEngine::UI::Editor::App::Internal diff --git a/new_editor/app/Composition/EditorWindowWorkspaceStore.cpp b/new_editor/app/Composition/EditorWindowWorkspaceStore.cpp index d2a0ec83..03175f54 100644 --- a/new_editor/app/Composition/EditorWindowWorkspaceStore.cpp +++ b/new_editor/app/Composition/EditorWindowWorkspaceStore.cpp @@ -1,9 +1,9 @@ -#include "Composition/EditorWindowWorkspaceStore.h" +#include "Composition/EditorWindowWorkspaceStore.h" #include #include -namespace XCEngine::UI::Editor::App::Internal { +namespace XCEngine::UI::Editor::App { namespace { @@ -166,4 +166,5 @@ void EditorWindowWorkspaceStore::RemoveWindow(std::string_view windowId, bool pr } } -} // namespace XCEngine::UI::Editor::App::Internal +} // namespace XCEngine::UI::Editor::App + diff --git a/new_editor/app/Composition/EditorWindowWorkspaceStore.h b/new_editor/app/Composition/EditorWindowWorkspaceStore.h index 32120a87..adc1ff65 100644 --- a/new_editor/app/Composition/EditorWindowWorkspaceStore.h +++ b/new_editor/app/Composition/EditorWindowWorkspaceStore.h @@ -6,7 +6,7 @@ #include #include -namespace XCEngine::UI::Editor::App::Internal { +namespace XCEngine::UI::Editor::App { class EditorWindowWorkspaceStore final { public: @@ -50,4 +50,4 @@ private: UIEditorWindowWorkspaceSet m_windowSet = {}; }; -} // namespace XCEngine::UI::Editor::App::Internal +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/ColorPicker/ColorPickerPanel.cpp b/new_editor/app/Features/ColorPicker/ColorPickerPanel.cpp new file mode 100644 index 00000000..0898c6ac --- /dev/null +++ b/new_editor/app/Features/ColorPicker/ColorPickerPanel.cpp @@ -0,0 +1,223 @@ +#include "ColorPickerPanel.h" + +#include "Composition/EditorContext.h" +#include "Composition/EditorPanelIds.h" +#include "State/EditorColorPickerToolState.h" + +#include +#include + +#include +#include + +namespace XCEngine::UI::Editor::App { + +namespace { + +using ::XCEngine::UI::UIColor; +using ::XCEngine::UI::UIDrawList; +using ::XCEngine::UI::UIInputEvent; +using ::XCEngine::UI::UIPoint; +using ::XCEngine::UI::UIRect; + +constexpr float kPanelPadding = 8.0f; +constexpr float kPlaceholderFontSize = 11.0f; +constexpr UIColor kSurfaceColor(0.10f, 0.10f, 0.10f, 1.0f); +constexpr UIColor kPlaceholderColor(0.62f, 0.62f, 0.62f, 1.0f); + +float ResolveTextTop(float rectY, float rectHeight, float fontSize) { + const float lineHeight = fontSize * 1.6f; + return rectY + std::floor((rectHeight - lineHeight) * 0.5f); +} + +Widgets::UIEditorColorFieldMetrics BuildColorPickerPanelMetrics( + const UIRect& bounds) { + Widgets::UIEditorColorFieldMetrics metrics = + BuildUIEditorPropertyGridColorFieldMetrics( + GetUIEditorFixedPropertyGridMetrics()); + metrics.rowHeight = 0.0f; + metrics.horizontalPadding = 0.0f; + metrics.labelControlGap = 0.0f; + metrics.controlColumnStart = 0.0f; + metrics.controlTrailingInset = 0.0f; + metrics.swatchWidth = 0.0f; + metrics.swatchInsetY = 0.0f; + metrics.popupHeaderHeight = 0.0f; + metrics.popupCloseButtonSize = 0.0f; + metrics.popupPadding = 12.0f; + metrics.popupGapY = 8.0f; + metrics.popupFieldInset = 8.0f; + metrics.popupWidth = (std::max)( + 292.0f, + bounds.width - kPanelPadding * 2.0f); + return metrics; +} + +Widgets::UIEditorColorFieldPalette BuildColorPickerPanelPalette() { + return BuildUIEditorPropertyGridColorFieldPalette( + GetUIEditorFixedPropertyGridPalette()); +} + +UIRect BuildColorPickerEditorRect( + const UIRect& bounds, + const Widgets::UIEditorColorFieldMetrics&) { + return UIRect( + bounds.x + kPanelPadding, + bounds.y + kPanelPadding, + (std::max)(0.0f, bounds.width - kPanelPadding * 2.0f), + (std::max)(0.0f, bounds.height - kPanelPadding * 2.0f)); +} + +} // namespace + +const UIEditorPanelContentHostPanelState* ColorPickerPanel::FindMountedColorPickerPanel( + const UIEditorPanelContentHostFrame& contentHostFrame) const { + for (const UIEditorPanelContentHostPanelState& panelState : contentHostFrame.panelStates) { + if (panelState.panelId == kColorPickerPanelId && panelState.mounted) { + return &panelState; + } + } + + return nullptr; +} + +void ColorPickerPanel::ResetPanelState() { + m_visible = false; + m_hasActiveTarget = false; + m_bounds = {}; + m_targetSubjectKey.clear(); + m_targetFieldId.clear(); + m_spec = {}; + m_metrics = {}; + m_palette = {}; + m_frame = {}; + m_interactionState = {}; +} + +void ColorPickerPanel::ResetInteractionState() { + m_interactionState = {}; + m_frame = {}; +} + +void ColorPickerPanel::Update( + EditorContext& context, + const UIEditorPanelContentHostFrame& contentHostFrame, + const std::vector& inputEvents, + const PanelInputContext& inputContext) { + const UIEditorPanelContentHostPanelState* panelState = + FindMountedColorPickerPanel(contentHostFrame); + if (panelState == nullptr) { + ResetPanelState(); + return; + } + + m_visible = true; + m_bounds = panelState->bounds; + m_metrics = BuildColorPickerPanelMetrics(m_bounds); + m_palette = BuildColorPickerPanelPalette(); + + EditorColorPickerToolState& toolState = context.GetColorPickerToolState(); + m_hasActiveTarget = + toolState.active && + !toolState.inspectorTarget.subjectKey.empty() && + !toolState.inspectorTarget.fieldId.empty(); + if (!m_hasActiveTarget) { + m_targetSubjectKey.clear(); + m_targetFieldId.clear(); + m_spec = {}; + m_frame = {}; + m_interactionState = {}; + return; + } + + const bool targetChanged = + m_targetSubjectKey != toolState.inspectorTarget.subjectKey || + m_targetFieldId != toolState.inspectorTarget.fieldId; + if (targetChanged) { + ResetInteractionState(); + m_targetSubjectKey = toolState.inspectorTarget.subjectKey; + m_targetFieldId = toolState.inspectorTarget.fieldId; + } + + m_spec = {}; + m_spec.fieldId = m_targetFieldId; + m_spec.value = toolState.color; + m_spec.showAlpha = toolState.showAlpha; + m_spec.readOnly = false; + m_spec.embeddedEditor = true; + + const std::vector filteredEvents = + BuildUIEditorPanelInputEvents( + m_bounds, + inputEvents, + UIEditorPanelInputFilterOptions{ + .allowPointerInBounds = inputContext.allowInteraction, + .allowPointerWhileCaptured = false, + .allowKeyboardInput = inputContext.hasInputFocus, + .allowFocusEvents = + inputContext.hasInputFocus || + inputContext.focusGained || + inputContext.focusLost, + .includePointerLeave = + inputContext.allowInteraction || inputContext.hasInputFocus + }, + inputContext.focusGained, + inputContext.focusLost); + + const UIRect editorRect = BuildColorPickerEditorRect(m_bounds, m_metrics); + m_frame = UpdateUIEditorColorFieldInteraction( + m_interactionState, + m_spec, + editorRect, + filteredEvents, + m_metrics, + m_bounds); + + if (m_frame.layout.popupRect.width <= 0.0f || + m_frame.layout.popupRect.height <= 0.0f) { + m_frame.layout = Widgets::BuildUIEditorColorFieldLayout( + editorRect, + m_spec, + m_metrics, + m_bounds); + } + + UpdateEditorColorPickerToolColor(toolState, m_spec.value); +} + +void ColorPickerPanel::Append(UIDrawList& drawList) const { + if (!m_visible || m_bounds.width <= 0.0f || m_bounds.height <= 0.0f) { + return; + } + + drawList.AddFilledRect(m_bounds, kSurfaceColor); + if (!m_hasActiveTarget) { + drawList.AddText( + UIPoint( + m_bounds.x + kPanelPadding, + ResolveTextTop(m_bounds.y + kPanelPadding, 18.0f, kPlaceholderFontSize)), + "No color field selected.", + kPlaceholderColor, + kPlaceholderFontSize); + return; + } + + drawList.PushClipRect(m_bounds); + Widgets::AppendUIEditorColorFieldBackground( + drawList, + m_frame.layout, + m_spec, + m_interactionState.colorFieldState, + m_palette, + m_metrics); + Widgets::AppendUIEditorColorFieldForeground( + drawList, + m_frame.layout, + m_spec, + m_interactionState.colorFieldState, + m_palette, + m_metrics); + drawList.PopClipRect(); +} + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/ColorPicker/ColorPickerPanel.h b/new_editor/app/Features/ColorPicker/ColorPickerPanel.h new file mode 100644 index 00000000..1f5229e4 --- /dev/null +++ b/new_editor/app/Features/ColorPicker/ColorPickerPanel.h @@ -0,0 +1,44 @@ +#pragma once + +#include "Features/PanelInputContext.h" + +#include +#include + +#include + +#include +#include + +namespace XCEngine::UI::Editor::App { + +class EditorContext; + +class ColorPickerPanel { +public: + void ResetInteractionState(); + void Update( + EditorContext& context, + const UIEditorPanelContentHostFrame& contentHostFrame, + const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents, + const PanelInputContext& inputContext); + void Append(::XCEngine::UI::UIDrawList& drawList) const; + +private: + const UIEditorPanelContentHostPanelState* FindMountedColorPickerPanel( + const UIEditorPanelContentHostFrame& contentHostFrame) const; + void ResetPanelState(); + + bool m_visible = false; + bool m_hasActiveTarget = false; + ::XCEngine::UI::UIRect m_bounds = {}; + std::string m_targetSubjectKey = {}; + std::string m_targetFieldId = {}; + Widgets::UIEditorColorFieldSpec m_spec = {}; + Widgets::UIEditorColorFieldMetrics m_metrics = {}; + Widgets::UIEditorColorFieldPalette m_palette = {}; + UIEditorColorFieldInteractionState m_interactionState = {}; + UIEditorColorFieldInteractionFrame m_frame = {}; +}; + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/Hierarchy/HierarchyPanel.cpp b/new_editor/app/Features/Hierarchy/HierarchyPanel.cpp index 6f84ad73..57a395f3 100644 --- a/new_editor/app/Features/Hierarchy/HierarchyPanel.cpp +++ b/new_editor/app/Features/Hierarchy/HierarchyPanel.cpp @@ -1,17 +1,37 @@ -#include "HierarchyPanelInternal.h" - +#include "HierarchyPanel.h" +#include "Rendering/Assets/BuiltInIcons.h" +#include "Composition/EditorPanelIds.h" +#include #include "Scene/EditorSceneRuntime.h" #include "State/EditorCommandFocusService.h" - #include #include #include - #include namespace XCEngine::UI::Editor::App { -using namespace HierarchyPanelInternal; +using ::XCEngine::UI::UIColor; + +inline constexpr float kDragThreshold = 4.0f; +inline constexpr UIColor kDragPreviewColor(0.92f, 0.92f, 0.92f, 0.42f); + +::XCEngine::UI::UITextureHandle ResolveGameObjectIcon(const BuiltInIcons* icons); + +} // namespace XCEngine::UI::Editor::App + +namespace XCEngine::UI::Editor::App { + +::XCEngine::UI::UITextureHandle ResolveGameObjectIcon(const BuiltInIcons* icons) { + return icons != nullptr + ? icons->Resolve(BuiltInIconKind::GameObject) + : ::XCEngine::UI::UITextureHandle {}; +} + +} // namespace XCEngine::UI::Editor::App + +namespace XCEngine::UI::Editor::App { + namespace TreeDrag = XCEngine::UI::Editor::Collections::TreeDragDrop; namespace { @@ -106,17 +126,10 @@ void HierarchyPanel::SyncModelFromScene() { : nullptr); if (!m_model.HasSameTree(sceneModel) || m_treeItems.empty()) { m_model = sceneModel; - if (m_sceneRuntime != nullptr && !m_model.Empty()) { - m_sceneRuntime->EnsureSceneSelection(); - } RebuildItems(); return; } - if (m_sceneRuntime != nullptr && !m_model.Empty()) { - m_sceneRuntime->EnsureSceneSelection(); - } - SyncTreeSelectionFromSceneRuntime(); } @@ -154,10 +167,6 @@ void HierarchyPanel::SyncSceneRuntimeSelectionFromTree() { m_sceneRuntime->ClearSelection(); } - if (!m_model.Empty()) { - m_sceneRuntime->EnsureSceneSelection(); - } - SyncTreeSelectionFromSceneRuntime(); } diff --git a/new_editor/app/Features/Hierarchy/HierarchyPanelInternal.cpp b/new_editor/app/Features/Hierarchy/HierarchyPanelInternal.cpp deleted file mode 100644 index 0049b971..00000000 --- a/new_editor/app/Features/Hierarchy/HierarchyPanelInternal.cpp +++ /dev/null @@ -1,11 +0,0 @@ -#include "HierarchyPanelInternal.h" - -namespace XCEngine::UI::Editor::App::HierarchyPanelInternal { - -::XCEngine::UI::UITextureHandle ResolveGameObjectIcon(const BuiltInIcons* icons) { - return icons != nullptr - ? icons->Resolve(BuiltInIconKind::GameObject) - : ::XCEngine::UI::UITextureHandle {}; -} - -} // namespace XCEngine::UI::Editor::App::HierarchyPanelInternal diff --git a/new_editor/app/Features/Hierarchy/HierarchyPanelInternal.h b/new_editor/app/Features/Hierarchy/HierarchyPanelInternal.h deleted file mode 100644 index 8a42684e..00000000 --- a/new_editor/app/Features/Hierarchy/HierarchyPanelInternal.h +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include "HierarchyPanel.h" - -#include "Rendering/Assets/BuiltInIcons.h" - -#include "Composition/EditorPanelIds.h" -#include - -namespace XCEngine::UI::Editor::App::HierarchyPanelInternal { - -using ::XCEngine::UI::UIColor; - -inline constexpr float kDragThreshold = 4.0f; -inline constexpr UIColor kDragPreviewColor(0.92f, 0.92f, 0.92f, 0.42f); - -::XCEngine::UI::UITextureHandle ResolveGameObjectIcon(const BuiltInIcons* icons); - -} // namespace XCEngine::UI::Editor::App::HierarchyPanelInternal diff --git a/new_editor/app/Features/Inspector/Components/AudioListenerInspectorComponentEditor.h b/new_editor/app/Features/Inspector/Components/AudioListenerInspectorComponentEditor.h index 11235c7c..57e8f1d8 100644 --- a/new_editor/app/Features/Inspector/Components/AudioListenerInspectorComponentEditor.h +++ b/new_editor/app/Features/Inspector/Components/AudioListenerInspectorComponentEditor.h @@ -1,13 +1,12 @@ #pragma once -#include "Features/Inspector/Components/IInspectorComponentEditor.h" -#include "Features/Inspector/Components/InspectorComponentEditorUtils.h" +#include "Features/Inspector/Components/InspectorBindingComponentEditor.h" #include namespace XCEngine::UI::Editor::App { -class AudioListenerInspectorComponentEditor final : public IInspectorComponentEditor { +class AudioListenerInspectorComponentEditor final : public InspectorBindingComponentEditor { public: std::string_view GetComponentTypeName() const override { return "AudioListener"; @@ -17,143 +16,115 @@ public: return "Audio Listener"; } - void BuildSections( +protected: + void BuildBindingSections( const InspectorComponentEditorContext& context, - std::vector& outSections) const override { + std::vector& outSections) const override { const auto* listener = - dynamic_cast(context.component); + ResolveInspectorComponent<::XCEngine::Components::AudioListenerComponent>(context); if (listener == nullptr) { return; } - Widgets::UIEditorPropertyGridSection section = {}; - section.sectionId = BuildInspectorComponentSectionId(context.componentId); + InspectorSectionBinding section = {}; section.title = std::string(GetDisplayName()); - section.fields.push_back(BuildInspectorNumberField( - BuildInspectorComponentFieldId(context.componentId, "master_volume"), + section.fields.push_back(BuildInspectorNumberFieldBinding( + "master_volume", "Master Volume", - listener->GetMasterVolume(), - 0.05, - 0.0, - 1.0)); - section.fields.push_back(BuildInspectorBoolField( - BuildInspectorComponentFieldId(context.componentId, "mute"), + InspectorNumberFieldBindingOptions{ + .step = 0.05, + .minValue = 0.0, + .maxValue = 1.0 + }, + [listener]() { return listener->GetMasterVolume(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + double value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::AudioListenerComponent>( + sceneRuntime, + bindingContext, + [value](::XCEngine::Components::AudioListenerComponent& typedListener) { + typedListener.SetMasterVolume(static_cast(value)); + }); + })); + section.fields.push_back(BuildInspectorBoolFieldBinding( + "mute", "Mute", - listener->IsMute())); - section.fields.push_back(BuildInspectorNumberField( - BuildInspectorComponentFieldId(context.componentId, "doppler_level"), + [listener]() { return listener->IsMute(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + bool value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::AudioListenerComponent>( + sceneRuntime, + bindingContext, + [value](::XCEngine::Components::AudioListenerComponent& typedListener) { + typedListener.SetMute(value); + }); + })); + section.fields.push_back(BuildInspectorNumberFieldBinding( + "doppler_level", "Doppler Level", - listener->GetDopplerLevel(), - 0.1, - 0.0, - 5.0)); - section.fields.push_back(BuildInspectorNumberField( - BuildInspectorComponentFieldId(context.componentId, "speed_of_sound"), + InspectorNumberFieldBindingOptions{ + .step = 0.1, + .minValue = 0.0, + .maxValue = 5.0 + }, + [listener]() { return listener->GetDopplerLevel(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + double value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::AudioListenerComponent>( + sceneRuntime, + bindingContext, + [value](::XCEngine::Components::AudioListenerComponent& typedListener) { + typedListener.SetDopplerLevel(static_cast(value)); + }); + })); + section.fields.push_back(BuildInspectorNumberFieldBinding( + "speed_of_sound", "Speed Of Sound", - listener->GetSpeedOfSound(), - 1.0, - 1.0, - 100000.0)); - section.fields.push_back(BuildInspectorNumberField( - BuildInspectorComponentFieldId(context.componentId, "reverb_level"), + InspectorNumberFieldBindingOptions{ + .step = 1.0, + .minValue = 1.0, + .maxValue = 100000.0 + }, + [listener]() { return listener->GetSpeedOfSound(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + double value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::AudioListenerComponent>( + sceneRuntime, + bindingContext, + [value](::XCEngine::Components::AudioListenerComponent& typedListener) { + typedListener.SetSpeedOfSound(static_cast(value)); + }); + })); + section.fields.push_back(BuildInspectorNumberFieldBinding( + "reverb_level", "Reverb Level", - listener->GetReverbLevel(), - 0.05, - 0.0, - 1.0)); + InspectorNumberFieldBindingOptions{ + .step = 0.05, + .minValue = 0.0, + .maxValue = 1.0 + }, + [listener]() { return listener->GetReverbLevel(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + double value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::AudioListenerComponent>( + sceneRuntime, + bindingContext, + [value](::XCEngine::Components::AudioListenerComponent& typedListener) { + typedListener.SetReverbLevel(static_cast(value)); + }); + })); outSections.push_back(std::move(section)); } - - bool ApplyFieldValue( - EditorSceneRuntime& sceneRuntime, - const InspectorComponentEditorContext& context, - const Widgets::UIEditorPropertyGridField& field) const override { - if (field.kind == Widgets::UIEditorPropertyGridFieldKind::Bool && - field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "mute")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* listener = - dynamic_cast<::XCEngine::Components::AudioListenerComponent*>(&component); - if (listener == nullptr) { - return false; - } - listener->SetMute(field.boolValue); - return true; - }); - } - - if (field.kind != Widgets::UIEditorPropertyGridFieldKind::Number) { - return false; - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "master_volume")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* listener = - dynamic_cast<::XCEngine::Components::AudioListenerComponent*>(&component); - if (listener == nullptr) { - return false; - } - listener->SetMasterVolume( - static_cast(field.numberValue.value)); - return true; - }); - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "doppler_level")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* listener = - dynamic_cast<::XCEngine::Components::AudioListenerComponent*>(&component); - if (listener == nullptr) { - return false; - } - listener->SetDopplerLevel( - static_cast(field.numberValue.value)); - return true; - }); - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "speed_of_sound")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* listener = - dynamic_cast<::XCEngine::Components::AudioListenerComponent*>(&component); - if (listener == nullptr) { - return false; - } - listener->SetSpeedOfSound( - static_cast(field.numberValue.value)); - return true; - }); - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "reverb_level")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* listener = - dynamic_cast<::XCEngine::Components::AudioListenerComponent*>(&component); - if (listener == nullptr) { - return false; - } - listener->SetReverbLevel( - static_cast(field.numberValue.value)); - return true; - }); - } - - return false; - } }; } // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/Inspector/Components/AudioSourceInspectorComponentEditor.h b/new_editor/app/Features/Inspector/Components/AudioSourceInspectorComponentEditor.h index dc8201ee..7a1eb202 100644 --- a/new_editor/app/Features/Inspector/Components/AudioSourceInspectorComponentEditor.h +++ b/new_editor/app/Features/Inspector/Components/AudioSourceInspectorComponentEditor.h @@ -1,14 +1,13 @@ #pragma once -#include "Features/Inspector/Components/IInspectorComponentEditor.h" -#include "Features/Inspector/Components/InspectorComponentEditorUtils.h" +#include "Features/Inspector/Components/InspectorBindingComponentEditor.h" #include #include namespace XCEngine::UI::Editor::App { -class AudioSourceInspectorComponentEditor final : public IInspectorComponentEditor { +class AudioSourceInspectorComponentEditor final : public InspectorBindingComponentEditor { public: std::string_view GetComponentTypeName() const override { return "AudioSource"; @@ -18,336 +17,324 @@ public: return "Audio Source"; } - void BuildSections( +protected: + void BuildBindingSections( const InspectorComponentEditorContext& context, - std::vector& outSections) const override { + std::vector& outSections) const override { const auto* source = - dynamic_cast(context.component); + ResolveInspectorComponent<::XCEngine::Components::AudioSourceComponent>(context); if (source == nullptr) { return; } - Widgets::UIEditorPropertyGridSection section = {}; - section.sectionId = BuildInspectorComponentSectionId(context.componentId); - section.title = std::string(GetDisplayName()); + const auto mutate3DParams = + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + const std::function& mutator) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::AudioSourceComponent>( + sceneRuntime, + bindingContext, + [&mutator](::XCEngine::Components::AudioSourceComponent& typedSource) { + ::XCEngine::Audio::Audio3DParams params = typedSource.Get3DParams(); + mutator(params); + typedSource.Set3DParams(params); + }); + }; - section.fields.push_back(BuildInspectorAssetField( - BuildInspectorComponentFieldId(context.componentId, "clip"), + InspectorSectionBinding section = {}; + section.title = std::string(GetDisplayName()); + section.fields.push_back(BuildInspectorAssetFieldBinding( + "clip", "Clip", - source->GetClipPath(), - "None")); - section.fields.push_back(BuildInspectorNumberField( - BuildInspectorComponentFieldId(context.componentId, "volume"), + InspectorAssetFieldBindingOptions{ .emptyText = "None" }, + [source]() { return source->GetClipPath(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + std::string_view value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::AudioSourceComponent>( + sceneRuntime, + bindingContext, + [assetId = std::string(value)]( + ::XCEngine::Components::AudioSourceComponent& typedSource) { + if (assetId.empty()) { + typedSource.ClearClip(); + } else { + typedSource.SetClipPath(assetId); + } + }); + })); + section.fields.push_back(BuildInspectorNumberFieldBinding( + "volume", "Volume", - source->GetVolume(), - 0.05, - 0.0, - 1.0)); - section.fields.push_back(BuildInspectorNumberField( - BuildInspectorComponentFieldId(context.componentId, "pitch"), + InspectorNumberFieldBindingOptions{ + .step = 0.05, + .minValue = 0.0, + .maxValue = 1.0 + }, + [source]() { return source->GetVolume(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + double value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::AudioSourceComponent>( + sceneRuntime, + bindingContext, + [value](::XCEngine::Components::AudioSourceComponent& typedSource) { + typedSource.SetVolume(static_cast(value)); + }); + })); + section.fields.push_back(BuildInspectorNumberFieldBinding( + "pitch", "Pitch", - source->GetPitch(), - 0.05, - 0.0, - 3.0)); - section.fields.push_back(BuildInspectorNumberField( - BuildInspectorComponentFieldId(context.componentId, "pan"), + InspectorNumberFieldBindingOptions{ + .step = 0.05, + .minValue = 0.0, + .maxValue = 3.0 + }, + [source]() { return source->GetPitch(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + double value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::AudioSourceComponent>( + sceneRuntime, + bindingContext, + [value](::XCEngine::Components::AudioSourceComponent& typedSource) { + typedSource.SetPitch(static_cast(value)); + }); + })); + section.fields.push_back(BuildInspectorNumberFieldBinding( + "pan", "Pan", - source->GetPan(), - 0.05, - -1.0, - 1.0)); - section.fields.push_back(BuildInspectorBoolField( - BuildInspectorComponentFieldId(context.componentId, "looping"), + InspectorNumberFieldBindingOptions{ + .step = 0.05, + .minValue = -1.0, + .maxValue = 1.0 + }, + [source]() { return source->GetPan(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + double value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::AudioSourceComponent>( + sceneRuntime, + bindingContext, + [value](::XCEngine::Components::AudioSourceComponent& typedSource) { + typedSource.SetPan(static_cast(value)); + }); + })); + section.fields.push_back(BuildInspectorBoolFieldBinding( + "looping", "Looping", - source->IsLooping())); - section.fields.push_back(BuildInspectorBoolField( - BuildInspectorComponentFieldId(context.componentId, "spatialize"), + [source]() { return source->IsLooping(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + bool value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::AudioSourceComponent>( + sceneRuntime, + bindingContext, + [value](::XCEngine::Components::AudioSourceComponent& typedSource) { + typedSource.SetLooping(value); + }); + })); + section.fields.push_back(BuildInspectorBoolFieldBinding( + "spatialize", "Spatialize", - source->IsSpatialize())); + [source]() { return source->IsSpatialize(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + bool value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::AudioSourceComponent>( + sceneRuntime, + bindingContext, + [value](::XCEngine::Components::AudioSourceComponent& typedSource) { + typedSource.SetSpatialize(value); + }); + })); if (source->IsSpatialize()) { - const ::XCEngine::Audio::Audio3DParams params = source->Get3DParams(); - section.fields.push_back(BuildInspectorNumberField( - BuildInspectorComponentFieldId(context.componentId, "min_distance"), + section.fields.push_back(BuildInspectorNumberFieldBinding( + "min_distance", "Min Distance", - params.minDistance, - 0.1, - 0.0, - 1000000.0)); - section.fields.push_back(BuildInspectorNumberField( - BuildInspectorComponentFieldId(context.componentId, "max_distance"), + InspectorNumberFieldBindingOptions{ + .step = 0.1, + .minValue = 0.0, + .maxValue = 1000000.0 + }, + [source]() { return source->Get3DParams().minDistance; }, + [mutate3DParams](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + double value) { + return mutate3DParams( + sceneRuntime, + bindingContext, + [value](::XCEngine::Audio::Audio3DParams& params) { + params.minDistance = static_cast(value); + }); + })); + section.fields.push_back(BuildInspectorNumberFieldBinding( + "max_distance", "Max Distance", - params.maxDistance, - 0.1, - 0.0, - 1000000.0)); - section.fields.push_back(BuildInspectorNumberField( - BuildInspectorComponentFieldId(context.componentId, "pan_level"), + InspectorNumberFieldBindingOptions{ + .step = 0.1, + .minValue = 0.0, + .maxValue = 1000000.0 + }, + [source]() { return source->Get3DParams().maxDistance; }, + [mutate3DParams](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + double value) { + return mutate3DParams( + sceneRuntime, + bindingContext, + [value](::XCEngine::Audio::Audio3DParams& params) { + params.maxDistance = static_cast(value); + }); + })); + section.fields.push_back(BuildInspectorNumberFieldBinding( + "pan_level", "Pan Level", - params.panLevel, - 0.05, - 0.0, - 1.0)); - section.fields.push_back(BuildInspectorNumberField( - BuildInspectorComponentFieldId(context.componentId, "spread"), + InspectorNumberFieldBindingOptions{ + .step = 0.05, + .minValue = 0.0, + .maxValue = 1.0 + }, + [source]() { return source->Get3DParams().panLevel; }, + [mutate3DParams](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + double value) { + return mutate3DParams( + sceneRuntime, + bindingContext, + [value](::XCEngine::Audio::Audio3DParams& params) { + params.panLevel = static_cast(value); + }); + })); + section.fields.push_back(BuildInspectorNumberFieldBinding( + "spread", "Spread", - params.spread, - 0.05, - 0.0, - 1.0)); - section.fields.push_back(BuildInspectorNumberField( - BuildInspectorComponentFieldId(context.componentId, "reverb_send"), + InspectorNumberFieldBindingOptions{ + .step = 0.05, + .minValue = 0.0, + .maxValue = 1.0 + }, + [source]() { return source->Get3DParams().spread; }, + [mutate3DParams](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + double value) { + return mutate3DParams( + sceneRuntime, + bindingContext, + [value](::XCEngine::Audio::Audio3DParams& params) { + params.spread = static_cast(value); + }); + })); + section.fields.push_back(BuildInspectorNumberFieldBinding( + "reverb_send", "Reverb Send", - params.reverbZoneMix, - 0.05, - 0.0, - 1.0)); - section.fields.push_back(BuildInspectorNumberField( - BuildInspectorComponentFieldId(context.componentId, "doppler_level"), + InspectorNumberFieldBindingOptions{ + .step = 0.05, + .minValue = 0.0, + .maxValue = 1.0 + }, + [source]() { return source->Get3DParams().reverbZoneMix; }, + [mutate3DParams](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + double value) { + return mutate3DParams( + sceneRuntime, + bindingContext, + [value](::XCEngine::Audio::Audio3DParams& params) { + params.reverbZoneMix = static_cast(value); + }); + })); + section.fields.push_back(BuildInspectorNumberFieldBinding( + "doppler_level", "Doppler Level", - params.dopplerLevel, - 0.1, - 0.0, - 10.0)); - section.fields.push_back(BuildInspectorBoolField( - BuildInspectorComponentFieldId(context.componentId, "use_hrtf"), + InspectorNumberFieldBindingOptions{ + .step = 0.1, + .minValue = 0.0, + .maxValue = 10.0 + }, + [source]() { return source->Get3DParams().dopplerLevel; }, + [mutate3DParams](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + double value) { + return mutate3DParams( + sceneRuntime, + bindingContext, + [value](::XCEngine::Audio::Audio3DParams& params) { + params.dopplerLevel = static_cast(value); + }); + })); + section.fields.push_back(BuildInspectorBoolFieldBinding( + "use_hrtf", "Use HRTF", - source->IsHRTFEnabled())); + [source]() { return source->IsHRTFEnabled(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + bool value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::AudioSourceComponent>( + sceneRuntime, + bindingContext, + [value](::XCEngine::Components::AudioSourceComponent& typedSource) { + typedSource.SetHRTFEnabled(value); + }); + })); if (source->IsHRTFEnabled()) { - section.fields.push_back(BuildInspectorNumberField( - BuildInspectorComponentFieldId(context.componentId, "hrtf_crossfeed"), + section.fields.push_back(BuildInspectorNumberFieldBinding( + "hrtf_crossfeed", "HRTF Crossfeed", - source->GetHRTFCrossFeed(), - 0.05, - 0.0, - 1.0)); - section.fields.push_back(BuildInspectorNumberField( - BuildInspectorComponentFieldId(context.componentId, "hrtf_quality"), + InspectorNumberFieldBindingOptions{ + .step = 0.05, + .minValue = 0.0, + .maxValue = 1.0 + }, + [source]() { return source->GetHRTFCrossFeed(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + double value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::AudioSourceComponent>( + sceneRuntime, + bindingContext, + [value](::XCEngine::Components::AudioSourceComponent& typedSource) { + typedSource.SetHRTFCrossFeed(static_cast(value)); + }); + })); + section.fields.push_back(BuildInspectorNumberFieldBinding( + "hrtf_quality", "HRTF Quality", - source->GetHRTFQuality(), - 1.0, - 1.0, - 3.0, - true)); + InspectorNumberFieldBindingOptions{ + .step = 1.0, + .minValue = 1.0, + .maxValue = 3.0, + .integerMode = true + }, + [source]() { return source->GetHRTFQuality(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + double value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::AudioSourceComponent>( + sceneRuntime, + bindingContext, + [value](::XCEngine::Components::AudioSourceComponent& typedSource) { + typedSource.SetHRTFQuality( + static_cast<::XCEngine::Audio::uint32>(value)); + }); + })); } } outSections.push_back(std::move(section)); } - - bool ApplyFieldValue( - EditorSceneRuntime& sceneRuntime, - const InspectorComponentEditorContext& context, - const Widgets::UIEditorPropertyGridField& field) const override { - auto mutate3DParams = - [&sceneRuntime, &context]( - const std::function& mutator) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&mutator](::XCEngine::Components::Component& component) { - auto* source = - dynamic_cast<::XCEngine::Components::AudioSourceComponent*>(&component); - if (source == nullptr) { - return false; - } - ::XCEngine::Audio::Audio3DParams params = source->Get3DParams(); - mutator(params); - source->Set3DParams(params); - return true; - }); - }; - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "clip") && - field.kind == Widgets::UIEditorPropertyGridFieldKind::Asset) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* source = - dynamic_cast<::XCEngine::Components::AudioSourceComponent*>(&component); - if (source == nullptr) { - return false; - } - - if (field.assetValue.assetId.empty()) { - source->ClearClip(); - } else { - source->SetClipPath(field.assetValue.assetId); - } - return true; - }); - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "looping")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* source = - dynamic_cast<::XCEngine::Components::AudioSourceComponent*>(&component); - if (source == nullptr || field.kind != Widgets::UIEditorPropertyGridFieldKind::Bool) { - return false; - } - source->SetLooping(field.boolValue); - return true; - }); - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "spatialize")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* source = - dynamic_cast<::XCEngine::Components::AudioSourceComponent*>(&component); - if (source == nullptr || field.kind != Widgets::UIEditorPropertyGridFieldKind::Bool) { - return false; - } - source->SetSpatialize(field.boolValue); - return true; - }); - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "use_hrtf")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* source = - dynamic_cast<::XCEngine::Components::AudioSourceComponent*>(&component); - if (source == nullptr || field.kind != Widgets::UIEditorPropertyGridFieldKind::Bool) { - return false; - } - source->SetHRTFEnabled(field.boolValue); - return true; - }); - } - - if (field.kind == Widgets::UIEditorPropertyGridFieldKind::Number) { - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "volume")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* source = - dynamic_cast<::XCEngine::Components::AudioSourceComponent*>(&component); - if (source == nullptr) { - return false; - } - source->SetVolume(static_cast(field.numberValue.value)); - return true; - }); - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "pitch")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* source = - dynamic_cast<::XCEngine::Components::AudioSourceComponent*>(&component); - if (source == nullptr) { - return false; - } - source->SetPitch(static_cast(field.numberValue.value)); - return true; - }); - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "pan")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* source = - dynamic_cast<::XCEngine::Components::AudioSourceComponent*>(&component); - if (source == nullptr) { - return false; - } - source->SetPan(static_cast(field.numberValue.value)); - return true; - }); - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "min_distance")) { - return mutate3DParams([&field](::XCEngine::Audio::Audio3DParams& params) { - params.minDistance = static_cast(field.numberValue.value); - }); - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "max_distance")) { - return mutate3DParams([&field](::XCEngine::Audio::Audio3DParams& params) { - params.maxDistance = static_cast(field.numberValue.value); - }); - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "pan_level")) { - return mutate3DParams([&field](::XCEngine::Audio::Audio3DParams& params) { - params.panLevel = static_cast(field.numberValue.value); - }); - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "spread")) { - return mutate3DParams([&field](::XCEngine::Audio::Audio3DParams& params) { - params.spread = static_cast(field.numberValue.value); - }); - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "reverb_send")) { - return mutate3DParams([&field](::XCEngine::Audio::Audio3DParams& params) { - params.reverbZoneMix = static_cast(field.numberValue.value); - }); - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "doppler_level")) { - return mutate3DParams([&field](::XCEngine::Audio::Audio3DParams& params) { - params.dopplerLevel = static_cast(field.numberValue.value); - }); - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "hrtf_crossfeed")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* source = - dynamic_cast<::XCEngine::Components::AudioSourceComponent*>(&component); - if (source == nullptr) { - return false; - } - source->SetHRTFCrossFeed( - static_cast(field.numberValue.value)); - return true; - }); - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "hrtf_quality")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* source = - dynamic_cast<::XCEngine::Components::AudioSourceComponent*>(&component); - if (source == nullptr) { - return false; - } - source->SetHRTFQuality( - static_cast<::XCEngine::Audio::uint32>(field.numberValue.value)); - return true; - }); - } - } - - return false; - } }; } // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/Inspector/Components/BoxColliderInspectorComponentEditor.h b/new_editor/app/Features/Inspector/Components/BoxColliderInspectorComponentEditor.h index fd143abc..7605d6a6 100644 --- a/new_editor/app/Features/Inspector/Components/BoxColliderInspectorComponentEditor.h +++ b/new_editor/app/Features/Inspector/Components/BoxColliderInspectorComponentEditor.h @@ -6,7 +6,7 @@ namespace XCEngine::UI::Editor::App { -class BoxColliderInspectorComponentEditor final : public IInspectorComponentEditor { +class BoxColliderInspectorComponentEditor final : public InspectorBindingComponentEditor { public: std::string_view GetComponentTypeName() const override { return "BoxCollider"; @@ -16,53 +16,37 @@ public: return "Box Collider"; } - void BuildSections( +protected: + void BuildBindingSections( const InspectorComponentEditorContext& context, - std::vector& outSections) const override { + std::vector& outSections) const override { const auto* collider = - dynamic_cast(context.component); + ResolveInspectorComponent<::XCEngine::Components::BoxColliderComponent>(context); if (collider == nullptr) { return; } - Widgets::UIEditorPropertyGridSection section = {}; - section.sectionId = BuildInspectorComponentSectionId(context.componentId); + InspectorSectionBinding section = {}; section.title = std::string(GetDisplayName()); - AppendColliderBaseFields(context.componentId, *collider, section.fields); - section.fields.push_back(BuildInspectorVector3Field( - BuildInspectorComponentFieldId(context.componentId, "size"), + AppendColliderBaseBindings(section.fields, *collider); + section.fields.push_back(BuildInspectorVector3FieldBinding( + "size", "Size", - collider->GetSize(), - 0.1)); + InspectorVector3FieldBindingOptions{ .step = 0.1 }, + [collider]() { return collider->GetSize(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + const ::XCEngine::Math::Vector3& value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::BoxColliderComponent>( + sceneRuntime, + bindingContext, + [value](::XCEngine::Components::BoxColliderComponent& typedCollider) { + typedCollider.SetSize(value); + }); + })); outSections.push_back(std::move(section)); } - - bool ApplyFieldValue( - EditorSceneRuntime& sceneRuntime, - const InspectorComponentEditorContext& context, - const Widgets::UIEditorPropertyGridField& field) const override { - if (ApplyColliderBaseFieldValue(sceneRuntime, context, field)) { - return true; - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "size")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* collider = - dynamic_cast<::XCEngine::Components::BoxColliderComponent*>(&component); - if (collider == nullptr || - field.kind != Widgets::UIEditorPropertyGridFieldKind::Vector3) { - return false; - } - collider->SetSize(ToMathVector3(field)); - return true; - }); - } - - return false; - } }; } // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/Inspector/Components/CameraInspectorComponentEditor.h b/new_editor/app/Features/Inspector/Components/CameraInspectorComponentEditor.h index a4c4c310..7763d9f8 100644 --- a/new_editor/app/Features/Inspector/Components/CameraInspectorComponentEditor.h +++ b/new_editor/app/Features/Inspector/Components/CameraInspectorComponentEditor.h @@ -1,13 +1,12 @@ #pragma once -#include "Features/Inspector/Components/IInspectorComponentEditor.h" -#include "Features/Inspector/Components/InspectorComponentEditorUtils.h" +#include "Features/Inspector/Components/InspectorBindingComponentEditor.h" #include namespace XCEngine::UI::Editor::App { -class CameraInspectorComponentEditor final : public IInspectorComponentEditor { +class CameraInspectorComponentEditor final : public InspectorBindingComponentEditor { public: std::string_view GetComponentTypeName() const override { return "Camera"; @@ -17,313 +16,258 @@ public: return "Camera"; } - void BuildSections( +protected: + void BuildBindingSections( const InspectorComponentEditorContext& context, - std::vector& outSections) const override { + std::vector& outSections) const override { const auto* camera = - dynamic_cast(context.component); + ResolveInspectorComponent<::XCEngine::Components::CameraComponent>(context); if (camera == nullptr) { return; } - Widgets::UIEditorPropertyGridSection section = {}; - section.sectionId = BuildInspectorComponentSectionId(context.componentId); + InspectorSectionBinding section = {}; section.title = std::string(GetDisplayName()); - section.fields.push_back(BuildInspectorEnumField( - BuildInspectorComponentFieldId(context.componentId, "projection"), + section.fields.push_back(BuildInspectorEnumFieldBinding( + "projection", "Projection", { "Perspective", "Orthographic" }, - static_cast(camera->GetProjectionType()))); + [camera]() { + return static_cast(camera->GetProjectionType()); + }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + std::size_t value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::CameraComponent>( + sceneRuntime, + bindingContext, + [value](::XCEngine::Components::CameraComponent& typedCamera) { + typedCamera.SetProjectionType( + static_cast<::XCEngine::Components::CameraProjectionType>(value)); + }); + })); if (camera->GetProjectionType() == ::XCEngine::Components::CameraProjectionType::Perspective) { - section.fields.push_back(BuildInspectorNumberField( - BuildInspectorComponentFieldId(context.componentId, "field_of_view"), + section.fields.push_back(BuildInspectorNumberFieldBinding( + "field_of_view", "Field Of View", - camera->GetFieldOfView(), - 0.5, - 1.0, - 179.0)); + InspectorNumberFieldBindingOptions{ + .step = 0.5, + .minValue = 1.0, + .maxValue = 179.0 + }, + [camera]() { return camera->GetFieldOfView(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + double value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::CameraComponent>( + sceneRuntime, + bindingContext, + [value](::XCEngine::Components::CameraComponent& typedCamera) { + typedCamera.SetFieldOfView(static_cast(value)); + }); + })); } else { - section.fields.push_back(BuildInspectorNumberField( - BuildInspectorComponentFieldId(context.componentId, "orthographic_size"), + section.fields.push_back(BuildInspectorNumberFieldBinding( + "orthographic_size", "Orthographic Size", - camera->GetOrthographicSize(), - 0.1, - 0.001, - 1000000.0)); + InspectorNumberFieldBindingOptions{ + .step = 0.1, + .minValue = 0.001, + .maxValue = 1000000.0 + }, + [camera]() { return camera->GetOrthographicSize(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + double value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::CameraComponent>( + sceneRuntime, + bindingContext, + [value](::XCEngine::Components::CameraComponent& typedCamera) { + typedCamera.SetOrthographicSize(static_cast(value)); + }); + })); } - section.fields.push_back(BuildInspectorNumberField( - BuildInspectorComponentFieldId(context.componentId, "near_clip"), + section.fields.push_back(BuildInspectorNumberFieldBinding( + "near_clip", "Near Clip", - camera->GetNearClipPlane(), - 0.01, - 0.001, - 1000000.0)); - section.fields.push_back(BuildInspectorNumberField( - BuildInspectorComponentFieldId(context.componentId, "far_clip"), + InspectorNumberFieldBindingOptions{ + .step = 0.01, + .minValue = 0.001, + .maxValue = 1000000.0 + }, + [camera]() { return camera->GetNearClipPlane(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + double value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::CameraComponent>( + sceneRuntime, + bindingContext, + [value](::XCEngine::Components::CameraComponent& typedCamera) { + typedCamera.SetNearClipPlane(static_cast(value)); + }); + })); + section.fields.push_back(BuildInspectorNumberFieldBinding( + "far_clip", "Far Clip", - camera->GetFarClipPlane(), - 0.1, - 0.001, - 1000000.0)); - section.fields.push_back(BuildInspectorNumberField( - BuildInspectorComponentFieldId(context.componentId, "depth"), + InspectorNumberFieldBindingOptions{ + .step = 0.1, + .minValue = 0.001, + .maxValue = 1000000.0 + }, + [camera]() { return camera->GetFarClipPlane(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + double value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::CameraComponent>( + sceneRuntime, + bindingContext, + [value](::XCEngine::Components::CameraComponent& typedCamera) { + typedCamera.SetFarClipPlane(static_cast(value)); + }); + })); + section.fields.push_back(BuildInspectorNumberFieldBinding( + "depth", "Depth", - camera->GetDepth(), - 0.1, - -1000000.0, - 1000000.0)); - section.fields.push_back(BuildInspectorBoolField( - BuildInspectorComponentFieldId(context.componentId, "primary"), + InspectorNumberFieldBindingOptions{ + .step = 0.1, + .minValue = -1000000.0, + .maxValue = 1000000.0 + }, + [camera]() { return camera->GetDepth(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + double value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::CameraComponent>( + sceneRuntime, + bindingContext, + [value](::XCEngine::Components::CameraComponent& typedCamera) { + typedCamera.SetDepth(static_cast(value)); + }); + })); + section.fields.push_back(BuildInspectorBoolFieldBinding( + "primary", "Primary", - camera->IsPrimary())); - section.fields.push_back(BuildInspectorBoolField( - BuildInspectorComponentFieldId(context.componentId, "skybox"), + [camera]() { return camera->IsPrimary(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + bool value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::CameraComponent>( + sceneRuntime, + bindingContext, + [value](::XCEngine::Components::CameraComponent& typedCamera) { + typedCamera.SetPrimary(value); + }); + })); + section.fields.push_back(BuildInspectorBoolFieldBinding( + "skybox", "Skybox", - camera->IsSkyboxEnabled())); + [camera]() { return camera->IsSkyboxEnabled(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + bool value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::CameraComponent>( + sceneRuntime, + bindingContext, + [value](::XCEngine::Components::CameraComponent& typedCamera) { + typedCamera.SetSkyboxEnabled(value); + }); + })); if (camera->IsSkyboxEnabled()) { - section.fields.push_back(BuildInspectorAssetField( - BuildInspectorComponentFieldId(context.componentId, "skybox_material"), + section.fields.push_back(BuildInspectorAssetFieldBinding( + "skybox_material", "Skybox Material", - camera->GetSkyboxMaterialPath(), - "None")); + InspectorAssetFieldBindingOptions{ .emptyText = "None" }, + [camera]() { return camera->GetSkyboxMaterialPath(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + std::string_view value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::CameraComponent>( + sceneRuntime, + bindingContext, + [assetId = std::string(value)]( + ::XCEngine::Components::CameraComponent& typedCamera) { + typedCamera.SetSkyboxMaterialPath(assetId); + }); + })); if (camera->GetSkyboxMaterialPath().empty()) { - section.fields.push_back(BuildInspectorColorField( - BuildInspectorComponentFieldId(context.componentId, "skybox_top"), + section.fields.push_back(BuildInspectorColorFieldBinding( + "skybox_top", "Skybox Top", - camera->GetSkyboxTopColor())); - section.fields.push_back(BuildInspectorColorField( - BuildInspectorComponentFieldId(context.componentId, "skybox_horizon"), + {}, + [camera]() { return camera->GetSkyboxTopColor(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + const ::XCEngine::Math::Color& value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::CameraComponent>( + sceneRuntime, + bindingContext, + [value](::XCEngine::Components::CameraComponent& typedCamera) { + typedCamera.SetSkyboxTopColor(value); + }); + })); + section.fields.push_back(BuildInspectorColorFieldBinding( + "skybox_horizon", "Skybox Horizon", - camera->GetSkyboxHorizonColor())); - section.fields.push_back(BuildInspectorColorField( - BuildInspectorComponentFieldId(context.componentId, "skybox_bottom"), + {}, + [camera]() { return camera->GetSkyboxHorizonColor(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + const ::XCEngine::Math::Color& value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::CameraComponent>( + sceneRuntime, + bindingContext, + [value](::XCEngine::Components::CameraComponent& typedCamera) { + typedCamera.SetSkyboxHorizonColor(value); + }); + })); + section.fields.push_back(BuildInspectorColorFieldBinding( + "skybox_bottom", "Skybox Bottom", - camera->GetSkyboxBottomColor())); + {}, + [camera]() { return camera->GetSkyboxBottomColor(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + const ::XCEngine::Math::Color& value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::CameraComponent>( + sceneRuntime, + bindingContext, + [value](::XCEngine::Components::CameraComponent& typedCamera) { + typedCamera.SetSkyboxBottomColor(value); + }); + })); } } - section.fields.push_back(BuildInspectorColorField( - BuildInspectorComponentFieldId(context.componentId, "clear_color"), + section.fields.push_back(BuildInspectorColorFieldBinding( + "clear_color", "Clear Color", - camera->GetClearColor())); + {}, + [camera]() { return camera->GetClearColor(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + const ::XCEngine::Math::Color& value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::CameraComponent>( + sceneRuntime, + bindingContext, + [value](::XCEngine::Components::CameraComponent& typedCamera) { + typedCamera.SetClearColor(value); + }); + })); outSections.push_back(std::move(section)); } - - bool ApplyFieldValue( - EditorSceneRuntime& sceneRuntime, - const InspectorComponentEditorContext& context, - const Widgets::UIEditorPropertyGridField& field) const override { - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "projection")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* camera = - dynamic_cast<::XCEngine::Components::CameraComponent*>(&component); - if (camera == nullptr || - field.kind != Widgets::UIEditorPropertyGridFieldKind::Enum) { - return false; - } - camera->SetProjectionType( - static_cast<::XCEngine::Components::CameraProjectionType>( - field.enumValue.selectedIndex)); - return true; - }); - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "primary")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* camera = - dynamic_cast<::XCEngine::Components::CameraComponent*>(&component); - if (camera == nullptr || - field.kind != Widgets::UIEditorPropertyGridFieldKind::Bool) { - return false; - } - camera->SetPrimary(field.boolValue); - return true; - }); - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "skybox")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* camera = - dynamic_cast<::XCEngine::Components::CameraComponent*>(&component); - if (camera == nullptr || - field.kind != Widgets::UIEditorPropertyGridFieldKind::Bool) { - return false; - } - camera->SetSkyboxEnabled(field.boolValue); - return true; - }); - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "skybox_material") && - field.kind == Widgets::UIEditorPropertyGridFieldKind::Asset) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* camera = - dynamic_cast<::XCEngine::Components::CameraComponent*>(&component); - if (camera == nullptr) { - return false; - } - camera->SetSkyboxMaterialPath(field.assetValue.assetId); - return true; - }); - } - - if (field.kind == Widgets::UIEditorPropertyGridFieldKind::Number) { - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "field_of_view")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* camera = - dynamic_cast<::XCEngine::Components::CameraComponent*>(&component); - if (camera == nullptr) { - return false; - } - camera->SetFieldOfView( - static_cast(field.numberValue.value)); - return true; - }); - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "orthographic_size")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* camera = - dynamic_cast<::XCEngine::Components::CameraComponent*>(&component); - if (camera == nullptr) { - return false; - } - camera->SetOrthographicSize( - static_cast(field.numberValue.value)); - return true; - }); - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "near_clip")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* camera = - dynamic_cast<::XCEngine::Components::CameraComponent*>(&component); - if (camera == nullptr) { - return false; - } - camera->SetNearClipPlane( - static_cast(field.numberValue.value)); - return true; - }); - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "far_clip")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* camera = - dynamic_cast<::XCEngine::Components::CameraComponent*>(&component); - if (camera == nullptr) { - return false; - } - camera->SetFarClipPlane( - static_cast(field.numberValue.value)); - return true; - }); - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "depth")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* camera = - dynamic_cast<::XCEngine::Components::CameraComponent*>(&component); - if (camera == nullptr) { - return false; - } - camera->SetDepth(static_cast(field.numberValue.value)); - return true; - }); - } - } - - if (field.kind == Widgets::UIEditorPropertyGridFieldKind::Color) { - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "clear_color")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* camera = - dynamic_cast<::XCEngine::Components::CameraComponent*>(&component); - if (camera == nullptr) { - return false; - } - camera->SetClearColor(ToMathColor(field)); - return true; - }); - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "skybox_top")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* camera = - dynamic_cast<::XCEngine::Components::CameraComponent*>(&component); - if (camera == nullptr) { - return false; - } - camera->SetSkyboxTopColor(ToMathColor(field)); - return true; - }); - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "skybox_horizon")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* camera = - dynamic_cast<::XCEngine::Components::CameraComponent*>(&component); - if (camera == nullptr) { - return false; - } - camera->SetSkyboxHorizonColor(ToMathColor(field)); - return true; - }); - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "skybox_bottom")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* camera = - dynamic_cast<::XCEngine::Components::CameraComponent*>(&component); - if (camera == nullptr) { - return false; - } - camera->SetSkyboxBottomColor(ToMathColor(field)); - return true; - }); - } - } - - return false; - } }; } // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/Inspector/Components/CapsuleColliderInspectorComponentEditor.h b/new_editor/app/Features/Inspector/Components/CapsuleColliderInspectorComponentEditor.h index 613f94b5..c3362a8b 100644 --- a/new_editor/app/Features/Inspector/Components/CapsuleColliderInspectorComponentEditor.h +++ b/new_editor/app/Features/Inspector/Components/CapsuleColliderInspectorComponentEditor.h @@ -6,7 +6,7 @@ namespace XCEngine::UI::Editor::App { -class CapsuleColliderInspectorComponentEditor final : public IInspectorComponentEditor { +class CapsuleColliderInspectorComponentEditor final : public InspectorBindingComponentEditor { public: std::string_view GetComponentTypeName() const override { return "CapsuleCollider"; @@ -16,103 +16,80 @@ public: return "Capsule Collider"; } - void BuildSections( +protected: + void BuildBindingSections( const InspectorComponentEditorContext& context, - std::vector& outSections) const override { + std::vector& outSections) const override { const auto* collider = - dynamic_cast(context.component); + ResolveInspectorComponent<::XCEngine::Components::CapsuleColliderComponent>(context); if (collider == nullptr) { return; } - Widgets::UIEditorPropertyGridSection section = {}; - section.sectionId = BuildInspectorComponentSectionId(context.componentId); + InspectorSectionBinding section = {}; section.title = std::string(GetDisplayName()); - AppendColliderBaseFields(context.componentId, *collider, section.fields); - section.fields.push_back(BuildInspectorNumberField( - BuildInspectorComponentFieldId(context.componentId, "radius"), + AppendColliderBaseBindings(section.fields, *collider); + section.fields.push_back(BuildInspectorNumberFieldBinding( + "radius", "Radius", - collider->GetRadius(), - 0.05, - 0.0001, - 1000000.0)); - section.fields.push_back(BuildInspectorNumberField( - BuildInspectorComponentFieldId(context.componentId, "height"), + InspectorNumberFieldBindingOptions{ + .step = 0.05, + .minValue = 0.0001, + .maxValue = 1000000.0 + }, + [collider]() { return collider->GetRadius(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + double value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::CapsuleColliderComponent>( + sceneRuntime, + bindingContext, + [value](::XCEngine::Components::CapsuleColliderComponent& typedCollider) { + typedCollider.SetRadius(static_cast(value)); + }); + })); + section.fields.push_back(BuildInspectorNumberFieldBinding( + "height", "Height", - collider->GetHeight(), - 0.05, - 0.0001, - 1000000.0)); - section.fields.push_back(BuildInspectorEnumField( - BuildInspectorComponentFieldId(context.componentId, "axis"), + InspectorNumberFieldBindingOptions{ + .step = 0.05, + .minValue = 0.0001, + .maxValue = 1000000.0 + }, + [collider]() { return collider->GetHeight(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + double value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::CapsuleColliderComponent>( + sceneRuntime, + bindingContext, + [value](::XCEngine::Components::CapsuleColliderComponent& typedCollider) { + typedCollider.SetHeight(static_cast(value)); + }); + })); + section.fields.push_back(BuildInspectorEnumFieldBinding( + "axis", "Axis", { "X", "Y", "Z" }, - static_cast(collider->GetAxis()))); + [collider]() { + return static_cast(collider->GetAxis()); + }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + std::size_t value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::CapsuleColliderComponent>( + sceneRuntime, + bindingContext, + [value](::XCEngine::Components::CapsuleColliderComponent& typedCollider) { + typedCollider.SetAxis( + static_cast<::XCEngine::Components::ColliderAxis>(value)); + }); + })); outSections.push_back(std::move(section)); } - - bool ApplyFieldValue( - EditorSceneRuntime& sceneRuntime, - const InspectorComponentEditorContext& context, - const Widgets::UIEditorPropertyGridField& field) const override { - if (ApplyColliderBaseFieldValue(sceneRuntime, context, field)) { - return true; - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "axis")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* collider = - dynamic_cast<::XCEngine::Components::CapsuleColliderComponent*>(&component); - if (collider == nullptr || - field.kind != Widgets::UIEditorPropertyGridFieldKind::Enum) { - return false; - } - collider->SetAxis( - static_cast<::XCEngine::Components::ColliderAxis>( - field.enumValue.selectedIndex)); - return true; - }); - } - - if (field.kind != Widgets::UIEditorPropertyGridFieldKind::Number) { - return false; - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "radius")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* collider = - dynamic_cast<::XCEngine::Components::CapsuleColliderComponent*>(&component); - if (collider == nullptr) { - return false; - } - collider->SetRadius(static_cast(field.numberValue.value)); - return true; - }); - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "height")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* collider = - dynamic_cast<::XCEngine::Components::CapsuleColliderComponent*>(&component); - if (collider == nullptr) { - return false; - } - collider->SetHeight(static_cast(field.numberValue.value)); - return true; - }); - } - - return false; - } }; } // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/Inspector/Components/ColliderInspectorComponentEditorUtils.h b/new_editor/app/Features/Inspector/Components/ColliderInspectorComponentEditorUtils.h index 9b90ba39..2dc6c8f6 100644 --- a/new_editor/app/Features/Inspector/Components/ColliderInspectorComponentEditorUtils.h +++ b/new_editor/app/Features/Inspector/Components/ColliderInspectorComponentEditorUtils.h @@ -1,7 +1,6 @@ #pragma once -#include "Features/Inspector/Components/IInspectorComponentEditor.h" -#include "Features/Inspector/Components/InspectorComponentEditorUtils.h" +#include "Features/Inspector/Components/InspectorBindingComponentEditor.h" #include @@ -9,125 +8,100 @@ namespace XCEngine::UI::Editor::App { -inline void AppendColliderBaseFields( - std::string_view componentId, - const ::XCEngine::Components::ColliderComponent& collider, - std::vector& fields) { - fields.push_back(BuildInspectorBoolField( - BuildInspectorComponentFieldId(componentId, "is_trigger"), +inline void AppendColliderBaseBindings( + std::vector& fields, + const ::XCEngine::Components::ColliderComponent& collider) { + fields.push_back(BuildInspectorBoolFieldBinding( + "is_trigger", "Is Trigger", - collider.IsTrigger())); - fields.push_back(BuildInspectorVector3Field( - BuildInspectorComponentFieldId(componentId, "center"), + [&collider]() { return collider.IsTrigger(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& context, + bool value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::ColliderComponent>( + sceneRuntime, + context, + [value](::XCEngine::Components::ColliderComponent& typedCollider) { + typedCollider.SetTrigger(value); + }); + })); + fields.push_back(BuildInspectorVector3FieldBinding( + "center", "Center", - collider.GetCenter(), - 0.1)); - fields.push_back(BuildInspectorNumberField( - BuildInspectorComponentFieldId(componentId, "static_friction"), + InspectorVector3FieldBindingOptions{ .step = 0.1 }, + [&collider]() { return collider.GetCenter(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& context, + const ::XCEngine::Math::Vector3& value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::ColliderComponent>( + sceneRuntime, + context, + [value](::XCEngine::Components::ColliderComponent& typedCollider) { + typedCollider.SetCenter(value); + }); + })); + fields.push_back(BuildInspectorNumberFieldBinding( + "static_friction", "Static Friction", - collider.GetStaticFriction(), - 0.05, - 0.0, - 1000.0)); - fields.push_back(BuildInspectorNumberField( - BuildInspectorComponentFieldId(componentId, "dynamic_friction"), + InspectorNumberFieldBindingOptions{ + .step = 0.05, + .minValue = 0.0, + .maxValue = 1000.0 + }, + [&collider]() { return collider.GetStaticFriction(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& context, + double value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::ColliderComponent>( + sceneRuntime, + context, + [value](::XCEngine::Components::ColliderComponent& typedCollider) { + typedCollider.SetStaticFriction(static_cast(value)); + }); + })); + fields.push_back(BuildInspectorNumberFieldBinding( + "dynamic_friction", "Dynamic Friction", - collider.GetDynamicFriction(), - 0.05, - 0.0, - 1000.0)); - fields.push_back(BuildInspectorNumberField( - BuildInspectorComponentFieldId(componentId, "restitution"), + InspectorNumberFieldBindingOptions{ + .step = 0.05, + .minValue = 0.0, + .maxValue = 1000.0 + }, + [&collider]() { return collider.GetDynamicFriction(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& context, + double value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::ColliderComponent>( + sceneRuntime, + context, + [value](::XCEngine::Components::ColliderComponent& typedCollider) { + typedCollider.SetDynamicFriction(static_cast(value)); + }); + })); + fields.push_back(BuildInspectorNumberFieldBinding( + "restitution", "Restitution", - collider.GetRestitution(), - 0.05, - 0.0, - 1.0)); -} - -inline bool ApplyColliderBaseFieldValue( - EditorSceneRuntime& sceneRuntime, - const InspectorComponentEditorContext& context, - const Widgets::UIEditorPropertyGridField& field) { - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "is_trigger")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* collider = - dynamic_cast<::XCEngine::Components::ColliderComponent*>(&component); - if (collider == nullptr || field.kind != Widgets::UIEditorPropertyGridFieldKind::Bool) { - return false; - } - collider->SetTrigger(field.boolValue); - return true; - }); - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "center")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* collider = - dynamic_cast<::XCEngine::Components::ColliderComponent*>(&component); - if (collider == nullptr || field.kind != Widgets::UIEditorPropertyGridFieldKind::Vector3) { - return false; - } - collider->SetCenter(ToMathVector3(field)); - return true; - }); - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "static_friction")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* collider = - dynamic_cast<::XCEngine::Components::ColliderComponent*>(&component); - if (collider == nullptr || field.kind != Widgets::UIEditorPropertyGridFieldKind::Number) { - return false; - } - collider->SetStaticFriction( - static_cast(field.numberValue.value)); - return true; - }); - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "dynamic_friction")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* collider = - dynamic_cast<::XCEngine::Components::ColliderComponent*>(&component); - if (collider == nullptr || field.kind != Widgets::UIEditorPropertyGridFieldKind::Number) { - return false; - } - collider->SetDynamicFriction( - static_cast(field.numberValue.value)); - return true; - }); - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "restitution")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* collider = - dynamic_cast<::XCEngine::Components::ColliderComponent*>(&component); - if (collider == nullptr || field.kind != Widgets::UIEditorPropertyGridFieldKind::Number) { - return false; - } - collider->SetRestitution( - static_cast(field.numberValue.value)); - return true; - }); - } - - return false; + InspectorNumberFieldBindingOptions{ + .step = 0.05, + .minValue = 0.0, + .maxValue = 1.0 + }, + [&collider]() { return collider.GetRestitution(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& context, + double value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::ColliderComponent>( + sceneRuntime, + context, + [value](::XCEngine::Components::ColliderComponent& typedCollider) { + typedCollider.SetRestitution(static_cast(value)); + }); + })); } } // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/Inspector/Components/IInspectorComponentEditor.h b/new_editor/app/Features/Inspector/Components/IInspectorComponentEditor.h index 04843f7a..462a4996 100644 --- a/new_editor/app/Features/Inspector/Components/IInspectorComponentEditor.h +++ b/new_editor/app/Features/Inspector/Components/IInspectorComponentEditor.h @@ -36,11 +36,22 @@ public: const InspectorComponentEditorContext& context, std::vector& outSections) const = 0; + virtual bool SyncFieldValue( + const InspectorComponentEditorContext& context, + Widgets::UIEditorPropertyGridField& field) const = 0; + virtual bool ApplyFieldValue( EditorSceneRuntime& sceneRuntime, const InspectorComponentEditorContext& context, const Widgets::UIEditorPropertyGridField& field) const = 0; + virtual void AppendStructureSignature( + const InspectorComponentEditorContext& context, + std::string& signature) const { + (void)context; + (void)signature; + } + virtual bool CanRemove( const InspectorComponentEditorContext& context) const { return context.removable; diff --git a/new_editor/app/Features/Inspector/Components/InspectorBindingComponentEditor.cpp b/new_editor/app/Features/Inspector/Components/InspectorBindingComponentEditor.cpp new file mode 100644 index 00000000..8bb4f250 --- /dev/null +++ b/new_editor/app/Features/Inspector/Components/InspectorBindingComponentEditor.cpp @@ -0,0 +1,348 @@ +#include "Features/Inspector/Components/InspectorBindingComponentEditor.h" + +#include + +namespace XCEngine::UI::Editor::App { + +namespace { + +std::string ResolveBindingFieldId( + std::string_view componentId, + std::string_view fieldIdSuffix) { + return BuildInspectorComponentFieldId(componentId, fieldIdSuffix); +} + +std::string ResolveBindingSectionId( + std::string_view componentId, + std::string_view sectionIdSuffix) { + return BuildInspectorComponentSectionId(componentId, sectionIdSuffix); +} + +bool TryResolveFieldBinding( + const InspectorComponentEditorContext& context, + const std::vector& sections, + std::string_view fieldId, + const InspectorFieldBinding*& outBinding) { + for (const InspectorSectionBinding& section : sections) { + for (const InspectorFieldBinding& binding : section.fields) { + if (ResolveBindingFieldId(context.componentId, binding.idSuffix) == fieldId) { + outBinding = &binding; + return true; + } + } + } + + outBinding = nullptr; + return false; +} + +} // namespace + +void InspectorBindingComponentEditor::BuildSections( + const InspectorComponentEditorContext& context, + std::vector& outSections) const { + std::vector bindingSections = {}; + BuildBindingSections(context, bindingSections); + outSections.reserve(outSections.size() + bindingSections.size()); + for (const InspectorSectionBinding& bindingSection : bindingSections) { + Widgets::UIEditorPropertyGridSection section = {}; + section.sectionId = + ResolveBindingSectionId(context.componentId, bindingSection.idSuffix); + section.title = bindingSection.title; + section.fields.reserve(bindingSection.fields.size()); + for (const InspectorFieldBinding& fieldBinding : bindingSection.fields) { + if (!fieldBinding.buildField) { + continue; + } + + section.fields.push_back( + fieldBinding.buildField( + ResolveBindingFieldId(context.componentId, fieldBinding.idSuffix))); + } + outSections.push_back(std::move(section)); + } +} + +bool InspectorBindingComponentEditor::SyncFieldValue( + const InspectorComponentEditorContext& context, + Widgets::UIEditorPropertyGridField& field) const { + std::vector bindingSections = {}; + BuildBindingSections(context, bindingSections); + + const InspectorFieldBinding* binding = nullptr; + if (!TryResolveFieldBinding(context, bindingSections, field.fieldId, binding) || + binding == nullptr || + !binding->syncField) { + return false; + } + + return binding->syncField(field); +} + +bool InspectorBindingComponentEditor::ApplyFieldValue( + EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& context, + const Widgets::UIEditorPropertyGridField& field) const { + std::vector bindingSections = {}; + BuildBindingSections(context, bindingSections); + + const InspectorFieldBinding* binding = nullptr; + if (!TryResolveFieldBinding(context, bindingSections, field.fieldId, binding) || + binding == nullptr || + !binding->applyField) { + return false; + } + + return binding->applyField(sceneRuntime, context, field); +} + +void InspectorBindingComponentEditor::AppendStructureSignature( + const InspectorComponentEditorContext& context, + std::string& signature) const { + std::vector bindingSections = {}; + BuildBindingSections(context, bindingSections); + AppendInspectorStructureToken(signature, std::string_view(GetComponentTypeName())); + for (const InspectorSectionBinding& section : bindingSections) { + AppendInspectorStructureToken(signature, section.idSuffix); + AppendInspectorStructureToken(signature, section.title); + for (const InspectorFieldBinding& field : section.fields) { + AppendInspectorStructureToken(signature, field.idSuffix); + } + } +} + +InspectorFieldBinding BuildInspectorBoolFieldBinding( + std::string idSuffix, + std::string label, + std::function getter, + std::function setter) { + InspectorFieldBinding binding = {}; + binding.idSuffix = std::move(idSuffix); + binding.buildField = + [label = std::move(label), getter](std::string fieldId) { + return BuildInspectorBoolField( + std::move(fieldId), + label, + getter()); + }; + binding.syncField = + [getter](Widgets::UIEditorPropertyGridField& field) { + return SyncInspectorBoolFieldValue(field, getter()); + }; + binding.applyField = + [setter = std::move(setter)]( + EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& context, + const Widgets::UIEditorPropertyGridField& field) { + if (field.kind != Widgets::UIEditorPropertyGridFieldKind::Bool) { + return false; + } + return setter(sceneRuntime, context, field.boolValue); + }; + return binding; +} + +InspectorFieldBinding BuildInspectorNumberFieldBinding( + std::string idSuffix, + std::string label, + InspectorNumberFieldBindingOptions options, + std::function getter, + std::function setter) { + InspectorFieldBinding binding = {}; + binding.idSuffix = std::move(idSuffix); + binding.buildField = + [label = std::move(label), + options, + getter](std::string fieldId) { + return BuildInspectorNumberField( + std::move(fieldId), + label, + getter(), + options.step, + options.minValue, + options.maxValue, + options.integerMode); + }; + binding.syncField = + [getter](Widgets::UIEditorPropertyGridField& field) { + return SyncInspectorNumberFieldValue(field, getter()); + }; + binding.applyField = + [setter = std::move(setter)]( + EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& context, + const Widgets::UIEditorPropertyGridField& field) { + if (field.kind != Widgets::UIEditorPropertyGridFieldKind::Number) { + return false; + } + return setter(sceneRuntime, context, field.numberValue.value); + }; + return binding; +} + +InspectorFieldBinding BuildInspectorEnumFieldBinding( + std::string idSuffix, + std::string label, + std::vector options, + std::function getter, + std::function setter) { + InspectorFieldBinding binding = {}; + binding.idSuffix = std::move(idSuffix); + binding.buildField = + [label = std::move(label), + options = std::move(options), + getter](std::string fieldId) { + return BuildInspectorEnumField( + std::move(fieldId), + label, + options, + getter()); + }; + binding.syncField = + [getter](Widgets::UIEditorPropertyGridField& field) { + return SyncInspectorEnumFieldValue(field, getter()); + }; + binding.applyField = + [setter = std::move(setter)]( + EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& context, + const Widgets::UIEditorPropertyGridField& field) { + if (field.kind != Widgets::UIEditorPropertyGridFieldKind::Enum) { + return false; + } + return setter(sceneRuntime, context, field.enumValue.selectedIndex); + }; + return binding; +} + +InspectorFieldBinding BuildInspectorAssetFieldBinding( + std::string idSuffix, + std::string label, + InspectorAssetFieldBindingOptions options, + std::function getter, + std::function setter) { + InspectorFieldBinding binding = {}; + binding.idSuffix = std::move(idSuffix); + binding.buildField = + [label = std::move(label), + options, + getter](std::string fieldId) { + return BuildInspectorAssetField( + std::move(fieldId), + label, + getter(), + options.emptyText, + options.showPickerButton); + }; + binding.syncField = + [options, getter](Widgets::UIEditorPropertyGridField& field) { + return SyncInspectorAssetFieldValue( + field, + getter(), + options.emptyText); + }; + binding.applyField = + [setter = std::move(setter)]( + EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& context, + const Widgets::UIEditorPropertyGridField& field) { + if (field.kind != Widgets::UIEditorPropertyGridFieldKind::Asset) { + return false; + } + return setter(sceneRuntime, context, field.assetValue.assetId); + }; + return binding; +} + +InspectorFieldBinding BuildInspectorColorFieldBinding( + std::string idSuffix, + std::string label, + InspectorColorFieldBindingOptions options, + std::function<::XCEngine::Math::Color()> getter, + std::function setter) { + InspectorFieldBinding binding = {}; + binding.idSuffix = std::move(idSuffix); + binding.buildField = + [label = std::move(label), + options, + getter](std::string fieldId) { + return BuildInspectorColorField( + std::move(fieldId), + label, + getter(), + options.showAlpha); + }; + binding.syncField = + [options, getter](Widgets::UIEditorPropertyGridField& field) { + return SyncInspectorColorFieldValue(field, getter(), options.showAlpha); + }; + binding.applyField = + [setter = std::move(setter)]( + EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& context, + const Widgets::UIEditorPropertyGridField& field) { + if (field.kind != Widgets::UIEditorPropertyGridFieldKind::Color) { + return false; + } + return setter(sceneRuntime, context, ToMathColor(field)); + }; + return binding; +} + +InspectorFieldBinding BuildInspectorVector3FieldBinding( + std::string idSuffix, + std::string label, + InspectorVector3FieldBindingOptions options, + std::function<::XCEngine::Math::Vector3()> getter, + std::function setter) { + InspectorFieldBinding binding = {}; + binding.idSuffix = std::move(idSuffix); + binding.buildField = + [label = std::move(label), + options, + getter](std::string fieldId) { + return BuildInspectorVector3Field( + std::move(fieldId), + label, + getter(), + options.step, + options.integerMode, + options.minValue, + options.maxValue); + }; + binding.syncField = + [getter](Widgets::UIEditorPropertyGridField& field) { + return SyncInspectorVector3FieldValue(field, getter()); + }; + binding.applyField = + [setter = std::move(setter)]( + EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& context, + const Widgets::UIEditorPropertyGridField& field) { + if (field.kind != Widgets::UIEditorPropertyGridFieldKind::Vector3) { + return false; + } + return setter(sceneRuntime, context, ToMathVector3(field)); + }; + return binding; +} + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/Inspector/Components/InspectorBindingComponentEditor.h b/new_editor/app/Features/Inspector/Components/InspectorBindingComponentEditor.h new file mode 100644 index 00000000..cdd343bf --- /dev/null +++ b/new_editor/app/Features/Inspector/Components/InspectorBindingComponentEditor.h @@ -0,0 +1,132 @@ +#pragma once + +#include "Features/Inspector/Components/IInspectorComponentEditor.h" +#include "Features/Inspector/Components/InspectorComponentEditorUtils.h" + +#include +#include +#include + +namespace XCEngine::UI::Editor::App { + +struct InspectorNumberFieldBindingOptions { + double step = 0.1; + double minValue = -1000000.0; + double maxValue = 1000000.0; + bool integerMode = false; +}; + +struct InspectorAssetFieldBindingOptions { + std::string emptyText = "None"; + bool showPickerButton = false; +}; + +struct InspectorColorFieldBindingOptions { + bool showAlpha = true; +}; + +struct InspectorVector3FieldBindingOptions { + double step = 0.1; + bool integerMode = false; + double minValue = -1000000.0; + double maxValue = 1000000.0; +}; + +struct InspectorFieldBinding { + std::string idSuffix = {}; + std::function buildField = {}; + std::function syncField = {}; + std::function applyField = {}; +}; + +struct InspectorSectionBinding { + std::string idSuffix = {}; + std::string title = {}; + std::vector fields = {}; +}; + +class InspectorBindingComponentEditor : public IInspectorComponentEditor { +public: + void BuildSections( + const InspectorComponentEditorContext& context, + std::vector& outSections) const final; + bool SyncFieldValue( + const InspectorComponentEditorContext& context, + Widgets::UIEditorPropertyGridField& field) const final; + bool ApplyFieldValue( + EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& context, + const Widgets::UIEditorPropertyGridField& field) const final; + void AppendStructureSignature( + const InspectorComponentEditorContext& context, + std::string& signature) const final; + +protected: + virtual void BuildBindingSections( + const InspectorComponentEditorContext& context, + std::vector& outSections) const = 0; +}; + +InspectorFieldBinding BuildInspectorBoolFieldBinding( + std::string idSuffix, + std::string label, + std::function getter, + std::function setter); + +InspectorFieldBinding BuildInspectorNumberFieldBinding( + std::string idSuffix, + std::string label, + InspectorNumberFieldBindingOptions options, + std::function getter, + std::function setter); + +InspectorFieldBinding BuildInspectorEnumFieldBinding( + std::string idSuffix, + std::string label, + std::vector options, + std::function getter, + std::function setter); + +InspectorFieldBinding BuildInspectorAssetFieldBinding( + std::string idSuffix, + std::string label, + InspectorAssetFieldBindingOptions options, + std::function getter, + std::function setter); + +InspectorFieldBinding BuildInspectorColorFieldBinding( + std::string idSuffix, + std::string label, + InspectorColorFieldBindingOptions options, + std::function<::XCEngine::Math::Color()> getter, + std::function setter); + +InspectorFieldBinding BuildInspectorVector3FieldBinding( + std::string idSuffix, + std::string label, + InspectorVector3FieldBindingOptions options, + std::function<::XCEngine::Math::Vector3()> getter, + std::function setter); + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/Inspector/Components/InspectorComponentEditorUtils.h b/new_editor/app/Features/Inspector/Components/InspectorComponentEditorUtils.h index 3be6f2e3..37ddd990 100644 --- a/new_editor/app/Features/Inspector/Components/InspectorComponentEditorUtils.h +++ b/new_editor/app/Features/Inspector/Components/InspectorComponentEditorUtils.h @@ -7,8 +7,11 @@ #include #include +#include #include #include +#include +#include #include namespace XCEngine::UI::Editor::App { @@ -74,6 +77,17 @@ inline Widgets::UIEditorPropertyGridField BuildInspectorBoolField( return field; } +inline bool SyncInspectorBoolFieldValue( + Widgets::UIEditorPropertyGridField& field, + bool value) { + if (field.kind != Widgets::UIEditorPropertyGridFieldKind::Bool) { + return false; + } + + field.boolValue = value; + return true; +} + inline Widgets::UIEditorPropertyGridField BuildInspectorNumberField( std::string fieldId, std::string label, @@ -94,6 +108,17 @@ inline Widgets::UIEditorPropertyGridField BuildInspectorNumberField( return field; } +inline bool SyncInspectorNumberFieldValue( + Widgets::UIEditorPropertyGridField& field, + double value) { + if (field.kind != Widgets::UIEditorPropertyGridFieldKind::Number) { + return false; + } + + field.numberValue.value = value; + return true; +} + inline Widgets::UIEditorPropertyGridField BuildInspectorEnumField( std::string fieldId, std::string label, @@ -108,6 +133,17 @@ inline Widgets::UIEditorPropertyGridField BuildInspectorEnumField( return field; } +inline bool SyncInspectorEnumFieldValue( + Widgets::UIEditorPropertyGridField& field, + std::size_t selectedIndex) { + if (field.kind != Widgets::UIEditorPropertyGridFieldKind::Enum) { + return false; + } + + field.enumValue.selectedIndex = selectedIndex; + return true; +} + inline std::string ResolveInspectorAssetDisplayName(std::string_view assetId) { const std::size_t separatorIndex = assetId.find_last_of("/\\"); return separatorIndex == std::string_view::npos @@ -149,6 +185,22 @@ inline Widgets::UIEditorPropertyGridField BuildInspectorAssetField( return field; } +inline bool SyncInspectorAssetFieldValue( + Widgets::UIEditorPropertyGridField& field, + std::string_view assetId, + std::string_view emptyText = "None") { + if (field.kind != Widgets::UIEditorPropertyGridFieldKind::Asset) { + return false; + } + + field.assetValue.assetId = std::string(assetId); + field.assetValue.displayName = ResolveInspectorAssetDisplayName(assetId); + field.assetValue.statusText = ResolveInspectorAssetStatusText(assetId); + field.assetValue.emptyText = std::string(emptyText); + field.assetValue.showStatusBadge = !field.assetValue.statusText.empty(); + return true; +} + inline ::XCEngine::UI::UIColor ToUIEditorColor(const ::XCEngine::Math::Color& value) { return ::XCEngine::UI::UIColor(value.r, value.g, value.b, value.a); } @@ -176,6 +228,19 @@ inline Widgets::UIEditorPropertyGridField BuildInspectorColorField( return field; } +inline bool SyncInspectorColorFieldValue( + Widgets::UIEditorPropertyGridField& field, + const ::XCEngine::Math::Color& value, + bool showAlpha = true) { + if (field.kind != Widgets::UIEditorPropertyGridFieldKind::Color) { + return false; + } + + field.colorValue.value = ToUIEditorColor(value); + field.colorValue.showAlpha = showAlpha; + return true; +} + inline std::array ToInspectorVector3Values( const ::XCEngine::Math::Vector3& value) { return { @@ -213,4 +278,62 @@ inline Widgets::UIEditorPropertyGridField BuildInspectorVector3Field( return field; } +inline bool SyncInspectorVector3FieldValue( + Widgets::UIEditorPropertyGridField& field, + const ::XCEngine::Math::Vector3& value) { + if (field.kind != Widgets::UIEditorPropertyGridFieldKind::Vector3) { + return false; + } + + field.vector3Value.values = ToInspectorVector3Values(value); + return true; +} + +inline void AppendInspectorStructureToken( + std::string& signature, + std::string_view token) { + signature += token; + signature.push_back('|'); +} + +inline void AppendInspectorStructureToken( + std::string& signature, + bool value) { + signature.push_back(value ? '1' : '0'); + signature.push_back('|'); +} + +inline void AppendInspectorStructureToken( + std::string& signature, + std::size_t value) { + signature += std::to_string(value); + signature.push_back('|'); +} + +template +const TComponent* ResolveInspectorComponent( + const InspectorComponentEditorContext& context) { + return dynamic_cast(context.component); +} + +template +bool MutateSelectedInspectorComponent( + EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& context, + TMutator&& mutator) { + using MutatorType = std::decay_t; + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [capturedMutator = MutatorType(std::forward(mutator))]( + ::XCEngine::Components::Component& component) mutable { + auto* typedComponent = dynamic_cast(&component); + if (typedComponent == nullptr) { + return false; + } + + capturedMutator(*typedComponent); + return true; + }); +} + } // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/Inspector/Components/LightInspectorComponentEditor.h b/new_editor/app/Features/Inspector/Components/LightInspectorComponentEditor.h index 3dc87e54..6ff4e183 100644 --- a/new_editor/app/Features/Inspector/Components/LightInspectorComponentEditor.h +++ b/new_editor/app/Features/Inspector/Components/LightInspectorComponentEditor.h @@ -1,13 +1,12 @@ #pragma once -#include "Features/Inspector/Components/IInspectorComponentEditor.h" -#include "Features/Inspector/Components/InspectorComponentEditorUtils.h" +#include "Features/Inspector/Components/InspectorBindingComponentEditor.h" #include namespace XCEngine::UI::Editor::App { -class LightInspectorComponentEditor final : public IInspectorComponentEditor { +class LightInspectorComponentEditor final : public InspectorBindingComponentEditor { public: std::string_view GetComponentTypeName() const override { return "Light"; @@ -17,311 +16,266 @@ public: return "Light"; } - void BuildSections( +protected: + void BuildBindingSections( const InspectorComponentEditorContext& context, - std::vector& outSections) const override { + std::vector& outSections) const override { const auto* light = - dynamic_cast(context.component); + ResolveInspectorComponent<::XCEngine::Components::LightComponent>(context); if (light == nullptr) { return; } - Widgets::UIEditorPropertyGridSection section = {}; - section.sectionId = BuildInspectorComponentSectionId(context.componentId); + InspectorSectionBinding section = {}; section.title = std::string(GetDisplayName()); - section.fields.push_back(BuildInspectorEnumField( - BuildInspectorComponentFieldId(context.componentId, "type"), + section.fields.push_back(BuildInspectorEnumFieldBinding( + "type", "Type", { "Directional", "Point", "Spot" }, - static_cast(light->GetLightType()))); - section.fields.push_back(BuildInspectorColorField( - BuildInspectorComponentFieldId(context.componentId, "color"), + [light]() { return static_cast(light->GetLightType()); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + std::size_t value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::LightComponent>( + sceneRuntime, + bindingContext, + [value](::XCEngine::Components::LightComponent& typedLight) { + typedLight.SetLightType( + static_cast<::XCEngine::Components::LightType>(value)); + }); + })); + section.fields.push_back(BuildInspectorColorFieldBinding( + "color", "Color", - light->GetColor())); - section.fields.push_back(BuildInspectorNumberField( - BuildInspectorComponentFieldId(context.componentId, "intensity"), + {}, + [light]() { return light->GetColor(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + const ::XCEngine::Math::Color& value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::LightComponent>( + sceneRuntime, + bindingContext, + [value](::XCEngine::Components::LightComponent& typedLight) { + typedLight.SetColor(value); + }); + })); + section.fields.push_back(BuildInspectorNumberFieldBinding( + "intensity", "Intensity", - light->GetIntensity(), - 0.1, - 0.0, - 1000000.0)); + InspectorNumberFieldBindingOptions{ + .step = 0.1, + .minValue = 0.0, + .maxValue = 1000000.0 + }, + [light]() { return light->GetIntensity(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + double value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::LightComponent>( + sceneRuntime, + bindingContext, + [value](::XCEngine::Components::LightComponent& typedLight) { + typedLight.SetIntensity(static_cast(value)); + }); + })); if (light->GetLightType() != ::XCEngine::Components::LightType::Directional) { - section.fields.push_back(BuildInspectorNumberField( - BuildInspectorComponentFieldId(context.componentId, "range"), + section.fields.push_back(BuildInspectorNumberFieldBinding( + "range", "Range", - light->GetRange(), - 0.1, - 0.001, - 1000000.0)); + InspectorNumberFieldBindingOptions{ + .step = 0.1, + .minValue = 0.001, + .maxValue = 1000000.0 + }, + [light]() { return light->GetRange(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + double value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::LightComponent>( + sceneRuntime, + bindingContext, + [value](::XCEngine::Components::LightComponent& typedLight) { + typedLight.SetRange(static_cast(value)); + }); + })); } if (light->GetLightType() == ::XCEngine::Components::LightType::Spot) { - section.fields.push_back(BuildInspectorNumberField( - BuildInspectorComponentFieldId(context.componentId, "spot_angle"), + section.fields.push_back(BuildInspectorNumberFieldBinding( + "spot_angle", "Spot Angle", - light->GetSpotAngle(), - 0.5, - 1.0, - 179.0)); + InspectorNumberFieldBindingOptions{ + .step = 0.5, + .minValue = 1.0, + .maxValue = 179.0 + }, + [light]() { return light->GetSpotAngle(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + double value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::LightComponent>( + sceneRuntime, + bindingContext, + [value](::XCEngine::Components::LightComponent& typedLight) { + typedLight.SetSpotAngle(static_cast(value)); + }); + })); } - section.fields.push_back(BuildInspectorBoolField( - BuildInspectorComponentFieldId(context.componentId, "cast_shadows"), + section.fields.push_back(BuildInspectorBoolFieldBinding( + "cast_shadows", "Cast Shadows", - light->GetCastsShadows())); + [light]() { return light->GetCastsShadows(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + bool value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::LightComponent>( + sceneRuntime, + bindingContext, + [value](::XCEngine::Components::LightComponent& typedLight) { + typedLight.SetCastsShadows(value); + }); + })); if (light->GetLightType() == ::XCEngine::Components::LightType::Directional && light->GetCastsShadows()) { - section.fields.push_back(BuildInspectorBoolField( - BuildInspectorComponentFieldId(context.componentId, "override_shadow_params"), + section.fields.push_back(BuildInspectorBoolFieldBinding( + "override_shadow_params", "Override Shadow Params", - light->GetOverridesDirectionalShadowSettings())); + [light]() { return light->GetOverridesDirectionalShadowSettings(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + bool value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::LightComponent>( + sceneRuntime, + bindingContext, + [value](::XCEngine::Components::LightComponent& typedLight) { + typedLight.SetOverridesDirectionalShadowSettings(value); + }); + })); if (light->GetOverridesDirectionalShadowSettings()) { - section.fields.push_back(BuildInspectorNumberField( - BuildInspectorComponentFieldId(context.componentId, "receiver_depth_bias"), + section.fields.push_back(BuildInspectorNumberFieldBinding( + "receiver_depth_bias", "Receiver Depth Bias", - light->GetDirectionalShadowReceiverDepthBias(), - 0.0001, - 0.0, - 1000.0)); - section.fields.push_back(BuildInspectorNumberField( - BuildInspectorComponentFieldId(context.componentId, "normal_bias_scale"), + InspectorNumberFieldBindingOptions{ + .step = 0.0001, + .minValue = 0.0, + .maxValue = 1000.0 + }, + [light]() { return light->GetDirectionalShadowReceiverDepthBias(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + double value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::LightComponent>( + sceneRuntime, + bindingContext, + [value](::XCEngine::Components::LightComponent& typedLight) { + typedLight.SetDirectionalShadowReceiverDepthBias( + static_cast(value)); + }); + })); + section.fields.push_back(BuildInspectorNumberFieldBinding( + "normal_bias_scale", "Normal Bias Scale", - light->GetDirectionalShadowNormalBiasScale(), - 0.05, - 0.0, - 1000.0)); - section.fields.push_back(BuildInspectorNumberField( - BuildInspectorComponentFieldId(context.componentId, "shadow_strength"), + InspectorNumberFieldBindingOptions{ + .step = 0.05, + .minValue = 0.0, + .maxValue = 1000.0 + }, + [light]() { return light->GetDirectionalShadowNormalBiasScale(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + double value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::LightComponent>( + sceneRuntime, + bindingContext, + [value](::XCEngine::Components::LightComponent& typedLight) { + typedLight.SetDirectionalShadowNormalBiasScale( + static_cast(value)); + }); + })); + section.fields.push_back(BuildInspectorNumberFieldBinding( + "shadow_strength", "Shadow Strength", - light->GetDirectionalShadowStrength(), - 0.05, - 0.0, - 1.0)); - section.fields.push_back(BuildInspectorNumberField( - BuildInspectorComponentFieldId(context.componentId, "depth_bias_factor"), + InspectorNumberFieldBindingOptions{ + .step = 0.05, + .minValue = 0.0, + .maxValue = 1.0 + }, + [light]() { return light->GetDirectionalShadowStrength(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + double value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::LightComponent>( + sceneRuntime, + bindingContext, + [value](::XCEngine::Components::LightComponent& typedLight) { + typedLight.SetDirectionalShadowStrength( + static_cast(value)); + }); + })); + section.fields.push_back(BuildInspectorNumberFieldBinding( + "depth_bias_factor", "Depth Bias Factor", - light->GetDirectionalShadowDepthBiasFactor(), - 0.1, - 0.0, - 1000.0)); - section.fields.push_back(BuildInspectorNumberField( - BuildInspectorComponentFieldId(context.componentId, "depth_bias_units"), + InspectorNumberFieldBindingOptions{ + .step = 0.1, + .minValue = 0.0, + .maxValue = 1000.0 + }, + [light]() { return light->GetDirectionalShadowDepthBiasFactor(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + double value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::LightComponent>( + sceneRuntime, + bindingContext, + [value](::XCEngine::Components::LightComponent& typedLight) { + typedLight.SetDirectionalShadowDepthBiasFactor( + static_cast(value)); + }); + })); + section.fields.push_back(BuildInspectorNumberFieldBinding( + "depth_bias_units", "Depth Bias Units", - light->GetDirectionalShadowDepthBiasUnits(), - 1.0, - 0.0, - 1000000.0, - true)); + InspectorNumberFieldBindingOptions{ + .step = 1.0, + .minValue = 0.0, + .maxValue = 1000000.0, + .integerMode = true + }, + [light]() { + return static_cast(light->GetDirectionalShadowDepthBiasUnits()); + }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + double value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::LightComponent>( + sceneRuntime, + bindingContext, + [value](::XCEngine::Components::LightComponent& typedLight) { + typedLight.SetDirectionalShadowDepthBiasUnits( + static_cast(value)); + }); + })); } } outSections.push_back(std::move(section)); } - - bool ApplyFieldValue( - EditorSceneRuntime& sceneRuntime, - const InspectorComponentEditorContext& context, - const Widgets::UIEditorPropertyGridField& field) const override { - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "type")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* light = - dynamic_cast<::XCEngine::Components::LightComponent*>(&component); - if (light == nullptr || - field.kind != Widgets::UIEditorPropertyGridFieldKind::Enum) { - return false; - } - light->SetLightType( - static_cast<::XCEngine::Components::LightType>( - field.enumValue.selectedIndex)); - return true; - }); - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "cast_shadows")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* light = - dynamic_cast<::XCEngine::Components::LightComponent*>(&component); - if (light == nullptr || - field.kind != Widgets::UIEditorPropertyGridFieldKind::Bool) { - return false; - } - light->SetCastsShadows(field.boolValue); - return true; - }); - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "override_shadow_params")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* light = - dynamic_cast<::XCEngine::Components::LightComponent*>(&component); - if (light == nullptr || - field.kind != Widgets::UIEditorPropertyGridFieldKind::Bool) { - return false; - } - light->SetOverridesDirectionalShadowSettings(field.boolValue); - return true; - }); - } - - if (field.kind == Widgets::UIEditorPropertyGridFieldKind::Color && - field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "color")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* light = - dynamic_cast<::XCEngine::Components::LightComponent*>(&component); - if (light == nullptr) { - return false; - } - light->SetColor(ToMathColor(field)); - return true; - }); - } - - if (field.kind != Widgets::UIEditorPropertyGridFieldKind::Number) { - return false; - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "intensity")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* light = - dynamic_cast<::XCEngine::Components::LightComponent*>(&component); - if (light == nullptr) { - return false; - } - light->SetIntensity(static_cast(field.numberValue.value)); - return true; - }); - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "range")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* light = - dynamic_cast<::XCEngine::Components::LightComponent*>(&component); - if (light == nullptr) { - return false; - } - light->SetRange(static_cast(field.numberValue.value)); - return true; - }); - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "spot_angle")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* light = - dynamic_cast<::XCEngine::Components::LightComponent*>(&component); - if (light == nullptr) { - return false; - } - light->SetSpotAngle(static_cast(field.numberValue.value)); - return true; - }); - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "receiver_depth_bias")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* light = - dynamic_cast<::XCEngine::Components::LightComponent*>(&component); - if (light == nullptr) { - return false; - } - light->SetDirectionalShadowReceiverDepthBias( - static_cast(field.numberValue.value)); - return true; - }); - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "normal_bias_scale")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* light = - dynamic_cast<::XCEngine::Components::LightComponent*>(&component); - if (light == nullptr) { - return false; - } - light->SetDirectionalShadowNormalBiasScale( - static_cast(field.numberValue.value)); - return true; - }); - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "shadow_strength")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* light = - dynamic_cast<::XCEngine::Components::LightComponent*>(&component); - if (light == nullptr) { - return false; - } - light->SetDirectionalShadowStrength( - static_cast(field.numberValue.value)); - return true; - }); - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "depth_bias_factor")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* light = - dynamic_cast<::XCEngine::Components::LightComponent*>(&component); - if (light == nullptr) { - return false; - } - light->SetDirectionalShadowDepthBiasFactor( - static_cast(field.numberValue.value)); - return true; - }); - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "depth_bias_units")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* light = - dynamic_cast<::XCEngine::Components::LightComponent*>(&component); - if (light == nullptr) { - return false; - } - light->SetDirectionalShadowDepthBiasUnits( - static_cast(field.numberValue.value)); - return true; - }); - } - - return false; - } }; } // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/Inspector/Components/MeshFilterInspectorComponentEditor.h b/new_editor/app/Features/Inspector/Components/MeshFilterInspectorComponentEditor.h index 85940849..cf998659 100644 --- a/new_editor/app/Features/Inspector/Components/MeshFilterInspectorComponentEditor.h +++ b/new_editor/app/Features/Inspector/Components/MeshFilterInspectorComponentEditor.h @@ -1,13 +1,12 @@ #pragma once -#include "Features/Inspector/Components/IInspectorComponentEditor.h" -#include "Features/Inspector/Components/InspectorComponentEditorUtils.h" +#include "Features/Inspector/Components/InspectorBindingComponentEditor.h" #include namespace XCEngine::UI::Editor::App { -class MeshFilterInspectorComponentEditor final : public IInspectorComponentEditor { +class MeshFilterInspectorComponentEditor final : public InspectorBindingComponentEditor { public: std::string_view GetComponentTypeName() const override { return "MeshFilter"; @@ -17,53 +16,41 @@ public: return "Mesh Filter"; } - void BuildSections( +protected: + void BuildBindingSections( const InspectorComponentEditorContext& context, - std::vector& outSections) const override { + std::vector& outSections) const override { const auto* meshFilter = - dynamic_cast(context.component); + ResolveInspectorComponent<::XCEngine::Components::MeshFilterComponent>(context); if (meshFilter == nullptr) { return; } - Widgets::UIEditorPropertyGridSection section = {}; - section.sectionId = BuildInspectorComponentSectionId(context.componentId); + InspectorSectionBinding section = {}; section.title = std::string(GetDisplayName()); - section.fields.push_back(BuildInspectorAssetField( - BuildInspectorComponentFieldId(context.componentId, "mesh"), + section.fields.push_back(BuildInspectorAssetFieldBinding( + "mesh", "Mesh", - meshFilter->GetMeshPath(), - "None")); + InspectorAssetFieldBindingOptions{ .emptyText = "None" }, + [meshFilter]() { return meshFilter->GetMeshPath(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + std::string_view value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::MeshFilterComponent>( + sceneRuntime, + bindingContext, + [assetId = std::string(value)]( + ::XCEngine::Components::MeshFilterComponent& typedMeshFilter) { + if (assetId.empty()) { + typedMeshFilter.ClearMesh(); + } else { + typedMeshFilter.SetMeshPath(assetId); + } + }); + })); outSections.push_back(std::move(section)); } - - bool ApplyFieldValue( - EditorSceneRuntime& sceneRuntime, - const InspectorComponentEditorContext& context, - const Widgets::UIEditorPropertyGridField& field) const override { - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "mesh") && - field.kind == Widgets::UIEditorPropertyGridFieldKind::Asset) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* meshFilter = - dynamic_cast<::XCEngine::Components::MeshFilterComponent*>(&component); - if (meshFilter == nullptr) { - return false; - } - - if (field.assetValue.assetId.empty()) { - meshFilter->ClearMesh(); - } else { - meshFilter->SetMeshPath(field.assetValue.assetId); - } - return true; - }); - } - - return false; - } }; } // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/Inspector/Components/MeshRendererInspectorComponentEditor.h b/new_editor/app/Features/Inspector/Components/MeshRendererInspectorComponentEditor.h index 04f5d3d7..00c12f03 100644 --- a/new_editor/app/Features/Inspector/Components/MeshRendererInspectorComponentEditor.h +++ b/new_editor/app/Features/Inspector/Components/MeshRendererInspectorComponentEditor.h @@ -1,7 +1,6 @@ #pragma once -#include "Features/Inspector/Components/IInspectorComponentEditor.h" -#include "Features/Inspector/Components/InspectorComponentEditorUtils.h" +#include "Features/Inspector/Components/InspectorBindingComponentEditor.h" #include #include @@ -11,7 +10,7 @@ namespace XCEngine::UI::Editor::App { -class MeshRendererInspectorComponentEditor final : public IInspectorComponentEditor { +class MeshRendererInspectorComponentEditor final : public InspectorBindingComponentEditor { public: std::string_view GetComponentTypeName() const override { return "MeshRenderer"; @@ -21,132 +20,101 @@ public: return "Mesh Renderer"; } - void BuildSections( +protected: + void BuildBindingSections( const InspectorComponentEditorContext& context, - std::vector& outSections) const override { + std::vector& outSections) const override { const auto* meshRenderer = - dynamic_cast(context.component); + ResolveInspectorComponent<::XCEngine::Components::MeshRendererComponent>(context); if (meshRenderer == nullptr) { return; } - Widgets::UIEditorPropertyGridSection section = {}; - section.sectionId = BuildInspectorComponentSectionId(context.componentId); + InspectorSectionBinding section = {}; section.title = std::string(GetDisplayName()); - section.fields.push_back(BuildInspectorBoolField( - BuildInspectorComponentFieldId(context.componentId, "cast_shadows"), + section.fields.push_back(BuildInspectorBoolFieldBinding( + "cast_shadows", "Cast Shadows", - meshRenderer->GetCastShadows())); - section.fields.push_back(BuildInspectorBoolField( - BuildInspectorComponentFieldId(context.componentId, "receive_shadows"), + [meshRenderer]() { return meshRenderer->GetCastShadows(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + bool value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::MeshRendererComponent>( + sceneRuntime, + bindingContext, + [value](::XCEngine::Components::MeshRendererComponent& typedMeshRenderer) { + typedMeshRenderer.SetCastShadows(value); + }); + })); + section.fields.push_back(BuildInspectorBoolFieldBinding( + "receive_shadows", "Receive Shadows", - meshRenderer->GetReceiveShadows())); - section.fields.push_back(BuildInspectorNumberField( - BuildInspectorComponentFieldId(context.componentId, "render_layer"), + [meshRenderer]() { return meshRenderer->GetReceiveShadows(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + bool value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::MeshRendererComponent>( + sceneRuntime, + bindingContext, + [value](::XCEngine::Components::MeshRendererComponent& typedMeshRenderer) { + typedMeshRenderer.SetReceiveShadows(value); + }); + })); + section.fields.push_back(BuildInspectorNumberFieldBinding( + "render_layer", "Render Layer", - meshRenderer->GetRenderLayer(), - 1.0, - 0.0, - 4294967295.0, - true)); + InspectorNumberFieldBindingOptions{ + .step = 1.0, + .minValue = 0.0, + .maxValue = 4294967295.0, + .integerMode = true + }, + [meshRenderer]() { + return static_cast(meshRenderer->GetRenderLayer()); + }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + double value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::MeshRendererComponent>( + sceneRuntime, + bindingContext, + [value](::XCEngine::Components::MeshRendererComponent& typedMeshRenderer) { + typedMeshRenderer.SetRenderLayer(static_cast(value)); + }); + })); - const std::size_t slotCount = GetVisibleMaterialSlotCount(context.gameObject, *meshRenderer); + const std::size_t slotCount = + GetVisibleMaterialSlotCount(context.gameObject, *meshRenderer); for (std::size_t slotIndex = 0u; slotIndex < slotCount; ++slotIndex) { - section.fields.push_back(BuildInspectorAssetField( - BuildInspectorComponentFieldId( - context.componentId, - "material_" + std::to_string(slotIndex)), - "Material " + std::to_string(slotIndex), - meshRenderer->GetMaterialPath(slotIndex), - "None")); + const std::string slotSuffix = "material_" + std::to_string(slotIndex); + const std::string slotLabel = "Material " + std::to_string(slotIndex); + section.fields.push_back(BuildInspectorAssetFieldBinding( + slotSuffix, + slotLabel, + InspectorAssetFieldBindingOptions{ .emptyText = "None" }, + [meshRenderer, slotIndex]() { + return meshRenderer->GetMaterialPath(slotIndex); + }, + [slotIndex](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + std::string_view value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::MeshRendererComponent>( + sceneRuntime, + bindingContext, + [slotIndex, assetId = std::string(value)]( + ::XCEngine::Components::MeshRendererComponent& typedMeshRenderer) { + typedMeshRenderer.SetMaterialPath(slotIndex, assetId); + }); + })); } outSections.push_back(std::move(section)); } - bool ApplyFieldValue( - EditorSceneRuntime& sceneRuntime, - const InspectorComponentEditorContext& context, - const Widgets::UIEditorPropertyGridField& field) const override { - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "cast_shadows")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* meshRenderer = - dynamic_cast<::XCEngine::Components::MeshRendererComponent*>(&component); - if (meshRenderer == nullptr || - field.kind != Widgets::UIEditorPropertyGridFieldKind::Bool) { - return false; - } - meshRenderer->SetCastShadows(field.boolValue); - return true; - }); - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "receive_shadows")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* meshRenderer = - dynamic_cast<::XCEngine::Components::MeshRendererComponent*>(&component); - if (meshRenderer == nullptr || - field.kind != Widgets::UIEditorPropertyGridFieldKind::Bool) { - return false; - } - meshRenderer->SetReceiveShadows(field.boolValue); - return true; - }); - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "render_layer") && - field.kind == Widgets::UIEditorPropertyGridFieldKind::Number) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* meshRenderer = - dynamic_cast<::XCEngine::Components::MeshRendererComponent*>(&component); - if (meshRenderer == nullptr) { - return false; - } - meshRenderer->SetRenderLayer( - static_cast(field.numberValue.value)); - return true; - }); - } - - if (field.kind == Widgets::UIEditorPropertyGridFieldKind::Asset) { - const std::string fieldPrefix = - BuildInspectorComponentFieldId(context.componentId, "material_"); - if (field.fieldId.rfind(fieldPrefix, 0u) == 0u) { - const std::string slotText = - field.fieldId.substr(fieldPrefix.size()); - std::size_t slotIndex = 0u; - try { - slotIndex = static_cast(std::stoull(slotText)); - } catch (...) { - return false; - } - - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field, slotIndex](::XCEngine::Components::Component& component) { - auto* meshRenderer = - dynamic_cast<::XCEngine::Components::MeshRendererComponent*>(&component); - if (meshRenderer == nullptr) { - return false; - } - meshRenderer->SetMaterialPath(slotIndex, field.assetValue.assetId); - return true; - }); - } - } - - return false; - } - private: static std::size_t GetVisibleMaterialSlotCount( const ::XCEngine::Components::GameObject* gameObject, diff --git a/new_editor/app/Features/Inspector/Components/RigidbodyInspectorComponentEditor.h b/new_editor/app/Features/Inspector/Components/RigidbodyInspectorComponentEditor.h index b6f59a30..36c11b30 100644 --- a/new_editor/app/Features/Inspector/Components/RigidbodyInspectorComponentEditor.h +++ b/new_editor/app/Features/Inspector/Components/RigidbodyInspectorComponentEditor.h @@ -1,13 +1,12 @@ #pragma once -#include "Features/Inspector/Components/IInspectorComponentEditor.h" -#include "Features/Inspector/Components/InspectorComponentEditorUtils.h" +#include "Features/Inspector/Components/InspectorBindingComponentEditor.h" #include namespace XCEngine::UI::Editor::App { -class RigidbodyInspectorComponentEditor final : public IInspectorComponentEditor { +class RigidbodyInspectorComponentEditor final : public InspectorBindingComponentEditor { public: std::string_view GetComponentTypeName() const override { return "Rigidbody"; @@ -17,162 +16,129 @@ public: return "Rigidbody"; } - void BuildSections( +protected: + void BuildBindingSections( const InspectorComponentEditorContext& context, - std::vector& outSections) const override { + std::vector& outSections) const override { const auto* rigidbody = - dynamic_cast(context.component); + ResolveInspectorComponent<::XCEngine::Components::RigidbodyComponent>(context); if (rigidbody == nullptr) { return; } - Widgets::UIEditorPropertyGridSection section = {}; - section.sectionId = BuildInspectorComponentSectionId(context.componentId); + InspectorSectionBinding section = {}; section.title = std::string(GetDisplayName()); - section.fields.push_back(BuildInspectorEnumField( - BuildInspectorComponentFieldId(context.componentId, "body_type"), + section.fields.push_back(BuildInspectorEnumFieldBinding( + "body_type", "Body Type", { "Static", "Dynamic", "Kinematic" }, - static_cast(rigidbody->GetBodyType()))); - section.fields.push_back(BuildInspectorNumberField( - BuildInspectorComponentFieldId(context.componentId, "mass"), + [rigidbody]() { + return static_cast(rigidbody->GetBodyType()); + }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + std::size_t value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::RigidbodyComponent>( + sceneRuntime, + bindingContext, + [value](::XCEngine::Components::RigidbodyComponent& typedRigidbody) { + typedRigidbody.SetBodyType( + static_cast<::XCEngine::Physics::PhysicsBodyType>(value)); + }); + })); + section.fields.push_back(BuildInspectorNumberFieldBinding( + "mass", "Mass", - rigidbody->GetMass(), - 0.1, - 0.0001, - 1000000.0)); - section.fields.push_back(BuildInspectorNumberField( - BuildInspectorComponentFieldId(context.componentId, "linear_damping"), + InspectorNumberFieldBindingOptions{ + .step = 0.1, + .minValue = 0.0001, + .maxValue = 1000000.0 + }, + [rigidbody]() { return rigidbody->GetMass(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + double value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::RigidbodyComponent>( + sceneRuntime, + bindingContext, + [value](::XCEngine::Components::RigidbodyComponent& typedRigidbody) { + typedRigidbody.SetMass(static_cast(value)); + }); + })); + section.fields.push_back(BuildInspectorNumberFieldBinding( + "linear_damping", "Linear Damping", - rigidbody->GetLinearDamping(), - 0.05, - 0.0, - 1000000.0)); - section.fields.push_back(BuildInspectorNumberField( - BuildInspectorComponentFieldId(context.componentId, "angular_damping"), + InspectorNumberFieldBindingOptions{ + .step = 0.05, + .minValue = 0.0, + .maxValue = 1000000.0 + }, + [rigidbody]() { return rigidbody->GetLinearDamping(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + double value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::RigidbodyComponent>( + sceneRuntime, + bindingContext, + [value](::XCEngine::Components::RigidbodyComponent& typedRigidbody) { + typedRigidbody.SetLinearDamping(static_cast(value)); + }); + })); + section.fields.push_back(BuildInspectorNumberFieldBinding( + "angular_damping", "Angular Damping", - rigidbody->GetAngularDamping(), - 0.05, - 0.0, - 1000000.0)); - section.fields.push_back(BuildInspectorBoolField( - BuildInspectorComponentFieldId(context.componentId, "use_gravity"), + InspectorNumberFieldBindingOptions{ + .step = 0.05, + .minValue = 0.0, + .maxValue = 1000000.0 + }, + [rigidbody]() { return rigidbody->GetAngularDamping(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + double value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::RigidbodyComponent>( + sceneRuntime, + bindingContext, + [value](::XCEngine::Components::RigidbodyComponent& typedRigidbody) { + typedRigidbody.SetAngularDamping(static_cast(value)); + }); + })); + section.fields.push_back(BuildInspectorBoolFieldBinding( + "use_gravity", "Use Gravity", - rigidbody->GetUseGravity())); - section.fields.push_back(BuildInspectorBoolField( - BuildInspectorComponentFieldId(context.componentId, "enable_ccd"), + [rigidbody]() { return rigidbody->GetUseGravity(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + bool value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::RigidbodyComponent>( + sceneRuntime, + bindingContext, + [value](::XCEngine::Components::RigidbodyComponent& typedRigidbody) { + typedRigidbody.SetUseGravity(value); + }); + })); + section.fields.push_back(BuildInspectorBoolFieldBinding( + "enable_ccd", "Enable CCD", - rigidbody->GetEnableCCD())); + [rigidbody]() { return rigidbody->GetEnableCCD(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + bool value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::RigidbodyComponent>( + sceneRuntime, + bindingContext, + [value](::XCEngine::Components::RigidbodyComponent& typedRigidbody) { + typedRigidbody.SetEnableCCD(value); + }); + })); outSections.push_back(std::move(section)); } - - bool ApplyFieldValue( - EditorSceneRuntime& sceneRuntime, - const InspectorComponentEditorContext& context, - const Widgets::UIEditorPropertyGridField& field) const override { - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "body_type")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* rigidbody = - dynamic_cast<::XCEngine::Components::RigidbodyComponent*>(&component); - if (rigidbody == nullptr || - field.kind != Widgets::UIEditorPropertyGridFieldKind::Enum) { - return false; - } - rigidbody->SetBodyType( - static_cast<::XCEngine::Physics::PhysicsBodyType>( - field.enumValue.selectedIndex)); - return true; - }); - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "use_gravity")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* rigidbody = - dynamic_cast<::XCEngine::Components::RigidbodyComponent*>(&component); - if (rigidbody == nullptr || - field.kind != Widgets::UIEditorPropertyGridFieldKind::Bool) { - return false; - } - rigidbody->SetUseGravity(field.boolValue); - return true; - }); - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "enable_ccd")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* rigidbody = - dynamic_cast<::XCEngine::Components::RigidbodyComponent*>(&component); - if (rigidbody == nullptr || - field.kind != Widgets::UIEditorPropertyGridFieldKind::Bool) { - return false; - } - rigidbody->SetEnableCCD(field.boolValue); - return true; - }); - } - - if (field.kind != Widgets::UIEditorPropertyGridFieldKind::Number) { - return false; - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "mass")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* rigidbody = - dynamic_cast<::XCEngine::Components::RigidbodyComponent*>(&component); - if (rigidbody == nullptr) { - return false; - } - rigidbody->SetMass(static_cast(field.numberValue.value)); - return true; - }); - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "linear_damping")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* rigidbody = - dynamic_cast<::XCEngine::Components::RigidbodyComponent*>(&component); - if (rigidbody == nullptr) { - return false; - } - rigidbody->SetLinearDamping( - static_cast(field.numberValue.value)); - return true; - }); - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "angular_damping")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* rigidbody = - dynamic_cast<::XCEngine::Components::RigidbodyComponent*>(&component); - if (rigidbody == nullptr) { - return false; - } - rigidbody->SetAngularDamping( - static_cast(field.numberValue.value)); - return true; - }); - } - - return false; - } }; } // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/Inspector/Components/SphereColliderInspectorComponentEditor.h b/new_editor/app/Features/Inspector/Components/SphereColliderInspectorComponentEditor.h index 82eba96d..0390f160 100644 --- a/new_editor/app/Features/Inspector/Components/SphereColliderInspectorComponentEditor.h +++ b/new_editor/app/Features/Inspector/Components/SphereColliderInspectorComponentEditor.h @@ -6,7 +6,7 @@ namespace XCEngine::UI::Editor::App { -class SphereColliderInspectorComponentEditor final : public IInspectorComponentEditor { +class SphereColliderInspectorComponentEditor final : public InspectorBindingComponentEditor { public: std::string_view GetComponentTypeName() const override { return "SphereCollider"; @@ -16,55 +16,41 @@ public: return "Sphere Collider"; } - void BuildSections( +protected: + void BuildBindingSections( const InspectorComponentEditorContext& context, - std::vector& outSections) const override { + std::vector& outSections) const override { const auto* collider = - dynamic_cast(context.component); + ResolveInspectorComponent<::XCEngine::Components::SphereColliderComponent>(context); if (collider == nullptr) { return; } - Widgets::UIEditorPropertyGridSection section = {}; - section.sectionId = BuildInspectorComponentSectionId(context.componentId); + InspectorSectionBinding section = {}; section.title = std::string(GetDisplayName()); - AppendColliderBaseFields(context.componentId, *collider, section.fields); - section.fields.push_back(BuildInspectorNumberField( - BuildInspectorComponentFieldId(context.componentId, "radius"), + AppendColliderBaseBindings(section.fields, *collider); + section.fields.push_back(BuildInspectorNumberFieldBinding( + "radius", "Radius", - collider->GetRadius(), - 0.05, - 0.0001, - 1000000.0)); + InspectorNumberFieldBindingOptions{ + .step = 0.05, + .minValue = 0.0001, + .maxValue = 1000000.0 + }, + [collider]() { return collider->GetRadius(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + double value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::SphereColliderComponent>( + sceneRuntime, + bindingContext, + [value](::XCEngine::Components::SphereColliderComponent& typedCollider) { + typedCollider.SetRadius(static_cast(value)); + }); + })); outSections.push_back(std::move(section)); } - - bool ApplyFieldValue( - EditorSceneRuntime& sceneRuntime, - const InspectorComponentEditorContext& context, - const Widgets::UIEditorPropertyGridField& field) const override { - if (ApplyColliderBaseFieldValue(sceneRuntime, context, field)) { - return true; - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "radius")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* collider = - dynamic_cast<::XCEngine::Components::SphereColliderComponent*>(&component); - if (collider == nullptr || - field.kind != Widgets::UIEditorPropertyGridFieldKind::Number) { - return false; - } - collider->SetRadius(static_cast(field.numberValue.value)); - return true; - }); - } - - return false; - } }; } // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/Inspector/Components/TransformInspectorComponentEditor.cpp b/new_editor/app/Features/Inspector/Components/TransformInspectorComponentEditor.cpp index a1aca63b..5dad9a4d 100644 --- a/new_editor/app/Features/Inspector/Components/TransformInspectorComponentEditor.cpp +++ b/new_editor/app/Features/Inspector/Components/TransformInspectorComponentEditor.cpp @@ -4,64 +4,6 @@ namespace XCEngine::UI::Editor::App { -namespace { - -using ::XCEngine::Components::TransformComponent; -using ::XCEngine::Math::Vector3; -using Widgets::UIEditorPropertyGridField; -using Widgets::UIEditorPropertyGridFieldKind; -using Widgets::UIEditorPropertyGridSection; - -std::string BuildTransformFieldId( - std::string_view componentId, - std::string_view fieldName) { - std::string fieldId = "component."; - fieldId += componentId; - fieldId += '.'; - fieldId += fieldName; - return fieldId; -} - -std::string BuildTransformSectionId(std::string_view componentId) { - std::string sectionId = "component."; - sectionId += componentId; - return sectionId; -} - -std::array ToPropertyValues(const Vector3& value) { - return { - static_cast(value.x), - static_cast(value.y), - static_cast(value.z) - }; -} - -Vector3 ToVector3(const UIEditorPropertyGridField& field) { - return Vector3( - static_cast(field.vector3Value.values[0]), - static_cast(field.vector3Value.values[1]), - static_cast(field.vector3Value.values[2])); -} - -UIEditorPropertyGridField BuildTransformVectorField( - std::string fieldId, - std::string label, - const Vector3& value, - double step) { - UIEditorPropertyGridField field = {}; - field.fieldId = std::move(fieldId); - field.label = std::move(label); - field.kind = UIEditorPropertyGridFieldKind::Vector3; - field.vector3Value.values = ToPropertyValues(value); - field.vector3Value.step = step; - field.vector3Value.minValue = -1000000.0; - field.vector3Value.maxValue = 1000000.0; - field.vector3Value.integerMode = false; - return field; -} - -} // namespace - std::string_view TransformInspectorComponentEditor::GetComponentTypeName() const { return "Transform"; } @@ -70,68 +12,56 @@ std::string_view TransformInspectorComponentEditor::GetDisplayName() const { return "Transform"; } -void TransformInspectorComponentEditor::BuildSections( +void TransformInspectorComponentEditor::BuildBindingSections( const InspectorComponentEditorContext& context, - std::vector& outSections) const { + std::vector& outSections) const { const auto* transform = - dynamic_cast(context.component); + ResolveInspectorComponent<::XCEngine::Components::TransformComponent>(context); if (transform == nullptr) { return; } - UIEditorPropertyGridSection section = {}; - section.sectionId = BuildTransformSectionId(context.componentId); + InspectorSectionBinding section = {}; section.title = std::string(GetDisplayName()); - section.fields.push_back(BuildTransformVectorField( - BuildTransformFieldId(context.componentId, "position"), + section.fields.push_back(BuildInspectorVector3FieldBinding( + "position", "Position", - transform->GetLocalPosition(), - 0.1)); - section.fields.push_back(BuildTransformVectorField( - BuildTransformFieldId(context.componentId, "rotation"), + InspectorVector3FieldBindingOptions{ .step = 0.1 }, + [transform]() { return transform->GetLocalPosition(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + const ::XCEngine::Math::Vector3& value) { + return sceneRuntime.SetSelectedTransformLocalPosition( + bindingContext.componentId, + value); + })); + section.fields.push_back(BuildInspectorVector3FieldBinding( + "rotation", "Rotation", - transform->GetLocalEulerAngles(), - 1.0)); - section.fields.push_back(BuildTransformVectorField( - BuildTransformFieldId(context.componentId, "scale"), + InspectorVector3FieldBindingOptions{ .step = 1.0 }, + [transform]() { return transform->GetLocalEulerAngles(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + const ::XCEngine::Math::Vector3& value) { + return sceneRuntime.SetSelectedTransformLocalEulerAngles( + bindingContext.componentId, + value); + })); + section.fields.push_back(BuildInspectorVector3FieldBinding( + "scale", "Scale", - transform->GetLocalScale(), - 0.1)); + InspectorVector3FieldBindingOptions{ .step = 0.1 }, + [transform]() { return transform->GetLocalScale(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + const ::XCEngine::Math::Vector3& value) { + return sceneRuntime.SetSelectedTransformLocalScale( + bindingContext.componentId, + value); + })); outSections.push_back(std::move(section)); } -bool TransformInspectorComponentEditor::ApplyFieldValue( - EditorSceneRuntime& sceneRuntime, - const InspectorComponentEditorContext& context, - const UIEditorPropertyGridField& field) const { - if (field.kind != UIEditorPropertyGridFieldKind::Vector3) { - return false; - } - - if (field.fieldId == - BuildTransformFieldId(context.componentId, "position")) { - return sceneRuntime.SetSelectedTransformLocalPosition( - context.componentId, - ToVector3(field)); - } - - if (field.fieldId == - BuildTransformFieldId(context.componentId, "rotation")) { - return sceneRuntime.SetSelectedTransformLocalEulerAngles( - context.componentId, - ToVector3(field)); - } - - if (field.fieldId == - BuildTransformFieldId(context.componentId, "scale")) { - return sceneRuntime.SetSelectedTransformLocalScale( - context.componentId, - ToVector3(field)); - } - - return false; -} - bool TransformInspectorComponentEditor::CanRemove( const InspectorComponentEditorContext& context) const { (void)context; diff --git a/new_editor/app/Features/Inspector/Components/TransformInspectorComponentEditor.h b/new_editor/app/Features/Inspector/Components/TransformInspectorComponentEditor.h index 39faa660..1d6c9bf7 100644 --- a/new_editor/app/Features/Inspector/Components/TransformInspectorComponentEditor.h +++ b/new_editor/app/Features/Inspector/Components/TransformInspectorComponentEditor.h @@ -1,25 +1,21 @@ #pragma once -#include "Features/Inspector/Components/IInspectorComponentEditor.h" +#include "Features/Inspector/Components/InspectorBindingComponentEditor.h" namespace XCEngine::UI::Editor::App { -class TransformInspectorComponentEditor final : public IInspectorComponentEditor { +class TransformInspectorComponentEditor final : public InspectorBindingComponentEditor { public: std::string_view GetComponentTypeName() const override; std::string_view GetDisplayName() const override; - void BuildSections( - const InspectorComponentEditorContext& context, - std::vector& outSections) const override; - - bool ApplyFieldValue( - EditorSceneRuntime& sceneRuntime, - const InspectorComponentEditorContext& context, - const Widgets::UIEditorPropertyGridField& field) const override; - bool CanRemove( const InspectorComponentEditorContext& context) const override; + +protected: + void BuildBindingSections( + const InspectorComponentEditorContext& context, + std::vector& outSections) const override; }; } // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/Inspector/Components/VolumeRendererInspectorComponentEditor.h b/new_editor/app/Features/Inspector/Components/VolumeRendererInspectorComponentEditor.h index da53b2e2..876f0752 100644 --- a/new_editor/app/Features/Inspector/Components/VolumeRendererInspectorComponentEditor.h +++ b/new_editor/app/Features/Inspector/Components/VolumeRendererInspectorComponentEditor.h @@ -1,13 +1,12 @@ #pragma once -#include "Features/Inspector/Components/IInspectorComponentEditor.h" -#include "Features/Inspector/Components/InspectorComponentEditorUtils.h" +#include "Features/Inspector/Components/InspectorBindingComponentEditor.h" #include namespace XCEngine::UI::Editor::App { -class VolumeRendererInspectorComponentEditor final : public IInspectorComponentEditor { +class VolumeRendererInspectorComponentEditor final : public InspectorBindingComponentEditor { public: std::string_view GetComponentTypeName() const override { return "VolumeRenderer"; @@ -17,119 +16,92 @@ public: return "Volume Renderer"; } - void BuildSections( +protected: + void BuildBindingSections( const InspectorComponentEditorContext& context, - std::vector& outSections) const override { + std::vector& outSections) const override { const auto* volumeRenderer = - dynamic_cast(context.component); + ResolveInspectorComponent<::XCEngine::Components::VolumeRendererComponent>(context); if (volumeRenderer == nullptr) { return; } - Widgets::UIEditorPropertyGridSection section = {}; - section.sectionId = BuildInspectorComponentSectionId(context.componentId); + InspectorSectionBinding section = {}; section.title = std::string(GetDisplayName()); - section.fields.push_back(BuildInspectorAssetField( - BuildInspectorComponentFieldId(context.componentId, "volume_field"), + section.fields.push_back(BuildInspectorAssetFieldBinding( + "volume_field", "Volume Field", - volumeRenderer->GetVolumeFieldPath(), - "None")); - section.fields.push_back(BuildInspectorAssetField( - BuildInspectorComponentFieldId(context.componentId, "material"), + InspectorAssetFieldBindingOptions{ .emptyText = "None" }, + [volumeRenderer]() { return volumeRenderer->GetVolumeFieldPath(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + std::string_view value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::VolumeRendererComponent>( + sceneRuntime, + bindingContext, + [assetId = std::string(value)]( + ::XCEngine::Components::VolumeRendererComponent& typedVolumeRenderer) { + if (assetId.empty()) { + typedVolumeRenderer.ClearVolumeField(); + } else { + typedVolumeRenderer.SetVolumeFieldPath(assetId); + } + }); + })); + section.fields.push_back(BuildInspectorAssetFieldBinding( + "material", "Material", - volumeRenderer->GetMaterialPath(), - "None")); - section.fields.push_back(BuildInspectorBoolField( - BuildInspectorComponentFieldId(context.componentId, "cast_shadows"), + InspectorAssetFieldBindingOptions{ .emptyText = "None" }, + [volumeRenderer]() { return volumeRenderer->GetMaterialPath(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + std::string_view value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::VolumeRendererComponent>( + sceneRuntime, + bindingContext, + [assetId = std::string(value)]( + ::XCEngine::Components::VolumeRendererComponent& typedVolumeRenderer) { + if (assetId.empty()) { + typedVolumeRenderer.ClearMaterial(); + } else { + typedVolumeRenderer.SetMaterialPath(assetId); + } + }); + })); + section.fields.push_back(BuildInspectorBoolFieldBinding( + "cast_shadows", "Cast Shadows", - volumeRenderer->GetCastShadows())); - section.fields.push_back(BuildInspectorBoolField( - BuildInspectorComponentFieldId(context.componentId, "receive_shadows"), + [volumeRenderer]() { return volumeRenderer->GetCastShadows(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + bool value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::VolumeRendererComponent>( + sceneRuntime, + bindingContext, + [value](::XCEngine::Components::VolumeRendererComponent& typedVolumeRenderer) { + typedVolumeRenderer.SetCastShadows(value); + }); + })); + section.fields.push_back(BuildInspectorBoolFieldBinding( + "receive_shadows", "Receive Shadows", - volumeRenderer->GetReceiveShadows())); + [volumeRenderer]() { return volumeRenderer->GetReceiveShadows(); }, + [](EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& bindingContext, + bool value) { + return MutateSelectedInspectorComponent< + ::XCEngine::Components::VolumeRendererComponent>( + sceneRuntime, + bindingContext, + [value](::XCEngine::Components::VolumeRendererComponent& typedVolumeRenderer) { + typedVolumeRenderer.SetReceiveShadows(value); + }); + })); outSections.push_back(std::move(section)); } - - bool ApplyFieldValue( - EditorSceneRuntime& sceneRuntime, - const InspectorComponentEditorContext& context, - const Widgets::UIEditorPropertyGridField& field) const override { - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "cast_shadows")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* volumeRenderer = - dynamic_cast<::XCEngine::Components::VolumeRendererComponent*>(&component); - if (volumeRenderer == nullptr || - field.kind != Widgets::UIEditorPropertyGridFieldKind::Bool) { - return false; - } - volumeRenderer->SetCastShadows(field.boolValue); - return true; - }); - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "receive_shadows")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* volumeRenderer = - dynamic_cast<::XCEngine::Components::VolumeRendererComponent*>(&component); - if (volumeRenderer == nullptr || - field.kind != Widgets::UIEditorPropertyGridFieldKind::Bool) { - return false; - } - volumeRenderer->SetReceiveShadows(field.boolValue); - return true; - }); - } - - if (field.kind == Widgets::UIEditorPropertyGridFieldKind::Asset) { - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "volume_field")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* volumeRenderer = - dynamic_cast<::XCEngine::Components::VolumeRendererComponent*>(&component); - if (volumeRenderer == nullptr) { - return false; - } - - if (field.assetValue.assetId.empty()) { - volumeRenderer->ClearVolumeField(); - } else { - volumeRenderer->SetVolumeFieldPath(field.assetValue.assetId); - } - return true; - }); - } - - if (field.fieldId == - BuildInspectorComponentFieldId(context.componentId, "material")) { - return sceneRuntime.ApplySelectedComponentMutation( - context.componentId, - [&field](::XCEngine::Components::Component& component) { - auto* volumeRenderer = - dynamic_cast<::XCEngine::Components::VolumeRendererComponent*>(&component); - if (volumeRenderer == nullptr) { - return false; - } - - if (field.assetValue.assetId.empty()) { - volumeRenderer->ClearMaterial(); - } else { - volumeRenderer->SetMaterialPath(field.assetValue.assetId); - } - return true; - }); - } - } - - return false; - } }; } // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/Inspector/InspectorPanel.cpp b/new_editor/app/Features/Inspector/InspectorPanel.cpp index c05455ee..a5950200 100644 --- a/new_editor/app/Features/Inspector/InspectorPanel.cpp +++ b/new_editor/app/Features/Inspector/InspectorPanel.cpp @@ -1,6 +1,8 @@ #include "InspectorPanel.h" +#include "Composition/EditorContext.h" #include "Composition/EditorPanelIds.h" +#include "State/EditorColorPickerToolState.h" #include #include #include @@ -33,6 +35,21 @@ constexpr UIColor kTitleColor(0.930f, 0.930f, 0.930f, 1.0f); constexpr UIColor kSubtitleColor(0.660f, 0.660f, 0.660f, 1.0f); constexpr UIColor kSurfaceColor(0.10f, 0.10f, 0.10f, 1.0f); +float ResolveInspectorHorizontalPadding( + const InspectorPresentationModel& presentation) { + return presentation.showHeader ? kPanelPadding : 0.0f; +} + +float ResolveInspectorTopPadding( + const InspectorPresentationModel& presentation) { + return presentation.showHeader ? kPanelPadding : 0.0f; +} + +float ResolveInspectorBottomPadding( + const InspectorPresentationModel& presentation) { + return presentation.showHeader ? kPanelPadding : 0.0f; +} + bool ContainsPoint(const UIRect& rect, const UIPoint& point) { return point.x >= rect.x && point.x <= rect.x + rect.width && @@ -40,6 +57,13 @@ bool ContainsPoint(const UIRect& rect, const UIPoint& point) { point.y <= rect.y + rect.height; } +bool AreColorsEqual(const UIColor& lhs, const UIColor& rhs) { + return lhs.r == rhs.r && + lhs.g == rhs.g && + lhs.b == rhs.b && + lhs.a == rhs.a; +} + float ResolveTextTop(float rectY, float rectHeight, float fontSize) { const float lineHeight = fontSize * 1.6f; return rectY + std::floor((rectHeight - lineHeight) * 0.5f); @@ -84,11 +108,15 @@ void InspectorPanel::SetCommandFocusService( void InspectorPanel::ResetPanelState() { m_visible = false; m_bounds = {}; + m_sceneRuntime = nullptr; m_subject = {}; m_subjectKey.clear(); m_presentation = {}; m_gridFrame = {}; m_knownSectionIds.clear(); + m_lastSceneSelectionStamp = 0u; + m_lastProjectSelectionStamp = 0u; + m_lastSceneInspectorRevision = 0u; ResetInteractionState(); } @@ -98,6 +126,7 @@ void InspectorPanel::ResetInteractionState() { m_propertyEditModel = {}; m_interactionState = {}; m_gridFrame = {}; + m_lastAppliedColorPickerRevision = 0u; } void InspectorPanel::SyncExpansionState(bool subjectChanged) { @@ -133,7 +162,7 @@ void InspectorPanel::SyncSelectionState() { m_propertyEditModel.GetActiveFieldId()) .IsValid()) { m_propertyEditModel.CancelEdit(); - m_interactionState.textInputState = {}; + m_interactionState.editableFieldSession = {}; } if (!m_interactionState.propertyGridState.popupFieldId.empty() && @@ -157,13 +186,25 @@ std::string InspectorPanel::BuildSubjectKey() const { } } +float InspectorPanel::ResolveHeaderHeight() const { + if (!m_presentation.showHeader) { + return 0.0f; + } + + return kTitleHeight + kSubtitleHeight + kHeaderGap; +} + UIRect InspectorPanel::BuildGridBounds() const { - const float x = m_bounds.x + kPanelPadding; - const float width = (std::max)(m_bounds.width - kPanelPadding * 2.0f, 0.0f); - const float y = - m_bounds.y + kPanelPadding + kTitleHeight + kSubtitleHeight + kHeaderGap; + const float horizontalPadding = + ResolveInspectorHorizontalPadding(m_presentation); + const float topPadding = ResolveInspectorTopPadding(m_presentation); + const float bottomPadding = ResolveInspectorBottomPadding(m_presentation); + const float x = m_bounds.x + horizontalPadding; + const float width = + (std::max)(m_bounds.width - horizontalPadding * 2.0f, 0.0f); + const float y = m_bounds.y + topPadding + ResolveHeaderHeight(); const float height = - (std::max)(m_bounds.y + m_bounds.height - kPanelPadding - y, 0.0f); + (std::max)(m_bounds.y + m_bounds.height - bottomPadding - y, 0.0f); return UIRect(x, y, width, height); } @@ -209,6 +250,193 @@ const InspectorPresentationComponentBinding* InspectorPanel::FindSelectedCompone return nullptr; } +const Widgets::UIEditorPropertyGridField* InspectorPanel::FindField( + std::string_view fieldId) const { + const auto location = + Widgets::FindUIEditorPropertyGridFieldLocation(m_presentation.sections, fieldId); + if (!location.IsValid() || + location.sectionIndex >= m_presentation.sections.size() || + location.fieldIndex >= m_presentation.sections[location.sectionIndex].fields.size()) { + return nullptr; + } + + return &m_presentation.sections[location.sectionIndex].fields[location.fieldIndex]; +} + +Widgets::UIEditorPropertyGridField* InspectorPanel::FindMutableField( + std::string_view fieldId) { + const auto location = + Widgets::FindUIEditorPropertyGridFieldLocation(m_presentation.sections, fieldId); + if (!location.IsValid() || + location.sectionIndex >= m_presentation.sections.size() || + location.fieldIndex >= m_presentation.sections[location.sectionIndex].fields.size()) { + return nullptr; + } + + return &m_presentation.sections[location.sectionIndex].fields[location.fieldIndex]; +} + +void InspectorPanel::CapturePresentationStamps(const EditorContext& context) { + m_lastSceneSelectionStamp = context.GetSceneRuntime().GetSelectionStamp(); + m_lastProjectSelectionStamp = context.GetProjectRuntime().GetSelectionStamp(); + m_lastSceneInspectorRevision = context.GetSceneRuntime().GetInspectorRevision(); +} + +void InspectorPanel::RebuildPresentation( + EditorContext& context, + bool subjectChanged) { + m_presentation = BuildInspectorPresentationModel( + m_subject, + context.GetSceneRuntime(), + InspectorComponentEditorRegistry::Get()); + CapturePresentationStamps(context); + SyncExpansionState(subjectChanged); + SyncSelectionState(); +} + +void InspectorPanel::ForceResyncPresentation(EditorContext& context) { + if (m_subject.kind != InspectorSubjectKind::SceneObject) { + RebuildPresentation(context, false); + return; + } + + const std::string structureSignature = BuildInspectorStructureSignature( + m_subject, + context.GetSceneRuntime(), + InspectorComponentEditorRegistry::Get()); + if (structureSignature != m_presentation.structureSignature) { + RebuildPresentation(context, false); + return; + } + + const InspectorPresentationSyncResult syncResult = + SyncInspectorPresentationModelValues( + m_presentation, + m_subject, + context.GetSceneRuntime(), + InspectorComponentEditorRegistry::Get()); + if (!syncResult.success) { + RebuildPresentation(context, false); + return; + } + + m_presentation.structureSignature = structureSignature; + CapturePresentationStamps(context); + SyncSelectionState(); +} + +void InspectorPanel::RefreshPresentation( + EditorContext& context, + bool subjectChanged) { + if (subjectChanged) { + RebuildPresentation(context, true); + return; + } + + switch (m_subject.kind) { + case InspectorSubjectKind::ProjectAsset: + if (m_lastProjectSelectionStamp != + context.GetProjectRuntime().GetSelectionStamp()) { + RebuildPresentation(context, false); + } + return; + + case InspectorSubjectKind::SceneObject: { + const EditorSceneRuntime& sceneRuntime = context.GetSceneRuntime(); + if (m_lastSceneSelectionStamp != sceneRuntime.GetSelectionStamp()) { + RebuildPresentation(context, false); + return; + } + + if (m_lastSceneInspectorRevision == sceneRuntime.GetInspectorRevision()) { + return; + } + + const std::string structureSignature = BuildInspectorStructureSignature( + m_subject, + sceneRuntime, + InspectorComponentEditorRegistry::Get()); + if (structureSignature != m_presentation.structureSignature) { + RebuildPresentation(context, false); + return; + } + + const InspectorPresentationSyncResult syncResult = + SyncInspectorPresentationModelValues( + m_presentation, + m_subject, + sceneRuntime, + InspectorComponentEditorRegistry::Get()); + if (!syncResult.success) { + RebuildPresentation(context, false); + return; + } + + m_presentation.structureSignature = structureSignature; + CapturePresentationStamps(context); + SyncSelectionState(); + return; + } + + case InspectorSubjectKind::None: + default: + return; + } +} + +bool InspectorPanel::ApplyColorPickerToolValue(EditorContext& context) { + EditorColorPickerToolState& toolState = context.GetColorPickerToolState(); + if (m_sceneRuntime == nullptr || + !toolState.active || + toolState.revision == m_lastAppliedColorPickerRevision || + !IsEditorColorPickerToolTarget( + toolState, + m_subjectKey, + toolState.inspectorTarget.fieldId)) { + return false; + } + + Widgets::UIEditorPropertyGridField* field = + FindMutableField(toolState.inspectorTarget.fieldId); + if (field == nullptr || field->kind != Widgets::UIEditorPropertyGridFieldKind::Color) { + return false; + } + + if (AreColorsEqual(field->colorValue.value, toolState.color)) { + m_lastAppliedColorPickerRevision = toolState.revision; + return false; + } + + field->colorValue.value = toolState.color; + const bool applied = ApplyChangedField(field->fieldId); + m_lastAppliedColorPickerRevision = toolState.revision; + if (!applied) { + ForceResyncPresentation(context); + return false; + } + + RefreshPresentation(context, false); + return true; +} + +void InspectorPanel::RequestColorPicker( + EditorContext& context, + std::string_view fieldId) { + const Widgets::UIEditorPropertyGridField* field = FindField(fieldId); + if (field == nullptr || + field->kind != Widgets::UIEditorPropertyGridFieldKind::Color || + field->readOnly) { + return; + } + + OpenEditorColorPickerToolForInspectorField( + context.GetColorPickerToolState(), + m_subjectKey, + field->fieldId, + field->colorValue.value, + field->colorValue.showAlpha); +} + bool InspectorPanel::ApplyChangedField(std::string_view fieldId) { if (m_sceneRuntime == nullptr || m_subject.kind != InspectorSubjectKind::SceneObject) { @@ -264,8 +492,7 @@ bool InspectorPanel::ApplyChangedField(std::string_view fieldId) { } void InspectorPanel::Update( - const EditorSession& session, - EditorSceneRuntime& sceneRuntime, + EditorContext& context, const UIEditorPanelContentHostFrame& contentHostFrame, const std::vector& inputEvents, const PanelInputContext& inputContext) { @@ -278,8 +505,8 @@ void InspectorPanel::Update( m_visible = true; m_bounds = panelState->bounds; - m_sceneRuntime = &sceneRuntime; - m_subject = BuildInspectorSubject(session, sceneRuntime); + m_sceneRuntime = &context.GetSceneRuntime(); + m_subject = BuildInspectorSubject(context.GetSession(), context.GetSceneRuntime()); const std::string nextSubjectKey = BuildSubjectKey(); const bool subjectChanged = m_subjectKey != nextSubjectKey; @@ -288,12 +515,8 @@ void InspectorPanel::Update( ResetInteractionState(); } - m_presentation = BuildInspectorPresentationModel( - m_subject, - sceneRuntime, - InspectorComponentEditorRegistry::Get()); - SyncExpansionState(subjectChanged); - SyncSelectionState(); + RefreshPresentation(context, subjectChanged); + ApplyColorPickerToolValue(context); if (m_presentation.sections.empty()) { m_gridFrame = {}; @@ -327,16 +550,18 @@ void InspectorPanel::Update( m_presentation.sections, filteredEvents, ::XCEngine::UI::Editor::GetUIEditorFixedPropertyGridMetrics()); + if (m_gridFrame.result.pickerRequested && + !m_gridFrame.result.requestedFieldId.empty()) { + RequestColorPicker(context, m_gridFrame.result.requestedFieldId); + } if (m_gridFrame.result.fieldValueChanged && - !m_gridFrame.result.changedFieldId.empty() && - !ApplyChangedField(m_gridFrame.result.changedFieldId)) { - m_presentation = BuildInspectorPresentationModel( - m_subject, - sceneRuntime, - InspectorComponentEditorRegistry::Get()); - SyncExpansionState(false); - SyncSelectionState(); + !m_gridFrame.result.changedFieldId.empty()) { + if (ApplyChangedField(m_gridFrame.result.changedFieldId)) { + RefreshPresentation(context, false); + } else { + ForceResyncPresentation(context); + } } } @@ -347,26 +572,32 @@ void InspectorPanel::Append(UIDrawList& drawList) const { drawList.AddFilledRect(m_bounds, kSurfaceColor); - const float contentX = m_bounds.x + kPanelPadding; - const float contentWidth = (std::max)(m_bounds.width - kPanelPadding * 2.0f, 0.0f); - float nextY = m_bounds.y + kPanelPadding; + const float horizontalPadding = + ResolveInspectorHorizontalPadding(m_presentation); + const float topPadding = ResolveInspectorTopPadding(m_presentation); + const float contentX = m_bounds.x + horizontalPadding; + const float contentWidth = + (std::max)(m_bounds.width - horizontalPadding * 2.0f, 0.0f); + float nextY = m_bounds.y + topPadding; - const UIRect titleRect(contentX, nextY, contentWidth, kTitleHeight); - drawList.AddText( - UIPoint(titleRect.x, ResolveTextTop(titleRect.y, titleRect.height, kTitleFontSize)), - m_presentation.title, - kTitleColor, - kTitleFontSize); - nextY += titleRect.height; + if (m_presentation.showHeader) { + const UIRect titleRect(contentX, nextY, contentWidth, kTitleHeight); + drawList.AddText( + UIPoint(titleRect.x, ResolveTextTop(titleRect.y, titleRect.height, kTitleFontSize)), + m_presentation.title, + kTitleColor, + kTitleFontSize); + nextY += titleRect.height; - const UIRect subtitleRect(contentX, nextY, contentWidth, kSubtitleHeight); - drawList.AddText( - UIPoint( - subtitleRect.x, - ResolveTextTop(subtitleRect.y, subtitleRect.height, kSubtitleFontSize)), - m_presentation.subtitle, - kSubtitleColor, - kSubtitleFontSize); + const UIRect subtitleRect(contentX, nextY, contentWidth, kSubtitleHeight); + drawList.AddText( + UIPoint( + subtitleRect.x, + ResolveTextTop(subtitleRect.y, subtitleRect.height, kSubtitleFontSize)), + m_presentation.subtitle, + kSubtitleColor, + kSubtitleFontSize); + } if (m_presentation.sections.empty()) { return; diff --git a/new_editor/app/Features/Inspector/InspectorPanel.h b/new_editor/app/Features/Inspector/InspectorPanel.h index 3e57f774..644c71aa 100644 --- a/new_editor/app/Features/Inspector/InspectorPanel.h +++ b/new_editor/app/Features/Inspector/InspectorPanel.h @@ -13,6 +13,7 @@ #include #include +#include #include #include #include @@ -20,14 +21,14 @@ namespace XCEngine::UI::Editor::App { class EditorCommandFocusService; +class EditorContext; class EditorSceneRuntime; class InspectorPanel final : public EditorEditCommandRoute { public: void SetCommandFocusService(EditorCommandFocusService* commandFocusService); void Update( - const EditorSession& session, - EditorSceneRuntime& sceneRuntime, + EditorContext& context, const UIEditorPanelContentHostFrame& contentHostFrame, const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents, const PanelInputContext& inputContext); @@ -46,11 +47,20 @@ private: void SyncExpansionState(bool subjectChanged); void SyncSelectionState(); std::string BuildSubjectKey() const; + float ResolveHeaderHeight() const; ::XCEngine::UI::UIRect BuildGridBounds() const; void ClaimCommandFocus( const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents, bool allowInteraction); const InspectorPresentationComponentBinding* FindSelectedComponentBinding() const; + const Widgets::UIEditorPropertyGridField* FindField(std::string_view fieldId) const; + Widgets::UIEditorPropertyGridField* FindMutableField(std::string_view fieldId); + void CapturePresentationStamps(const EditorContext& context); + void RebuildPresentation(EditorContext& context, bool subjectChanged); + void RefreshPresentation(EditorContext& context, bool subjectChanged); + void ForceResyncPresentation(EditorContext& context); + bool ApplyColorPickerToolValue(EditorContext& context); + void RequestColorPicker(EditorContext& context, std::string_view fieldId); bool ApplyChangedField(std::string_view fieldId); EditorCommandFocusService* m_commandFocusService = nullptr; @@ -66,6 +76,10 @@ private: UIEditorPropertyGridInteractionState m_interactionState = {}; UIEditorPropertyGridInteractionFrame m_gridFrame = {}; std::unordered_set m_knownSectionIds = {}; + std::uint64_t m_lastSceneSelectionStamp = 0u; + std::uint64_t m_lastProjectSelectionStamp = 0u; + std::uint64_t m_lastSceneInspectorRevision = 0u; + std::uint64_t m_lastAppliedColorPickerRevision = 0u; }; } // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/Inspector/InspectorPresentationModel.cpp b/new_editor/app/Features/Inspector/InspectorPresentationModel.cpp index cb9d8852..e21e1bea 100644 --- a/new_editor/app/Features/Inspector/InspectorPresentationModel.cpp +++ b/new_editor/app/Features/Inspector/InspectorPresentationModel.cpp @@ -1,11 +1,13 @@ #include "Features/Inspector/InspectorPresentationModel.h" #include "Features/Inspector/Components/IInspectorComponentEditor.h" +#include "Features/Inspector/Components/InspectorComponentEditorUtils.h" #include "Features/Inspector/Components/InspectorComponentEditorRegistry.h" #include "Scene/EditorSceneRuntime.h" #include +#include #include namespace XCEngine::UI::Editor::App { @@ -40,6 +42,14 @@ std::string ResolveProjectSelectionTitle( : std::string("Asset"); } +std::string ResolveNoneSelectionTitle() { + return "Nothing selected"; +} + +std::string ResolveNoneSelectionSubtitle() { + return "Select a hierarchy item or project asset."; +} + UIEditorPropertyGridField BuildReadOnlyTextField( std::string fieldId, std::string label, @@ -65,6 +75,45 @@ void AppendReadOnlySection( model.sections.push_back(std::move(section)); } +const EditorSceneComponentDescriptor* FindComponentDescriptor( + const std::vector& descriptors, + std::string_view componentId) { + const auto it = std::find_if( + descriptors.begin(), + descriptors.end(), + [componentId](const EditorSceneComponentDescriptor& descriptor) { + return descriptor.componentId == componentId; + }); + return it != descriptors.end() ? &(*it) : nullptr; +} + +InspectorComponentEditorContext BuildComponentEditorContext( + const EditorSceneComponentDescriptor& descriptor, + const GameObject* gameObject, + const IInspectorComponentEditor* editor) { + InspectorComponentEditorContext context = {}; + context.gameObject = gameObject; + context.component = descriptor.component; + context.componentId = descriptor.componentId; + context.typeName = descriptor.typeName; + context.displayName = editor != nullptr + ? std::string(editor->GetDisplayName()) + : descriptor.typeName; + context.removable = descriptor.removable; + return context; +} + +void AppendProjectStructureSignature( + const EditorSelectionState& selection, + std::string& signature) { + AppendInspectorStructureToken(signature, "project"); + AppendInspectorStructureToken(signature, selection.directory); +} + +void AppendNoneSelectionStructureSignature(std::string& signature) { + AppendInspectorStructureToken(signature, "none"); +} + void AppendFallbackComponentSection( InspectorPresentationModel& model, const EditorSceneComponentDescriptor& descriptor) { @@ -85,28 +134,57 @@ void AppendFallbackComponentSection( std::move(fields)); } +void AppendFallbackComponentStructureSignature( + const EditorSceneComponentDescriptor& descriptor, + std::string& signature) { + AppendInspectorStructureToken(signature, "fallback"); + AppendInspectorStructureToken(signature, descriptor.componentId); + AppendInspectorStructureToken(signature, descriptor.typeName); + AppendInspectorStructureToken(signature, descriptor.removable); +} + +void AppendComponentStructureSignature( + const EditorSceneComponentDescriptor& descriptor, + const GameObject* gameObject, + const InspectorComponentEditorRegistry& componentEditorRegistry, + std::string& signature) { + const IInspectorComponentEditor* editor = + componentEditorRegistry.FindEditor(descriptor.typeName); + if (editor == nullptr) { + AppendFallbackComponentStructureSignature(descriptor, signature); + return; + } + + const InspectorComponentEditorContext context = + BuildComponentEditorContext(descriptor, gameObject, editor); + AppendInspectorStructureToken(signature, descriptor.componentId); + AppendInspectorStructureToken(signature, descriptor.typeName); + AppendInspectorStructureToken(signature, descriptor.removable); + editor->AppendStructureSignature(context, signature); +} + void AppendComponentPresentation( InspectorPresentationModel& model, const EditorSceneComponentDescriptor& descriptor, const GameObject* gameObject, - const InspectorComponentEditorRegistry& componentEditorRegistry) { + const InspectorComponentEditorRegistry& componentEditorRegistry, + std::string& structureSignature) { InspectorPresentationComponentBinding binding = {}; binding.componentId = descriptor.componentId; binding.typeName = descriptor.typeName; binding.removable = descriptor.removable; - InspectorComponentEditorContext context = {}; - context.gameObject = gameObject; - context.component = descriptor.component; - context.componentId = descriptor.componentId; - context.typeName = descriptor.typeName; - context.removable = descriptor.removable; - const IInspectorComponentEditor* editor = componentEditorRegistry.FindEditor(descriptor.typeName); if (editor != nullptr) { - context.displayName = std::string(editor->GetDisplayName()); + const InspectorComponentEditorContext context = + BuildComponentEditorContext(descriptor, gameObject, editor); binding.displayName = context.displayName; + AppendComponentStructureSignature( + descriptor, + gameObject, + componentEditorRegistry, + structureSignature); const std::size_t sectionCountBefore = model.sections.size(); editor->BuildSections(context, model.sections); @@ -119,8 +197,8 @@ void AppendComponentPresentation( } } } else { - context.displayName = descriptor.typeName; binding.displayName = descriptor.typeName; + AppendFallbackComponentStructureSignature(descriptor, structureSignature); AppendFallbackComponentSection(model, descriptor); if (!model.sections.empty()) { for (const UIEditorPropertyGridField& field : @@ -133,6 +211,72 @@ void AppendComponentPresentation( model.componentBindings.push_back(std::move(binding)); } +std::string BuildSceneObjectStructureSignature( + const InspectorSceneObjectSubject& sceneObject, + const EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorRegistry& componentEditorRegistry) { + std::string signature = {}; + AppendInspectorStructureToken(signature, "scene"); + for (const EditorSceneComponentDescriptor& descriptor : + sceneRuntime.GetSelectedComponents()) { + AppendComponentStructureSignature( + descriptor, + sceneObject.gameObject, + componentEditorRegistry, + signature); + } + return signature; +} + +InspectorPresentationSyncResult SyncSceneObjectPresentationValues( + InspectorPresentationModel& model, + const InspectorSceneObjectSubject& sceneObject, + const EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorRegistry& componentEditorRegistry) { + InspectorPresentationSyncResult result = {}; + const std::vector descriptors = + sceneRuntime.GetSelectedComponents(); + + model.hasSelection = true; + model.showHeader = false; + model.title = ResolveSceneObjectDisplayName(sceneObject); + model.subtitle = "GameObject"; + + for (const InspectorPresentationComponentBinding& binding : + model.componentBindings) { + const EditorSceneComponentDescriptor* descriptor = + FindComponentDescriptor(descriptors, binding.componentId); + if (descriptor == nullptr || descriptor->typeName != binding.typeName) { + return result; + } + + const IInspectorComponentEditor* editor = + componentEditorRegistry.FindEditor(binding.typeName); + if (editor == nullptr) { + continue; + } + + const InspectorComponentEditorContext context = + BuildComponentEditorContext(*descriptor, sceneObject.gameObject, editor); + for (const std::string& fieldId : binding.fieldIds) { + const auto location = + Widgets::FindUIEditorPropertyGridFieldLocation(model.sections, fieldId); + if (!location.IsValid() || + location.sectionIndex >= model.sections.size() || + location.fieldIndex >= model.sections[location.sectionIndex].fields.size()) { + return result; + } + + result.valueChanged |= editor->SyncFieldValue( + context, + model.sections[location.sectionIndex].fields[location.fieldIndex]); + } + } + + result.success = true; + return result; +} + } // namespace InspectorPresentationModel BuildInspectorPresentationModel( @@ -151,6 +295,7 @@ InspectorPresentationModel BuildInspectorPresentationModel( model.title = title; model.subtitle = typeLabel; + AppendProjectStructureSignature(selection, model.structureSignature); AppendReadOnlySection( model, @@ -176,44 +321,10 @@ InspectorPresentationModel BuildInspectorPresentationModel( case InspectorSubjectKind::SceneObject: { const InspectorSceneObjectSubject& sceneObject = subject.sceneObject; - const std::string title = ResolveSceneObjectDisplayName(sceneObject); - - model.title = title; + model.showHeader = false; + model.title = ResolveSceneObjectDisplayName(sceneObject); model.subtitle = "GameObject"; - - AppendReadOnlySection( - model, - "scene.identity", - "Identity", - { - BuildReadOnlyTextField("scene.identity.type", "Type", "GameObject"), - BuildReadOnlyTextField("scene.identity.name", "Name", title), - BuildReadOnlyTextField("scene.identity.id", "Id", sceneObject.itemId) - }); - - if (sceneObject.gameObject != nullptr) { - const GameObject* parent = sceneObject.gameObject->GetParent(); - const std::string parentName = - parent != nullptr - ? (parent->GetName().empty() - ? std::string("GameObject") - : parent->GetName()) - : std::string("Scene Root"); - AppendReadOnlySection( - model, - "scene.hierarchy", - "Hierarchy", - { - BuildReadOnlyTextField( - "scene.hierarchy.children", - "Children", - std::to_string(sceneObject.gameObject->GetChildCount())), - BuildReadOnlyTextField( - "scene.hierarchy.parent", - "Parent", - parentName) - }); - } + AppendInspectorStructureToken(model.structureSignature, "scene"); for (const EditorSceneComponentDescriptor& descriptor : sceneRuntime.GetSelectedComponents()) { @@ -221,17 +332,82 @@ InspectorPresentationModel BuildInspectorPresentationModel( model, descriptor, sceneObject.gameObject, - componentEditorRegistry); + componentEditorRegistry, + model.structureSignature); } return model; } case InspectorSubjectKind::None: default: - model.title = "Nothing selected"; - model.subtitle = "Select a hierarchy item or project asset."; + model.title = ResolveNoneSelectionTitle(); + model.subtitle = ResolveNoneSelectionSubtitle(); + AppendNoneSelectionStructureSignature(model.structureSignature); return model; } } +std::string BuildInspectorStructureSignature( + const InspectorSubject& subject, + const EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorRegistry& componentEditorRegistry) { + switch (subject.kind) { + case InspectorSubjectKind::ProjectAsset: { + std::string signature = {}; + AppendProjectStructureSignature(subject.projectAsset.selection, signature); + return signature; + } + + case InspectorSubjectKind::SceneObject: + return BuildSceneObjectStructureSignature( + subject.sceneObject, + sceneRuntime, + componentEditorRegistry); + + case InspectorSubjectKind::None: + default: { + std::string signature = {}; + AppendNoneSelectionStructureSignature(signature); + return signature; + } + } +} + +InspectorPresentationSyncResult SyncInspectorPresentationModelValues( + InspectorPresentationModel& model, + const InspectorSubject& subject, + const EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorRegistry& componentEditorRegistry) { + switch (subject.kind) { + case InspectorSubjectKind::ProjectAsset: + model.hasSelection = true; + model.showHeader = true; + model.title = ResolveProjectSelectionTitle(subject.projectAsset.selection); + model.subtitle = + subject.projectAsset.selection.directory ? "Folder" : "Asset"; + return InspectorPresentationSyncResult{ + .success = true, + .valueChanged = false + }; + + case InspectorSubjectKind::SceneObject: + return SyncSceneObjectPresentationValues( + model, + subject.sceneObject, + sceneRuntime, + componentEditorRegistry); + + case InspectorSubjectKind::None: + default: + model.hasSelection = false; + model.showHeader = true; + model.title = ResolveNoneSelectionTitle(); + model.subtitle = ResolveNoneSelectionSubtitle(); + return InspectorPresentationSyncResult{ + .success = true, + .valueChanged = false + }; + } +} + } // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/Inspector/InspectorPresentationModel.h b/new_editor/app/Features/Inspector/InspectorPresentationModel.h index 9befb2a0..2606e9f8 100644 --- a/new_editor/app/Features/Inspector/InspectorPresentationModel.h +++ b/new_editor/app/Features/Inspector/InspectorPresentationModel.h @@ -22,15 +22,33 @@ struct InspectorPresentationComponentBinding { struct InspectorPresentationModel { bool hasSelection = false; + bool showHeader = true; std::string title = {}; std::string subtitle = {}; + std::string structureSignature = {}; std::vector sections = {}; std::vector componentBindings = {}; }; +struct InspectorPresentationSyncResult { + bool success = false; + bool valueChanged = false; +}; + InspectorPresentationModel BuildInspectorPresentationModel( const InspectorSubject& subject, const EditorSceneRuntime& sceneRuntime, const InspectorComponentEditorRegistry& componentEditorRegistry); +std::string BuildInspectorStructureSignature( + const InspectorSubject& subject, + const EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorRegistry& componentEditorRegistry); + +InspectorPresentationSyncResult SyncInspectorPresentationModelValues( + InspectorPresentationModel& model, + const InspectorSubject& subject, + const EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorRegistry& componentEditorRegistry); + } // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/Project/ProjectBrowserModel.cpp b/new_editor/app/Features/Project/ProjectBrowserModel.cpp index e48f01f5..ed8fc185 100644 --- a/new_editor/app/Features/Project/ProjectBrowserModel.cpp +++ b/new_editor/app/Features/Project/ProjectBrowserModel.cpp @@ -1,11 +1,484 @@ -#include "ProjectBrowserModel.h" -#include "ProjectBrowserModelInternal.h" - +#include "ProjectBrowserModel.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include #include namespace XCEngine::UI::Editor::App { -using namespace ProjectBrowserModelInternal; +inline constexpr std::string_view kAssetsRootId = "Assets"; + +std::string ToLowerCopy(std::string value); +std::string PathToUtf8String(const std::filesystem::path& path); +std::filesystem::path BuildPathFromUtf8(std::string_view value); +std::string NormalizePathSeparators(std::string value); +std::string BuildRelativeItemId( + const std::filesystem::path& path, + const std::filesystem::path& assetsRoot); +std::string BuildRelativeProjectPath( + const std::filesystem::path& path, + const std::filesystem::path& projectRoot); +std::string BuildAssetDisplayName(const std::filesystem::path& path, bool directory); +std::string BuildAssetNameWithExtension(const std::filesystem::path& path, bool directory); +bool IsMetaFile(const std::filesystem::path& path); +bool HasChildDirectories(const std::filesystem::path& folderPath); +std::vector CollectSortedChildDirectories( + const std::filesystem::path& folderPath); +std::wstring MakePathKey(const std::filesystem::path& path); +bool IsSameOrDescendantPath( + const std::filesystem::path& path, + const std::filesystem::path& ancestor); +std::string TrimAssetName(std::string_view name); +bool HasInvalidAssetName(std::string_view name); +std::filesystem::path MakeUniqueFolderPath( + const std::filesystem::path& parentPath, + std::string_view preferredName); +std::filesystem::path MakeUniqueFilePath( + const std::filesystem::path& parentPath, + const std::filesystem::path& preferredFileName); +std::string BuildRenamedEntryName( + const std::filesystem::path& sourcePath, + std::string_view requestedName); +std::filesystem::path GetMetaSidecarPath(const std::filesystem::path& assetPath); +void RemoveMetaSidecarIfPresent(const std::filesystem::path& assetPath); +bool RenamePathCaseAware( + const std::filesystem::path& sourcePath, + const std::filesystem::path& destPath); +void MoveMetaSidecarIfPresent( + const std::filesystem::path& sourcePath, + const std::filesystem::path& destPath); +bool MovePathWithOptionalMeta( + const std::filesystem::path& sourcePath, + const std::filesystem::path& destinationPath); +ProjectBrowserModel::ItemKind ResolveItemKind( + const std::filesystem::path& path, + bool directory); +bool CanOpenItemKind(ProjectBrowserModel::ItemKind kind); +bool CanPreviewItem( + const std::filesystem::path& path, + ProjectBrowserModel::ItemKind kind, + bool directory); + +} // namespace XCEngine::UI::Editor::App + +namespace XCEngine::UI::Editor::App { + +namespace { + +std::string BuildUtf8String(std::u8string_view value) { + std::string result = {}; + result.reserve(value.size()); + for (const char8_t character : value) { + result.push_back(static_cast(character)); + } + return result; +} + +std::u8string BuildU8String(std::string_view value) { + std::u8string result = {}; + result.reserve(value.size()); + for (const char character : value) { + result.push_back(static_cast(character)); + } + return result; +} + +} // namespace + +std::string ToLowerCopy(std::string value) { + std::transform( + value.begin(), + value.end(), + value.begin(), + [](unsigned char character) { + return static_cast(std::tolower(character)); + }); + return value; +} + +std::string PathToUtf8String(const std::filesystem::path& path) { + return BuildUtf8String(path.u8string()); +} + +std::filesystem::path BuildPathFromUtf8(std::string_view value) { + return std::filesystem::path(BuildU8String(value)); +} + +std::string NormalizePathSeparators(std::string value) { + std::replace(value.begin(), value.end(), '\\', '/'); + return value; +} + +std::string BuildRelativeItemId( + const std::filesystem::path& path, + const std::filesystem::path& assetsRoot) { + const std::filesystem::path relative = + std::filesystem::relative(path, assetsRoot.parent_path()); + const std::string normalized = + NormalizePathSeparators(PathToUtf8String(relative.lexically_normal())); + return normalized.empty() ? std::string(kAssetsRootId) : normalized; +} + +std::string BuildRelativeProjectPath( + const std::filesystem::path& path, + const std::filesystem::path& projectRoot) { + if (projectRoot.empty() || path.empty()) { + return {}; + } + + const std::filesystem::path relative = + std::filesystem::relative(path, projectRoot); + const std::string normalized = + NormalizePathSeparators(PathToUtf8String(relative.lexically_normal())); + return normalized; +} + +std::string BuildAssetDisplayName(const std::filesystem::path& path, bool directory) { + if (directory) { + return PathToUtf8String(path.filename()); + } + + const std::string filename = PathToUtf8String(path.filename()); + const std::size_t extensionOffset = filename.find_last_of('.'); + if (extensionOffset == std::string::npos || extensionOffset == 0u) { + return filename; + } + + return filename.substr(0u, extensionOffset); +} + +std::string BuildAssetNameWithExtension(const std::filesystem::path& path, bool directory) { + return directory + ? PathToUtf8String(path.filename()) + : PathToUtf8String(path.filename()); +} + +bool IsMetaFile(const std::filesystem::path& path) { + return ToLowerCopy(path.extension().string()) == ".meta"; +} + +bool HasChildDirectories(const std::filesystem::path& folderPath) { + std::error_code errorCode = {}; + const std::filesystem::directory_iterator end = {}; + for (std::filesystem::directory_iterator iterator(folderPath, errorCode); + !errorCode && iterator != end; + iterator.increment(errorCode)) { + if (iterator->is_directory(errorCode)) { + return true; + } + } + + return false; +} + +std::vector CollectSortedChildDirectories( + const std::filesystem::path& folderPath) { + std::vector paths = {}; + std::error_code errorCode = {}; + const std::filesystem::directory_iterator end = {}; + for (std::filesystem::directory_iterator iterator(folderPath, errorCode); + !errorCode && iterator != end; + iterator.increment(errorCode)) { + if (iterator->is_directory(errorCode)) { + paths.push_back(iterator->path()); + } + } + + std::sort( + paths.begin(), + paths.end(), + [](const std::filesystem::path& lhs, const std::filesystem::path& rhs) { + return ToLowerCopy(PathToUtf8String(lhs.filename())) < + ToLowerCopy(PathToUtf8String(rhs.filename())); + }); + return paths; +} + +std::wstring MakePathKey(const std::filesystem::path& path) { + std::wstring key = path.lexically_normal().generic_wstring(); + std::transform(key.begin(), key.end(), key.begin(), ::towlower); + return key; +} + +bool IsSameOrDescendantPath( + const std::filesystem::path& path, + const std::filesystem::path& ancestor) { + const std::wstring pathKey = MakePathKey(path); + std::wstring ancestorKey = MakePathKey(ancestor); + if (pathKey.empty() || ancestorKey.empty()) { + return false; + } + + if (pathKey == ancestorKey) { + return true; + } + + if (ancestorKey.back() != L'/') { + ancestorKey += L'/'; + } + return pathKey.rfind(ancestorKey, 0) == 0; +} + +std::string TrimAssetName(std::string_view name) { + const auto first = + std::find_if_not(name.begin(), name.end(), [](unsigned char character) { + return std::isspace(character) != 0; + }); + if (first == name.end()) { + return {}; + } + + const auto last = + std::find_if_not(name.rbegin(), name.rend(), [](unsigned char character) { + return std::isspace(character) != 0; + }).base(); + return std::string(first, last); +} + +bool HasInvalidAssetName(std::string_view name) { + if (name.empty() || name == "." || name == "..") { + return true; + } + + return name.find_first_of("\\/:*?\"<>|") != std::string_view::npos; +} + +std::filesystem::path MakeUniqueFolderPath( + const std::filesystem::path& parentPath, + std::string_view preferredName) { + std::filesystem::path candidatePath = + parentPath / BuildPathFromUtf8(preferredName); + if (!std::filesystem::exists(candidatePath)) { + return candidatePath; + } + + for (std::size_t suffix = 1u;; ++suffix) { + candidatePath = + parentPath / BuildPathFromUtf8( + std::string(preferredName) + " " + std::to_string(suffix)); + if (!std::filesystem::exists(candidatePath)) { + return candidatePath; + } + } +} + +std::filesystem::path MakeUniqueFilePath( + const std::filesystem::path& parentPath, + const std::filesystem::path& preferredFileName) { + std::filesystem::path candidatePath = parentPath / preferredFileName; + if (!std::filesystem::exists(candidatePath)) { + return candidatePath; + } + + const std::wstring stem = preferredFileName.stem().wstring(); + const std::wstring extension = preferredFileName.extension().wstring(); + for (std::size_t suffix = 1u;; ++suffix) { + candidatePath = + parentPath / + std::filesystem::path( + stem + L" " + std::to_wstring(suffix) + extension); + if (!std::filesystem::exists(candidatePath)) { + return candidatePath; + } + } +} + +std::string BuildRenamedEntryName( + const std::filesystem::path& sourcePath, + std::string_view requestedName) { + if (std::filesystem::is_directory(sourcePath)) { + return std::string(requestedName); + } + + const std::filesystem::path requestedPath = + BuildPathFromUtf8(requestedName); + if (requestedPath.has_extension()) { + return PathToUtf8String(requestedPath.filename()); + } + + return std::string(requestedName) + PathToUtf8String(sourcePath.extension()); +} + +std::filesystem::path GetMetaSidecarPath(const std::filesystem::path& assetPath) { + return std::filesystem::path(assetPath.native() + std::filesystem::path(L".meta").native()); +} + +void RemoveMetaSidecarIfPresent(const std::filesystem::path& assetPath) { + std::error_code errorCode = {}; + std::filesystem::remove_all(GetMetaSidecarPath(assetPath), errorCode); +} + +namespace { + +std::filesystem::path MakeCaseOnlyRenameTempPath(const std::filesystem::path& sourcePath) { + const std::filesystem::path parentPath = sourcePath.parent_path(); + const std::wstring sourceStem = sourcePath.filename().wstring(); + + for (std::size_t suffix = 0u;; ++suffix) { + std::wstring tempName = sourceStem + L".xc_tmp_rename"; + if (suffix > 0u) { + tempName += std::to_wstring(suffix); + } + + const std::filesystem::path tempPath = parentPath / tempName; + if (!std::filesystem::exists(tempPath)) { + return tempPath; + } + } +} + +bool MatchesExtension( + std::wstring_view extension, + std::initializer_list candidates) { + for (const std::wstring_view candidate : candidates) { + if (extension == candidate) { + return true; + } + } + + return false; +} + +} // namespace + +bool RenamePathCaseAware( + const std::filesystem::path& sourcePath, + const std::filesystem::path& destPath) { + if (MakePathKey(sourcePath) != MakePathKey(destPath)) { + if (std::filesystem::exists(destPath)) { + return false; + } + + std::filesystem::rename(sourcePath, destPath); + return true; + } + + if (sourcePath.filename() == destPath.filename()) { + return true; + } + + const std::filesystem::path tempPath = MakeCaseOnlyRenameTempPath(sourcePath); + std::filesystem::rename(sourcePath, tempPath); + std::filesystem::rename(tempPath, destPath); + return true; +} + +void MoveMetaSidecarIfPresent( + const std::filesystem::path& sourcePath, + const std::filesystem::path& destPath) { + const std::filesystem::path sourceMetaPath = GetMetaSidecarPath(sourcePath); + if (!std::filesystem::exists(sourceMetaPath)) { + return; + } + + const std::filesystem::path destMetaPath = GetMetaSidecarPath(destPath); + RenamePathCaseAware(sourceMetaPath, destMetaPath); +} + +bool MovePathWithOptionalMeta( + const std::filesystem::path& sourcePath, + const std::filesystem::path& destinationPath) { + std::error_code errorCode = {}; + const std::filesystem::path sourceMetaPath = GetMetaSidecarPath(sourcePath); + const std::filesystem::path destinationMetaPath = GetMetaSidecarPath(destinationPath); + const bool moveMeta = std::filesystem::exists(sourceMetaPath, errorCode); + if (errorCode) { + return false; + } + + std::filesystem::rename(sourcePath, destinationPath, errorCode); + if (errorCode) { + return false; + } + + if (!moveMeta) { + return true; + } + + std::filesystem::rename(sourceMetaPath, destinationMetaPath, errorCode); + if (!errorCode) { + return true; + } + + std::error_code rollbackError = {}; + std::filesystem::rename(destinationPath, sourcePath, rollbackError); + return false; +} + +ProjectBrowserModel::ItemKind ResolveItemKind( + const std::filesystem::path& path, + bool directory) { + if (directory) { + return ProjectBrowserModel::ItemKind::Folder; + } + + std::wstring extension = path.extension().wstring(); + std::transform(extension.begin(), extension.end(), extension.begin(), ::towlower); + + if (MatchesExtension(extension, { L".xc", L".unity", L".scene" })) { + return ProjectBrowserModel::ItemKind::Scene; + } + if (MatchesExtension(extension, { L".fbx", L".obj", L".gltf", L".glb" })) { + return ProjectBrowserModel::ItemKind::Model; + } + if (extension == L".mat") { + return ProjectBrowserModel::ItemKind::Material; + } + if (MatchesExtension(extension, { + L".png", L".jpg", L".jpeg", L".tga", L".bmp", L".gif", + L".psd", L".hdr", L".pic", L".ppm", L".pgm", L".pbm", + L".pnm", L".dds", L".ktx", L".ktx2", L".webp" })) { + return ProjectBrowserModel::ItemKind::Texture; + } + if (MatchesExtension(extension, { L".cs", L".cpp", L".c", L".h", L".hpp" })) { + return ProjectBrowserModel::ItemKind::Script; + } + + return ProjectBrowserModel::ItemKind::File; +} + +bool CanOpenItemKind(ProjectBrowserModel::ItemKind kind) { + return kind == ProjectBrowserModel::ItemKind::Folder || + kind == ProjectBrowserModel::ItemKind::Scene; +} + +bool CanPreviewItem( + const std::filesystem::path& path, + ProjectBrowserModel::ItemKind kind, + bool directory) { + if (directory || kind != ProjectBrowserModel::ItemKind::Texture) { + return false; + } + + std::wstring extension = path.extension().wstring(); + std::transform(extension.begin(), extension.end(), extension.begin(), ::towlower); + return MatchesExtension(extension, { + L".png", + L".jpg", + L".jpeg", + L".tga", + L".bmp", + L".gif", + L".psd", + L".hdr", + L".pic", + L".ppm", + L".pgm", + L".pbm", + L".pnm" + }); +} + +} // namespace XCEngine::UI::Editor::App + +namespace XCEngine::UI::Editor::App { + namespace { @@ -708,3 +1181,133 @@ void ProjectBrowserModel::EnsureValidCurrentFolder() { } } // namespace XCEngine::UI::Editor::App + +namespace XCEngine::UI::Editor::App { + + +void ProjectBrowserModel::RefreshAssetList() { + EnsureValidCurrentFolder(); + + m_assetEntries.clear(); + const FolderEntry* currentFolder = FindFolderEntry(m_currentFolderId); + if (currentFolder == nullptr) { + return; + } + + std::vector entries = {}; + std::error_code errorCode = {}; + const std::filesystem::directory_iterator end = {}; + for (std::filesystem::directory_iterator iterator(currentFolder->absolutePath, errorCode); + !errorCode && iterator != end; + iterator.increment(errorCode)) { + if (!iterator->exists(errorCode) || IsMetaFile(iterator->path())) { + continue; + } + if (!iterator->is_directory(errorCode) && !iterator->is_regular_file(errorCode)) { + continue; + } + + entries.push_back(*iterator); + } + + std::sort( + entries.begin(), + entries.end(), + [](const std::filesystem::directory_entry& lhs, const std::filesystem::directory_entry& rhs) { + const bool lhsDirectory = lhs.is_directory(); + const bool rhsDirectory = rhs.is_directory(); + if (lhsDirectory != rhsDirectory) { + return lhsDirectory && !rhsDirectory; + } + + return ToLowerCopy(PathToUtf8String(lhs.path().filename())) < + ToLowerCopy(PathToUtf8String(rhs.path().filename())); + }); + + for (const std::filesystem::directory_entry& entry : entries) { + AssetEntry assetEntry = {}; + assetEntry.itemId = BuildRelativeItemId(entry.path(), m_assetsRootPath); + assetEntry.absolutePath = entry.path(); + assetEntry.nameWithExtension = + BuildAssetNameWithExtension(entry.path(), entry.is_directory()); + assetEntry.displayName = BuildAssetDisplayName(entry.path(), entry.is_directory()); + assetEntry.extensionLower = ToLowerCopy(PathToUtf8String(entry.path().extension())); + assetEntry.kind = ResolveItemKind(entry.path(), entry.is_directory()); + assetEntry.directory = entry.is_directory(); + assetEntry.canOpen = CanOpenItemKind(assetEntry.kind); + assetEntry.canPreview = + CanPreviewItem(entry.path(), assetEntry.kind, assetEntry.directory); + m_assetEntries.push_back(std::move(assetEntry)); + } +} + +} // namespace XCEngine::UI::Editor::App + +namespace XCEngine::UI::Editor::App { + + +void ProjectBrowserModel::RefreshFolderTree() { + m_folderEntries.clear(); + m_treeItems.clear(); + + if (m_assetsRootPath.empty() || !std::filesystem::exists(m_assetsRootPath)) { + return; + } + + const auto appendFolderRecursive = + [&](auto&& self, const std::filesystem::path& folderPath, std::uint32_t depth) -> void { + const std::string itemId = BuildRelativeItemId(folderPath, m_assetsRootPath); + + FolderEntry folderEntry = {}; + folderEntry.itemId = itemId; + folderEntry.absolutePath = folderPath; + folderEntry.label = PathToUtf8String(folderPath.filename()); + m_folderEntries.push_back(folderEntry); + + Widgets::UIEditorTreeViewItem item = {}; + item.itemId = itemId; + item.label = folderEntry.label; + item.depth = depth; + item.forceLeaf = !HasChildDirectories(folderPath); + item.leadingIcon = m_folderIcon; + m_treeItems.push_back(std::move(item)); + + const std::vector childFolders = + CollectSortedChildDirectories(folderPath); + for (const std::filesystem::path& childPath : childFolders) { + self(self, childPath, depth + 1u); + } + }; + + appendFolderRecursive(appendFolderRecursive, m_assetsRootPath, 0u); +} + +std::vector ProjectBrowserModel::CollectCurrentFolderAncestorIds() const { + return BuildAncestorFolderIds(m_currentFolderId); +} + +std::vector ProjectBrowserModel::BuildAncestorFolderIds( + std::string_view itemId) const { + std::vector ancestors = {}; + const FolderEntry* folderEntry = FindFolderEntry(itemId); + if (folderEntry == nullptr) { + return ancestors; + } + + std::filesystem::path path = folderEntry->absolutePath; + while (true) { + ancestors.push_back(BuildRelativeItemId(path, m_assetsRootPath)); + if (path == m_assetsRootPath) { + break; + } + path = path.parent_path(); + } + + std::reverse(ancestors.begin(), ancestors.end()); + return ancestors; +} + +} // namespace XCEngine::UI::Editor::App + + + diff --git a/new_editor/app/Features/Project/ProjectBrowserModelAssets.cpp b/new_editor/app/Features/Project/ProjectBrowserModelAssets.cpp deleted file mode 100644 index 0797b422..00000000 --- a/new_editor/app/Features/Project/ProjectBrowserModelAssets.cpp +++ /dev/null @@ -1,63 +0,0 @@ -#include "ProjectBrowserModel.h" -#include "ProjectBrowserModelInternal.h" - -namespace XCEngine::UI::Editor::App { - -using namespace ProjectBrowserModelInternal; - -void ProjectBrowserModel::RefreshAssetList() { - EnsureValidCurrentFolder(); - - m_assetEntries.clear(); - const FolderEntry* currentFolder = FindFolderEntry(m_currentFolderId); - if (currentFolder == nullptr) { - return; - } - - std::vector entries = {}; - std::error_code errorCode = {}; - const std::filesystem::directory_iterator end = {}; - for (std::filesystem::directory_iterator iterator(currentFolder->absolutePath, errorCode); - !errorCode && iterator != end; - iterator.increment(errorCode)) { - if (!iterator->exists(errorCode) || IsMetaFile(iterator->path())) { - continue; - } - if (!iterator->is_directory(errorCode) && !iterator->is_regular_file(errorCode)) { - continue; - } - - entries.push_back(*iterator); - } - - std::sort( - entries.begin(), - entries.end(), - [](const std::filesystem::directory_entry& lhs, const std::filesystem::directory_entry& rhs) { - const bool lhsDirectory = lhs.is_directory(); - const bool rhsDirectory = rhs.is_directory(); - if (lhsDirectory != rhsDirectory) { - return lhsDirectory && !rhsDirectory; - } - - return ToLowerCopy(PathToUtf8String(lhs.path().filename())) < - ToLowerCopy(PathToUtf8String(rhs.path().filename())); - }); - - for (const std::filesystem::directory_entry& entry : entries) { - AssetEntry assetEntry = {}; - assetEntry.itemId = BuildRelativeItemId(entry.path(), m_assetsRootPath); - assetEntry.absolutePath = entry.path(); - assetEntry.nameWithExtension = - BuildAssetNameWithExtension(entry.path(), entry.is_directory()); - assetEntry.displayName = BuildAssetDisplayName(entry.path(), entry.is_directory()); - assetEntry.extensionLower = ToLowerCopy(PathToUtf8String(entry.path().extension())); - assetEntry.kind = ResolveItemKind(entry.path(), entry.is_directory()); - assetEntry.directory = entry.is_directory(); - assetEntry.canOpen = CanOpenItemKind(assetEntry.kind); - assetEntry.canPreview = CanPreviewItemKind(assetEntry.kind); - m_assetEntries.push_back(std::move(assetEntry)); - } -} - -} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/Project/ProjectBrowserModelFolders.cpp b/new_editor/app/Features/Project/ProjectBrowserModelFolders.cpp deleted file mode 100644 index 57a2b594..00000000 --- a/new_editor/app/Features/Project/ProjectBrowserModelFolders.cpp +++ /dev/null @@ -1,69 +0,0 @@ -#include "ProjectBrowserModel.h" -#include "ProjectBrowserModelInternal.h" - -namespace XCEngine::UI::Editor::App { - -using namespace ProjectBrowserModelInternal; - -void ProjectBrowserModel::RefreshFolderTree() { - m_folderEntries.clear(); - m_treeItems.clear(); - - if (m_assetsRootPath.empty() || !std::filesystem::exists(m_assetsRootPath)) { - return; - } - - const auto appendFolderRecursive = - [&](auto&& self, const std::filesystem::path& folderPath, std::uint32_t depth) -> void { - const std::string itemId = BuildRelativeItemId(folderPath, m_assetsRootPath); - - FolderEntry folderEntry = {}; - folderEntry.itemId = itemId; - folderEntry.absolutePath = folderPath; - folderEntry.label = PathToUtf8String(folderPath.filename()); - m_folderEntries.push_back(folderEntry); - - Widgets::UIEditorTreeViewItem item = {}; - item.itemId = itemId; - item.label = folderEntry.label; - item.depth = depth; - item.forceLeaf = !HasChildDirectories(folderPath); - item.leadingIcon = m_folderIcon; - m_treeItems.push_back(std::move(item)); - - const std::vector childFolders = - CollectSortedChildDirectories(folderPath); - for (const std::filesystem::path& childPath : childFolders) { - self(self, childPath, depth + 1u); - } - }; - - appendFolderRecursive(appendFolderRecursive, m_assetsRootPath, 0u); -} - -std::vector ProjectBrowserModel::CollectCurrentFolderAncestorIds() const { - return BuildAncestorFolderIds(m_currentFolderId); -} - -std::vector ProjectBrowserModel::BuildAncestorFolderIds( - std::string_view itemId) const { - std::vector ancestors = {}; - const FolderEntry* folderEntry = FindFolderEntry(itemId); - if (folderEntry == nullptr) { - return ancestors; - } - - std::filesystem::path path = folderEntry->absolutePath; - while (true) { - ancestors.push_back(BuildRelativeItemId(path, m_assetsRootPath)); - if (path == m_assetsRootPath) { - break; - } - path = path.parent_path(); - } - - std::reverse(ancestors.begin(), ancestors.end()); - return ancestors; -} - -} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/Project/ProjectBrowserModelInternal.cpp b/new_editor/app/Features/Project/ProjectBrowserModelInternal.cpp deleted file mode 100644 index fcc4dabf..00000000 --- a/new_editor/app/Features/Project/ProjectBrowserModelInternal.cpp +++ /dev/null @@ -1,394 +0,0 @@ -#include "ProjectBrowserModelInternal.h" - -#include -#include -#include -#include -#include - -namespace XCEngine::UI::Editor::App::ProjectBrowserModelInternal { - -namespace { - -std::string BuildUtf8String(std::u8string_view value) { - std::string result = {}; - result.reserve(value.size()); - for (const char8_t character : value) { - result.push_back(static_cast(character)); - } - return result; -} - -std::u8string BuildU8String(std::string_view value) { - std::u8string result = {}; - result.reserve(value.size()); - for (const char character : value) { - result.push_back(static_cast(character)); - } - return result; -} - -} // namespace - -std::string ToLowerCopy(std::string value) { - std::transform( - value.begin(), - value.end(), - value.begin(), - [](unsigned char character) { - return static_cast(std::tolower(character)); - }); - return value; -} - -std::string PathToUtf8String(const std::filesystem::path& path) { - return BuildUtf8String(path.u8string()); -} - -std::filesystem::path BuildPathFromUtf8(std::string_view value) { - return std::filesystem::path(BuildU8String(value)); -} - -std::string NormalizePathSeparators(std::string value) { - std::replace(value.begin(), value.end(), '\\', '/'); - return value; -} - -std::string BuildRelativeItemId( - const std::filesystem::path& path, - const std::filesystem::path& assetsRoot) { - const std::filesystem::path relative = - std::filesystem::relative(path, assetsRoot.parent_path()); - const std::string normalized = - NormalizePathSeparators(PathToUtf8String(relative.lexically_normal())); - return normalized.empty() ? std::string(kAssetsRootId) : normalized; -} - -std::string BuildRelativeProjectPath( - const std::filesystem::path& path, - const std::filesystem::path& projectRoot) { - if (projectRoot.empty() || path.empty()) { - return {}; - } - - const std::filesystem::path relative = - std::filesystem::relative(path, projectRoot); - const std::string normalized = - NormalizePathSeparators(PathToUtf8String(relative.lexically_normal())); - return normalized; -} - -std::string BuildAssetDisplayName(const std::filesystem::path& path, bool directory) { - if (directory) { - return PathToUtf8String(path.filename()); - } - - const std::string filename = PathToUtf8String(path.filename()); - const std::size_t extensionOffset = filename.find_last_of('.'); - if (extensionOffset == std::string::npos || extensionOffset == 0u) { - return filename; - } - - return filename.substr(0u, extensionOffset); -} - -std::string BuildAssetNameWithExtension(const std::filesystem::path& path, bool directory) { - return directory - ? PathToUtf8String(path.filename()) - : PathToUtf8String(path.filename()); -} - -bool IsMetaFile(const std::filesystem::path& path) { - return ToLowerCopy(path.extension().string()) == ".meta"; -} - -bool HasChildDirectories(const std::filesystem::path& folderPath) { - std::error_code errorCode = {}; - const std::filesystem::directory_iterator end = {}; - for (std::filesystem::directory_iterator iterator(folderPath, errorCode); - !errorCode && iterator != end; - iterator.increment(errorCode)) { - if (iterator->is_directory(errorCode)) { - return true; - } - } - - return false; -} - -std::vector CollectSortedChildDirectories( - const std::filesystem::path& folderPath) { - std::vector paths = {}; - std::error_code errorCode = {}; - const std::filesystem::directory_iterator end = {}; - for (std::filesystem::directory_iterator iterator(folderPath, errorCode); - !errorCode && iterator != end; - iterator.increment(errorCode)) { - if (iterator->is_directory(errorCode)) { - paths.push_back(iterator->path()); - } - } - - std::sort( - paths.begin(), - paths.end(), - [](const std::filesystem::path& lhs, const std::filesystem::path& rhs) { - return ToLowerCopy(PathToUtf8String(lhs.filename())) < - ToLowerCopy(PathToUtf8String(rhs.filename())); - }); - return paths; -} - -std::wstring MakePathKey(const std::filesystem::path& path) { - std::wstring key = path.lexically_normal().generic_wstring(); - std::transform(key.begin(), key.end(), key.begin(), ::towlower); - return key; -} - -bool IsSameOrDescendantPath( - const std::filesystem::path& path, - const std::filesystem::path& ancestor) { - const std::wstring pathKey = MakePathKey(path); - std::wstring ancestorKey = MakePathKey(ancestor); - if (pathKey.empty() || ancestorKey.empty()) { - return false; - } - - if (pathKey == ancestorKey) { - return true; - } - - if (ancestorKey.back() != L'/') { - ancestorKey += L'/'; - } - return pathKey.rfind(ancestorKey, 0) == 0; -} - -std::string TrimAssetName(std::string_view name) { - const auto first = - std::find_if_not(name.begin(), name.end(), [](unsigned char character) { - return std::isspace(character) != 0; - }); - if (first == name.end()) { - return {}; - } - - const auto last = - std::find_if_not(name.rbegin(), name.rend(), [](unsigned char character) { - return std::isspace(character) != 0; - }).base(); - return std::string(first, last); -} - -bool HasInvalidAssetName(std::string_view name) { - if (name.empty() || name == "." || name == "..") { - return true; - } - - return name.find_first_of("\\/:*?\"<>|") != std::string_view::npos; -} - -std::filesystem::path MakeUniqueFolderPath( - const std::filesystem::path& parentPath, - std::string_view preferredName) { - std::filesystem::path candidatePath = - parentPath / BuildPathFromUtf8(preferredName); - if (!std::filesystem::exists(candidatePath)) { - return candidatePath; - } - - for (std::size_t suffix = 1u;; ++suffix) { - candidatePath = - parentPath / BuildPathFromUtf8( - std::string(preferredName) + " " + std::to_string(suffix)); - if (!std::filesystem::exists(candidatePath)) { - return candidatePath; - } - } -} - -std::filesystem::path MakeUniqueFilePath( - const std::filesystem::path& parentPath, - const std::filesystem::path& preferredFileName) { - std::filesystem::path candidatePath = parentPath / preferredFileName; - if (!std::filesystem::exists(candidatePath)) { - return candidatePath; - } - - const std::wstring stem = preferredFileName.stem().wstring(); - const std::wstring extension = preferredFileName.extension().wstring(); - for (std::size_t suffix = 1u;; ++suffix) { - candidatePath = - parentPath / - std::filesystem::path( - stem + L" " + std::to_wstring(suffix) + extension); - if (!std::filesystem::exists(candidatePath)) { - return candidatePath; - } - } -} - -std::string BuildRenamedEntryName( - const std::filesystem::path& sourcePath, - std::string_view requestedName) { - if (std::filesystem::is_directory(sourcePath)) { - return std::string(requestedName); - } - - const std::filesystem::path requestedPath = - BuildPathFromUtf8(requestedName); - if (requestedPath.has_extension()) { - return PathToUtf8String(requestedPath.filename()); - } - - return std::string(requestedName) + PathToUtf8String(sourcePath.extension()); -} - -std::filesystem::path GetMetaSidecarPath(const std::filesystem::path& assetPath) { - return std::filesystem::path(assetPath.native() + std::filesystem::path(L".meta").native()); -} - -void RemoveMetaSidecarIfPresent(const std::filesystem::path& assetPath) { - std::error_code errorCode = {}; - std::filesystem::remove_all(GetMetaSidecarPath(assetPath), errorCode); -} - -namespace { - -std::filesystem::path MakeCaseOnlyRenameTempPath(const std::filesystem::path& sourcePath) { - const std::filesystem::path parentPath = sourcePath.parent_path(); - const std::wstring sourceStem = sourcePath.filename().wstring(); - - for (std::size_t suffix = 0u;; ++suffix) { - std::wstring tempName = sourceStem + L".xc_tmp_rename"; - if (suffix > 0u) { - tempName += std::to_wstring(suffix); - } - - const std::filesystem::path tempPath = parentPath / tempName; - if (!std::filesystem::exists(tempPath)) { - return tempPath; - } - } -} - -bool MatchesExtension( - std::wstring_view extension, - std::initializer_list candidates) { - for (const std::wstring_view candidate : candidates) { - if (extension == candidate) { - return true; - } - } - - return false; -} - -} // namespace - -bool RenamePathCaseAware( - const std::filesystem::path& sourcePath, - const std::filesystem::path& destPath) { - if (MakePathKey(sourcePath) != MakePathKey(destPath)) { - if (std::filesystem::exists(destPath)) { - return false; - } - - std::filesystem::rename(sourcePath, destPath); - return true; - } - - if (sourcePath.filename() == destPath.filename()) { - return true; - } - - const std::filesystem::path tempPath = MakeCaseOnlyRenameTempPath(sourcePath); - std::filesystem::rename(sourcePath, tempPath); - std::filesystem::rename(tempPath, destPath); - return true; -} - -void MoveMetaSidecarIfPresent( - const std::filesystem::path& sourcePath, - const std::filesystem::path& destPath) { - const std::filesystem::path sourceMetaPath = GetMetaSidecarPath(sourcePath); - if (!std::filesystem::exists(sourceMetaPath)) { - return; - } - - const std::filesystem::path destMetaPath = GetMetaSidecarPath(destPath); - RenamePathCaseAware(sourceMetaPath, destMetaPath); -} - -bool MovePathWithOptionalMeta( - const std::filesystem::path& sourcePath, - const std::filesystem::path& destinationPath) { - std::error_code errorCode = {}; - const std::filesystem::path sourceMetaPath = GetMetaSidecarPath(sourcePath); - const std::filesystem::path destinationMetaPath = GetMetaSidecarPath(destinationPath); - const bool moveMeta = std::filesystem::exists(sourceMetaPath, errorCode); - if (errorCode) { - return false; - } - - std::filesystem::rename(sourcePath, destinationPath, errorCode); - if (errorCode) { - return false; - } - - if (!moveMeta) { - return true; - } - - std::filesystem::rename(sourceMetaPath, destinationMetaPath, errorCode); - if (!errorCode) { - return true; - } - - std::error_code rollbackError = {}; - std::filesystem::rename(destinationPath, sourcePath, rollbackError); - return false; -} - -ProjectBrowserModel::ItemKind ResolveItemKind( - const std::filesystem::path& path, - bool directory) { - if (directory) { - return ProjectBrowserModel::ItemKind::Folder; - } - - std::wstring extension = path.extension().wstring(); - std::transform(extension.begin(), extension.end(), extension.begin(), ::towlower); - - if (MatchesExtension(extension, { L".xc", L".unity", L".scene" })) { - return ProjectBrowserModel::ItemKind::Scene; - } - if (MatchesExtension(extension, { L".fbx", L".obj", L".gltf", L".glb" })) { - return ProjectBrowserModel::ItemKind::Model; - } - if (extension == L".mat") { - return ProjectBrowserModel::ItemKind::Material; - } - if (MatchesExtension(extension, { - L".png", L".jpg", L".jpeg", L".tga", L".bmp", L".gif", - L".psd", L".hdr", L".pic", L".ppm", L".pgm", L".pbm", - L".pnm", L".dds", L".ktx", L".ktx2", L".webp" })) { - return ProjectBrowserModel::ItemKind::Texture; - } - if (MatchesExtension(extension, { L".cs", L".cpp", L".c", L".h", L".hpp" })) { - return ProjectBrowserModel::ItemKind::Script; - } - - return ProjectBrowserModel::ItemKind::File; -} - -bool CanOpenItemKind(ProjectBrowserModel::ItemKind kind) { - return kind == ProjectBrowserModel::ItemKind::Folder || - kind == ProjectBrowserModel::ItemKind::Scene; -} - -bool CanPreviewItemKind(ProjectBrowserModel::ItemKind kind) { - return kind == ProjectBrowserModel::ItemKind::Texture; -} - -} // namespace XCEngine::UI::Editor::App::ProjectBrowserModelInternal diff --git a/new_editor/app/Features/Project/ProjectBrowserModelInternal.h b/new_editor/app/Features/Project/ProjectBrowserModelInternal.h deleted file mode 100644 index 5607dc64..00000000 --- a/new_editor/app/Features/Project/ProjectBrowserModelInternal.h +++ /dev/null @@ -1,62 +0,0 @@ -#pragma once - -#include "ProjectBrowserModel.h" - -#include -#include -#include -#include - -namespace XCEngine::UI::Editor::App::ProjectBrowserModelInternal { - -inline constexpr std::string_view kAssetsRootId = "Assets"; - -std::string ToLowerCopy(std::string value); -std::string PathToUtf8String(const std::filesystem::path& path); -std::filesystem::path BuildPathFromUtf8(std::string_view value); -std::string NormalizePathSeparators(std::string value); -std::string BuildRelativeItemId( - const std::filesystem::path& path, - const std::filesystem::path& assetsRoot); -std::string BuildRelativeProjectPath( - const std::filesystem::path& path, - const std::filesystem::path& projectRoot); -std::string BuildAssetDisplayName(const std::filesystem::path& path, bool directory); -std::string BuildAssetNameWithExtension(const std::filesystem::path& path, bool directory); -bool IsMetaFile(const std::filesystem::path& path); -bool HasChildDirectories(const std::filesystem::path& folderPath); -std::vector CollectSortedChildDirectories( - const std::filesystem::path& folderPath); -std::wstring MakePathKey(const std::filesystem::path& path); -bool IsSameOrDescendantPath( - const std::filesystem::path& path, - const std::filesystem::path& ancestor); -std::string TrimAssetName(std::string_view name); -bool HasInvalidAssetName(std::string_view name); -std::filesystem::path MakeUniqueFolderPath( - const std::filesystem::path& parentPath, - std::string_view preferredName); -std::filesystem::path MakeUniqueFilePath( - const std::filesystem::path& parentPath, - const std::filesystem::path& preferredFileName); -std::string BuildRenamedEntryName( - const std::filesystem::path& sourcePath, - std::string_view requestedName); -std::filesystem::path GetMetaSidecarPath(const std::filesystem::path& assetPath); -void RemoveMetaSidecarIfPresent(const std::filesystem::path& assetPath); -bool RenamePathCaseAware( - const std::filesystem::path& sourcePath, - const std::filesystem::path& destPath); -void MoveMetaSidecarIfPresent( - const std::filesystem::path& sourcePath, - const std::filesystem::path& destPath); -bool MovePathWithOptionalMeta( - const std::filesystem::path& sourcePath, - const std::filesystem::path& destinationPath); -ProjectBrowserModel::ItemKind ResolveItemKind( - const std::filesystem::path& path, - bool directory); -bool CanOpenItemKind(ProjectBrowserModel::ItemKind kind); -bool CanPreviewItemKind(ProjectBrowserModel::ItemKind kind); - -} // namespace XCEngine::UI::Editor::App::ProjectBrowserModelInternal diff --git a/new_editor/app/Features/Project/ProjectPanel.cpp b/new_editor/app/Features/Project/ProjectPanel.cpp index b2e1cad9..389f7bff 100644 --- a/new_editor/app/Features/Project/ProjectPanel.cpp +++ b/new_editor/app/Features/Project/ProjectPanel.cpp @@ -1,24 +1,225 @@ -#include "ProjectPanelInternal.h" - +#include "ProjectPanel.h" +#include "Rendering/Assets/BuiltInIcons.h" +#include "Composition/EditorPanelIds.h" +#include +#include +#include +#include +#include #include "Ports/SystemInteractionPort.h" #include "Project/EditorProjectRuntime.h" #include "State/EditorCommandFocusService.h" - #include #include #include #include - #include #include - #include #include #include namespace XCEngine::UI::Editor::App { -using namespace ProjectPanelInternal; +using ::XCEngine::UI::Editor::UIEditorTextMeasureRequest; +using ::XCEngine::UI::Editor::UIEditorTextMeasurer; +using ::XCEngine::UI::UIColor; +using ::XCEngine::UI::UIDrawList; +using ::XCEngine::UI::UIInputEvent; +using ::XCEngine::UI::UIPoint; +using ::XCEngine::UI::UIRect; +using ::XCEngine::UI::UITextureHandle; + +inline constexpr std::size_t kInvalidLayoutIndex = static_cast(-1); + +inline constexpr float kBrowserHeaderHeight = 24.0f; +inline constexpr float kNavigationMinWidth = 180.0f; +inline constexpr float kBrowserMinWidth = 260.0f; +inline constexpr float kHeaderHorizontalPadding = 10.0f; +inline constexpr float kHeaderBottomBorderThickness = 1.0f; +inline constexpr float kBreadcrumbItemPaddingX = 4.0f; +inline constexpr float kBreadcrumbItemPaddingY = 1.0f; +inline constexpr float kBreadcrumbSpacing = 3.0f; +inline constexpr float kTreeTopPadding = 0.0f; +inline constexpr float kGridInsetX = 16.0f; +inline constexpr float kGridInsetY = 12.0f; +inline constexpr float kGridTileWidth = 92.0f; +inline constexpr float kGridTileHeight = 92.0f; +inline constexpr float kGridTileGapX = 12.0f; +inline constexpr float kGridTileGapY = 12.0f; +inline constexpr float kGridPreviewWidth = 68.0f; +inline constexpr float kGridPreviewHeight = 54.0f; +inline constexpr float kHeaderFontSize = 12.0f; +inline constexpr float kTileLabelFontSize = 11.0f; + +inline constexpr UIColor kSurfaceColor(0.10f, 0.10f, 0.10f, 1.0f); +inline constexpr UIColor kPaneColor(0.10f, 0.10f, 0.10f, 1.0f); +inline constexpr UIColor kHeaderColor(0.11f, 0.11f, 0.11f, 1.0f); +inline constexpr UIColor kTextPrimary(0.880f, 0.880f, 0.880f, 1.0f); +inline constexpr UIColor kTextStrong(0.930f, 0.930f, 0.930f, 1.0f); +inline constexpr UIColor kTextMuted(0.640f, 0.640f, 0.640f, 1.0f); +inline constexpr UIColor kTileHoverColor(0.14f, 0.14f, 0.14f, 1.0f); +inline constexpr UIColor kTileSelectedColor(0.18f, 0.18f, 0.18f, 1.0f); +inline constexpr UIColor kTilePreviewFillColor(0.15f, 0.15f, 0.15f, 1.0f); +inline constexpr UIColor kTilePreviewShadeColor(0.12f, 0.12f, 0.12f, 1.0f); +inline constexpr UIColor kTilePreviewOutlineColor(0.920f, 0.920f, 0.920f, 0.20f); +inline constexpr UIColor kDropPreviewColor(0.92f, 0.92f, 0.92f, 0.42f); + +bool ContainsPoint(const UIRect& rect, const UIPoint& point); +float ClampNonNegative(float value); +float ResolveTextTop(float rectY, float rectHeight, float fontSize); +float MeasureTextWidth( + const UIEditorTextMeasurer* textMeasurer, + std::string_view text, + float fontSize); +::XCEngine::UI::UITextureHandle ResolveFolderIcon(const BuiltInIcons* icons); +float ClampNavigationWidth(float value, float totalWidth); +UIRect ComputeFittedTextureRect( + const UIRect& bounds, + const UITextureHandle& texture); +void AppendTexturePreview( + UIDrawList& drawList, + const UIRect& bounds, + const UITextureHandle& texture); +void AppendTilePreview( + UIDrawList& drawList, + const UIRect& previewRect, + bool directory, + const UITextureHandle* texture); + +} // namespace XCEngine::UI::Editor::App + +namespace XCEngine::UI::Editor::App { + +using ::XCEngine::UI::UIInputEventType; + +bool ContainsPoint(const UIRect& rect, const UIPoint& point) { + return point.x >= rect.x && + point.x <= rect.x + rect.width && + point.y >= rect.y && + point.y <= rect.y + rect.height; +} + +float ClampNonNegative(float value) { + return (std::max)(value, 0.0f); +} + +float ResolveTextTop(float rectY, float rectHeight, float fontSize) { + const float lineHeight = fontSize * 1.6f; + return rectY + std::floor((rectHeight - lineHeight) * 0.5f); +} + +float MeasureTextWidth( + const UIEditorTextMeasurer* textMeasurer, + std::string_view text, + float fontSize) { + if (text.empty()) { + return 0.0f; + } + + if (textMeasurer != nullptr) { + const float measuredWidth = + textMeasurer->MeasureTextWidth(UIEditorTextMeasureRequest{ text, fontSize }); + if (measuredWidth > 0.0f) { + return measuredWidth; + } + } + + return static_cast(text.size()) * fontSize * 0.56f; +} + +::XCEngine::UI::UITextureHandle ResolveFolderIcon(const BuiltInIcons* icons) { + return icons != nullptr + ? icons->Resolve(BuiltInIconKind::Folder) + : ::XCEngine::UI::UITextureHandle {}; +} + +float ClampNavigationWidth(float value, float totalWidth) { + const float maxWidth = + (std::max)( + kNavigationMinWidth, + totalWidth - kBrowserMinWidth - ResolveUIEditorDockHostMetrics().splitterMetrics.thickness); + return std::clamp(value, kNavigationMinWidth, maxWidth); +} + +UIRect ComputeFittedTextureRect( + const UIRect& bounds, + const UITextureHandle& texture) { + if (!texture.IsValid() || + bounds.width <= 0.0f || + bounds.height <= 0.0f) { + return bounds; + } + + const float scale = (std::min)( + bounds.width / static_cast(texture.width), + bounds.height / static_cast(texture.height)); + const float width = static_cast(texture.width) * scale; + const float height = static_cast(texture.height) * scale; + return UIRect( + bounds.x + (bounds.width - width) * 0.5f, + bounds.y + (bounds.height - height) * 0.5f, + width, + height); +} + +void AppendTexturePreview( + UIDrawList& drawList, + const UIRect& bounds, + const UITextureHandle& texture) { + if (!texture.IsValid()) { + return; + } + + drawList.AddImage(ComputeFittedTextureRect(bounds, texture), texture); +} + +void AppendTilePreview( + UIDrawList& drawList, + const UIRect& previewRect, + bool directory, + const UITextureHandle* texture) { + if (texture != nullptr && texture->IsValid()) { + AppendTexturePreview(drawList, previewRect, *texture); + return; + } + + if (directory) { + const UIRect tabRect( + previewRect.x + 8.0f, + previewRect.y + 7.0f, + 22.0f, + 7.0f); + const UIRect bodyRect( + previewRect.x + 4.0f, + previewRect.y + 13.0f, + previewRect.width - 8.0f, + previewRect.height - 18.0f); + drawList.AddFilledRect(tabRect, kTilePreviewShadeColor, 1.0f); + drawList.AddFilledRect(bodyRect, kTilePreviewFillColor, 1.0f); + drawList.AddRectOutline(bodyRect, kTilePreviewOutlineColor, 1.0f, 1.0f); + return; + } + + const UIRect sheetRect( + previewRect.x + 10.0f, + previewRect.y + 4.0f, + previewRect.width - 20.0f, + previewRect.height - 8.0f); + const UIRect accentRect( + sheetRect.x, + sheetRect.y, + sheetRect.width, + 7.0f); + drawList.AddFilledRect(sheetRect, kTilePreviewFillColor, 1.0f); + drawList.AddFilledRect(accentRect, kTilePreviewShadeColor, 1.0f); + drawList.AddRectOutline(sheetRect, kTilePreviewOutlineColor, 1.0f, 1.0f); +} + +} // namespace XCEngine::UI::Editor::App + +namespace XCEngine::UI::Editor::App { + using ::XCEngine::Input::KeyCode; namespace GridDrag = XCEngine::UI::Editor::Collections::GridDragDrop; namespace TreeDrag = XCEngine::UI::Editor::Collections::TreeDragDrop; @@ -134,7 +335,7 @@ void ProjectPanel::SetSystemInteractionHost( m_systemInteractionHost = systemInteractionHost; } -void ProjectPanel::SetBuiltInIcons(const BuiltInIcons* icons) { +void ProjectPanel::SetBuiltInIcons(BuiltInIcons* icons) { m_icons = icons; if (EditorProjectRuntime* runtime = ResolveProjectRuntime(); runtime != nullptr) { @@ -1967,7 +2168,13 @@ void ProjectPanel::Append(UIDrawList& drawList) const { return; } - const auto& assetEntries = GetBrowserModel().GetAssetEntries(); + if (m_icons != nullptr) { + m_icons->BeginFrame(); + } + + const BrowserModel& browserModel = GetBrowserModel(); + const auto& assetEntries = browserModel.GetAssetEntries(); + const std::filesystem::path projectRootPath = browserModel.GetProjectRootPath(); drawList.AddFilledRect(m_layout.bounds, kSurfaceColor); drawList.AddFilledRect(m_layout.leftPaneRect, kPaneColor); @@ -2078,7 +2285,21 @@ void ProjectPanel::Append(UIDrawList& drawList) const { selected ? kTileSelectedColor : kTileHoverColor); } - AppendTilePreview(drawList, tile.previewRect, assetEntry.directory); + const UITextureHandle* tileTexture = nullptr; + if (assetEntry.directory) { + tileTexture = m_icons != nullptr + ? &m_icons->Resolve(BuiltInIconKind::Folder) + : nullptr; + } else if (assetEntry.canPreview && m_icons != nullptr) { + tileTexture = + m_icons->ResolveAssetPreview(assetEntry.absolutePath, projectRootPath); + } + + AppendTilePreview( + drawList, + tile.previewRect, + assetEntry.directory, + tileTexture); drawList.PushClipRect(tile.labelRect); const float textWidth = @@ -2151,3 +2372,6 @@ void ProjectPanel::Append(UIDrawList& drawList) const { } } // namespace XCEngine::UI::Editor::App + + + diff --git a/new_editor/app/Features/Project/ProjectPanel.h b/new_editor/app/Features/Project/ProjectPanel.h index 3893169a..114bc2e8 100644 --- a/new_editor/app/Features/Project/ProjectPanel.h +++ b/new_editor/app/Features/Project/ProjectPanel.h @@ -85,7 +85,7 @@ public: void SetProjectRuntime(EditorProjectRuntime* projectRuntime); void SetCommandFocusService(EditorCommandFocusService* commandFocusService); void SetSystemInteractionHost(Ports::SystemInteractionPort* systemInteractionHost); - void SetBuiltInIcons(const BuiltInIcons* icons); + void SetBuiltInIcons(BuiltInIcons* icons); void SetTextMeasurer(const ::XCEngine::UI::Editor::UIEditorTextMeasurer* textMeasurer); void ResetInteractionState(); void Update( @@ -244,7 +244,7 @@ private: EditorProjectRuntime* m_projectRuntime = nullptr; EditorCommandFocusService* m_commandFocusService = nullptr; Ports::SystemInteractionPort* m_systemInteractionHost = nullptr; - const BuiltInIcons* m_icons = nullptr; + BuiltInIcons* m_icons = nullptr; const ::XCEngine::UI::Editor::UIEditorTextMeasurer* m_textMeasurer = nullptr; ::XCEngine::UI::Widgets::UISelectionModel m_folderSelection = {}; ::XCEngine::UI::Widgets::UIExpansionModel m_folderExpansion = {}; diff --git a/new_editor/app/Features/Project/ProjectPanelInternal.cpp b/new_editor/app/Features/Project/ProjectPanelInternal.cpp deleted file mode 100644 index d8c29782..00000000 --- a/new_editor/app/Features/Project/ProjectPanelInternal.cpp +++ /dev/null @@ -1,92 +0,0 @@ -#include "ProjectPanelInternal.h" - -namespace XCEngine::UI::Editor::App::ProjectPanelInternal { - -using ::XCEngine::UI::UIInputEventType; - -bool ContainsPoint(const UIRect& rect, const UIPoint& point) { - return point.x >= rect.x && - point.x <= rect.x + rect.width && - point.y >= rect.y && - point.y <= rect.y + rect.height; -} - -float ClampNonNegative(float value) { - return (std::max)(value, 0.0f); -} - -float ResolveTextTop(float rectY, float rectHeight, float fontSize) { - const float lineHeight = fontSize * 1.6f; - return rectY + std::floor((rectHeight - lineHeight) * 0.5f); -} - -float MeasureTextWidth( - const UIEditorTextMeasurer* textMeasurer, - std::string_view text, - float fontSize) { - if (text.empty()) { - return 0.0f; - } - - if (textMeasurer != nullptr) { - const float measuredWidth = - textMeasurer->MeasureTextWidth(UIEditorTextMeasureRequest{ text, fontSize }); - if (measuredWidth > 0.0f) { - return measuredWidth; - } - } - - return static_cast(text.size()) * fontSize * 0.56f; -} - -::XCEngine::UI::UITextureHandle ResolveFolderIcon(const BuiltInIcons* icons) { - return icons != nullptr - ? icons->Resolve(BuiltInIconKind::Folder) - : ::XCEngine::UI::UITextureHandle {}; -} - -float ClampNavigationWidth(float value, float totalWidth) { - const float maxWidth = - (std::max)( - kNavigationMinWidth, - totalWidth - kBrowserMinWidth - ResolveUIEditorDockHostMetrics().splitterMetrics.thickness); - return std::clamp(value, kNavigationMinWidth, maxWidth); -} - -void AppendTilePreview( - UIDrawList& drawList, - const UIRect& previewRect, - bool directory) { - if (directory) { - const UIRect tabRect( - previewRect.x + 8.0f, - previewRect.y + 7.0f, - 22.0f, - 7.0f); - const UIRect bodyRect( - previewRect.x + 4.0f, - previewRect.y + 13.0f, - previewRect.width - 8.0f, - previewRect.height - 18.0f); - drawList.AddFilledRect(tabRect, kTilePreviewShadeColor, 1.0f); - drawList.AddFilledRect(bodyRect, kTilePreviewFillColor, 1.0f); - drawList.AddRectOutline(bodyRect, kTilePreviewOutlineColor, 1.0f, 1.0f); - return; - } - - const UIRect sheetRect( - previewRect.x + 10.0f, - previewRect.y + 4.0f, - previewRect.width - 20.0f, - previewRect.height - 8.0f); - const UIRect accentRect( - sheetRect.x, - sheetRect.y, - sheetRect.width, - 7.0f); - drawList.AddFilledRect(sheetRect, kTilePreviewFillColor, 1.0f); - drawList.AddFilledRect(accentRect, kTilePreviewShadeColor, 1.0f); - drawList.AddRectOutline(sheetRect, kTilePreviewOutlineColor, 1.0f, 1.0f); -} - -} // namespace XCEngine::UI::Editor::App::ProjectPanelInternal diff --git a/new_editor/app/Features/Project/ProjectPanelInternal.h b/new_editor/app/Features/Project/ProjectPanelInternal.h deleted file mode 100644 index 89169a0b..00000000 --- a/new_editor/app/Features/Project/ProjectPanelInternal.h +++ /dev/null @@ -1,74 +0,0 @@ -#pragma once - -#include "ProjectPanel.h" - -#include "Rendering/Assets/BuiltInIcons.h" - -#include "Composition/EditorPanelIds.h" -#include -#include - -#include -#include -#include - -namespace XCEngine::UI::Editor::App::ProjectPanelInternal { - -using ::XCEngine::UI::Editor::UIEditorTextMeasureRequest; -using ::XCEngine::UI::Editor::UIEditorTextMeasurer; -using ::XCEngine::UI::UIColor; -using ::XCEngine::UI::UIDrawList; -using ::XCEngine::UI::UIInputEvent; -using ::XCEngine::UI::UIPoint; -using ::XCEngine::UI::UIRect; - -inline constexpr std::size_t kInvalidLayoutIndex = static_cast(-1); - -inline constexpr float kBrowserHeaderHeight = 24.0f; -inline constexpr float kNavigationMinWidth = 180.0f; -inline constexpr float kBrowserMinWidth = 260.0f; -inline constexpr float kHeaderHorizontalPadding = 10.0f; -inline constexpr float kHeaderBottomBorderThickness = 1.0f; -inline constexpr float kBreadcrumbItemPaddingX = 4.0f; -inline constexpr float kBreadcrumbItemPaddingY = 1.0f; -inline constexpr float kBreadcrumbSpacing = 3.0f; -inline constexpr float kTreeTopPadding = 0.0f; -inline constexpr float kGridInsetX = 16.0f; -inline constexpr float kGridInsetY = 12.0f; -inline constexpr float kGridTileWidth = 92.0f; -inline constexpr float kGridTileHeight = 92.0f; -inline constexpr float kGridTileGapX = 12.0f; -inline constexpr float kGridTileGapY = 12.0f; -inline constexpr float kGridPreviewWidth = 68.0f; -inline constexpr float kGridPreviewHeight = 54.0f; -inline constexpr float kHeaderFontSize = 12.0f; -inline constexpr float kTileLabelFontSize = 11.0f; - -inline constexpr UIColor kSurfaceColor(0.10f, 0.10f, 0.10f, 1.0f); -inline constexpr UIColor kPaneColor(0.10f, 0.10f, 0.10f, 1.0f); -inline constexpr UIColor kHeaderColor(0.11f, 0.11f, 0.11f, 1.0f); -inline constexpr UIColor kTextPrimary(0.880f, 0.880f, 0.880f, 1.0f); -inline constexpr UIColor kTextStrong(0.930f, 0.930f, 0.930f, 1.0f); -inline constexpr UIColor kTextMuted(0.640f, 0.640f, 0.640f, 1.0f); -inline constexpr UIColor kTileHoverColor(0.14f, 0.14f, 0.14f, 1.0f); -inline constexpr UIColor kTileSelectedColor(0.18f, 0.18f, 0.18f, 1.0f); -inline constexpr UIColor kTilePreviewFillColor(0.15f, 0.15f, 0.15f, 1.0f); -inline constexpr UIColor kTilePreviewShadeColor(0.12f, 0.12f, 0.12f, 1.0f); -inline constexpr UIColor kTilePreviewOutlineColor(0.920f, 0.920f, 0.920f, 0.20f); -inline constexpr UIColor kDropPreviewColor(0.92f, 0.92f, 0.92f, 0.42f); - -bool ContainsPoint(const UIRect& rect, const UIPoint& point); -float ClampNonNegative(float value); -float ResolveTextTop(float rectY, float rectHeight, float fontSize); -float MeasureTextWidth( - const UIEditorTextMeasurer* textMeasurer, - std::string_view text, - float fontSize); -::XCEngine::UI::UITextureHandle ResolveFolderIcon(const BuiltInIcons* icons); -float ClampNavigationWidth(float value, float totalWidth); -void AppendTilePreview( - UIDrawList& drawList, - const UIRect& previewRect, - bool directory); - -} // namespace XCEngine::UI::Editor::App::ProjectPanelInternal diff --git a/new_editor/app/Features/Scene/SceneViewportController.cpp b/new_editor/app/Features/Scene/SceneViewportController.cpp index 956289ee..7ec883d6 100644 --- a/new_editor/app/Features/Scene/SceneViewportController.cpp +++ b/new_editor/app/Features/Scene/SceneViewportController.cpp @@ -115,12 +115,33 @@ void ApplySceneToolMode( sceneRuntime.SetToolMode(mode); } +void ApplySceneViewportToggleButton( + std::size_t toggleIndex, + EditorSceneRuntime& sceneRuntime) { + if (toggleIndex == 0u) { + sceneRuntime.SetToolPivotMode( + sceneRuntime.GetToolPivotMode() == SceneToolPivotMode::Pivot + ? SceneToolPivotMode::Center + : SceneToolPivotMode::Pivot); + return; + } + + if (toggleIndex == 1u) { + sceneRuntime.SetToolSpaceMode( + sceneRuntime.GetToolSpaceMode() == SceneToolSpaceMode::World + ? SceneToolSpaceMode::Local + : SceneToolSpaceMode::World); + } +} + } // namespace void SceneViewportController::Initialize( const std::filesystem::path& repoRoot, - Ports::TexturePort& renderer) { + Ports::TexturePort& renderer, + const BuiltInIcons* builtInIcons) { m_toolOverlay.Initialize(repoRoot, renderer); + m_sceneOverlay.SetBuiltInIcons(builtInIcons); ResetInteractionState(); } @@ -131,6 +152,7 @@ void SceneViewportController::Shutdown(Ports::TexturePort& renderer) { void SceneViewportController::ResetInteractionState() { ResetFrameState(); + m_sceneOverlay.ResetFrame(); m_toolOverlay.ResetFrame(); m_transformGizmo.ResetVisualState(); } @@ -181,13 +203,15 @@ void SceneViewportController::Update( inputFrame.pointerButtonTransitions.end(), [&](const auto& transition) { return transition.pressed && - ContainsPoint(slotLayout.inputRect, transition.screenPosition); + ContainsPoint(slotLayout.bounds, transition.screenPosition); }))) { m_commandFocusService->ClaimFocus(EditorActionRoute::Scene); } if (inputFrame.focusLost) { m_navigationState = {}; + m_hoveredToggleOverlayIndex = kSceneViewportToolOverlayInvalidIndex; + m_activeToggleOverlayIndex = kSceneViewportToolOverlayInvalidIndex; m_hoveredToolOverlayIndex = kSceneViewportToolOverlayInvalidIndex; m_activeToolOverlayIndex = kSceneViewportToolOverlayInvalidIndex; if (m_transformGizmo.IsActive()) { @@ -197,12 +221,22 @@ void SceneViewportController::Update( m_toolOverlay.BuildFrame( slotLayout.inputRect, + slotLayout.topBarRect, + slotLayout.bounds, sceneRuntime.GetToolMode(), + sceneRuntime.GetToolPivotMode(), + sceneRuntime.GetToolSpaceMode(), + kSceneViewportToolOverlayInvalidIndex, + kSceneViewportToolOverlayInvalidIndex, kSceneViewportToolOverlayInvalidIndex, kSceneViewportToolOverlayInvalidIndex); - const bool pointerOverToolOverlay = + const bool pointerOverOverlay = inputState.hasPointerPosition && m_toolOverlay.Contains(pointerScreen); + const std::size_t hoveredToggleOverlayIndex = + inputState.hasPointerPosition + ? m_toolOverlay.HitTestToggle(pointerScreen) + : kSceneViewportToolOverlayInvalidIndex; const std::size_t hoveredToolOverlayIndex = inputState.hasPointerPosition ? m_toolOverlay.HitTest(pointerScreen) @@ -213,6 +247,19 @@ void SceneViewportController::Update( continue; } + const std::size_t transitionHoveredToggleOverlayIndex = + m_toolOverlay.HitTestToggle(transition.screenPosition); + if (transition.pressed && + transitionHoveredToggleOverlayIndex != + kSceneViewportToolOverlayInvalidIndex && + !m_transformGizmo.IsActive()) { + m_activeToggleOverlayIndex = transitionHoveredToggleOverlayIndex; + ApplySceneViewportToggleButton( + m_activeToggleOverlayIndex, + sceneRuntime); + continue; + } + const std::size_t transitionHoveredToolOverlayIndex = m_toolOverlay.HitTest(transition.screenPosition); if (transition.pressed && @@ -229,21 +276,31 @@ void SceneViewportController::Update( continue; } + m_activeToggleOverlayIndex = kSceneViewportToolOverlayInvalidIndex; m_activeToolOverlayIndex = kSceneViewportToolOverlayInvalidIndex; } if (!leftMouseDown) { + m_activeToggleOverlayIndex = kSceneViewportToolOverlayInvalidIndex; m_activeToolOverlayIndex = kSceneViewportToolOverlayInvalidIndex; } + m_hoveredToggleOverlayIndex = hoveredToggleOverlayIndex; m_hoveredToolOverlayIndex = hoveredToolOverlayIndex; m_toolOverlay.BuildFrame( slotLayout.inputRect, + slotLayout.topBarRect, + slotLayout.bounds, sceneRuntime.GetToolMode(), + sceneRuntime.GetToolPivotMode(), + sceneRuntime.GetToolSpaceMode(), m_hoveredToolOverlayIndex, - m_activeToolOverlayIndex); + m_activeToolOverlayIndex, + m_hoveredToggleOverlayIndex, + m_activeToggleOverlayIndex); - const bool toolOverlayInteractionActive = - m_activeToolOverlayIndex != kSceneViewportToolOverlayInvalidIndex; + const bool overlayInteractionActive = + m_activeToolOverlayIndex != kSceneViewportToolOverlayInvalidIndex || + m_activeToggleOverlayIndex != kSceneViewportToolOverlayInvalidIndex; const bool usingViewMoveTool = IsViewMoveTool(sceneRuntime.GetToolMode()); @@ -284,13 +341,13 @@ void SceneViewportController::Update( const bool viewportHoverEligible = inputState.hasPointerPosition && ContainsPoint(slotLayout.inputRect, pointerScreen) && - !pointerOverToolOverlay && - !toolOverlayInteractionActive && + !pointerOverOverlay && + !overlayInteractionActive && !m_navigationState.lookDragging && !m_navigationState.panDragging; if (!m_transformGizmo.IsActive() && - !toolOverlayInteractionActive && + !overlayInteractionActive && inputFrame.focused && !m_navigationState.lookDragging && !m_navigationState.panDragging) { @@ -306,7 +363,7 @@ void SceneViewportController::Update( sceneRuntime.FocusSceneSelection(); } - m_transformGizmo.Refresh( + RefreshSceneOverlays( sceneRuntime, slotLayout.inputRect, pointerScreen, @@ -316,7 +373,7 @@ void SceneViewportController::Update( if (m_transformGizmo.IsActive()) { if (WasKeyPressed(inputFrame, KeyCode::Escape)) { m_transformGizmo.CancelDrag(sceneRuntime); - m_transformGizmo.Refresh( + RefreshSceneOverlays( sceneRuntime, slotLayout.inputRect, pointerScreen, @@ -326,7 +383,7 @@ void SceneViewportController::Update( if (!leftMouseDown) { m_transformGizmo.EndDrag(sceneRuntime); - m_transformGizmo.Refresh( + RefreshSceneOverlays( sceneRuntime, slotLayout.inputRect, pointerScreen, @@ -335,7 +392,7 @@ void SceneViewportController::Update( } m_transformGizmo.UpdateDrag(sceneRuntime); - m_transformGizmo.Refresh( + RefreshSceneOverlays( sceneRuntime, slotLayout.inputRect, pointerScreen, @@ -352,7 +409,7 @@ void SceneViewportController::Update( continue; } - m_transformGizmo.Refresh( + RefreshSceneOverlays( sceneRuntime, slotLayout.inputRect, transition.screenPosition, @@ -360,7 +417,19 @@ void SceneViewportController::Update( if (m_transformGizmo.IsHoveringHandle() && m_transformGizmo.TryBeginDrag(sceneRuntime)) { m_navigationState = {}; - m_transformGizmo.Refresh( + RefreshSceneOverlays( + sceneRuntime, + slotLayout.inputRect, + pointerScreen, + viewportHoverEligible); + return; + } + + const SceneViewportSceneOverlay::HitResult sceneIconHit = + m_sceneOverlay.HitTest(transition.screenPosition); + if (sceneIconHit.HasHit()) { + sceneRuntime.SetSelection(sceneIconHit.entityId); + RefreshSceneOverlays( sceneRuntime, slotLayout.inputRect, pointerScreen, @@ -380,7 +449,7 @@ void SceneViewportController::Update( } } - m_transformGizmo.Refresh( + RefreshSceneOverlays( sceneRuntime, slotLayout.inputRect, pointerScreen, @@ -388,7 +457,7 @@ void SceneViewportController::Update( } } - if (toolOverlayInteractionActive) { + if (overlayInteractionActive) { return; } @@ -414,27 +483,32 @@ void SceneViewportController::Update( } else if (m_navigationState.panDragging) { input.panDeltaX = inputFrame.pointerDelta.x; input.panDeltaY = inputFrame.pointerDelta.y; - } else if (ContainsPoint(slotLayout.inputRect, pointerScreen) && !pointerOverToolOverlay) { + } else if (ContainsPoint(slotLayout.inputRect, pointerScreen) && !pointerOverOverlay) { input.zoomDelta = NormalizeWheelDelta(inputFrame.wheelDelta); } if (HasCameraInput(input)) { sceneRuntime.ApplySceneViewportCameraInput(input); - m_transformGizmo.Refresh( + RefreshSceneOverlays( sceneRuntime, slotLayout.inputRect, pointerScreen, viewportHoverEligible); + } else { + m_sceneOverlay.Refresh(sceneRuntime, slotLayout.inputRect); } } void SceneViewportController::Append(::XCEngine::UI::UIDrawList& drawList) const { + m_sceneOverlay.Append(drawList); AppendSceneViewportTransformGizmo(drawList, m_transformGizmo.GetFrame()); AppendSceneViewportToolOverlay(drawList, m_toolOverlay.GetFrame()); } void SceneViewportController::ResetFrameState() { m_navigationState = {}; + m_hoveredToggleOverlayIndex = kSceneViewportToolOverlayInvalidIndex; + m_activeToggleOverlayIndex = kSceneViewportToolOverlayInvalidIndex; m_hoveredToolOverlayIndex = kSceneViewportToolOverlayInvalidIndex; m_activeToolOverlayIndex = kSceneViewportToolOverlayInvalidIndex; m_lastUpdateTime = {}; @@ -455,4 +529,17 @@ float SceneViewportController::ConsumeDeltaTimeSeconds() { return std::clamp(deltaTime, 0.0f, 0.1f); } +void SceneViewportController::RefreshSceneOverlays( + EditorSceneRuntime& sceneRuntime, + const UIRect& viewportRect, + const UIPoint& pointerScreen, + bool hoverEnabled) { + m_sceneOverlay.Refresh(sceneRuntime, viewportRect); + m_transformGizmo.Refresh( + sceneRuntime, + viewportRect, + pointerScreen, + hoverEnabled); +} + } // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/Scene/SceneViewportController.h b/new_editor/app/Features/Scene/SceneViewportController.h index dac89d7b..e507b3ac 100644 --- a/new_editor/app/Features/Scene/SceneViewportController.h +++ b/new_editor/app/Features/Scene/SceneViewportController.h @@ -1,5 +1,6 @@ #pragma once +#include "Features/Scene/SceneViewportSceneOverlay.h" #include "Features/Scene/SceneViewportTransformGizmo.h" #include "Features/Scene/SceneViewportToolOverlay.h" @@ -14,6 +15,7 @@ namespace XCEngine::UI::Editor::App { class EditorCommandFocusService; +class BuiltInIcons; class EditorSceneRuntime; class IViewportObjectPickerService; @@ -31,7 +33,8 @@ class SceneViewportController { public: void Initialize( const std::filesystem::path& repoRoot, - Ports::TexturePort& renderer); + Ports::TexturePort& renderer, + const BuiltInIcons* builtInIcons); void Shutdown(Ports::TexturePort& renderer); void ResetInteractionState(); void SetCommandFocusService(EditorCommandFocusService* commandFocusService); @@ -53,11 +56,19 @@ private: void ResetFrameState(); float ConsumeDeltaTimeSeconds(); + void RefreshSceneOverlays( + EditorSceneRuntime& sceneRuntime, + const ::XCEngine::UI::UIRect& viewportRect, + const ::XCEngine::UI::UIPoint& pointerScreen, + bool hoverEnabled); NavigationState m_navigationState = {}; EditorCommandFocusService* m_commandFocusService = nullptr; + SceneViewportSceneOverlay m_sceneOverlay = {}; SceneViewportTransformGizmo m_transformGizmo = {}; SceneViewportToolOverlay m_toolOverlay = {}; + std::size_t m_hoveredToggleOverlayIndex = kSceneViewportToolOverlayInvalidIndex; + std::size_t m_activeToggleOverlayIndex = kSceneViewportToolOverlayInvalidIndex; std::size_t m_hoveredToolOverlayIndex = kSceneViewportToolOverlayInvalidIndex; std::size_t m_activeToolOverlayIndex = kSceneViewportToolOverlayInvalidIndex; std::chrono::steady_clock::time_point m_lastUpdateTime = {}; diff --git a/new_editor/app/Features/Scene/SceneViewportFeature.cpp b/new_editor/app/Features/Scene/SceneViewportFeature.cpp index 6616f348..60ec73d7 100644 --- a/new_editor/app/Features/Scene/SceneViewportFeature.cpp +++ b/new_editor/app/Features/Scene/SceneViewportFeature.cpp @@ -11,12 +11,13 @@ namespace XCEngine::UI::Editor::App { void SceneViewportFeature::Initialize( const std::filesystem::path& repoRoot, Ports::TexturePort& textureHost, + const BuiltInIcons* builtInIcons, ViewportHostService& viewportHostService) { viewportHostService.SetContentRenderer( kScenePanelId, &m_renderService, SceneViewportRenderService::GetViewportResourceRequirements()); - m_controller.Initialize(repoRoot, textureHost); + m_controller.Initialize(repoRoot, textureHost, builtInIcons); } void SceneViewportFeature::Shutdown( diff --git a/new_editor/app/Features/Scene/SceneViewportFeature.h b/new_editor/app/Features/Scene/SceneViewportFeature.h index 6a9db88a..ec43f7c8 100644 --- a/new_editor/app/Features/Scene/SceneViewportFeature.h +++ b/new_editor/app/Features/Scene/SceneViewportFeature.h @@ -12,6 +12,7 @@ namespace XCEngine::UI::Editor::App { class EditorCommandFocusService; +class BuiltInIcons; class EditorSceneRuntime; } // namespace XCEngine::UI::Editor::App @@ -31,6 +32,7 @@ public: void Initialize( const std::filesystem::path& repoRoot, Ports::TexturePort& textureHost, + const BuiltInIcons* builtInIcons, ViewportHostService& viewportHostService); void Shutdown( Ports::TexturePort& textureHost, diff --git a/new_editor/app/Features/Scene/SceneViewportSceneOverlay.cpp b/new_editor/app/Features/Scene/SceneViewportSceneOverlay.cpp new file mode 100644 index 00000000..e85a7b6a --- /dev/null +++ b/new_editor/app/Features/Scene/SceneViewportSceneOverlay.cpp @@ -0,0 +1,249 @@ +#include "Features/Scene/SceneViewportSceneOverlay.h" + +#include "Features/Scene/SceneViewportTransformGizmoSupport.h" +#include "Rendering/Assets/BuiltInIcons.h" +#include "Scene/EditorSceneRuntime.h" + +#include +#include +#include +#include +#include + +#include +#include + +namespace XCEngine::UI::Editor::App { + +namespace { + +using ::XCEngine::Components::CameraComponent; +using ::XCEngine::Components::GameObject; +using ::XCEngine::Components::LightComponent; +using ::XCEngine::Components::Scene; +using ::XCEngine::Components::TransformComponent; +using ::XCEngine::Math::Vector2; +using ::XCEngine::UI::UIPoint; +using ::XCEngine::UI::UIRect; +namespace SceneViewportGizmoSupport = ::XCEngine::UI::Editor::App::SceneViewportGizmoSupport; + +constexpr Vector2 kCameraIconSize(90.0f, 90.0f); +constexpr Vector2 kLightIconSize(100.0f, 100.0f); + +bool CanBuildSceneIcon(const GameObject* gameObject) { + return gameObject != nullptr && + gameObject->GetTransform() != nullptr && + gameObject->IsActiveInHierarchy(); +} + +bool ContainsPoint(const UIRect& rect, const UIPoint& point) { + return point.x >= rect.x && + point.x <= rect.x + rect.width && + point.y >= rect.y && + point.y <= rect.y + rect.height; +} + +SceneViewportGizmoSupport::SceneViewportOverlayData BuildOverlayData( + const EditorSceneRuntime& sceneRuntime) { + SceneViewportGizmoSupport::SceneViewportOverlayData overlay = {}; + const CameraComponent* camera = sceneRuntime.GetSceneViewCamera(); + if (camera == nullptr || camera->GetGameObject() == nullptr) { + return overlay; + } + + const TransformComponent* transform = camera->GetGameObject()->GetTransform(); + if (transform == nullptr) { + return overlay; + } + + overlay.valid = true; + overlay.cameraPosition = transform->GetPosition(); + overlay.cameraForward = transform->GetForward(); + overlay.cameraRight = transform->GetRight(); + overlay.cameraUp = transform->GetUp(); + overlay.verticalFovDegrees = camera->GetFieldOfView(); + overlay.nearClipPlane = camera->GetNearClipPlane(); + overlay.farClipPlane = camera->GetFarClipPlane(); + overlay.orbitDistance = sceneRuntime.GetSceneViewOrbitDistance(); + return overlay; +} + +::XCEngine::UI::UITextureHandle ResolveCameraIcon(const BuiltInIcons* icons) { + return icons != nullptr + ? icons->Resolve(BuiltInIconKind::CameraGizmo) + : ::XCEngine::UI::UITextureHandle(); +} + +::XCEngine::UI::UITextureHandle ResolveLightIcon( + const BuiltInIcons* icons, + const LightComponent& light) { + if (icons == nullptr) { + return {}; + } + + switch (light.GetLightType()) { + case ::XCEngine::Components::LightType::Directional: + return icons->Resolve(BuiltInIconKind::DirectionalLightGizmo); + case ::XCEngine::Components::LightType::Point: + return icons->Resolve(BuiltInIconKind::PointLightGizmo); + case ::XCEngine::Components::LightType::Spot: + return icons->Resolve(BuiltInIconKind::SpotLightGizmo); + default: + return {}; + } +} + +} // namespace + +void SceneViewportSceneOverlay::SetBuiltInIcons(const BuiltInIcons* icons) { + m_icons = icons; + if (m_icons == nullptr) { + ResetFrame(); + } +} + +void SceneViewportSceneOverlay::ResetFrame() { + m_frame = {}; +} + +void SceneViewportSceneOverlay::Refresh( + EditorSceneRuntime& sceneRuntime, + const UIRect& viewportRect) { + m_frame = {}; + m_frame.clipRect = viewportRect; + + if (m_icons == nullptr || + viewportRect.width <= 1.0f || + viewportRect.height <= 1.0f) { + return; + } + + Scene* scene = sceneRuntime.GetActiveScene(); + if (scene == nullptr) { + return; + } + + const SceneViewportGizmoSupport::SceneViewportOverlayData overlay = + BuildOverlayData(sceneRuntime); + if (!overlay.valid) { + return; + } + + std::vector icons = {}; + const auto tryAppendIcon = + [&](const GameObject& gameObject, + const Vector2& iconSize, + const ::XCEngine::UI::UITextureHandle& texture) { + if (!texture.IsValid()) { + return; + } + + const TransformComponent* transform = gameObject.GetTransform(); + if (transform == nullptr) { + return; + } + + const SceneViewportGizmoSupport::SceneViewportProjectedPoint projectedPoint = + SceneViewportGizmoSupport::ProjectSceneViewportWorldPoint( + overlay, + viewportRect.width, + viewportRect.height, + transform->GetPosition()); + if (!projectedPoint.visible) { + return; + } + + IconFrame icon = {}; + icon.entityId = gameObject.GetID(); + icon.rect = UIRect( + viewportRect.x + projectedPoint.screenPosition.x - iconSize.x * 0.5f, + viewportRect.y + projectedPoint.screenPosition.y - iconSize.y * 0.5f, + iconSize.x, + iconSize.y); + icon.texture = texture; + icon.sortDepth = projectedPoint.ndcDepth; + icons.push_back(std::move(icon)); + }; + + const ::XCEngine::UI::UITextureHandle cameraIcon = ResolveCameraIcon(m_icons); + for (CameraComponent* camera : scene->FindObjectsOfType()) { + if (camera == nullptr || !camera->IsEnabled()) { + continue; + } + + GameObject* gameObject = camera->GetGameObject(); + if (!CanBuildSceneIcon(gameObject)) { + continue; + } + + tryAppendIcon(*gameObject, kCameraIconSize, cameraIcon); + } + + for (LightComponent* light : scene->FindObjectsOfType()) { + if (light == nullptr || !light->IsEnabled()) { + continue; + } + + GameObject* gameObject = light->GetGameObject(); + if (!CanBuildSceneIcon(gameObject)) { + continue; + } + + tryAppendIcon( + *gameObject, + kLightIconSize, + ResolveLightIcon(m_icons, *light)); + } + + if (icons.empty()) { + return; + } + + std::sort( + icons.begin(), + icons.end(), + [](const IconFrame& lhs, const IconFrame& rhs) { + return lhs.sortDepth > rhs.sortDepth; + }); + m_frame.visible = true; + m_frame.icons = std::move(icons); +} + +SceneViewportSceneOverlay::HitResult SceneViewportSceneOverlay::HitTest( + const UIPoint& point) const { + HitResult result = {}; + if (!m_frame.visible) { + return result; + } + + constexpr float kMetricEpsilon = 0.001f; + for (const IconFrame& icon : m_frame.icons) { + if (icon.entityId == 0 || + !icon.texture.IsValid() || + !ContainsPoint(icon.rect, point)) { + continue; + } + + if (!result.HasHit() || + icon.sortDepth + kMetricEpsilon < result.sortDepth) { + result.entityId = icon.entityId; + result.sortDepth = icon.sortDepth; + } + } + + return result; +} + +void SceneViewportSceneOverlay::Append(::XCEngine::UI::UIDrawList& drawList) const { + if (!m_frame.visible || m_frame.icons.empty()) { + return; + } + + drawList.PushClipRect(m_frame.clipRect); + for (const IconFrame& icon : m_frame.icons) { + drawList.AddImage(icon.rect, icon.texture); + } + drawList.PopClipRect(); +} + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/Scene/SceneViewportSceneOverlay.h b/new_editor/app/Features/Scene/SceneViewportSceneOverlay.h new file mode 100644 index 00000000..1e6a2a41 --- /dev/null +++ b/new_editor/app/Features/Scene/SceneViewportSceneOverlay.h @@ -0,0 +1,49 @@ +#pragma once + +#include +#include + +#include + +namespace XCEngine::UI::Editor::App { + +class BuiltInIcons; +class EditorSceneRuntime; + +class SceneViewportSceneOverlay { +public: + struct HitResult { + std::uint64_t entityId = 0; + float sortDepth = 0.0f; + + bool HasHit() const { + return entityId != 0; + } + }; + + void SetBuiltInIcons(const BuiltInIcons* icons); + void ResetFrame(); + void Refresh( + EditorSceneRuntime& sceneRuntime, + const ::XCEngine::UI::UIRect& viewportRect); + HitResult HitTest(const ::XCEngine::UI::UIPoint& point) const; + void Append(::XCEngine::UI::UIDrawList& drawList) const; + struct IconFrame { + std::uint64_t entityId = 0; + ::XCEngine::UI::UIRect rect = {}; + ::XCEngine::UI::UITextureHandle texture = {}; + float sortDepth = 0.0f; + }; + + struct Frame { + bool visible = false; + ::XCEngine::UI::UIRect clipRect = {}; + std::vector icons = {}; + }; + +private: + const BuiltInIcons* m_icons = nullptr; + Frame m_frame = {}; +}; + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/Scene/SceneViewportToolOverlay.cpp b/new_editor/app/Features/Scene/SceneViewportToolOverlay.cpp index 6df050fa..4eb91fbf 100644 --- a/new_editor/app/Features/Scene/SceneViewportToolOverlay.cpp +++ b/new_editor/app/Features/Scene/SceneViewportToolOverlay.cpp @@ -2,8 +2,11 @@ #include "Ports/TexturePort.h" +#include + #include #include +#include namespace XCEngine::UI::Editor::App { @@ -13,6 +16,9 @@ using ::XCEngine::UI::UIColor; using ::XCEngine::UI::UIDrawList; using ::XCEngine::UI::UIPoint; using ::XCEngine::UI::UIRect; +using Widgets::MeasureUIEditorTextWidth; +using Widgets::ResolveUIEditorTextClipRect; +using Widgets::ResolveUIEditorTextTop; constexpr float kButtonExtent = 26.0f; constexpr float kButtonSpacing = 4.0f; @@ -22,6 +28,13 @@ constexpr float kIconInset = 3.0f; constexpr float kPanelCornerRounding = 6.0f; constexpr float kButtonCornerRounding = 4.0f; constexpr float kFallbackFontSize = 10.0f; +constexpr float kToggleFontSize = 13.0f; +constexpr float kToggleHorizontalPadding = 8.0f; +constexpr float kTopBarButtonInsetX = 0.0f; +constexpr float kTopBarButtonInsetY = 3.0f; +constexpr float kToggleButtonSpacing = 6.0f; +constexpr float kToggleButtonCornerRounding = 5.0f; +constexpr float kToggleMinWidth = 60.0f; struct ToolButtonSpec { SceneToolMode mode = SceneToolMode::View; @@ -45,6 +58,45 @@ bool ContainsPoint(const UIRect& rect, const UIPoint& point) { point.y <= rect.y + rect.height; } +std::string_view ResolveToggleLabel( + SceneViewportToggleButtonKind kind, + SceneToolPivotMode pivotMode, + SceneToolSpaceMode spaceMode) { + switch (kind) { + case SceneViewportToggleButtonKind::Pivot: + return pivotMode == SceneToolPivotMode::Center + ? std::string_view("Center") + : std::string_view("Pivot"); + case SceneViewportToggleButtonKind::Space: + return spaceMode == SceneToolSpaceMode::Local + ? std::string_view("Local") + : std::string_view("Global"); + default: + return {}; + } +} + +float ResolveToggleWidth(std::string_view firstLabel, std::string_view secondLabel) { + const float firstWidth = + MeasureUIEditorTextWidth(std::string(firstLabel), kToggleFontSize); + const float secondWidth = + MeasureUIEditorTextWidth(std::string(secondLabel), kToggleFontSize); + return (std::max)( + kToggleMinWidth, + std::ceil((std::max)(firstWidth, secondWidth) + kToggleHorizontalPadding * 2.0f)); +} + +float ResolveToggleWidth(SceneViewportToggleButtonKind kind) { + switch (kind) { + case SceneViewportToggleButtonKind::Pivot: + return ResolveToggleWidth("Pivot", "Center"); + case SceneViewportToggleButtonKind::Space: + return ResolveToggleWidth("Global", "Local"); + default: + return kToggleMinWidth; + } +} + UIRect BuildButtonRect(const UIRect& panelRect, std::size_t index) { return UIRect( panelRect.x + kPanelPadding, @@ -53,6 +105,99 @@ UIRect BuildButtonRect(const UIRect& panelRect, std::size_t index) { kButtonExtent); } +UIRect BuildToggleButtonRect( + const UIRect& topBarRect, + std::size_t index, + float width) { + const float buttonHeight = + (std::max)(topBarRect.height - kTopBarButtonInsetY * 2.0f, 0.0f); + return UIRect( + topBarRect.x + kTopBarButtonInsetX + static_cast(index) * (width + kToggleButtonSpacing), + topBarRect.y + kTopBarButtonInsetY, + width, + buttonHeight); +} + +::XCEngine::UI::UIColor ResolveToolButtonFill( + bool active, + bool hovered, + bool pressed) { + constexpr UIColor kButtonIdle( + 44.0f / 255.0f, + 44.0f / 255.0f, + 44.0f / 255.0f, + 230.0f / 255.0f); + constexpr UIColor kButtonHover( + 66.0f / 255.0f, + 66.0f / 255.0f, + 66.0f / 255.0f, + 240.0f / 255.0f); + constexpr UIColor kButtonPressed( + 92.0f / 255.0f, + 92.0f / 255.0f, + 92.0f / 255.0f, + 245.0f / 255.0f); + constexpr UIColor kButtonActive( + 84.0f / 255.0f, + 84.0f / 255.0f, + 84.0f / 255.0f, + 245.0f / 255.0f); + + if (pressed) { + return kButtonPressed; + } + if (hovered) { + return active ? kButtonActive : kButtonHover; + } + return active ? kButtonActive : kButtonIdle; +} + +::XCEngine::UI::UIColor ResolveToolIconTint( + bool active, + bool hovered, + bool pressed) { + if (pressed) { + return UIColor(1.0f, 1.0f, 1.0f, 1.0f); + } + if (active) { + return hovered + ? UIColor(1.0f, 1.0f, 1.0f, 1.0f) + : UIColor(0.98f, 0.98f, 0.98f, 1.0f); + } + if (hovered) { + return UIColor(0.90f, 0.90f, 0.90f, 1.0f); + } + return UIColor(0.80f, 0.80f, 0.80f, 1.0f); +} + +::XCEngine::UI::UIColor ResolveToggleButtonFill( + bool hovered, + bool pressed) { + constexpr UIColor kToggleIdle( + 38.0f / 255.0f, + 38.0f / 255.0f, + 38.0f / 255.0f, + 1.0f); + constexpr UIColor kToggleHover( + 47.0f / 255.0f, + 47.0f / 255.0f, + 47.0f / 255.0f, + 1.0f); + constexpr UIColor kTogglePressed( + 56.0f / 255.0f, + 56.0f / 255.0f, + 56.0f / 255.0f, + 1.0f); + + if (pressed) { + return kTogglePressed; + } + if (hovered) { + return kToggleHover; + } + return kToggleIdle; +} + } // namespace bool SceneViewportToolOverlay::Initialize( @@ -61,7 +206,7 @@ bool SceneViewportToolOverlay::Initialize( Shutdown(renderer); const std::filesystem::path iconRoot = - (repoRoot / "editor" / "resources" / "Icons").lexically_normal(); + (repoRoot / "new_editor" / "resources" / "Icons").lexically_normal(); bool loadedAnyTexture = false; for (std::size_t index = 0; index < kToolButtonSpecs.size(); ++index) { const ToolButtonSpec& path = kToolButtonSpecs[index]; @@ -103,16 +248,53 @@ void SceneViewportToolOverlay::ResetFrame() { void SceneViewportToolOverlay::BuildFrame( const UIRect& viewportRect, + const UIRect& topBarRect, + const UIRect& clipRect, SceneToolMode activeMode, + SceneToolPivotMode pivotMode, + SceneToolSpaceMode spaceMode, std::size_t hoveredIndex, - std::size_t pressedIndex) { + std::size_t pressedIndex, + std::size_t hoveredToggleIndex, + std::size_t pressedToggleIndex) { m_frame = {}; if (viewportRect.width <= 1.0f || viewportRect.height <= 1.0f) { return; } m_frame.visible = true; - m_frame.clipRect = viewportRect; + m_frame.clipRect = clipRect; + m_frame.toggleFrame.visible = + topBarRect.width > 1.0f && topBarRect.height > 1.0f; + const float pivotToggleWidth = + ResolveToggleWidth(SceneViewportToggleButtonKind::Pivot); + const float spaceToggleWidth = + ResolveToggleWidth(SceneViewportToggleButtonKind::Space); + m_frame.toggleFrame.panelRect = UIRect( + topBarRect.x + kTopBarButtonInsetX, + topBarRect.y, + pivotToggleWidth + + spaceToggleWidth + + kToggleButtonSpacing, + topBarRect.height); + for (std::size_t index = 0; index < m_frame.toggleFrame.buttons.size(); ++index) { + SceneViewportToggleButtonFrame& button = + m_frame.toggleFrame.buttons[index]; + button = {}; + button.kind = index == 0u + ? SceneViewportToggleButtonKind::Pivot + : SceneViewportToggleButtonKind::Space; + button.label = std::string( + ResolveToggleLabel(button.kind, pivotMode, spaceMode)); + button.rect = BuildToggleButtonRect( + topBarRect, + index, + index == 0u ? pivotToggleWidth : spaceToggleWidth); + button.active = true; + button.hovered = index == hoveredToggleIndex; + button.pressed = index == pressedToggleIndex; + } + m_frame.panelRect = UIRect( viewportRect.x + kViewportInset, viewportRect.y + kViewportInset, @@ -132,14 +314,14 @@ void SceneViewportToolOverlay::BuildFrame( button.active = spec.mode == activeMode; button.hovered = index == hoveredIndex; button.pressed = index == pressedIndex; - button.texture = button.active + button.texture = spec.mode == activeMode ? textureSet.activeTexture : textureSet.inactiveTexture; } } std::size_t SceneViewportToolOverlay::HitTest(const UIPoint& point) const { - if (!m_frame.visible || !Contains(point)) { + if (!m_frame.visible || !ContainsPoint(m_frame.panelRect, point)) { return kSceneViewportToolOverlayInvalidIndex; } @@ -152,8 +334,27 @@ std::size_t SceneViewportToolOverlay::HitTest(const UIPoint& point) const { return kSceneViewportToolOverlayInvalidIndex; } +std::size_t SceneViewportToolOverlay::HitTestToggle(const UIPoint& point) const { + if (!m_frame.visible || + !m_frame.toggleFrame.visible || + !ContainsPoint(m_frame.toggleFrame.panelRect, point)) { + return kSceneViewportToolOverlayInvalidIndex; + } + + for (std::size_t index = 0; index < m_frame.toggleFrame.buttons.size(); ++index) { + if (ContainsPoint(m_frame.toggleFrame.buttons[index].rect, point)) { + return index; + } + } + + return kSceneViewportToolOverlayInvalidIndex; +} + bool SceneViewportToolOverlay::Contains(const UIPoint& point) const { - return m_frame.visible && ContainsPoint(m_frame.panelRect, point); + return m_frame.visible && + (ContainsPoint(m_frame.panelRect, point) || + (m_frame.toggleFrame.visible && + ContainsPoint(m_frame.toggleFrame.panelRect, point))); } const SceneViewportToolOverlayFrame& SceneViewportToolOverlay::GetFrame() const { @@ -169,31 +370,46 @@ void AppendSceneViewportToolOverlay( constexpr UIColor kPanelFill(24.0f / 255.0f, 26.0f / 255.0f, 29.0f / 255.0f, 220.0f / 255.0f); constexpr UIColor kPanelOutline(1.0f, 1.0f, 1.0f, 28.0f / 255.0f); - constexpr UIColor kButtonIdle(44.0f / 255.0f, 47.0f / 255.0f, 54.0f / 255.0f, 230.0f / 255.0f); - constexpr UIColor kButtonHover(68.0f / 255.0f, 74.0f / 255.0f, 84.0f / 255.0f, 240.0f / 255.0f); - constexpr UIColor kButtonPressed(86.0f / 255.0f, 96.0f / 255.0f, 109.0f / 255.0f, 245.0f / 255.0f); - constexpr UIColor kButtonActive(78.0f / 255.0f, 102.0f / 255.0f, 126.0f / 255.0f, 245.0f / 255.0f); constexpr UIColor kButtonOutline(1.0f, 1.0f, 1.0f, 24.0f / 255.0f); - constexpr UIColor kButtonActiveOutline(1.0f, 1.0f, 1.0f, 48.0f / 255.0f); + constexpr UIColor kButtonActiveOutline(1.0f, 1.0f, 1.0f, 64.0f / 255.0f); + constexpr UIColor kToggleOutline(1.0f, 1.0f, 1.0f, 24.0f / 255.0f); + constexpr UIColor kToggleHoverOutline(1.0f, 1.0f, 1.0f, 40.0f / 255.0f); constexpr UIColor kFallbackText(230.0f / 255.0f, 230.0f / 255.0f, 230.0f / 255.0f, 1.0f); drawList.PushClipRect(frame.clipRect); + if (frame.toggleFrame.visible) { + for (const SceneViewportToggleButtonFrame& button : frame.toggleFrame.buttons) { + drawList.AddFilledRect( + button.rect, + ResolveToggleButtonFill(button.hovered, button.pressed), + kToggleButtonCornerRounding); + drawList.AddRectOutline( + button.rect, + button.hovered || button.pressed + ? kToggleHoverOutline + : kToggleOutline, + 1.0f, + kToggleButtonCornerRounding); + const float textWidth = + MeasureUIEditorTextWidth(button.label, kToggleFontSize); + const float textLeft = button.rect.x + + (std::max)(0.0f, std::floor((button.rect.width - textWidth) * 0.5f)); + drawList.AddText( + UIPoint(textLeft, ResolveUIEditorTextTop(button.rect, kToggleFontSize)), + button.label, + kFallbackText, + kToggleFontSize); + } + } + drawList.AddFilledRect(frame.panelRect, kPanelFill, kPanelCornerRounding); drawList.AddRectOutline(frame.panelRect, kPanelOutline, 1.0f, kPanelCornerRounding); for (const SceneViewportToolOverlayButtonFrame& button : frame.buttons) { - UIColor fill = kButtonIdle; - if (button.active) { - fill = kButtonActive; - } - if (button.hovered) { - fill = button.active ? kButtonActive : kButtonHover; - } - if (button.pressed) { - fill = kButtonPressed; - } - - drawList.AddFilledRect(button.rect, fill, kButtonCornerRounding); + drawList.AddFilledRect( + button.rect, + ResolveToolButtonFill(button.active, button.hovered, button.pressed), + kButtonCornerRounding); drawList.AddRectOutline( button.rect, button.active ? kButtonActiveOutline : kButtonOutline, @@ -206,7 +422,10 @@ void AppendSceneViewportToolOverlay( button.rect.y + kIconInset, button.rect.width - kIconInset * 2.0f, button.rect.height - kIconInset * 2.0f); - drawList.AddImage(iconRect, button.texture); + drawList.AddImage( + iconRect, + button.texture, + ResolveToolIconTint(button.active, button.hovered, button.pressed)); continue; } diff --git a/new_editor/app/Features/Scene/SceneViewportToolOverlay.h b/new_editor/app/Features/Scene/SceneViewportToolOverlay.h index 74339761..6f1094fc 100644 --- a/new_editor/app/Features/Scene/SceneViewportToolOverlay.h +++ b/new_editor/app/Features/Scene/SceneViewportToolOverlay.h @@ -7,14 +7,36 @@ #include #include -#include #include +#include +#include +#include namespace XCEngine::UI::Editor::App { inline constexpr std::size_t kSceneViewportToolOverlayInvalidIndex = static_cast(-1); +enum class SceneViewportToggleButtonKind : std::uint8_t { + Pivot = 0, + Space +}; + +struct SceneViewportToggleButtonFrame { + SceneViewportToggleButtonKind kind = SceneViewportToggleButtonKind::Pivot; + ::XCEngine::UI::UIRect rect = {}; + std::string label = {}; + bool active = false; + bool hovered = false; + bool pressed = false; +}; + +struct SceneViewportToggleOverlayFrame { + bool visible = false; + ::XCEngine::UI::UIRect panelRect = {}; + std::array buttons = {}; +}; + struct SceneViewportToolOverlayButtonFrame { SceneToolMode mode = SceneToolMode::View; ::XCEngine::UI::UIRect rect = {}; @@ -28,6 +50,7 @@ struct SceneViewportToolOverlayButtonFrame { struct SceneViewportToolOverlayFrame { bool visible = false; ::XCEngine::UI::UIRect clipRect = {}; + SceneViewportToggleOverlayFrame toggleFrame = {}; ::XCEngine::UI::UIRect panelRect = {}; std::array buttons = {}; }; @@ -42,11 +65,18 @@ public: void BuildFrame( const ::XCEngine::UI::UIRect& viewportRect, + const ::XCEngine::UI::UIRect& topBarRect, + const ::XCEngine::UI::UIRect& clipRect, SceneToolMode activeMode, + SceneToolPivotMode pivotMode, + SceneToolSpaceMode spaceMode, std::size_t hoveredIndex, - std::size_t pressedIndex); + std::size_t pressedIndex, + std::size_t hoveredToggleIndex, + std::size_t pressedToggleIndex); std::size_t HitTest(const ::XCEngine::UI::UIPoint& point) const; + std::size_t HitTestToggle(const ::XCEngine::UI::UIPoint& point) const; bool Contains(const ::XCEngine::UI::UIPoint& point) const; const SceneViewportToolOverlayFrame& GetFrame() const; diff --git a/new_editor/app/Features/Scene/SceneViewportTransformGizmo.cpp b/new_editor/app/Features/Scene/SceneViewportTransformGizmo.cpp index 4ec4fd28..272754d2 100644 --- a/new_editor/app/Features/Scene/SceneViewportTransformGizmo.cpp +++ b/new_editor/app/Features/Scene/SceneViewportTransformGizmo.cpp @@ -28,18 +28,18 @@ using ::XCEngine::Math::Vector3; using ::XCEngine::UI::UIColor; using ::XCEngine::UI::UIPoint; using ::XCEngine::UI::UIRect; -namespace SceneViewportGizmoInternal = ::XCEngine::UI::Editor::App::SceneViewportGizmoInternal; -using SceneViewportGizmoInternal::BuildSceneViewportTransformGizmoOverlayFrameData; -using SceneViewportGizmoInternal::IUndoManager; -using SceneViewportGizmoInternal::SceneViewportMoveGizmo; -using SceneViewportGizmoInternal::SceneViewportMoveGizmoContext; -using SceneViewportGizmoInternal::SceneViewportOverlayData; -using SceneViewportGizmoInternal::SceneViewportOverlayFrameData; -using SceneViewportGizmoInternal::SceneViewportRotateGizmo; -using SceneViewportGizmoInternal::SceneViewportRotateGizmoContext; -using SceneViewportGizmoInternal::SceneViewportScaleGizmo; -using SceneViewportGizmoInternal::SceneViewportScaleGizmoContext; -using SceneViewportGizmoInternal::SceneViewportTransformGizmoHandleBuildInputs; +namespace SceneViewportGizmoSupport = ::XCEngine::UI::Editor::App::SceneViewportGizmoSupport; +using SceneViewportGizmoSupport::BuildSceneViewportTransformGizmoOverlayFrameData; +using SceneViewportGizmoSupport::IUndoManager; +using SceneViewportGizmoSupport::SceneViewportMoveGizmo; +using SceneViewportGizmoSupport::SceneViewportMoveGizmoContext; +using SceneViewportGizmoSupport::SceneViewportOverlayData; +using SceneViewportGizmoSupport::SceneViewportOverlayFrameData; +using SceneViewportGizmoSupport::SceneViewportRotateGizmo; +using SceneViewportGizmoSupport::SceneViewportRotateGizmoContext; +using SceneViewportGizmoSupport::SceneViewportScaleGizmo; +using SceneViewportGizmoSupport::SceneViewportScaleGizmoContext; +using SceneViewportGizmoSupport::SceneViewportTransformGizmoHandleBuildInputs; enum class ActiveTransformGizmoKind : std::uint8_t { None = 0, diff --git a/new_editor/app/Features/Scene/SceneViewportTransformGizmoSupport.cpp b/new_editor/app/Features/Scene/SceneViewportTransformGizmoSupport.cpp index 92146453..7926d9d1 100644 --- a/new_editor/app/Features/Scene/SceneViewportTransformGizmoSupport.cpp +++ b/new_editor/app/Features/Scene/SceneViewportTransformGizmoSupport.cpp @@ -6,7 +6,7 @@ #include #include -namespace XCEngine::UI::Editor::App::SceneViewportGizmoInternal { +namespace XCEngine::UI::Editor::App::SceneViewportGizmoSupport { namespace { @@ -2459,4 +2459,4 @@ SceneViewportOverlayFrameData BuildSceneViewportTransformGizmoOverlayFrameData( return frameData; } -} // namespace XCEngine::UI::Editor::App::SceneViewportGizmoInternal +} // namespace XCEngine::UI::Editor::App::SceneViewportGizmoSupport diff --git a/new_editor/app/Features/Scene/SceneViewportTransformGizmoSupport.h b/new_editor/app/Features/Scene/SceneViewportTransformGizmoSupport.h index e58fbf1a..7c887e15 100644 --- a/new_editor/app/Features/Scene/SceneViewportTransformGizmoSupport.h +++ b/new_editor/app/Features/Scene/SceneViewportTransformGizmoSupport.h @@ -20,7 +20,7 @@ class GameObject; } // namespace XCEngine::Components -namespace XCEngine::UI::Editor::App::SceneViewportGizmoInternal { +namespace XCEngine::UI::Editor::App::SceneViewportGizmoSupport { class IUndoManager { public: @@ -428,4 +428,4 @@ SceneViewportOverlayFrameData BuildSceneViewportTransformGizmoOverlayFrameData( const SceneViewportOverlayData& overlay, const SceneViewportTransformGizmoHandleBuildInputs& inputs); -} // namespace XCEngine::UI::Editor::App::SceneViewportGizmoInternal +} // namespace XCEngine::UI::Editor::App::SceneViewportGizmoSupport diff --git a/new_editor/app/Platform/Win32/BorderlessWindowChrome.cpp b/new_editor/app/Platform/Win32/BorderlessWindowChrome.cpp index 711f5500..c2b6d20f 100644 --- a/new_editor/app/Platform/Win32/BorderlessWindowChrome.cpp +++ b/new_editor/app/Platform/Win32/BorderlessWindowChrome.cpp @@ -1,6 +1,7 @@ -#include "BorderlessWindowChrome.h" - +#include "BorderlessWindowChrome.h" #include +#include +#include namespace XCEngine::UI::Editor::Host { @@ -70,3 +71,355 @@ BorderlessWindowChromeHitTarget HitTestBorderlessWindowChrome( } } // namespace XCEngine::UI::Editor::Host + +namespace XCEngine::UI::Editor::Host { + +namespace { + +using ::XCEngine::UI::UIColor; +using ::XCEngine::UI::UIDrawList; +using ::XCEngine::UI::UIPoint; +using ::XCEngine::UI::UIRect; + +constexpr UIColor kTransparentColor(0.0f, 0.0f, 0.0f, 0.0f); + +float ResolveGlyphBoxSize(const UIRect& rect, float ratio, float minSize) { + return (std::max)(minSize, static_cast(std::round(rect.height * ratio))); +} + +void AppendMinimizeGlyph( + UIDrawList& drawList, + const UIRect& rect, + const UIColor& color, + float thickness) { + const float centerX = rect.x + rect.width * 0.5f; + const float centerY = rect.y + rect.height * 0.5f; + const float halfWidth = ResolveGlyphBoxSize(rect, 0.38f, 10.0f) * 0.5f; + const float y = centerY; + drawList.AddLine( + UIPoint(centerX - halfWidth, y), + UIPoint(centerX + halfWidth, y), + color, + thickness); +} + +void AppendMaximizeGlyph( + UIDrawList& drawList, + const UIRect& rect, + const UIColor& color, + float thickness) { + const float centerX = rect.x + rect.width * 0.5f; + const float centerY = rect.y + rect.height * 0.5f; + const float boxSize = ResolveGlyphBoxSize(rect, 0.32f, 9.0f); + const float halfExtent = boxSize * 0.5f; + const float left = centerX - halfExtent; + const float top = centerY - halfExtent; + const float right = left + boxSize; + const float bottom = top + boxSize; + drawList.AddLine(UIPoint(left, top), UIPoint(right, top), color, thickness); + drawList.AddLine(UIPoint(left, top), UIPoint(left, bottom), color, thickness); + drawList.AddLine(UIPoint(right, top), UIPoint(right, bottom), color, thickness); + drawList.AddLine(UIPoint(left, bottom), UIPoint(right, bottom), color, thickness); +} + +void AppendRestoreGlyph( + UIDrawList& drawList, + const UIRect& rect, + const UIColor& color, + float thickness) { + const float centerX = rect.x + rect.width * 0.5f; + const float centerY = rect.y + rect.height * 0.5f; + const float boxSize = ResolveGlyphBoxSize(rect, 0.29f, 8.0f); + const float halfExtent = boxSize * 0.5f; + const float offset = 1.0f; + + const float backLeft = centerX - halfExtent + offset; + const float backTop = centerY - halfExtent - offset; + const float backRight = backLeft + boxSize; + const float backBottom = backTop + boxSize; + + const float frontLeft = centerX - halfExtent - offset; + const float frontTop = centerY - halfExtent + offset; + const float frontRight = frontLeft + boxSize; + const float frontBottom = frontTop + boxSize; + + drawList.AddLine(UIPoint(backLeft, backTop), UIPoint(backRight, backTop), color, thickness); + drawList.AddLine(UIPoint(backLeft, backTop), UIPoint(backLeft, frontTop), color, thickness); + drawList.AddLine(UIPoint(backRight, backTop), UIPoint(backRight, backBottom), color, thickness); + drawList.AddLine(UIPoint(frontRight, backBottom), UIPoint(backRight, backBottom), color, thickness); + + drawList.AddLine(UIPoint(frontLeft, frontTop), UIPoint(frontRight, frontTop), color, thickness); + drawList.AddLine(UIPoint(frontLeft, frontTop), UIPoint(frontLeft, frontBottom), color, thickness); + drawList.AddLine(UIPoint(frontRight, frontTop), UIPoint(frontRight, frontBottom), color, thickness); + drawList.AddLine(UIPoint(frontLeft, frontBottom), UIPoint(frontRight, frontBottom), color, thickness); +} + +void AppendCloseGlyph( + UIDrawList& drawList, + const UIRect& rect, + const UIColor& color, + float thickness) { + const float centerX = rect.x + rect.width * 0.5f; + const float centerY = rect.y + rect.height * 0.5f; + const float halfWidth = ResolveGlyphBoxSize(rect, 0.29f, 8.0f) * 0.5f; + const float halfHeight = halfWidth; + drawList.AddLine( + UIPoint(centerX - halfWidth, centerY - halfHeight), + UIPoint(centerX + halfWidth, centerY + halfHeight), + color, + thickness); + drawList.AddLine( + UIPoint(centerX + halfWidth, centerY - halfHeight), + UIPoint(centerX - halfWidth, centerY + halfHeight), + color, + thickness); +} + +UIColor ResolveButtonFillColor( + BorderlessWindowChromeHitTarget target, + const BorderlessWindowChromeState& state, + const BorderlessWindowChromePalette& palette) { + const bool hovered = state.hoveredTarget == target; + const bool pressed = state.pressedTarget == target; + if (target == BorderlessWindowChromeHitTarget::CloseButton) { + if (pressed) { + return palette.closeButtonPressedColor; + } + if (hovered) { + return palette.closeButtonHoverColor; + } + return kTransparentColor; + } + + if (pressed) { + return palette.buttonPressedColor; + } + if (hovered) { + return palette.buttonHoverColor; + } + return kTransparentColor; +} + +UIColor ResolveIconColor( + BorderlessWindowChromeHitTarget target, + const BorderlessWindowChromeState& state, + const BorderlessWindowChromePalette& palette) { + if (target == BorderlessWindowChromeHitTarget::CloseButton && + (state.hoveredTarget == target || state.pressedTarget == target)) { + return palette.closeIconHoverColor; + } + + return palette.iconColor; +} + +} // namespace + +void AppendBorderlessWindowChrome( + UIDrawList& drawList, + const BorderlessWindowChromeLayout& layout, + const BorderlessWindowChromeState& state, + bool maximized, + const BorderlessWindowChromePalette& palette, + const BorderlessWindowChromeMetrics& metrics) { + const struct ButtonEntry { + BorderlessWindowChromeHitTarget target; + UIRect rect; + } buttons[] = { + { BorderlessWindowChromeHitTarget::MinimizeButton, layout.minimizeButtonRect }, + { BorderlessWindowChromeHitTarget::MaximizeRestoreButton, layout.maximizeRestoreButtonRect }, + { BorderlessWindowChromeHitTarget::CloseButton, layout.closeButtonRect } + }; + + for (const ButtonEntry& button : buttons) { + const UIColor fill = ResolveButtonFillColor(button.target, state, palette); + if (fill.a > 0.0f) { + drawList.AddFilledRect(button.rect, fill); + } + + const UIColor iconColor = ResolveIconColor(button.target, state, palette); + switch (button.target) { + case BorderlessWindowChromeHitTarget::MinimizeButton: + AppendMinimizeGlyph(drawList, button.rect, iconColor, metrics.iconThickness); + break; + case BorderlessWindowChromeHitTarget::MaximizeRestoreButton: + if (maximized) { + AppendRestoreGlyph(drawList, button.rect, iconColor, metrics.iconThickness); + } else { + AppendMaximizeGlyph(drawList, button.rect, iconColor, metrics.iconThickness); + } + break; + case BorderlessWindowChromeHitTarget::CloseButton: + AppendCloseGlyph(drawList, button.rect, iconColor, metrics.iconThickness); + break; + case BorderlessWindowChromeHitTarget::DragRegion: + case BorderlessWindowChromeHitTarget::None: + default: + break; + } + } +} + +} // namespace XCEngine::UI::Editor::Host + +namespace XCEngine::UI::Editor::Host { + +namespace { + +using DwmSetWindowAttributeFn = HRESULT(WINAPI*)(HWND, DWORD, LPCVOID, DWORD); +using DwmExtendFrameIntoClientAreaFn = HRESULT(WINAPI*)(HWND, const MARGINS*); + +HMODULE GetDwmApiModule() { + static HMODULE dwmapi = []() -> HMODULE { + HMODULE module = GetModuleHandleW(L"dwmapi.dll"); + if (module == nullptr) { + module = LoadLibraryW(L"dwmapi.dll"); + } + return module; + }(); + + return dwmapi; +} + +template +ProcedureType GetDwmApiProcedure(const char* name) { + const HMODULE dwmapi = GetDwmApiModule(); + if (dwmapi == nullptr) { + return nullptr; + } + + return reinterpret_cast(GetProcAddress(dwmapi, name)); +} + +bool IsWindowAlignedToMonitorWorkArea(HWND hwnd) { + if (hwnd == nullptr) { + return false; + } + + RECT windowRect = {}; + if (!GetWindowRect(hwnd, &windowRect)) { + return false; + } + + const HMONITOR monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); + if (monitor == nullptr) { + return false; + } + + MONITORINFO monitorInfo = {}; + monitorInfo.cbSize = sizeof(monitorInfo); + if (!GetMonitorInfoW(monitor, &monitorInfo)) { + return false; + } + + const RECT& workArea = monitorInfo.rcWork; + return windowRect.left == workArea.left && + windowRect.top == workArea.top && + windowRect.right == workArea.right && + windowRect.bottom == workArea.bottom; +} + +template +void ApplyDwmWindowAttribute(HWND hwnd, DWORD attribute, const ValueType& value) { + if (hwnd == nullptr) { + return; + } + + static const DwmSetWindowAttributeFn setWindowAttribute = + GetDwmApiProcedure("DwmSetWindowAttribute"); + if (setWindowAttribute != nullptr) { + setWindowAttribute(hwnd, attribute, &value, sizeof(value)); + } +} + +} // namespace + +void EnableBorderlessWindowShadow(HWND hwnd) { + if (hwnd == nullptr) { + return; + } + + static const DwmExtendFrameIntoClientAreaFn extendFrameIntoClientArea = + GetDwmApiProcedure("DwmExtendFrameIntoClientArea"); + if (extendFrameIntoClientArea != nullptr) { + const bool maximized = IsZoomed(hwnd) || IsWindowAlignedToMonitorWorkArea(hwnd); + const MARGINS margins = maximized + ? MARGINS{ 0, 0, 0, 0 } + : MARGINS{ 1, 1, 1, 1 }; + extendFrameIntoClientArea(hwnd, &margins); + } +} + +void RefreshBorderlessWindowDwmDecorations(HWND hwnd) { + if (hwnd == nullptr) { + return; + } + + const int ncRenderingPolicy = DWMNCRP_DISABLED; + const BOOL allowNcPaint = FALSE; + const BOOL disableTransitions = TRUE; + const BOOL useImmersiveDarkMode = TRUE; + const COLORREF captionColor = RGB(26, 26, 26); + const COLORREF textColor = RGB(235, 235, 235); + const COLORREF borderColor = RGB(26, 26, 26); + + ApplyDwmWindowAttribute(hwnd, DWMWA_NCRENDERING_POLICY, ncRenderingPolicy); + ApplyDwmWindowAttribute(hwnd, DWMWA_ALLOW_NCPAINT, allowNcPaint); + ApplyDwmWindowAttribute(hwnd, DWMWA_TRANSITIONS_FORCEDISABLED, disableTransitions); + ApplyDwmWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, useImmersiveDarkMode); + ApplyDwmWindowAttribute(hwnd, DWMWA_CAPTION_COLOR, captionColor); + ApplyDwmWindowAttribute(hwnd, DWMWA_TEXT_COLOR, textColor); + ApplyDwmWindowAttribute(hwnd, DWMWA_BORDER_COLOR, borderColor); + EnableBorderlessWindowShadow(hwnd); +} + +bool HandleBorderlessWindowGetMinMaxInfo( + HWND hwnd, + LPARAM lParam, + int minimumOuterWidth, + int minimumOuterHeight) { + if (hwnd == nullptr || lParam == 0) { + return false; + } + + auto* minMaxInfo = reinterpret_cast(lParam); + if (minMaxInfo == nullptr) { + return false; + } + + const HMONITOR monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); + if (monitor == nullptr) { + return false; + } + + MONITORINFO monitorInfo = {}; + monitorInfo.cbSize = sizeof(monitorInfo); + if (!GetMonitorInfoW(monitor, &monitorInfo)) { + return false; + } + + const RECT& workArea = monitorInfo.rcWork; + const RECT& monitorArea = monitorInfo.rcMonitor; + minMaxInfo->ptMaxPosition.x = workArea.left - monitorArea.left; + minMaxInfo->ptMaxPosition.y = workArea.top - monitorArea.top; + minMaxInfo->ptMaxSize.x = workArea.right - workArea.left; + minMaxInfo->ptMaxSize.y = workArea.bottom - workArea.top; + minMaxInfo->ptMaxTrackSize = minMaxInfo->ptMaxSize; + minMaxInfo->ptMinTrackSize.x = (std::max)(minimumOuterWidth, 1); + minMaxInfo->ptMinTrackSize.y = (std::max)(minimumOuterHeight, 1); + return true; +} + +LRESULT HandleBorderlessWindowNcCalcSize( + HWND hwnd, + WPARAM wParam, + LPARAM lParam, + UINT dpi) { + (void)hwnd; + (void)wParam; + (void)lParam; + (void)dpi; + return 0; +} + +} // namespace XCEngine::UI::Editor::Host + diff --git a/new_editor/app/Platform/Win32/BorderlessWindowChrome.h b/new_editor/app/Platform/Win32/BorderlessWindowChrome.h index b4c7a6f6..afd465e5 100644 --- a/new_editor/app/Platform/Win32/BorderlessWindowChrome.h +++ b/new_editor/app/Platform/Win32/BorderlessWindowChrome.h @@ -74,7 +74,11 @@ void AppendBorderlessWindowChrome( void RefreshBorderlessWindowDwmDecorations(HWND hwnd); void EnableBorderlessWindowShadow(HWND hwnd); -bool HandleBorderlessWindowGetMinMaxInfo(HWND hwnd, LPARAM lParam); +bool HandleBorderlessWindowGetMinMaxInfo( + HWND hwnd, + LPARAM lParam, + int minimumOuterWidth, + int minimumOuterHeight); LRESULT HandleBorderlessWindowNcCalcSize( HWND hwnd, diff --git a/new_editor/app/Platform/Win32/BorderlessWindowChromeDwm.cpp b/new_editor/app/Platform/Win32/BorderlessWindowChromeDwm.cpp deleted file mode 100644 index 94fbff81..00000000 --- a/new_editor/app/Platform/Win32/BorderlessWindowChromeDwm.cpp +++ /dev/null @@ -1,159 +0,0 @@ -#include "BorderlessWindowChrome.h" - -#include - -namespace XCEngine::UI::Editor::Host { - -namespace { - -using DwmSetWindowAttributeFn = HRESULT(WINAPI*)(HWND, DWORD, LPCVOID, DWORD); -using DwmExtendFrameIntoClientAreaFn = HRESULT(WINAPI*)(HWND, const MARGINS*); - -HMODULE GetDwmApiModule() { - static HMODULE dwmapi = []() -> HMODULE { - HMODULE module = GetModuleHandleW(L"dwmapi.dll"); - if (module == nullptr) { - module = LoadLibraryW(L"dwmapi.dll"); - } - return module; - }(); - - return dwmapi; -} - -template -ProcedureType GetDwmApiProcedure(const char* name) { - const HMODULE dwmapi = GetDwmApiModule(); - if (dwmapi == nullptr) { - return nullptr; - } - - return reinterpret_cast(GetProcAddress(dwmapi, name)); -} - -bool IsWindowAlignedToMonitorWorkArea(HWND hwnd) { - if (hwnd == nullptr) { - return false; - } - - RECT windowRect = {}; - if (!GetWindowRect(hwnd, &windowRect)) { - return false; - } - - const HMONITOR monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); - if (monitor == nullptr) { - return false; - } - - MONITORINFO monitorInfo = {}; - monitorInfo.cbSize = sizeof(monitorInfo); - if (!GetMonitorInfoW(monitor, &monitorInfo)) { - return false; - } - - const RECT& workArea = monitorInfo.rcWork; - return windowRect.left == workArea.left && - windowRect.top == workArea.top && - windowRect.right == workArea.right && - windowRect.bottom == workArea.bottom; -} - -template -void ApplyDwmWindowAttribute(HWND hwnd, DWORD attribute, const ValueType& value) { - if (hwnd == nullptr) { - return; - } - - static const DwmSetWindowAttributeFn setWindowAttribute = - GetDwmApiProcedure("DwmSetWindowAttribute"); - if (setWindowAttribute != nullptr) { - setWindowAttribute(hwnd, attribute, &value, sizeof(value)); - } -} - -} // namespace - -void EnableBorderlessWindowShadow(HWND hwnd) { - if (hwnd == nullptr) { - return; - } - - static const DwmExtendFrameIntoClientAreaFn extendFrameIntoClientArea = - GetDwmApiProcedure("DwmExtendFrameIntoClientArea"); - if (extendFrameIntoClientArea != nullptr) { - const bool maximized = IsZoomed(hwnd) || IsWindowAlignedToMonitorWorkArea(hwnd); - const MARGINS margins = maximized - ? MARGINS{ 0, 0, 0, 0 } - : MARGINS{ 1, 1, 1, 1 }; - extendFrameIntoClientArea(hwnd, &margins); - } -} - -void RefreshBorderlessWindowDwmDecorations(HWND hwnd) { - if (hwnd == nullptr) { - return; - } - - const int ncRenderingPolicy = DWMNCRP_DISABLED; - const BOOL allowNcPaint = FALSE; - const BOOL disableTransitions = TRUE; - const BOOL useImmersiveDarkMode = TRUE; - const COLORREF captionColor = RGB(26, 26, 26); - const COLORREF textColor = RGB(235, 235, 235); - const COLORREF borderColor = RGB(26, 26, 26); - - ApplyDwmWindowAttribute(hwnd, DWMWA_NCRENDERING_POLICY, ncRenderingPolicy); - ApplyDwmWindowAttribute(hwnd, DWMWA_ALLOW_NCPAINT, allowNcPaint); - ApplyDwmWindowAttribute(hwnd, DWMWA_TRANSITIONS_FORCEDISABLED, disableTransitions); - ApplyDwmWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, useImmersiveDarkMode); - ApplyDwmWindowAttribute(hwnd, DWMWA_CAPTION_COLOR, captionColor); - ApplyDwmWindowAttribute(hwnd, DWMWA_TEXT_COLOR, textColor); - ApplyDwmWindowAttribute(hwnd, DWMWA_BORDER_COLOR, borderColor); - EnableBorderlessWindowShadow(hwnd); -} - -bool HandleBorderlessWindowGetMinMaxInfo(HWND hwnd, LPARAM lParam) { - if (hwnd == nullptr || lParam == 0) { - return false; - } - - auto* minMaxInfo = reinterpret_cast(lParam); - if (minMaxInfo == nullptr) { - return false; - } - - const HMONITOR monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); - if (monitor == nullptr) { - return false; - } - - MONITORINFO monitorInfo = {}; - monitorInfo.cbSize = sizeof(monitorInfo); - if (!GetMonitorInfoW(monitor, &monitorInfo)) { - return false; - } - - const RECT& workArea = monitorInfo.rcWork; - const RECT& monitorArea = monitorInfo.rcMonitor; - minMaxInfo->ptMaxPosition.x = workArea.left - monitorArea.left; - minMaxInfo->ptMaxPosition.y = workArea.top - monitorArea.top; - minMaxInfo->ptMaxSize.x = workArea.right - workArea.left; - minMaxInfo->ptMaxSize.y = workArea.bottom - workArea.top; - minMaxInfo->ptMaxTrackSize = minMaxInfo->ptMaxSize; - return true; -} - -LRESULT HandleBorderlessWindowNcCalcSize( - HWND hwnd, - WPARAM wParam, - LPARAM lParam, - UINT dpi) { - (void)hwnd; - (void)wParam; - (void)lParam; - (void)dpi; - return 0; -} - -} // namespace XCEngine::UI::Editor::Host diff --git a/new_editor/app/Platform/Win32/BorderlessWindowChromeRendering.cpp b/new_editor/app/Platform/Win32/BorderlessWindowChromeRendering.cpp deleted file mode 100644 index c6d7599f..00000000 --- a/new_editor/app/Platform/Win32/BorderlessWindowChromeRendering.cpp +++ /dev/null @@ -1,193 +0,0 @@ -#include "BorderlessWindowChrome.h" - -#include -#include - -namespace XCEngine::UI::Editor::Host { - -namespace { - -using ::XCEngine::UI::UIColor; -using ::XCEngine::UI::UIDrawList; -using ::XCEngine::UI::UIPoint; -using ::XCEngine::UI::UIRect; - -constexpr UIColor kTransparentColor(0.0f, 0.0f, 0.0f, 0.0f); - -float ResolveGlyphBoxSize(const UIRect& rect, float ratio, float minSize) { - return (std::max)(minSize, static_cast(std::round(rect.height * ratio))); -} - -void AppendMinimizeGlyph( - UIDrawList& drawList, - const UIRect& rect, - const UIColor& color, - float thickness) { - const float centerX = rect.x + rect.width * 0.5f; - const float centerY = rect.y + rect.height * 0.5f; - const float halfWidth = ResolveGlyphBoxSize(rect, 0.38f, 10.0f) * 0.5f; - const float y = centerY; - drawList.AddLine( - UIPoint(centerX - halfWidth, y), - UIPoint(centerX + halfWidth, y), - color, - thickness); -} - -void AppendMaximizeGlyph( - UIDrawList& drawList, - const UIRect& rect, - const UIColor& color, - float thickness) { - const float centerX = rect.x + rect.width * 0.5f; - const float centerY = rect.y + rect.height * 0.5f; - const float boxSize = ResolveGlyphBoxSize(rect, 0.32f, 9.0f); - const float halfExtent = boxSize * 0.5f; - const float left = centerX - halfExtent; - const float top = centerY - halfExtent; - const float right = left + boxSize; - const float bottom = top + boxSize; - drawList.AddLine(UIPoint(left, top), UIPoint(right, top), color, thickness); - drawList.AddLine(UIPoint(left, top), UIPoint(left, bottom), color, thickness); - drawList.AddLine(UIPoint(right, top), UIPoint(right, bottom), color, thickness); - drawList.AddLine(UIPoint(left, bottom), UIPoint(right, bottom), color, thickness); -} - -void AppendRestoreGlyph( - UIDrawList& drawList, - const UIRect& rect, - const UIColor& color, - float thickness) { - const float centerX = rect.x + rect.width * 0.5f; - const float centerY = rect.y + rect.height * 0.5f; - const float boxSize = ResolveGlyphBoxSize(rect, 0.29f, 8.0f); - const float halfExtent = boxSize * 0.5f; - const float offset = 1.0f; - - const float backLeft = centerX - halfExtent + offset; - const float backTop = centerY - halfExtent - offset; - const float backRight = backLeft + boxSize; - const float backBottom = backTop + boxSize; - - const float frontLeft = centerX - halfExtent - offset; - const float frontTop = centerY - halfExtent + offset; - const float frontRight = frontLeft + boxSize; - const float frontBottom = frontTop + boxSize; - - drawList.AddLine(UIPoint(backLeft, backTop), UIPoint(backRight, backTop), color, thickness); - drawList.AddLine(UIPoint(backLeft, backTop), UIPoint(backLeft, frontTop), color, thickness); - drawList.AddLine(UIPoint(backRight, backTop), UIPoint(backRight, backBottom), color, thickness); - drawList.AddLine(UIPoint(frontRight, backBottom), UIPoint(backRight, backBottom), color, thickness); - - drawList.AddLine(UIPoint(frontLeft, frontTop), UIPoint(frontRight, frontTop), color, thickness); - drawList.AddLine(UIPoint(frontLeft, frontTop), UIPoint(frontLeft, frontBottom), color, thickness); - drawList.AddLine(UIPoint(frontRight, frontTop), UIPoint(frontRight, frontBottom), color, thickness); - drawList.AddLine(UIPoint(frontLeft, frontBottom), UIPoint(frontRight, frontBottom), color, thickness); -} - -void AppendCloseGlyph( - UIDrawList& drawList, - const UIRect& rect, - const UIColor& color, - float thickness) { - const float centerX = rect.x + rect.width * 0.5f; - const float centerY = rect.y + rect.height * 0.5f; - const float halfWidth = ResolveGlyphBoxSize(rect, 0.29f, 8.0f) * 0.5f; - const float halfHeight = halfWidth; - drawList.AddLine( - UIPoint(centerX - halfWidth, centerY - halfHeight), - UIPoint(centerX + halfWidth, centerY + halfHeight), - color, - thickness); - drawList.AddLine( - UIPoint(centerX + halfWidth, centerY - halfHeight), - UIPoint(centerX - halfWidth, centerY + halfHeight), - color, - thickness); -} - -UIColor ResolveButtonFillColor( - BorderlessWindowChromeHitTarget target, - const BorderlessWindowChromeState& state, - const BorderlessWindowChromePalette& palette) { - const bool hovered = state.hoveredTarget == target; - const bool pressed = state.pressedTarget == target; - if (target == BorderlessWindowChromeHitTarget::CloseButton) { - if (pressed) { - return palette.closeButtonPressedColor; - } - if (hovered) { - return palette.closeButtonHoverColor; - } - return kTransparentColor; - } - - if (pressed) { - return palette.buttonPressedColor; - } - if (hovered) { - return palette.buttonHoverColor; - } - return kTransparentColor; -} - -UIColor ResolveIconColor( - BorderlessWindowChromeHitTarget target, - const BorderlessWindowChromeState& state, - const BorderlessWindowChromePalette& palette) { - if (target == BorderlessWindowChromeHitTarget::CloseButton && - (state.hoveredTarget == target || state.pressedTarget == target)) { - return palette.closeIconHoverColor; - } - - return palette.iconColor; -} - -} // namespace - -void AppendBorderlessWindowChrome( - UIDrawList& drawList, - const BorderlessWindowChromeLayout& layout, - const BorderlessWindowChromeState& state, - bool maximized, - const BorderlessWindowChromePalette& palette, - const BorderlessWindowChromeMetrics& metrics) { - const struct ButtonEntry { - BorderlessWindowChromeHitTarget target; - UIRect rect; - } buttons[] = { - { BorderlessWindowChromeHitTarget::MinimizeButton, layout.minimizeButtonRect }, - { BorderlessWindowChromeHitTarget::MaximizeRestoreButton, layout.maximizeRestoreButtonRect }, - { BorderlessWindowChromeHitTarget::CloseButton, layout.closeButtonRect } - }; - - for (const ButtonEntry& button : buttons) { - const UIColor fill = ResolveButtonFillColor(button.target, state, palette); - if (fill.a > 0.0f) { - drawList.AddFilledRect(button.rect, fill); - } - - const UIColor iconColor = ResolveIconColor(button.target, state, palette); - switch (button.target) { - case BorderlessWindowChromeHitTarget::MinimizeButton: - AppendMinimizeGlyph(drawList, button.rect, iconColor, metrics.iconThickness); - break; - case BorderlessWindowChromeHitTarget::MaximizeRestoreButton: - if (maximized) { - AppendRestoreGlyph(drawList, button.rect, iconColor, metrics.iconThickness); - } else { - AppendMaximizeGlyph(drawList, button.rect, iconColor, metrics.iconThickness); - } - break; - case BorderlessWindowChromeHitTarget::CloseButton: - AppendCloseGlyph(drawList, button.rect, iconColor, metrics.iconThickness); - break; - case BorderlessWindowChromeHitTarget::DragRegion: - case BorderlessWindowChromeHitTarget::None: - default: - break; - } - } -} - -} // namespace XCEngine::UI::Editor::Host diff --git a/new_editor/app/Platform/Win32/EditorWindow.cpp b/new_editor/app/Platform/Win32/EditorWindow.cpp new file mode 100644 index 00000000..abc2cdcf --- /dev/null +++ b/new_editor/app/Platform/Win32/EditorWindow.cpp @@ -0,0 +1,1056 @@ +#include "Platform/Win32/EditorWindow.h" +#include "Bootstrap/EditorResources.h" +#include "Platform/Win32/EditorWindowChromeController.h" +#include "Platform/Win32/EditorWindowSupport.h" +#include "Platform/Win32/EditorWindowFrameOrchestrator.h" +#include "Platform/Win32/EditorWindowInputController.h" +#include "Platform/Win32/EditorWindowState.h" +#include "Platform/Win32/EditorWindowRuntimeController.h" +#include "Composition/EditorContext.h" +#include +#include +#include +#include +#include +#include +#include "Composition/EditorShellPointerInteraction.h" +#include +#include +#include +#include + +namespace XCEngine::UI::Editor::App::EditorWindowSupport { + +UINT QuerySystemDpi() { + HDC screenDc = GetDC(nullptr); + if (screenDc == nullptr) { + return kDefaultDpi; + } + + const int dpiX = GetDeviceCaps(screenDc, LOGPIXELSX); + ReleaseDC(nullptr, screenDc); + return dpiX > 0 ? static_cast(dpiX) : kDefaultDpi; +} + +UINT QueryWindowDpi(HWND hwnd) { + if (hwnd != nullptr) { + const HMODULE user32 = GetModuleHandleW(L"user32.dll"); + if (user32 != nullptr) { + using GetDpiForWindowFn = UINT(WINAPI*)(HWND); + const auto getDpiForWindow = + reinterpret_cast(GetProcAddress(user32, "GetDpiForWindow")); + if (getDpiForWindow != nullptr) { + const UINT dpi = getDpiForWindow(hwnd); + if (dpi != 0u) { + return dpi; + } + } + } + } + + return QuerySystemDpi(); +} + +bool ResolveVerboseRuntimeTraceEnabled() { + wchar_t buffer[8] = {}; + const DWORD length = GetEnvironmentVariableW( + L"XCUIEDITOR_VERBOSE_TRACE", + buffer, + static_cast(std::size(buffer))); + return length > 0u && buffer[0] != L'0'; +} + +void LogRuntimeTrace(std::string_view channel, std::string_view message) { + AppendUIEditorRuntimeTrace(channel, message); +} + +bool IsAutoCaptureOnStartupEnabled() { + return App::IsEnvironmentFlagEnabled("XCUI_AUTO_CAPTURE_ON_STARTUP"); +} + +} // namespace XCEngine::UI::Editor::App::EditorWindowSupport + +namespace XCEngine::UI::Editor::App { + +using namespace EditorWindowSupport; +using ::XCEngine::UI::UIPoint; + +EditorWindow::EditorWindow( + std::string windowId, + std::wstring title, + bool primary, + UIEditorWorkspaceController workspaceController) + : m_state(std::make_unique()) + , m_chromeController(std::make_unique()) + , m_frameOrchestrator(std::make_unique()) + , m_inputController(std::make_unique()) + , m_runtime(std::make_unique( + std::move(workspaceController))) { + m_state->window.windowId = std::move(windowId); + m_state->window.title = std::move(title); + m_state->window.primary = primary; + UpdateCachedTitleText(); +} + +EditorWindow::~EditorWindow() = default; + +std::string_view EditorWindow::GetWindowId() const { + return m_state->window.windowId; +} + +HWND EditorWindow::GetHwnd() const { + return m_state->window.hwnd; +} + +bool EditorWindow::HasHwnd() const { + return m_state->window.hwnd != nullptr; +} + +bool EditorWindow::IsPrimary() const { + return m_state->window.primary; +} + +bool EditorWindow::IsClosing() const { + return m_state->window.closing; +} + +bool EditorWindow::IsRenderReady() const { + return m_runtime->IsReady(); +} + +bool EditorWindow::IsTrackingMouseLeave() const { + return m_inputController->IsTrackingMouseLeave(); +} + +bool EditorWindow::HasHoveredBorderlessResizeEdge() const { + return m_chromeController->GetHoveredBorderlessResizeEdge() != + Host::BorderlessWindowResizeEdge::None; +} + +const std::wstring& EditorWindow::GetTitle() const { + return m_state->window.title; +} + +const UIEditorWorkspaceController& EditorWindow::GetWorkspaceController() const { + return m_runtime->GetWorkspaceController(); +} + +UIEditorWorkspaceController& EditorWindow::GetMutableWorkspaceController() { + return m_runtime->GetMutableWorkspaceController(); +} + +const EditorShellRuntime& EditorWindow::GetShellRuntime() const { + return m_runtime->GetShellRuntime(); +} + +EditorShellRuntime& EditorWindow::GetShellRuntime() { + return m_runtime->GetShellRuntime(); +} + +const UIEditorShellInteractionFrame& EditorWindow::GetShellFrame() const { + return m_runtime->GetShellFrame(); +} + +const UIEditorShellInteractionState& EditorWindow::GetShellInteractionState() const { + return m_runtime->GetShellInteractionState(); +} + +void EditorWindow::SetExternalDockHostDropPreview( + const Widgets::UIEditorDockHostDropPreviewState& preview) { + m_runtime->SetExternalDockHostDropPreview(preview); +} + +void EditorWindow::ClearExternalDockHostDropPreview() { + m_runtime->ClearExternalDockHostDropPreview(); +} + +void EditorWindow::AttachHwnd(HWND hwnd) { + m_state->window.hwnd = hwnd; + m_state->window.closing = false; +} + +void EditorWindow::MarkDestroyed() { + m_state->window.hwnd = nullptr; + m_state->window.closing = false; + m_inputController->ResetWindowState(); +} + +void EditorWindow::MarkClosing() { + m_state->window.closing = true; +} + +void EditorWindow::ClearClosing() { + m_state->window.closing = false; +} + +void EditorWindow::SetTrackingMouseLeave(bool trackingMouseLeave) { + m_inputController->SetTrackingMouseLeave(trackingMouseLeave); +} + +void EditorWindow::SetTitle(std::wstring title) { + m_state->window.title = std::move(title); + UpdateCachedTitleText(); +} + +void EditorWindow::ReplaceWorkspaceController(UIEditorWorkspaceController workspaceController) { + m_runtime->ReplaceWorkspaceController(std::move(workspaceController)); +} + +void EditorWindow::InvalidateHostWindow() const { + if (m_state->window.hwnd != nullptr && IsWindow(m_state->window.hwnd)) { + InvalidateRect(m_state->window.hwnd, nullptr, FALSE); + } +} + +bool EditorWindow::Initialize( + const std::filesystem::path& repoRoot, + EditorContext& editorContext, + const std::filesystem::path& captureRoot, + bool autoCaptureOnStartup) { + if (m_state->window.hwnd == nullptr) { + LogRuntimeTrace("app", "window initialize skipped: hwnd is null"); + return false; + } + + Host::RefreshBorderlessWindowDwmDecorations(m_state->window.hwnd); + m_chromeController->Reset(); + m_chromeController->SetWindowDpi(QueryWindowDpi(m_state->window.hwnd)); + m_runtime->SetDpiScale(GetDpiScale()); + + std::ostringstream dpiTrace = {}; + dpiTrace << "initial dpi=" << m_chromeController->GetWindowDpi() + << " scale=" << GetDpiScale(); + LogRuntimeTrace("window", dpiTrace.str()); + + return m_runtime->Initialize( + m_state->window.hwnd, + repoRoot, + editorContext, + captureRoot, + autoCaptureOnStartup); +} + +void EditorWindow::Shutdown() { + ForceReleasePointerCapture(); + + m_runtime->Shutdown(); + m_inputController->ClearPendingEvents(); + m_chromeController->Reset(); +} + +void EditorWindow::ResetInteractionState() { + ForceReleasePointerCapture(); + + m_inputController->ResetInteractionState(); + m_runtime->ResetInteractionState(); + m_chromeController->ResetChromeState(); + m_chromeController->EndBorderlessResize(); + m_chromeController->EndBorderlessWindowDragRestore(); + m_chromeController->EndInteractiveResize(); + m_chromeController->SetHoveredBorderlessResizeEdge( + Host::BorderlessWindowResizeEdge::None); + m_chromeController->ClearPredictedClientPixelSize(); +} + +bool EditorWindow::ApplyWindowResize(UINT width, UINT height) { + if (!m_runtime->IsReady() || width == 0u || height == 0u) { + return false; + } + return m_runtime->ApplyResize(width, height); +} + +bool EditorWindow::QueryCurrentClientPixelSize(UINT& outWidth, UINT& outHeight) const { + outWidth = 0u; + outHeight = 0u; + if (m_state->window.hwnd == nullptr || !IsWindow(m_state->window.hwnd)) { + return false; + } + + RECT clientRect = {}; + if (!GetClientRect(m_state->window.hwnd, &clientRect)) { + return false; + } + + const LONG width = clientRect.right - clientRect.left; + const LONG height = clientRect.bottom - clientRect.top; + if (width <= 0 || height <= 0) { + return false; + } + + outWidth = static_cast(width); + outHeight = static_cast(height); + return true; +} + +bool EditorWindow::ResolveRenderClientPixelSize(UINT& outWidth, UINT& outHeight) const { + if (m_chromeController->TryGetPredictedClientPixelSize(outWidth, outHeight)) { + return true; + } + + return QueryCurrentClientPixelSize(outWidth, outHeight); +} + +float EditorWindow::GetDpiScale() const { + return m_chromeController->GetDpiScale(kBaseDpiScale); +} + +float EditorWindow::PixelsToDips(float pixels) const { + const float dpiScale = GetDpiScale(); + return dpiScale > 0.0f ? pixels / dpiScale : pixels; +} + +UIPoint EditorWindow::ConvertClientPixelsToDips(LONG x, LONG y) const { + return UIPoint( + PixelsToDips(static_cast(x)), + PixelsToDips(static_cast(y))); +} + +UIPoint EditorWindow::ConvertScreenPixelsToClientDips(const POINT& screenPoint) const { + POINT clientPoint = screenPoint; + if (m_state->window.hwnd != nullptr) { + ScreenToClient(m_state->window.hwnd, &clientPoint); + } + + const float dpiScale = m_chromeController->GetDpiScale(kBaseDpiScale); + return UIPoint( + dpiScale > 0.0f + ? static_cast(clientPoint.x) / dpiScale + : static_cast(clientPoint.x), + dpiScale > 0.0f + ? static_cast(clientPoint.y) / dpiScale + : static_cast(clientPoint.y)); +} + +bool EditorWindow::TryResolveDockTabDragHotspot( + std::string_view nodeId, + std::string_view panelId, + const POINT& screenPoint, + POINT& outHotspot) const { + const UIPoint clientPointDips = ConvertScreenPixelsToClientDips(screenPoint); + UIPoint hotspotDips = {}; + if (!m_runtime->GetShellRuntime().TryResolveDockTabDragHotspot( + nodeId, + panelId, + clientPointDips, + hotspotDips)) { + outHotspot = {}; + return false; + } + + const float dpiScale = GetDpiScale(); + outHotspot.x = static_cast(std::lround(hotspotDips.x * dpiScale)); + outHotspot.y = static_cast(std::lround(hotspotDips.y * dpiScale)); + return true; +} + +bool EditorWindow::TryResolveDockTabDropTarget( + const POINT& screenPoint, + UIEditorDockHostTabDropTarget& outTarget) const { + outTarget = m_runtime->GetShellRuntime().ResolveDockTabDropTarget( + ConvertScreenPixelsToClientDips(screenPoint)); + return outTarget.valid; +} + +void EditorWindow::OnResize(UINT width, UINT height) { + bool matchesPredictedClientSize = false; + UINT predictedWidth = 0u; + UINT predictedHeight = 0u; + if (m_chromeController->TryGetPredictedClientPixelSize( + predictedWidth, + predictedHeight)) { + matchesPredictedClientSize = + predictedWidth == width && + predictedHeight == height; + } + + m_chromeController->ClearPredictedClientPixelSize(); + if (IsBorderlessWindowEnabled() && m_state->window.hwnd != nullptr) { + Host::RefreshBorderlessWindowDwmDecorations(m_state->window.hwnd); + } + + if (!matchesPredictedClientSize) { + ApplyWindowResize(width, height); + } +} + +void EditorWindow::OnEnterSizeMove() { + m_chromeController->BeginInteractiveResize(); +} + +void EditorWindow::OnExitSizeMove() { + m_chromeController->EndInteractiveResize(); + m_chromeController->ClearPredictedClientPixelSize(); + UINT width = 0u; + UINT height = 0u; + if (QueryCurrentClientPixelSize(width, height)) { + ApplyWindowResize(width, height); + } +} + +void EditorWindow::OnDpiChanged(UINT dpi, const RECT& suggestedRect) { + m_chromeController->SetWindowDpi(dpi == 0u ? kDefaultDpi : dpi); + m_runtime->SetDpiScale(GetDpiScale()); + if (m_state->window.hwnd != nullptr) { + const LONG windowWidth = suggestedRect.right - suggestedRect.left; + const LONG windowHeight = suggestedRect.bottom - suggestedRect.top; + SetWindowPos( + m_state->window.hwnd, + nullptr, + suggestedRect.left, + suggestedRect.top, + windowWidth, + windowHeight, + SWP_NOZORDER | SWP_NOACTIVATE); + UINT clientWidth = 0u; + UINT clientHeight = 0u; + if (QueryCurrentClientPixelSize(clientWidth, clientHeight)) { + ApplyWindowResize(clientWidth, clientHeight); + } + Host::RefreshBorderlessWindowDwmDecorations(m_state->window.hwnd); + } + + std::ostringstream trace = {}; + trace << "dpi changed to " << m_chromeController->GetWindowDpi() + << " scale=" << GetDpiScale(); + LogRuntimeTrace("window", trace.str()); +} + +bool EditorWindow::IsVerboseRuntimeTraceEnabled() { + static const bool s_enabled = ResolveVerboseRuntimeTraceEnabled(); + return s_enabled; +} + +void EditorWindow::UpdateCachedTitleText() { + m_state->window.titleText = WideToUtf8(m_state->window.title); +} + +} // namespace XCEngine::UI::Editor::App + +namespace XCEngine::UI::Editor::App { + +bool EditorWindow::IsBorderlessWindowEnabled() const { + return true; +} + +bool EditorWindow::IsBorderlessWindowMaximized() const { + return m_chromeController->IsBorderlessWindowMaximized(); +} + +bool EditorWindow::HandleBorderlessWindowSystemCommand( + EditorContext& editorContext, + bool globalTabDragActive, + WPARAM wParam) { + return m_chromeController->HandleSystemCommand( + *this, + editorContext, + globalTabDragActive, + wParam); +} + +bool EditorWindow::HandleBorderlessWindowGetMinMaxInfo(LPARAM lParam) const { + return m_chromeController->HandleGetMinMaxInfo(*this, lParam); +} + +LRESULT EditorWindow::HandleBorderlessWindowNcCalcSize(WPARAM wParam, LPARAM lParam) const { + return m_chromeController->HandleNcCalcSize(*this, wParam, lParam); +} + +bool EditorWindow::QueryCurrentWindowRect(RECT& outRect) const { + return m_chromeController->QueryCurrentWindowRect(*this, outRect); +} + +bool EditorWindow::QueryBorderlessWindowWorkAreaRect(RECT& outRect) const { + return m_chromeController->QueryBorderlessWindowWorkAreaRect(*this, outRect); +} + +bool EditorWindow::ApplyPredictedWindowRectTransition( + EditorContext& editorContext, + bool globalTabDragActive, + const RECT& targetRect) { + return m_chromeController->ApplyPredictedWindowRectTransition( + *this, + editorContext, + globalTabDragActive, + targetRect); +} + +void EditorWindow::ToggleBorderlessWindowMaximizeRestore( + EditorContext& editorContext, + bool globalTabDragActive) { + m_chromeController->ToggleMaximizeRestore(*this, editorContext, globalTabDragActive); +} + +} // namespace XCEngine::UI::Editor::App + +namespace XCEngine::UI::Editor::App { + +using ::XCEngine::UI::UIRect; + +bool EditorWindow::UpdateBorderlessWindowResizeHover(LPARAM lParam) { + return m_chromeController->UpdateResizeHover(*this, lParam); +} + +bool EditorWindow::HandleBorderlessWindowResizeButtonDown(LPARAM lParam) { + return m_chromeController->HandleResizeButtonDown(*this, lParam); +} + +bool EditorWindow::HandleBorderlessWindowResizeButtonUp() { + return m_chromeController->HandleResizeButtonUp(*this); +} + +bool EditorWindow::HandleBorderlessWindowResizePointerMove( + EditorContext& editorContext, + bool globalTabDragActive) { + return m_chromeController->HandleResizePointerMove( + *this, + editorContext, + globalTabDragActive); +} + +void EditorWindow::ClearBorderlessWindowResizeState() { + m_chromeController->ClearResizeState(*this); +} + +void EditorWindow::ForceClearBorderlessWindowResizeState() { + m_chromeController->ForceClearResizeState(*this); +} + +Host::BorderlessWindowResizeEdge EditorWindow::HitTestBorderlessWindowResizeEdge( + LPARAM lParam) const { + return m_chromeController->HitTestResizeEdge(*this, lParam); +} + +void EditorWindow::ApplyBorderlessWindowResizeCursorHoverPriority() { + m_chromeController->ApplyResizeCursorHoverPriority(); +} + +} // namespace XCEngine::UI::Editor::App + +namespace XCEngine::UI::Editor::App { + +using namespace EditorWindowSupport; +using ::XCEngine::UI::UIDrawData; +using ::XCEngine::UI::UIDrawList; +using ::XCEngine::UI::UIInputEvent; +using ::XCEngine::UI::UIInputModifiers; +using ::XCEngine::UI::UIPointerButton; +using ::XCEngine::UI::UIRect; + +namespace { + +std::uint8_t ButtonMask(UIPointerButton button) { + switch (button) { + case UIPointerButton::Left: return 1u << 0u; + case UIPointerButton::Right: return 1u << 1u; + case UIPointerButton::Middle: return 1u << 2u; + case UIPointerButton::X1: return 1u << 3u; + case UIPointerButton::X2: return 1u << 4u; + case UIPointerButton::None: + default: + return 0u; + } +} + +std::uint8_t ButtonMaskFromModifiers(const UIInputModifiers& modifiers) { + std::uint8_t mask = 0u; + if (modifiers.leftMouse) { + mask |= ButtonMask(UIPointerButton::Left); + } + if (modifiers.rightMouse) { + mask |= ButtonMask(UIPointerButton::Right); + } + if (modifiers.middleMouse) { + mask |= ButtonMask(UIPointerButton::Middle); + } + if (modifiers.x1Mouse) { + mask |= ButtonMask(UIPointerButton::X1); + } + if (modifiers.x2Mouse) { + mask |= ButtonMask(UIPointerButton::X2); + } + return mask; +} + +std::uint8_t ResolveExpectedShellCaptureButtons( + const EditorShellRuntime& shellRuntime) { + std::uint8_t expectedButtons = 0u; + const auto& shellState = shellRuntime.GetShellInteractionState(); + const auto& dockHostState = + shellState.workspaceInteractionState.dockHostInteractionState; + if (dockHostState.splitterDragState.active || + !dockHostState.activeTabDragNodeId.empty()) { + expectedButtons |= ButtonMask(UIPointerButton::Left); + } + + for (const auto& panelState : + shellState.workspaceInteractionState.composeState.panelStates) { + const auto& inputBridgeState = panelState.viewportShellState.inputBridgeState; + if (inputBridgeState.captured) { + expectedButtons |= ButtonMask(inputBridgeState.captureButton); + } + } + + return expectedButtons; +} + +} // namespace + +EditorWindowFrameTransferRequests EditorWindow::RenderFrame( + EditorContext& editorContext, + bool globalTabDragActive) { + if (!m_runtime->IsReady() || m_state->window.hwnd == nullptr) { + return {}; + } + + UINT pixelWidth = 0u; + UINT pixelHeight = 0u; + if (!ResolveRenderClientPixelSize(pixelWidth, pixelHeight)) { + return {}; + } + + const float width = PixelsToDips(static_cast(pixelWidth)); + const float height = PixelsToDips(static_cast(pixelHeight)); + const UIRect workspaceBounds = ResolveWorkspaceBounds(width, height); + + UIDrawData drawData = {}; + UIDrawList& drawList = drawData.EmplaceDrawList("XCEditorShell"); + drawList.AddFilledRect( + UIRect(0.0f, 0.0f, width, height), + kShellSurfaceColor); + + EditorWindowFrameTransferRequests transferRequests = {}; + if (editorContext.IsValid()) { + transferRequests = + RenderRuntimeFrame(editorContext, globalTabDragActive, workspaceBounds, drawList); + } else { + m_frameOrchestrator->AppendInvalidFrame(editorContext, drawList); + } + + AppendBorderlessWindowChrome(drawList, width); + + const Host::D3D12WindowRenderLoopPresentResult presentResult = m_runtime->Present(drawData); + if (!presentResult.warning.empty()) { + LogRuntimeTrace("present", presentResult.warning); + } + + m_runtime->CaptureIfRequested( + drawData, + pixelWidth, + pixelHeight, + presentResult.framePresented); + return transferRequests; +} + +EditorWindowFrameTransferRequests EditorWindow::OnPaintMessage( + EditorContext& editorContext, + bool globalTabDragActive) { + if (!m_runtime->IsReady() || m_state->window.hwnd == nullptr) { + return {}; + } + + PAINTSTRUCT paintStruct = {}; + BeginPaint(m_state->window.hwnd, &paintStruct); + const EditorWindowFrameTransferRequests transferRequests = + RenderFrame(editorContext, globalTabDragActive); + EndPaint(m_state->window.hwnd, &paintStruct); + return transferRequests; +} + +UIRect EditorWindow::ResolveWorkspaceBounds(float clientWidthDips, float clientHeightDips) const { + if (!IsBorderlessWindowEnabled()) { + return UIRect(0.0f, 0.0f, clientWidthDips, clientHeightDips); + } + + if (ShouldUseDetachedTitleBarTabStrip()) { + return UIRect(0.0f, 0.0f, clientWidthDips, clientHeightDips); + } + + const float titleBarHeight = (std::min)(kBorderlessTitleBarHeightDips, clientHeightDips); + return UIRect( + 0.0f, + titleBarHeight, + clientWidthDips, + (std::max)(0.0f, clientHeightDips - titleBarHeight)); +} + +EditorWindowFrameTransferRequests EditorWindow::RenderRuntimeFrame( + EditorContext& editorContext, + bool globalTabDragActive, + const UIRect& workspaceBounds, + UIDrawList& drawList) { + SyncShellCapturedPointerButtonsFromSystemState(); + std::vector frameEvents = m_inputController->TakePendingEvents(); + const bool useDetachedTitleBarTabStrip = ShouldUseDetachedTitleBarTabStrip(); + const EditorWindowFrameTransferRequests transferRequests = + m_frameOrchestrator->UpdateAndAppend( + editorContext, + *m_runtime, + workspaceBounds, + frameEvents, + m_runtime->BuildCaptureStatusText(), + m_state->window.primary, + globalTabDragActive, + useDetachedTitleBarTabStrip, + drawList); + + ApplyShellRuntimePointerCapture(); + ApplyCurrentCursor(); + return transferRequests; +} + +void EditorWindow::SyncShellCapturedPointerButtonsFromSystemState() { + m_inputController->SyncInputModifiersFromSystemState(); + + const std::uint8_t expectedButtons = ResolveExpectedShellCaptureButtons( + m_runtime->GetShellRuntime()); + if (expectedButtons == 0u || + m_inputController->HasPendingPointerStateReconciliationEvent()) { + return; + } + + const UIInputModifiers modifiers = m_inputController->GetCurrentModifiers(); + if ((ButtonMaskFromModifiers(modifiers) & expectedButtons) == expectedButtons) { + return; + } + + QueueSyntheticPointerStateSyncEvent(modifiers); +} + +void EditorWindow::ApplyShellRuntimePointerCapture() { + const EditorShellPointerOwner owner = m_runtime->GetShellRuntime().GetPointerOwner(); + if (IsShellPointerOwner(owner)) { + AcquirePointerCapture(EditorWindowPointerCaptureOwner::Shell); + return; + } + + if (IsHostedContentPointerOwner(owner)) { + AcquirePointerCapture(EditorWindowPointerCaptureOwner::HostedContent); + return; + } + + if (OwnsPointerCapture(EditorWindowPointerCaptureOwner::Shell)) { + ReleasePointerCapture(EditorWindowPointerCaptureOwner::Shell); + } + + if (OwnsPointerCapture(EditorWindowPointerCaptureOwner::HostedContent)) { + ReleasePointerCapture(EditorWindowPointerCaptureOwner::HostedContent); + } +} + +} // namespace XCEngine::UI::Editor::App + +namespace XCEngine::UI::Editor::App { + +using ::XCEngine::UI::UIInputEvent; +using ::XCEngine::UI::UIInputEventType; +using ::XCEngine::UI::UIPointerButton; + +namespace { + +bool IsScreenPointOverWindow(HWND hwnd, const POINT& screenPoint) { + if (hwnd == nullptr || !IsWindow(hwnd)) { + return false; + } + + const HWND hitWindow = WindowFromPoint(screenPoint); + if (hitWindow == nullptr || GetAncestor(hitWindow, GA_ROOT) != hwnd) { + return false; + } + + RECT windowRect = {}; + if (!GetWindowRect(hwnd, &windowRect)) { + return false; + } + + return screenPoint.x >= windowRect.left && screenPoint.x < windowRect.right && + screenPoint.y >= windowRect.top && screenPoint.y < windowRect.bottom; +} +} // namespace + +bool EditorWindow::ApplyCurrentCursor() const { + if (!HasInteractiveCaptureState() && !IsPointerInsideClientArea()) { + return false; + } + + const HCURSOR cursor = LoadCursorW(nullptr, ResolveCurrentCursorResource()); + if (cursor == nullptr) { + return false; + } + + SetCursor(cursor); + return true; +} + + +bool EditorWindow::HasInteractiveCaptureState() const { + return m_runtime->GetShellRuntime().HasInteractiveCapture() || + m_chromeController->IsBorderlessWindowDragRestoreArmed() || + m_chromeController->IsBorderlessResizeActive() || + m_inputController->HasPointerCaptureOwner(); +} + +EditorWindowPointerCaptureOwner EditorWindow::GetPointerCaptureOwner() const { + return m_inputController->GetPointerCaptureOwner(); +} + +bool EditorWindow::OwnsPointerCapture(EditorWindowPointerCaptureOwner owner) const { + return m_inputController->OwnsPointerCapture(owner); +} + +void EditorWindow::AcquirePointerCapture(EditorWindowPointerCaptureOwner owner) { + m_inputController->AcquirePointerCapture(m_state->window.hwnd, owner); +} + +void EditorWindow::ReleasePointerCapture(EditorWindowPointerCaptureOwner owner) { + m_inputController->ReleasePointerCapture(m_state->window.hwnd, owner); +} + +void EditorWindow::ForceReleasePointerCapture() { + m_inputController->ForceReleasePointerCapture(m_state->window.hwnd); +} + +void EditorWindow::ClearPointerCaptureOwner() { + m_inputController->ClearPointerCaptureOwner(); +} + +void EditorWindow::TryStartImmediateShellPointerCapture(LPARAM lParam) { + if (m_state->window.hwnd == nullptr || + !IsWindow(m_state->window.hwnd) || + GetCapture() == m_state->window.hwnd) { + return; + } + + const ::XCEngine::UI::UIPoint clientPoint = ConvertClientPixelsToDips( + GET_X_LPARAM(lParam), + GET_Y_LPARAM(lParam)); + if (!ShouldStartImmediateUIEditorShellPointerCapture( + m_runtime->GetShellFrame(), + clientPoint)) { + return; + } + + AcquirePointerCapture(EditorWindowPointerCaptureOwner::Shell); +} + +void EditorWindow::QueuePointerEvent( + UIInputEventType type, + UIPointerButton button, + WPARAM wParam, + LPARAM lParam, + bool doubleClick) { + UIInputEvent event = {}; + m_inputController->QueuePointerEvent( + type, + button, + ConvertClientPixelsToDips(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)), + wParam, + doubleClick); +} + +void EditorWindow::QueueSyntheticPointerStateSyncEvent( + const ::XCEngine::UI::UIInputModifiers& modifiers) { + if (m_state->window.hwnd == nullptr || !IsWindow(m_state->window.hwnd)) { + return; + } + + POINT screenPoint = {}; + if (!GetCursorPos(&screenPoint)) { + return; + } + if (!ScreenToClient(m_state->window.hwnd, &screenPoint)) { + return; + } + + m_inputController->QueueSyntheticPointerStateSyncEvent( + ConvertClientPixelsToDips(screenPoint.x, screenPoint.y), + modifiers); +} + +void EditorWindow::QueuePointerLeaveEvent() { + ::XCEngine::UI::UIPoint position = {}; + if (m_state->window.hwnd != nullptr) { + POINT clientPoint = {}; + GetCursorPos(&clientPoint); + ScreenToClient(m_state->window.hwnd, &clientPoint); + position = ConvertClientPixelsToDips(clientPoint.x, clientPoint.y); + } + m_inputController->QueuePointerLeaveEvent(position); +} + +void EditorWindow::QueuePointerWheelEvent(short wheelDelta, WPARAM wParam, LPARAM lParam) { + if (m_state->window.hwnd == nullptr) { + return; + } + + POINT screenPoint = { + GET_X_LPARAM(lParam), + GET_Y_LPARAM(lParam) + }; + ScreenToClient(m_state->window.hwnd, &screenPoint); + + m_inputController->QueuePointerWheelEvent( + ConvertClientPixelsToDips(screenPoint.x, screenPoint.y), + wheelDelta, + wParam); +} + +void EditorWindow::QueueKeyEvent(UIInputEventType type, WPARAM wParam, LPARAM lParam) { + m_inputController->QueueKeyEvent(type, wParam, lParam); +} + +void EditorWindow::QueueCharacterEvent(WPARAM wParam, LPARAM) { + m_inputController->QueueCharacterEvent(wParam); +} + +void EditorWindow::QueueWindowFocusEvent(UIInputEventType type) { + m_inputController->QueueWindowFocusEvent(type); +} + +void EditorWindow::SyncInputModifiersFromSystemState() { + m_inputController->SyncInputModifiersFromSystemState(); +} + +void EditorWindow::ResetInputModifiers() { + m_inputController->ResetInputModifiers(); +} + +void EditorWindow::RequestManualScreenshot() { + m_runtime->RequestManualScreenshot("manual_f12"); +} + +bool EditorWindow::IsPointerInsideClientArea() const { + if (m_state->window.hwnd == nullptr || !IsWindow(m_state->window.hwnd)) { + return false; + } + + POINT screenPoint = {}; + if (!GetCursorPos(&screenPoint)) { + return false; + } + + if (!IsScreenPointOverWindow(m_state->window.hwnd, screenPoint)) { + return false; + } + + const LPARAM pointParam = MAKELPARAM( + static_cast(screenPoint.x), + static_cast(screenPoint.y)); + return SendMessageW(m_state->window.hwnd, WM_NCHITTEST, 0, pointParam) == HTCLIENT; +} + +LPCWSTR EditorWindow::ResolveCurrentCursorResource() const { + const Host::BorderlessWindowResizeEdge borderlessResizeEdge = + m_chromeController->IsBorderlessResizeActive() + ? m_chromeController->GetBorderlessResizeEdge() + : m_chromeController->GetHoveredBorderlessResizeEdge(); + if (borderlessResizeEdge != Host::BorderlessWindowResizeEdge::None) { + return Host::ResolveBorderlessWindowResizeCursor(borderlessResizeEdge); + } + + switch (m_runtime->GetShellRuntime().GetHostedContentCursorKind()) { + case ProjectPanel::CursorKind::ResizeEW: + return IDC_SIZEWE; + case ProjectPanel::CursorKind::Arrow: + default: + break; + } + + switch (m_runtime->GetShellRuntime().GetDockCursorKind()) { + case Widgets::UIEditorDockHostCursorKind::ResizeEW: + return IDC_SIZEWE; + case Widgets::UIEditorDockHostCursorKind::ResizeNS: + return IDC_SIZENS; + case Widgets::UIEditorDockHostCursorKind::Arrow: + default: + return IDC_ARROW; + } +} + +} // namespace XCEngine::UI::Editor::App + + +namespace XCEngine::UI::Editor::App { + +using namespace EditorWindowSupport; + +bool EditorWindow::UpdateBorderlessWindowChromeHover(LPARAM lParam) { + return m_chromeController->UpdateChromeHover(*this, lParam); +} + +bool EditorWindow::HandleBorderlessWindowChromeButtonDown(LPARAM lParam) { + return m_chromeController->HandleChromeButtonDown(*this, lParam); +} + +bool EditorWindow::HandleBorderlessWindowChromeButtonUp( + EditorContext& editorContext, + bool globalTabDragActive, + LPARAM lParam) { + return m_chromeController->HandleChromeButtonUp( + *this, + editorContext, + globalTabDragActive, + lParam); +} + +bool EditorWindow::HandleBorderlessWindowChromeDoubleClick( + EditorContext& editorContext, + bool globalTabDragActive, + LPARAM lParam) { + return m_chromeController->HandleChromeDoubleClick( + *this, + editorContext, + globalTabDragActive, + lParam); +} + +bool EditorWindow::HandleBorderlessWindowChromeDragRestorePointerMove( + EditorContext& editorContext, + bool globalTabDragActive) { + return m_chromeController->HandleChromeDragRestorePointerMove( + *this, + editorContext, + globalTabDragActive); +} + +void EditorWindow::ClearBorderlessWindowChromeDragRestoreState() { + m_chromeController->ClearChromeDragRestoreState(*this); +} + +void EditorWindow::ClearBorderlessWindowChromeState() { + m_chromeController->ClearChromeState(*this); +} + +void EditorWindow::ExecuteBorderlessWindowChromeAction( + EditorContext& editorContext, + bool globalTabDragActive, + Host::BorderlessWindowChromeHitTarget target) { + m_chromeController->ExecuteChromeAction(*this, editorContext, globalTabDragActive, target); +} + +} // namespace XCEngine::UI::Editor::App + +namespace XCEngine::UI::Editor::App { + +bool EditorWindow::ShouldUseDetachedTitleBarTabStrip() const { + return m_chromeController->ShouldUseDetachedTitleBarTabStrip(*this); +} + +Host::BorderlessWindowChromeHitTarget EditorWindow::HitTestBorderlessWindowChrome( + LPARAM lParam) const { + return m_chromeController->HitTestChrome(*this, lParam); +} + +Host::BorderlessWindowChromeLayout EditorWindow::ResolveBorderlessWindowChromeLayout( + float clientWidthDips) const { + return m_chromeController->ResolveChromeLayout(*this, clientWidthDips); +} + +void EditorWindow::AppendBorderlessWindowChrome( + ::XCEngine::UI::UIDrawList& drawList, + float clientWidthDips) const { + m_chromeController->AppendChrome(*this, drawList, clientWidthDips); +} + +} // namespace XCEngine::UI::Editor::App + + diff --git a/new_editor/app/Platform/Win32/EditorWindow.h b/new_editor/app/Platform/Win32/EditorWindow.h index aa06ca95..fd58012e 100644 --- a/new_editor/app/Platform/Win32/EditorWindow.h +++ b/new_editor/app/Platform/Win32/EditorWindow.h @@ -32,6 +32,7 @@ enum class UIInputEventType : std::uint8_t; namespace XCEngine::UI::Editor { +struct UIEditorDockHostTabDropTarget; class UIEditorWorkspaceController; struct UIEditorDockHostInteractionState; @@ -58,9 +59,6 @@ namespace XCEngine::UI::Editor::App { class EditorContext; class EditorShellRuntime; -struct EditorWindowState; - -namespace Internal { class EditorWindowChromeController; class EditorWindowFrameOrchestrator; class EditorWindowHostRuntime; @@ -68,7 +66,7 @@ class EditorWindowInputController; class EditorWindowMessageDispatcher; class EditorWindowRuntimeController; class EditorWindowWorkspaceCoordinator; -} +struct EditorWindowState; class EditorWindow { public: @@ -95,10 +93,10 @@ public: ::XCEngine::UI::UIPoint ConvertScreenPixelsToClientDips(const POINT& screenPoint) const; private: - friend class Internal::EditorWindowChromeController; - friend class Internal::EditorWindowHostRuntime; - friend class Internal::EditorWindowMessageDispatcher; - friend class Internal::EditorWindowWorkspaceCoordinator; + friend class EditorWindowChromeController; + friend class EditorWindowHostRuntime; + friend class EditorWindowMessageDispatcher; + friend class EditorWindowWorkspaceCoordinator; bool IsRenderReady() const; bool IsTrackingMouseLeave() const; @@ -106,6 +104,14 @@ private: const EditorShellRuntime& GetShellRuntime() const; EditorShellRuntime& GetShellRuntime(); const UIEditorShellInteractionState& GetShellInteractionState() const; + bool TryResolveDockTabDragHotspot( + std::string_view nodeId, + std::string_view panelId, + const POINT& screenPoint, + POINT& outHotspot) const; + bool TryResolveDockTabDropTarget( + const POINT& screenPoint, + UIEditorDockHostTabDropTarget& outTarget) const; void InvalidateHostWindow() const; void SetExternalDockHostDropPreview( const Widgets::UIEditorDockHostDropPreviewState& preview); @@ -185,7 +191,8 @@ private: ::XCEngine::UI::UIInputEventType type, ::XCEngine::UI::UIPointerButton button, WPARAM wParam, - LPARAM lParam); + LPARAM lParam, + bool doubleClick = false); void QueueSyntheticPointerStateSyncEvent( const ::XCEngine::UI::UIInputModifiers& modifiers); void QueuePointerLeaveEvent(); @@ -240,10 +247,10 @@ private: static bool IsVerboseRuntimeTraceEnabled(); std::unique_ptr m_state = {}; - std::unique_ptr m_chromeController = {}; - std::unique_ptr m_frameOrchestrator = {}; - std::unique_ptr m_inputController = {}; - std::unique_ptr m_runtime = {}; + std::unique_ptr m_chromeController = {}; + std::unique_ptr m_frameOrchestrator = {}; + std::unique_ptr m_inputController = {}; + std::unique_ptr m_runtime = {}; }; } // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Platform/Win32/EditorWindowBorderlessPlacement.cpp b/new_editor/app/Platform/Win32/EditorWindowBorderlessPlacement.cpp deleted file mode 100644 index 6da5f3aa..00000000 --- a/new_editor/app/Platform/Win32/EditorWindowBorderlessPlacement.cpp +++ /dev/null @@ -1,58 +0,0 @@ -#include "Platform/Win32/EditorWindow.h" -#include "Platform/Win32/EditorWindowChromeController.h" -#include "Platform/Win32/EditorWindowInternalState.h" -namespace XCEngine::UI::Editor::App { - -bool EditorWindow::IsBorderlessWindowEnabled() const { - return true; -} - -bool EditorWindow::IsBorderlessWindowMaximized() const { - return m_chromeController->IsBorderlessWindowMaximized(); -} - -bool EditorWindow::HandleBorderlessWindowSystemCommand( - EditorContext& editorContext, - bool globalTabDragActive, - WPARAM wParam) { - return m_chromeController->HandleSystemCommand( - *this, - editorContext, - globalTabDragActive, - wParam); -} - -bool EditorWindow::HandleBorderlessWindowGetMinMaxInfo(LPARAM lParam) const { - return m_chromeController->HandleGetMinMaxInfo(*this, lParam); -} - -LRESULT EditorWindow::HandleBorderlessWindowNcCalcSize(WPARAM wParam, LPARAM lParam) const { - return m_chromeController->HandleNcCalcSize(*this, wParam, lParam); -} - -bool EditorWindow::QueryCurrentWindowRect(RECT& outRect) const { - return m_chromeController->QueryCurrentWindowRect(*this, outRect); -} - -bool EditorWindow::QueryBorderlessWindowWorkAreaRect(RECT& outRect) const { - return m_chromeController->QueryBorderlessWindowWorkAreaRect(*this, outRect); -} - -bool EditorWindow::ApplyPredictedWindowRectTransition( - EditorContext& editorContext, - bool globalTabDragActive, - const RECT& targetRect) { - return m_chromeController->ApplyPredictedWindowRectTransition( - *this, - editorContext, - globalTabDragActive, - targetRect); -} - -void EditorWindow::ToggleBorderlessWindowMaximizeRestore( - EditorContext& editorContext, - bool globalTabDragActive) { - m_chromeController->ToggleMaximizeRestore(*this, editorContext, globalTabDragActive); -} - -} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Platform/Win32/EditorWindowBorderlessResize.cpp b/new_editor/app/Platform/Win32/EditorWindowBorderlessResize.cpp deleted file mode 100644 index e1a128a5..00000000 --- a/new_editor/app/Platform/Win32/EditorWindowBorderlessResize.cpp +++ /dev/null @@ -1,53 +0,0 @@ -#include "Platform/Win32/EditorWindow.h" -#include "Platform/Win32/EditorWindowChromeController.h" -#include "Platform/Win32/EditorWindowInternalState.h" - -#include - -#include - -#include - -namespace XCEngine::UI::Editor::App { - -using ::XCEngine::UI::UIRect; - -bool EditorWindow::UpdateBorderlessWindowResizeHover(LPARAM lParam) { - return m_chromeController->UpdateResizeHover(*this, lParam); -} - -bool EditorWindow::HandleBorderlessWindowResizeButtonDown(LPARAM lParam) { - return m_chromeController->HandleResizeButtonDown(*this, lParam); -} - -bool EditorWindow::HandleBorderlessWindowResizeButtonUp() { - return m_chromeController->HandleResizeButtonUp(*this); -} - -bool EditorWindow::HandleBorderlessWindowResizePointerMove( - EditorContext& editorContext, - bool globalTabDragActive) { - return m_chromeController->HandleResizePointerMove( - *this, - editorContext, - globalTabDragActive); -} - -void EditorWindow::ClearBorderlessWindowResizeState() { - m_chromeController->ClearResizeState(*this); -} - -void EditorWindow::ForceClearBorderlessWindowResizeState() { - m_chromeController->ForceClearResizeState(*this); -} - -Host::BorderlessWindowResizeEdge EditorWindow::HitTestBorderlessWindowResizeEdge( - LPARAM lParam) const { - return m_chromeController->HitTestResizeEdge(*this, lParam); -} - -void EditorWindow::ApplyBorderlessWindowResizeCursorHoverPriority() { - m_chromeController->ApplyResizeCursorHoverPriority(); -} - -} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Platform/Win32/EditorWindowChromeController.cpp b/new_editor/app/Platform/Win32/EditorWindowChromeController.cpp index bb75129c..d7ae3aaa 100644 --- a/new_editor/app/Platform/Win32/EditorWindowChromeController.cpp +++ b/new_editor/app/Platform/Win32/EditorWindowChromeController.cpp @@ -1,10 +1,9 @@ #include "Platform/Win32/EditorWindowChromeController.h" #include "Platform/Win32/EditorWindow.h" -#include "Platform/Win32/EditorWindowConstants.h" -#include "Platform/Win32/EditorWindowInternalState.h" #include "Platform/Win32/EditorWindowRuntimeController.h" -#include "Platform/Win32/EditorWindowStyle.h" +#include "Platform/Win32/EditorWindowState.h" +#include "Platform/Win32/EditorWindowSupport.h" #include #include @@ -17,9 +16,9 @@ #include -namespace XCEngine::UI::Editor::App::Internal { +namespace XCEngine::UI::Editor::App { -using namespace EditorWindowInternal; +using namespace EditorWindowSupport; using ::XCEngine::UI::Layout::MeasureUITabStripHeaderWidth; using ::XCEngine::UI::UIColor; using ::XCEngine::UI::UIDrawList; @@ -31,6 +30,15 @@ namespace { constexpr float kTitleBarLogoExtent = 16.0f; constexpr float kTitleBarLogoInsetLeft = 8.0f; constexpr float kTitleBarLogoTextGap = 8.0f; +constexpr float kTitleBarFrameStatsInsetRight = 12.0f; + +bool IsRootPanelVisible( + const UIEditorWorkspaceController& controller, + std::string_view panelId) { + const UIEditorPanelSessionState* panelState = + FindUIEditorPanelSessionState(controller.GetSession(), panelId); + return panelState != nullptr && panelState->open && panelState->visible; +} std::string ResolveDetachedTitleTabText(const EditorWindow& window) { const auto& workspaceController = window.GetWorkspaceController(); @@ -49,6 +57,61 @@ std::string ResolveDetachedTitleTabText(const EditorWindow& window) { return std::string("Panel"); } +const UIEditorPanelDescriptor* ResolveSingleVisibleRootPanelDescriptor( + const EditorWindow& window) { + const UIEditorWorkspaceController& workspaceController = window.GetWorkspaceController(); + const UIEditorWorkspaceNode& root = workspaceController.GetWorkspace().root; + if (root.kind != UIEditorWorkspaceNodeKind::TabStack) { + return nullptr; + } + + const UIEditorPanelRegistry& panelRegistry = workspaceController.GetPanelRegistry(); + const UIEditorPanelDescriptor* visibleDescriptor = nullptr; + std::size_t visibleCount = 0u; + for (const UIEditorWorkspaceNode& child : root.children) { + if (child.kind != UIEditorWorkspaceNodeKind::Panel || + !IsRootPanelVisible(workspaceController, child.panel.panelId)) { + continue; + } + + const UIEditorPanelDescriptor* descriptor = + FindUIEditorPanelDescriptor(panelRegistry, child.panel.panelId); + if (descriptor == nullptr) { + return nullptr; + } + + ++visibleCount; + visibleDescriptor = descriptor; + if (visibleCount > 1u) { + return nullptr; + } + } + + return visibleCount == 1u ? visibleDescriptor : nullptr; +} + +bool IsToolWindow(const EditorWindow& window) { + if (window.IsPrimary()) { + return false; + } + + const UIEditorPanelDescriptor* descriptor = + ResolveSingleVisibleRootPanelDescriptor(window); + return descriptor != nullptr && descriptor->toolWindow; +} + +::XCEngine::UI::UISize ResolveMinimumOuterSize(const EditorWindow& window) { + if (const UIEditorPanelDescriptor* descriptor = + ResolveSingleVisibleRootPanelDescriptor(window); + descriptor != nullptr && + descriptor->minimumDetachedWindowSize.width > 0.0f && + descriptor->minimumDetachedWindowSize.height > 0.0f) { + return descriptor->minimumDetachedWindowSize; + } + + return ::XCEngine::UI::UISize(640.0f, 360.0f); +} + float ResolveDetachedTabWidth(std::string_view text) { const Widgets::UIEditorTabStripMetrics& metrics = ResolveUIEditorTabStripMetrics(); Widgets::UIEditorTabStripItem item = {}; @@ -58,14 +121,6 @@ float ResolveDetachedTabWidth(std::string_view text) { return MeasureUITabStripHeaderWidth(desiredLabelWidth, metrics.layoutMetrics); } -bool IsRootPanelVisible( - const UIEditorWorkspaceController& controller, - std::string_view panelId) { - const UIEditorPanelSessionState* panelState = - FindUIEditorPanelSessionState(controller.GetSession(), panelId); - return panelState != nullptr && panelState->open && panelState->visible; -} - bool HasSingleVisibleRootTab(const UIEditorWorkspaceController& controller) { const UIEditorWorkspaceNode& root = controller.GetWorkspace().root; if (root.kind != UIEditorWorkspaceNodeKind::TabStack) { @@ -272,7 +327,12 @@ bool EditorWindowChromeController::HandleSystemCommand( bool EditorWindowChromeController::HandleGetMinMaxInfo( const EditorWindow& window, LPARAM lParam) const { - return Host::HandleBorderlessWindowGetMinMaxInfo(window.m_state->window.hwnd, lParam); + const ::XCEngine::UI::UISize minimumOuterSize = ResolveMinimumOuterSize(window); + return Host::HandleBorderlessWindowGetMinMaxInfo( + window.m_state->window.hwnd, + lParam, + static_cast(minimumOuterSize.width), + static_cast(minimumOuterSize.height)); } LRESULT EditorWindowChromeController::HandleNcCalcSize( @@ -345,13 +405,14 @@ bool EditorWindowChromeController::HandleResizePointerMove( return false; } + const ::XCEngine::UI::UISize minimumOuterSize = ResolveMinimumOuterSize(window); RECT targetRect = Host::ComputeBorderlessWindowResizeRect( GetBorderlessResizeInitialWindowRect(), GetBorderlessResizeInitialScreenPoint(), currentScreenPoint, GetBorderlessResizeEdge(), - 640, - 360); + static_cast(minimumOuterSize.width), + static_cast(minimumOuterSize.height)); const int width = targetRect.right - targetRect.left; const int height = targetRect.bottom - targetRect.top; if (width <= 0 || height <= 0) { @@ -660,6 +721,7 @@ Host::BorderlessWindowChromeLayout EditorWindowChromeController::ResolveChromeLa bool EditorWindowChromeController::ShouldUseDetachedTitleBarTabStrip( const EditorWindow& window) const { return !window.m_state->window.primary && + !IsToolWindow(window) && HasSingleVisibleRootTab(window.m_runtime->GetWorkspaceController()); } @@ -686,11 +748,43 @@ void EditorWindowChromeController::AppendChrome( } if (!window.m_state->window.primary) { - if (window.m_runtime->GetTitleBarLogoIcon().IsValid()) { - drawList.AddImage( - BuildDetachedTitleLogoRect(layout), - window.m_runtime->GetTitleBarLogoIcon(), - UIColor(1.0f, 1.0f, 1.0f, 1.0f)); + if (useDetachedTitleBarTabStrip) { + if (window.m_runtime->GetTitleBarLogoIcon().IsValid()) { + drawList.AddImage( + BuildDetachedTitleLogoRect(layout), + window.m_runtime->GetTitleBarLogoIcon(), + UIColor(1.0f, 1.0f, 1.0f, 1.0f)); + } + } else { + const float iconX = layout.titleBarRect.x + kTitleBarLogoInsetLeft; + const float iconY = + layout.titleBarRect.y + + (std::max)(0.0f, (layout.titleBarRect.height - kTitleBarLogoExtent) * 0.5f); + if (window.m_runtime->GetTitleBarLogoIcon().IsValid()) { + drawList.AddImage( + UIRect(iconX, iconY, kTitleBarLogoExtent, kTitleBarLogoExtent), + window.m_runtime->GetTitleBarLogoIcon(), + UIColor(1.0f, 1.0f, 1.0f, 1.0f)); + } + + drawList.AddText( + UIPoint( + iconX + + (window.m_runtime->GetTitleBarLogoIcon().IsValid() + ? (kTitleBarLogoExtent + kTitleBarLogoTextGap) + : 4.0f), + layout.titleBarRect.y + + (std::max)( + 0.0f, + (layout.titleBarRect.height - kBorderlessTitleBarFontSize) * 0.5f - + 1.0f)), + IsToolWindow(window) + ? ResolveDetachedTitleTabText(window) + : (window.m_state->window.titleText.empty() + ? std::string("XCEngine Editor") + : window.m_state->window.titleText), + kShellTextColor, + kBorderlessTitleBarFontSize); } } else { const float iconX = layout.titleBarRect.x + kTitleBarLogoInsetLeft; @@ -708,6 +802,7 @@ void EditorWindowChromeController::AppendChrome( window.m_state->window.titleText.empty() ? std::string("XCEngine Editor") : window.m_state->window.titleText; + const std::string frameRateText = window.m_runtime->BuildFrameRateText(); drawList.AddText( UIPoint( iconX + @@ -716,11 +811,32 @@ void EditorWindowChromeController::AppendChrome( : 4.0f), layout.titleBarRect.y + (std::max)( - 0.0f, - (layout.titleBarRect.height - kBorderlessTitleBarFontSize) * 0.5f - 1.0f)), + 0.0f, + (layout.titleBarRect.height - kBorderlessTitleBarFontSize) * 0.5f - 1.0f)), titleText, kShellTextColor, kBorderlessTitleBarFontSize); + if (!frameRateText.empty()) { + const float frameRateTextWidth = + window.m_runtime->GetRenderer().MeasureTextWidth( + UIEditorTextMeasureRequest{ + frameRateText, + kBorderlessTitleBarFontSize }); + const float frameRateX = + layout.dragRect.x + layout.dragRect.width - + kTitleBarFrameStatsInsetRight - frameRateTextWidth; + drawList.AddText( + UIPoint( + (std::max)(frameRateX, layout.dragRect.x + kTitleBarLogoInsetLeft), + layout.titleBarRect.y + + (std::max)( + 0.0f, + (layout.titleBarRect.height - kBorderlessTitleBarFontSize) * 0.5f - + 1.0f)), + frameRateText, + kShellMutedTextColor, + kBorderlessTitleBarFontSize); + } } Host::AppendBorderlessWindowChrome( @@ -858,4 +974,5 @@ void EditorWindowChromeController::ExecuteChromeAction( window.InvalidateHostWindow(); } -} // namespace XCEngine::UI::Editor::App::Internal +} // namespace XCEngine::UI::Editor::App + diff --git a/new_editor/app/Platform/Win32/EditorWindowChromeController.h b/new_editor/app/Platform/Win32/EditorWindowChromeController.h index 7015a800..9c35610e 100644 --- a/new_editor/app/Platform/Win32/EditorWindowChromeController.h +++ b/new_editor/app/Platform/Win32/EditorWindowChromeController.h @@ -16,7 +16,7 @@ class EditorWindow; } // namespace XCEngine::UI::Editor::App -namespace XCEngine::UI::Editor::App::Internal { +namespace XCEngine::UI::Editor::App { class EditorWindowChromeController final { public: @@ -144,4 +144,4 @@ private: Host::HostRuntimeState m_runtimeState = {}; }; -} // namespace XCEngine::UI::Editor::App::Internal +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Platform/Win32/EditorWindowConstants.h b/new_editor/app/Platform/Win32/EditorWindowConstants.h deleted file mode 100644 index 09df1f30..00000000 --- a/new_editor/app/Platform/Win32/EditorWindowConstants.h +++ /dev/null @@ -1,10 +0,0 @@ -#pragma once - -namespace XCEngine::UI::Editor::App::EditorWindowInternal { - -inline constexpr unsigned int kDefaultDpi = 96u; -inline constexpr float kBaseDpiScale = 96.0f; -inline constexpr float kBorderlessTitleBarHeightDips = 28.0f; -inline constexpr float kBorderlessTitleBarFontSize = 12.0f; - -} // namespace XCEngine::UI::Editor::App::EditorWindowInternal diff --git a/new_editor/app/Platform/Win32/EditorWindowFrame.cpp b/new_editor/app/Platform/Win32/EditorWindowFrame.cpp deleted file mode 100644 index ca9f0e7f..00000000 --- a/new_editor/app/Platform/Win32/EditorWindowFrame.cpp +++ /dev/null @@ -1,229 +0,0 @@ -#include "Platform/Win32/EditorWindow.h" -#include "Platform/Win32/EditorWindowConstants.h" -#include "Platform/Win32/EditorWindowFrameOrchestrator.h" -#include "Platform/Win32/EditorWindowInputController.h" -#include "Platform/Win32/EditorWindowInternalState.h" -#include "Platform/Win32/EditorWindowRuntimeController.h" -#include "Platform/Win32/EditorWindowRuntimeInternal.h" -#include "Platform/Win32/EditorWindowStyle.h" -#include "Composition/EditorShellPointerInteraction.h" -#include "Composition/EditorContext.h" - -#include -#include - -#include -#include - -namespace XCEngine::UI::Editor::App { - -using namespace EditorWindowInternal; -using ::XCEngine::UI::UIDrawData; -using ::XCEngine::UI::UIDrawList; -using ::XCEngine::UI::UIInputEvent; -using ::XCEngine::UI::UIInputModifiers; -using ::XCEngine::UI::UIPointerButton; -using ::XCEngine::UI::UIRect; - -namespace { - -std::uint8_t ButtonMask(UIPointerButton button) { - switch (button) { - case UIPointerButton::Left: return 1u << 0u; - case UIPointerButton::Right: return 1u << 1u; - case UIPointerButton::Middle: return 1u << 2u; - case UIPointerButton::X1: return 1u << 3u; - case UIPointerButton::X2: return 1u << 4u; - case UIPointerButton::None: - default: - return 0u; - } -} - -std::uint8_t ButtonMaskFromModifiers(const UIInputModifiers& modifiers) { - std::uint8_t mask = 0u; - if (modifiers.leftMouse) { - mask |= ButtonMask(UIPointerButton::Left); - } - if (modifiers.rightMouse) { - mask |= ButtonMask(UIPointerButton::Right); - } - if (modifiers.middleMouse) { - mask |= ButtonMask(UIPointerButton::Middle); - } - if (modifiers.x1Mouse) { - mask |= ButtonMask(UIPointerButton::X1); - } - if (modifiers.x2Mouse) { - mask |= ButtonMask(UIPointerButton::X2); - } - return mask; -} - -std::uint8_t ResolveExpectedShellCaptureButtons( - const EditorShellRuntime& shellRuntime) { - std::uint8_t expectedButtons = 0u; - const auto& shellState = shellRuntime.GetShellInteractionState(); - const auto& dockHostState = - shellState.workspaceInteractionState.dockHostInteractionState; - if (dockHostState.splitterDragState.active || - !dockHostState.activeTabDragNodeId.empty()) { - expectedButtons |= ButtonMask(UIPointerButton::Left); - } - - for (const auto& panelState : - shellState.workspaceInteractionState.composeState.panelStates) { - const auto& inputBridgeState = panelState.viewportShellState.inputBridgeState; - if (inputBridgeState.captured) { - expectedButtons |= ButtonMask(inputBridgeState.captureButton); - } - } - - return expectedButtons; -} - -} // namespace - -EditorWindowFrameTransferRequests EditorWindow::RenderFrame( - EditorContext& editorContext, - bool globalTabDragActive) { - if (!m_runtime->IsReady() || m_state->window.hwnd == nullptr) { - return {}; - } - - UINT pixelWidth = 0u; - UINT pixelHeight = 0u; - if (!ResolveRenderClientPixelSize(pixelWidth, pixelHeight)) { - return {}; - } - - const float width = PixelsToDips(static_cast(pixelWidth)); - const float height = PixelsToDips(static_cast(pixelHeight)); - const UIRect workspaceBounds = ResolveWorkspaceBounds(width, height); - - UIDrawData drawData = {}; - UIDrawList& drawList = drawData.EmplaceDrawList("XCEditorShell"); - drawList.AddFilledRect( - UIRect(0.0f, 0.0f, width, height), - kShellSurfaceColor); - - EditorWindowFrameTransferRequests transferRequests = {}; - if (editorContext.IsValid()) { - transferRequests = - RenderRuntimeFrame(editorContext, globalTabDragActive, workspaceBounds, drawList); - } else { - m_frameOrchestrator->AppendInvalidFrame(editorContext, drawList); - } - - AppendBorderlessWindowChrome(drawList, width); - - const Host::D3D12WindowRenderLoopPresentResult presentResult = m_runtime->Present(drawData); - if (!presentResult.warning.empty()) { - LogRuntimeTrace("present", presentResult.warning); - } - - m_runtime->CaptureIfRequested( - drawData, - pixelWidth, - pixelHeight, - presentResult.framePresented); - return transferRequests; -} - -EditorWindowFrameTransferRequests EditorWindow::OnPaintMessage( - EditorContext& editorContext, - bool globalTabDragActive) { - if (!m_runtime->IsReady() || m_state->window.hwnd == nullptr) { - return {}; - } - - PAINTSTRUCT paintStruct = {}; - BeginPaint(m_state->window.hwnd, &paintStruct); - const EditorWindowFrameTransferRequests transferRequests = - RenderFrame(editorContext, globalTabDragActive); - EndPaint(m_state->window.hwnd, &paintStruct); - return transferRequests; -} - -UIRect EditorWindow::ResolveWorkspaceBounds(float clientWidthDips, float clientHeightDips) const { - if (!IsBorderlessWindowEnabled()) { - return UIRect(0.0f, 0.0f, clientWidthDips, clientHeightDips); - } - - if (ShouldUseDetachedTitleBarTabStrip()) { - return UIRect(0.0f, 0.0f, clientWidthDips, clientHeightDips); - } - - const float titleBarHeight = (std::min)(kBorderlessTitleBarHeightDips, clientHeightDips); - return UIRect( - 0.0f, - titleBarHeight, - clientWidthDips, - (std::max)(0.0f, clientHeightDips - titleBarHeight)); -} - -EditorWindowFrameTransferRequests EditorWindow::RenderRuntimeFrame( - EditorContext& editorContext, - bool globalTabDragActive, - const UIRect& workspaceBounds, - UIDrawList& drawList) { - SyncShellCapturedPointerButtonsFromSystemState(); - std::vector frameEvents = m_inputController->TakePendingEvents(); - const bool useDetachedTitleBarTabStrip = ShouldUseDetachedTitleBarTabStrip(); - const EditorWindowFrameTransferRequests transferRequests = - m_frameOrchestrator->UpdateAndAppend( - editorContext, - *m_runtime, - workspaceBounds, - frameEvents, - m_runtime->BuildCaptureStatusText(), - m_state->window.primary, - globalTabDragActive, - useDetachedTitleBarTabStrip, - drawList); - - ApplyShellRuntimePointerCapture(); - ApplyCurrentCursor(); - return transferRequests; -} - -void EditorWindow::SyncShellCapturedPointerButtonsFromSystemState() { - m_inputController->SyncInputModifiersFromSystemState(); - - const std::uint8_t expectedButtons = ResolveExpectedShellCaptureButtons( - m_runtime->GetShellRuntime()); - if (expectedButtons == 0u || - m_inputController->HasPendingPointerStateReconciliationEvent()) { - return; - } - - const UIInputModifiers modifiers = m_inputController->GetCurrentModifiers(); - if ((ButtonMaskFromModifiers(modifiers) & expectedButtons) == expectedButtons) { - return; - } - - QueueSyntheticPointerStateSyncEvent(modifiers); -} - -void EditorWindow::ApplyShellRuntimePointerCapture() { - const EditorShellPointerOwner owner = m_runtime->GetShellRuntime().GetPointerOwner(); - if (IsShellPointerOwner(owner)) { - AcquirePointerCapture(EditorWindowPointerCaptureOwner::Shell); - return; - } - - if (IsHostedContentPointerOwner(owner)) { - AcquirePointerCapture(EditorWindowPointerCaptureOwner::HostedContent); - return; - } - - if (OwnsPointerCapture(EditorWindowPointerCaptureOwner::Shell)) { - ReleasePointerCapture(EditorWindowPointerCaptureOwner::Shell); - } - - if (OwnsPointerCapture(EditorWindowPointerCaptureOwner::HostedContent)) { - ReleasePointerCapture(EditorWindowPointerCaptureOwner::HostedContent); - } -} - -} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Platform/Win32/EditorWindowFrameOrchestrator.cpp b/new_editor/app/Platform/Win32/EditorWindowFrameOrchestrator.cpp index b6996b28..a5e27421 100644 --- a/new_editor/app/Platform/Win32/EditorWindowFrameOrchestrator.cpp +++ b/new_editor/app/Platform/Win32/EditorWindowFrameOrchestrator.cpp @@ -1,19 +1,18 @@ -#include "Platform/Win32/EditorWindowFrameOrchestrator.h" +#include "Platform/Win32/EditorWindowFrameOrchestrator.h" -#include "Composition/EditorShellVariant.h" -#include "Platform/Win32/EditorWindowConstants.h" -#include "Platform/Win32/EditorWindowRuntimeController.h" -#include "Platform/Win32/EditorWindowRuntimeInternal.h" -#include "Platform/Win32/EditorWindowStyle.h" #include "Composition/EditorContext.h" +#include "Composition/EditorPanelIds.h" +#include "Composition/EditorShellVariant.h" +#include "Platform/Win32/EditorWindowRuntimeController.h" +#include "Platform/Win32/EditorWindowSupport.h" #include #include -namespace XCEngine::UI::Editor::App::Internal { +namespace XCEngine::UI::Editor::App { -using namespace EditorWindowInternal; +using namespace EditorWindowSupport; using ::XCEngine::UI::UIDrawList; using ::XCEngine::UI::UIInputEvent; using ::XCEngine::UI::UIInputEventType; @@ -43,6 +42,22 @@ std::string DescribeInputEventType(const UIInputEvent& event) { } } +::XCEngine::UI::UISize ResolveDetachedPanelPreferredSize( + const EditorContext& editorContext, + std::string_view panelId, + float fallbackWidth, + float fallbackHeight) { + const UIEditorPanelDescriptor* descriptor = + FindUIEditorPanelDescriptor(editorContext.GetShellAsset().panelRegistry, panelId); + if (descriptor == nullptr || + descriptor->preferredDetachedWindowSize.width <= 0.0f || + descriptor->preferredDetachedWindowSize.height <= 0.0f) { + return ::XCEngine::UI::UISize(fallbackWidth, fallbackHeight); + } + + return descriptor->preferredDetachedWindowSize; +} + } // namespace EditorWindowFrameTransferRequests EditorWindowFrameOrchestrator::UpdateAndAppend( @@ -74,14 +89,32 @@ EditorWindowFrameTransferRequests EditorWindowFrameOrchestrator::UpdateAndAppend captureStatusText, primary ? EditorShellVariant::Primary : EditorShellVariant::DetachedWindow, useDetachedTitleBarTabStrip, - useDetachedTitleBarTabStrip ? kBorderlessTitleBarHeightDips : 0.0f); + useDetachedTitleBarTabStrip ? kBorderlessTitleBarHeightDips : 0.0f, + primary ? 0.0f : kBorderlessTitleBarHeightDips); const UIEditorShellInteractionFrame& shellFrame = shellRuntime.GetShellFrame(); const UIEditorDockHostInteractionState& dockHostInteractionState = shellRuntime.GetShellInteractionState().workspaceInteractionState.dockHostInteractionState; LogFrameInteractionTrace(workspaceController, frameEvents, shellFrame); - const EditorWindowFrameTransferRequests transferRequests = + EditorWindowFrameTransferRequests transferRequests = BuildShellTransferRequests(globalTabDragActive, dockHostInteractionState, shellFrame); + POINT screenPoint = {}; + if (ConsumeEditorColorPickerOpenDetachedPanelRequest( + editorContext.GetColorPickerToolState()) && + GetCursorPos(&screenPoint) != FALSE) { + const ::XCEngine::UI::UISize preferredSize = + ResolveDetachedPanelPreferredSize( + editorContext, + kColorPickerPanelId, + 360.0f, + 520.0f); + transferRequests.openDetachedPanel = EditorWindowOpenDetachedPanelRequest{ + std::string(kColorPickerPanelId), + screenPoint, + static_cast(preferredSize.width), + static_cast(preferredSize.height), + }; + } for (const WorkspaceTraceEntry& entry : shellRuntime.GetTraceEntries()) { LogRuntimeTrace(entry.channel, entry.message); @@ -205,4 +238,5 @@ EditorWindowFrameTransferRequests EditorWindowFrameOrchestrator::BuildShellTrans return transferRequests; } -} // namespace XCEngine::UI::Editor::App::Internal +} // namespace XCEngine::UI::Editor::App + diff --git a/new_editor/app/Platform/Win32/EditorWindowFrameOrchestrator.h b/new_editor/app/Platform/Win32/EditorWindowFrameOrchestrator.h index a6740f41..74d559f4 100644 --- a/new_editor/app/Platform/Win32/EditorWindowFrameOrchestrator.h +++ b/new_editor/app/Platform/Win32/EditorWindowFrameOrchestrator.h @@ -29,9 +29,6 @@ struct UIEditorShellInteractionFrame; namespace XCEngine::UI::Editor::App { class EditorContext; - -namespace Internal { - class EditorWindowRuntimeController; class EditorWindowFrameOrchestrator final { @@ -75,5 +72,4 @@ private: const UIEditorShellInteractionFrame& shellFrame) const; }; -} // namespace Internal } // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Platform/Win32/EditorWindowInput.cpp b/new_editor/app/Platform/Win32/EditorWindowInput.cpp deleted file mode 100644 index 2bf7dbd7..00000000 --- a/new_editor/app/Platform/Win32/EditorWindowInput.cpp +++ /dev/null @@ -1,238 +0,0 @@ -#include "Platform/Win32/EditorWindow.h" -#include "Platform/Win32/EditorWindowChromeController.h" -#include "Platform/Win32/EditorWindowInputController.h" -#include "Platform/Win32/EditorWindowInternalState.h" -#include "Platform/Win32/EditorWindowRuntimeController.h" - -#include -#include - -#include - -namespace XCEngine::UI::Editor::App { - -using ::XCEngine::UI::UIInputEvent; -using ::XCEngine::UI::UIInputEventType; -using ::XCEngine::UI::UIPointerButton; - -namespace { - -bool IsScreenPointOverWindow(HWND hwnd, const POINT& screenPoint) { - if (hwnd == nullptr || !IsWindow(hwnd)) { - return false; - } - - const HWND hitWindow = WindowFromPoint(screenPoint); - if (hitWindow == nullptr || GetAncestor(hitWindow, GA_ROOT) != hwnd) { - return false; - } - - RECT windowRect = {}; - if (!GetWindowRect(hwnd, &windowRect)) { - return false; - } - - return screenPoint.x >= windowRect.left && screenPoint.x < windowRect.right && - screenPoint.y >= windowRect.top && screenPoint.y < windowRect.bottom; -} -} // namespace - -bool EditorWindow::ApplyCurrentCursor() const { - if (!HasInteractiveCaptureState() && !IsPointerInsideClientArea()) { - return false; - } - - const HCURSOR cursor = LoadCursorW(nullptr, ResolveCurrentCursorResource()); - if (cursor == nullptr) { - return false; - } - - SetCursor(cursor); - return true; -} - - -bool EditorWindow::HasInteractiveCaptureState() const { - return m_runtime->GetShellRuntime().HasInteractiveCapture() || - m_chromeController->IsBorderlessWindowDragRestoreArmed() || - m_chromeController->IsBorderlessResizeActive() || - m_inputController->HasPointerCaptureOwner(); -} - -EditorWindowPointerCaptureOwner EditorWindow::GetPointerCaptureOwner() const { - return m_inputController->GetPointerCaptureOwner(); -} - -bool EditorWindow::OwnsPointerCapture(EditorWindowPointerCaptureOwner owner) const { - return m_inputController->OwnsPointerCapture(owner); -} - -void EditorWindow::AcquirePointerCapture(EditorWindowPointerCaptureOwner owner) { - m_inputController->AcquirePointerCapture(m_state->window.hwnd, owner); -} - -void EditorWindow::ReleasePointerCapture(EditorWindowPointerCaptureOwner owner) { - m_inputController->ReleasePointerCapture(m_state->window.hwnd, owner); -} - -void EditorWindow::ForceReleasePointerCapture() { - m_inputController->ForceReleasePointerCapture(m_state->window.hwnd); -} - -void EditorWindow::ClearPointerCaptureOwner() { - m_inputController->ClearPointerCaptureOwner(); -} - -void EditorWindow::TryStartImmediateShellPointerCapture(LPARAM lParam) { - if (m_state->window.hwnd == nullptr || - !IsWindow(m_state->window.hwnd) || - GetCapture() == m_state->window.hwnd) { - return; - } - - const ::XCEngine::UI::UIPoint clientPoint = ConvertClientPixelsToDips( - GET_X_LPARAM(lParam), - GET_Y_LPARAM(lParam)); - if (!ShouldStartImmediateUIEditorShellPointerCapture( - m_runtime->GetShellFrame(), - clientPoint)) { - return; - } - - AcquirePointerCapture(EditorWindowPointerCaptureOwner::Shell); -} - -void EditorWindow::QueuePointerEvent( - UIInputEventType type, - UIPointerButton button, - WPARAM wParam, - LPARAM lParam) { - UIInputEvent event = {}; - m_inputController->QueuePointerEvent( - type, - button, - ConvertClientPixelsToDips(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)), - wParam); -} - -void EditorWindow::QueueSyntheticPointerStateSyncEvent( - const ::XCEngine::UI::UIInputModifiers& modifiers) { - if (m_state->window.hwnd == nullptr || !IsWindow(m_state->window.hwnd)) { - return; - } - - POINT screenPoint = {}; - if (!GetCursorPos(&screenPoint)) { - return; - } - if (!ScreenToClient(m_state->window.hwnd, &screenPoint)) { - return; - } - - m_inputController->QueueSyntheticPointerStateSyncEvent( - ConvertClientPixelsToDips(screenPoint.x, screenPoint.y), - modifiers); -} - -void EditorWindow::QueuePointerLeaveEvent() { - ::XCEngine::UI::UIPoint position = {}; - if (m_state->window.hwnd != nullptr) { - POINT clientPoint = {}; - GetCursorPos(&clientPoint); - ScreenToClient(m_state->window.hwnd, &clientPoint); - position = ConvertClientPixelsToDips(clientPoint.x, clientPoint.y); - } - m_inputController->QueuePointerLeaveEvent(position); -} - -void EditorWindow::QueuePointerWheelEvent(short wheelDelta, WPARAM wParam, LPARAM lParam) { - if (m_state->window.hwnd == nullptr) { - return; - } - - POINT screenPoint = { - GET_X_LPARAM(lParam), - GET_Y_LPARAM(lParam) - }; - ScreenToClient(m_state->window.hwnd, &screenPoint); - - m_inputController->QueuePointerWheelEvent( - ConvertClientPixelsToDips(screenPoint.x, screenPoint.y), - wheelDelta, - wParam); -} - -void EditorWindow::QueueKeyEvent(UIInputEventType type, WPARAM wParam, LPARAM lParam) { - m_inputController->QueueKeyEvent(type, wParam, lParam); -} - -void EditorWindow::QueueCharacterEvent(WPARAM wParam, LPARAM) { - m_inputController->QueueCharacterEvent(wParam); -} - -void EditorWindow::QueueWindowFocusEvent(UIInputEventType type) { - m_inputController->QueueWindowFocusEvent(type); -} - -void EditorWindow::SyncInputModifiersFromSystemState() { - m_inputController->SyncInputModifiersFromSystemState(); -} - -void EditorWindow::ResetInputModifiers() { - m_inputController->ResetInputModifiers(); -} - -void EditorWindow::RequestManualScreenshot() { - m_runtime->RequestManualScreenshot("manual_f12"); -} - -bool EditorWindow::IsPointerInsideClientArea() const { - if (m_state->window.hwnd == nullptr || !IsWindow(m_state->window.hwnd)) { - return false; - } - - POINT screenPoint = {}; - if (!GetCursorPos(&screenPoint)) { - return false; - } - - if (!IsScreenPointOverWindow(m_state->window.hwnd, screenPoint)) { - return false; - } - - const LPARAM pointParam = MAKELPARAM( - static_cast(screenPoint.x), - static_cast(screenPoint.y)); - return SendMessageW(m_state->window.hwnd, WM_NCHITTEST, 0, pointParam) == HTCLIENT; -} - -LPCWSTR EditorWindow::ResolveCurrentCursorResource() const { - const Host::BorderlessWindowResizeEdge borderlessResizeEdge = - m_chromeController->IsBorderlessResizeActive() - ? m_chromeController->GetBorderlessResizeEdge() - : m_chromeController->GetHoveredBorderlessResizeEdge(); - if (borderlessResizeEdge != Host::BorderlessWindowResizeEdge::None) { - return Host::ResolveBorderlessWindowResizeCursor(borderlessResizeEdge); - } - - switch (m_runtime->GetShellRuntime().GetHostedContentCursorKind()) { - case ProjectPanel::CursorKind::ResizeEW: - return IDC_SIZEWE; - case ProjectPanel::CursorKind::Arrow: - default: - break; - } - - switch (m_runtime->GetShellRuntime().GetDockCursorKind()) { - case Widgets::UIEditorDockHostCursorKind::ResizeEW: - return IDC_SIZEWE; - case Widgets::UIEditorDockHostCursorKind::ResizeNS: - return IDC_SIZENS; - case Widgets::UIEditorDockHostCursorKind::Arrow: - default: - return IDC_ARROW; - } -} - -} // namespace XCEngine::UI::Editor::App - diff --git a/new_editor/app/Platform/Win32/EditorWindowInputController.cpp b/new_editor/app/Platform/Win32/EditorWindowInputController.cpp index 7fd024b5..55a65a7f 100644 --- a/new_editor/app/Platform/Win32/EditorWindowInputController.cpp +++ b/new_editor/app/Platform/Win32/EditorWindowInputController.cpp @@ -1,14 +1,41 @@ -#include "Platform/Win32/EditorWindowInputController.h" +#include "Platform/Win32/EditorWindowInputController.h" #include -namespace XCEngine::UI::Editor::App::Internal { +#include + +namespace XCEngine::UI::Editor::App { using ::XCEngine::UI::UIInputEvent; using ::XCEngine::UI::UIInputEventType; using ::XCEngine::UI::UIInputModifiers; using ::XCEngine::UI::UIPointerButton; +namespace { + +std::uint8_t ButtonMask(UIPointerButton button) { + switch (button) { + case UIPointerButton::Left: return 1u << 0u; + case UIPointerButton::Right: return 1u << 1u; + case UIPointerButton::Middle: return 1u << 2u; + case UIPointerButton::X1: return 1u << 3u; + case UIPointerButton::X2: return 1u << 4u; + case UIPointerButton::None: + default: + return 0u; + } +} + +std::uint64_t GetInputEventTimestampNanoseconds() { + using Clock = std::chrono::steady_clock; + return static_cast( + std::chrono::duration_cast( + Clock::now().time_since_epoch()) + .count()); +} + +} // namespace + bool EditorWindowInputController::IsTrackingMouseLeave() const { return m_trackingMouseLeave; } @@ -72,15 +99,26 @@ void EditorWindowInputController::QueuePointerEvent( UIInputEventType type, UIPointerButton button, const ::XCEngine::UI::UIPoint& position, - WPARAM wParam) { + WPARAM wParam, + bool doubleClick) { UIInputEvent event = {}; event.type = type; event.pointerButton = button; event.position = position; + event.timestampNanoseconds = GetInputEventTimestampNanoseconds(); event.modifiers = m_modifierTracker.ApplyPointerMessage( type, button, static_cast(wParam)); + const std::uint8_t buttonMask = ButtonMask(button); + if (doubleClick && type == UIInputEventType::PointerButtonDown) { + m_pendingDoubleClickButtons |= buttonMask; + } + if (type == UIInputEventType::PointerButtonUp && + (m_pendingDoubleClickButtons & buttonMask) != 0u) { + event.doubleClick = true; + m_pendingDoubleClickButtons &= static_cast(~buttonMask); + } m_pendingEvents.push_back(event); } @@ -90,6 +128,7 @@ void EditorWindowInputController::QueueSyntheticPointerStateSyncEvent( UIInputEvent event = {}; event.type = UIInputEventType::PointerMove; event.position = position; + event.timestampNanoseconds = GetInputEventTimestampNanoseconds(); event.modifiers = modifiers; m_pendingEvents.push_back(event); } @@ -99,6 +138,7 @@ void EditorWindowInputController::QueuePointerLeaveEvent( UIInputEvent event = {}; event.type = UIInputEventType::PointerLeave; event.position = position; + event.timestampNanoseconds = GetInputEventTimestampNanoseconds(); event.modifiers = m_modifierTracker.GetCurrentModifiers(); m_pendingEvents.push_back(event); } @@ -111,6 +151,7 @@ void EditorWindowInputController::QueuePointerWheelEvent( event.type = UIInputEventType::PointerWheel; event.position = position; event.wheelDelta = static_cast(wheelDelta); + event.timestampNanoseconds = GetInputEventTimestampNanoseconds(); event.modifiers = m_modifierTracker.ApplyPointerMessage( UIInputEventType::PointerWheel, UIPointerButton::None, @@ -122,6 +163,7 @@ void EditorWindowInputController::QueueKeyEvent(UIInputEventType type, WPARAM wP UIInputEvent event = {}; event.type = type; event.keyCode = MapVirtualKeyToUIKeyCode(wParam); + event.timestampNanoseconds = GetInputEventTimestampNanoseconds(); event.modifiers = m_modifierTracker.ApplyKeyMessage(type, wParam, lParam); event.repeat = IsRepeatKeyMessage(lParam); m_pendingEvents.push_back(event); @@ -131,6 +173,7 @@ void EditorWindowInputController::QueueCharacterEvent(WPARAM wParam) { UIInputEvent event = {}; event.type = UIInputEventType::Character; event.character = static_cast(wParam); + event.timestampNanoseconds = GetInputEventTimestampNanoseconds(); event.modifiers = m_modifierTracker.GetCurrentModifiers(); m_pendingEvents.push_back(event); } @@ -138,6 +181,7 @@ void EditorWindowInputController::QueueCharacterEvent(WPARAM wParam) { void EditorWindowInputController::QueueWindowFocusEvent(UIInputEventType type) { UIInputEvent event = {}; event.type = type; + event.timestampNanoseconds = GetInputEventTimestampNanoseconds(); m_pendingEvents.push_back(event); } @@ -185,6 +229,7 @@ std::vector EditorWindowInputController::TakePendingEvents() { void EditorWindowInputController::ClearPendingEvents() { m_pendingEvents.clear(); + m_pendingDoubleClickButtons = 0u; } void EditorWindowInputController::ResetInteractionState() { @@ -197,6 +242,7 @@ void EditorWindowInputController::ResetInteractionState() { void EditorWindowInputController::ResetWindowState() { m_trackingMouseLeave = false; m_pointerCaptureOwner = EditorWindowPointerCaptureOwner::None; + m_pendingDoubleClickButtons = 0u; } std::int32_t EditorWindowInputController::MapVirtualKeyToUIKeyCode(WPARAM wParam) { @@ -276,4 +322,4 @@ bool EditorWindowInputController::IsRepeatKeyMessage(LPARAM lParam) { return (static_cast(lParam) & (1ul << 30)) != 0ul; } -} // namespace XCEngine::UI::Editor::App::Internal +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Platform/Win32/EditorWindowInputController.h b/new_editor/app/Platform/Win32/EditorWindowInputController.h index c6a82485..6ac870d0 100644 --- a/new_editor/app/Platform/Win32/EditorWindowInputController.h +++ b/new_editor/app/Platform/Win32/EditorWindowInputController.h @@ -14,7 +14,7 @@ #include -namespace XCEngine::UI::Editor::App::Internal { +namespace XCEngine::UI::Editor::App { class EditorWindowInputController final { public: @@ -41,7 +41,8 @@ public: ::XCEngine::UI::UIInputEventType type, ::XCEngine::UI::UIPointerButton button, const ::XCEngine::UI::UIPoint& position, - WPARAM wParam); + WPARAM wParam, + bool doubleClick = false); void QueueSyntheticPointerStateSyncEvent( const ::XCEngine::UI::UIPoint& position, const ::XCEngine::UI::UIInputModifiers& modifiers); @@ -70,9 +71,10 @@ private: Host::InputModifierTracker m_modifierTracker = {}; std::vector<::XCEngine::UI::UIInputEvent> m_pendingEvents = {}; + std::uint8_t m_pendingDoubleClickButtons = 0u; bool m_trackingMouseLeave = false; EditorWindowPointerCaptureOwner m_pointerCaptureOwner = EditorWindowPointerCaptureOwner::None; }; -} // namespace XCEngine::UI::Editor::App::Internal +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Platform/Win32/EditorWindowLifecycle.cpp b/new_editor/app/Platform/Win32/EditorWindowLifecycle.cpp deleted file mode 100644 index 167c12bf..00000000 --- a/new_editor/app/Platform/Win32/EditorWindowLifecycle.cpp +++ /dev/null @@ -1,395 +0,0 @@ -#include "Platform/Win32/EditorWindow.h" - -#include "Bootstrap/EditorResources.h" -#include "Platform/Win32/EditorWindowChromeController.h" -#include "Platform/Win32/EditorWindowConstants.h" -#include "Platform/Win32/EditorWindowFrameOrchestrator.h" -#include "Platform/Win32/EditorWindowInputController.h" -#include "Platform/Win32/EditorWindowInternalState.h" -#include "Platform/Win32/EditorWindowPlatformInternal.h" -#include "Platform/Win32/EditorWindowRuntimeController.h" -#include "Platform/Win32/EditorWindowRuntimeInternal.h" -#include "Composition/EditorContext.h" - -#include -#include - -#include -#include - -namespace XCEngine::UI::Editor::App::EditorWindowInternal { - -UINT QuerySystemDpi() { - HDC screenDc = GetDC(nullptr); - if (screenDc == nullptr) { - return kDefaultDpi; - } - - const int dpiX = GetDeviceCaps(screenDc, LOGPIXELSX); - ReleaseDC(nullptr, screenDc); - return dpiX > 0 ? static_cast(dpiX) : kDefaultDpi; -} - -UINT QueryWindowDpi(HWND hwnd) { - if (hwnd != nullptr) { - const HMODULE user32 = GetModuleHandleW(L"user32.dll"); - if (user32 != nullptr) { - using GetDpiForWindowFn = UINT(WINAPI*)(HWND); - const auto getDpiForWindow = - reinterpret_cast(GetProcAddress(user32, "GetDpiForWindow")); - if (getDpiForWindow != nullptr) { - const UINT dpi = getDpiForWindow(hwnd); - if (dpi != 0u) { - return dpi; - } - } - } - } - - return QuerySystemDpi(); -} - -bool ResolveVerboseRuntimeTraceEnabled() { - wchar_t buffer[8] = {}; - const DWORD length = GetEnvironmentVariableW( - L"XCUIEDITOR_VERBOSE_TRACE", - buffer, - static_cast(std::size(buffer))); - return length > 0u && buffer[0] != L'0'; -} - -void LogRuntimeTrace(std::string_view channel, std::string_view message) { - AppendUIEditorRuntimeTrace(channel, message); -} - -bool IsAutoCaptureOnStartupEnabled() { - return App::Internal::IsEnvironmentFlagEnabled("XCUI_AUTO_CAPTURE_ON_STARTUP"); -} - -} // namespace XCEngine::UI::Editor::App::EditorWindowInternal - -namespace XCEngine::UI::Editor::App { - -using namespace EditorWindowInternal; -using ::XCEngine::UI::UIPoint; - -EditorWindow::EditorWindow( - std::string windowId, - std::wstring title, - bool primary, - UIEditorWorkspaceController workspaceController) - : m_state(std::make_unique()) - , m_chromeController(std::make_unique()) - , m_frameOrchestrator(std::make_unique()) - , m_inputController(std::make_unique()) - , m_runtime(std::make_unique( - std::move(workspaceController))) { - m_state->window.windowId = std::move(windowId); - m_state->window.title = std::move(title); - m_state->window.primary = primary; - UpdateCachedTitleText(); -} - -EditorWindow::~EditorWindow() = default; - -std::string_view EditorWindow::GetWindowId() const { - return m_state->window.windowId; -} - -HWND EditorWindow::GetHwnd() const { - return m_state->window.hwnd; -} - -bool EditorWindow::HasHwnd() const { - return m_state->window.hwnd != nullptr; -} - -bool EditorWindow::IsPrimary() const { - return m_state->window.primary; -} - -bool EditorWindow::IsClosing() const { - return m_state->window.closing; -} - -bool EditorWindow::IsRenderReady() const { - return m_runtime->IsReady(); -} - -bool EditorWindow::IsTrackingMouseLeave() const { - return m_inputController->IsTrackingMouseLeave(); -} - -bool EditorWindow::HasHoveredBorderlessResizeEdge() const { - return m_chromeController->GetHoveredBorderlessResizeEdge() != - Host::BorderlessWindowResizeEdge::None; -} - -const std::wstring& EditorWindow::GetTitle() const { - return m_state->window.title; -} - -const UIEditorWorkspaceController& EditorWindow::GetWorkspaceController() const { - return m_runtime->GetWorkspaceController(); -} - -UIEditorWorkspaceController& EditorWindow::GetMutableWorkspaceController() { - return m_runtime->GetMutableWorkspaceController(); -} - -const EditorShellRuntime& EditorWindow::GetShellRuntime() const { - return m_runtime->GetShellRuntime(); -} - -EditorShellRuntime& EditorWindow::GetShellRuntime() { - return m_runtime->GetShellRuntime(); -} - -const UIEditorShellInteractionFrame& EditorWindow::GetShellFrame() const { - return m_runtime->GetShellFrame(); -} - -const UIEditorShellInteractionState& EditorWindow::GetShellInteractionState() const { - return m_runtime->GetShellInteractionState(); -} - -void EditorWindow::SetExternalDockHostDropPreview( - const Widgets::UIEditorDockHostDropPreviewState& preview) { - m_runtime->SetExternalDockHostDropPreview(preview); -} - -void EditorWindow::ClearExternalDockHostDropPreview() { - m_runtime->ClearExternalDockHostDropPreview(); -} - -void EditorWindow::AttachHwnd(HWND hwnd) { - m_state->window.hwnd = hwnd; - m_state->window.closing = false; -} - -void EditorWindow::MarkDestroyed() { - m_state->window.hwnd = nullptr; - m_state->window.closing = false; - m_inputController->ResetWindowState(); -} - -void EditorWindow::MarkClosing() { - m_state->window.closing = true; -} - -void EditorWindow::ClearClosing() { - m_state->window.closing = false; -} - -void EditorWindow::SetTrackingMouseLeave(bool trackingMouseLeave) { - m_inputController->SetTrackingMouseLeave(trackingMouseLeave); -} - -void EditorWindow::SetTitle(std::wstring title) { - m_state->window.title = std::move(title); - UpdateCachedTitleText(); -} - -void EditorWindow::ReplaceWorkspaceController(UIEditorWorkspaceController workspaceController) { - m_runtime->ReplaceWorkspaceController(std::move(workspaceController)); -} - -void EditorWindow::InvalidateHostWindow() const { - if (m_state->window.hwnd != nullptr && IsWindow(m_state->window.hwnd)) { - InvalidateRect(m_state->window.hwnd, nullptr, FALSE); - } -} - -bool EditorWindow::Initialize( - const std::filesystem::path& repoRoot, - EditorContext& editorContext, - const std::filesystem::path& captureRoot, - bool autoCaptureOnStartup) { - if (m_state->window.hwnd == nullptr) { - LogRuntimeTrace("app", "window initialize skipped: hwnd is null"); - return false; - } - - Host::RefreshBorderlessWindowDwmDecorations(m_state->window.hwnd); - m_chromeController->Reset(); - m_chromeController->SetWindowDpi(QueryWindowDpi(m_state->window.hwnd)); - m_runtime->SetDpiScale(GetDpiScale()); - - std::ostringstream dpiTrace = {}; - dpiTrace << "initial dpi=" << m_chromeController->GetWindowDpi() - << " scale=" << GetDpiScale(); - LogRuntimeTrace("window", dpiTrace.str()); - - return m_runtime->Initialize( - m_state->window.hwnd, - repoRoot, - editorContext, - captureRoot, - autoCaptureOnStartup); -} - -void EditorWindow::Shutdown() { - ForceReleasePointerCapture(); - - m_runtime->Shutdown(); - m_inputController->ClearPendingEvents(); - m_chromeController->Reset(); -} - -void EditorWindow::ResetInteractionState() { - ForceReleasePointerCapture(); - - m_inputController->ResetInteractionState(); - m_runtime->ResetInteractionState(); - m_chromeController->ResetChromeState(); - m_chromeController->EndBorderlessResize(); - m_chromeController->EndBorderlessWindowDragRestore(); - m_chromeController->EndInteractiveResize(); - m_chromeController->SetHoveredBorderlessResizeEdge( - Host::BorderlessWindowResizeEdge::None); - m_chromeController->ClearPredictedClientPixelSize(); -} - -bool EditorWindow::ApplyWindowResize(UINT width, UINT height) { - if (!m_runtime->IsReady() || width == 0u || height == 0u) { - return false; - } - return m_runtime->ApplyResize(width, height); -} - -bool EditorWindow::QueryCurrentClientPixelSize(UINT& outWidth, UINT& outHeight) const { - outWidth = 0u; - outHeight = 0u; - if (m_state->window.hwnd == nullptr || !IsWindow(m_state->window.hwnd)) { - return false; - } - - RECT clientRect = {}; - if (!GetClientRect(m_state->window.hwnd, &clientRect)) { - return false; - } - - const LONG width = clientRect.right - clientRect.left; - const LONG height = clientRect.bottom - clientRect.top; - if (width <= 0 || height <= 0) { - return false; - } - - outWidth = static_cast(width); - outHeight = static_cast(height); - return true; -} - -bool EditorWindow::ResolveRenderClientPixelSize(UINT& outWidth, UINT& outHeight) const { - if (m_chromeController->TryGetPredictedClientPixelSize(outWidth, outHeight)) { - return true; - } - - return QueryCurrentClientPixelSize(outWidth, outHeight); -} - -float EditorWindow::GetDpiScale() const { - return m_chromeController->GetDpiScale(kBaseDpiScale); -} - -float EditorWindow::PixelsToDips(float pixels) const { - const float dpiScale = GetDpiScale(); - return dpiScale > 0.0f ? pixels / dpiScale : pixels; -} - -UIPoint EditorWindow::ConvertClientPixelsToDips(LONG x, LONG y) const { - return UIPoint( - PixelsToDips(static_cast(x)), - PixelsToDips(static_cast(y))); -} - -UIPoint EditorWindow::ConvertScreenPixelsToClientDips(const POINT& screenPoint) const { - POINT clientPoint = screenPoint; - if (m_state->window.hwnd != nullptr) { - ScreenToClient(m_state->window.hwnd, &clientPoint); - } - - const float dpiScale = m_chromeController->GetDpiScale(kBaseDpiScale); - return UIPoint( - dpiScale > 0.0f - ? static_cast(clientPoint.x) / dpiScale - : static_cast(clientPoint.x), - dpiScale > 0.0f - ? static_cast(clientPoint.y) / dpiScale - : static_cast(clientPoint.y)); -} - -void EditorWindow::OnResize(UINT width, UINT height) { - bool matchesPredictedClientSize = false; - UINT predictedWidth = 0u; - UINT predictedHeight = 0u; - if (m_chromeController->TryGetPredictedClientPixelSize( - predictedWidth, - predictedHeight)) { - matchesPredictedClientSize = - predictedWidth == width && - predictedHeight == height; - } - - m_chromeController->ClearPredictedClientPixelSize(); - if (IsBorderlessWindowEnabled() && m_state->window.hwnd != nullptr) { - Host::RefreshBorderlessWindowDwmDecorations(m_state->window.hwnd); - } - - if (!matchesPredictedClientSize) { - ApplyWindowResize(width, height); - } -} - -void EditorWindow::OnEnterSizeMove() { - m_chromeController->BeginInteractiveResize(); -} - -void EditorWindow::OnExitSizeMove() { - m_chromeController->EndInteractiveResize(); - m_chromeController->ClearPredictedClientPixelSize(); - UINT width = 0u; - UINT height = 0u; - if (QueryCurrentClientPixelSize(width, height)) { - ApplyWindowResize(width, height); - } -} - -void EditorWindow::OnDpiChanged(UINT dpi, const RECT& suggestedRect) { - m_chromeController->SetWindowDpi(dpi == 0u ? kDefaultDpi : dpi); - m_runtime->SetDpiScale(GetDpiScale()); - if (m_state->window.hwnd != nullptr) { - const LONG windowWidth = suggestedRect.right - suggestedRect.left; - const LONG windowHeight = suggestedRect.bottom - suggestedRect.top; - SetWindowPos( - m_state->window.hwnd, - nullptr, - suggestedRect.left, - suggestedRect.top, - windowWidth, - windowHeight, - SWP_NOZORDER | SWP_NOACTIVATE); - UINT clientWidth = 0u; - UINT clientHeight = 0u; - if (QueryCurrentClientPixelSize(clientWidth, clientHeight)) { - ApplyWindowResize(clientWidth, clientHeight); - } - Host::RefreshBorderlessWindowDwmDecorations(m_state->window.hwnd); - } - - std::ostringstream trace = {}; - trace << "dpi changed to " << m_chromeController->GetWindowDpi() - << " scale=" << GetDpiScale(); - LogRuntimeTrace("window", trace.str()); -} - -bool EditorWindow::IsVerboseRuntimeTraceEnabled() { - static const bool s_enabled = ResolveVerboseRuntimeTraceEnabled(); - return s_enabled; -} - -void EditorWindow::UpdateCachedTitleText() { - m_state->window.titleText = WideToUtf8(m_state->window.title); -} - -} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Platform/Win32/EditorWindowManager.h b/new_editor/app/Platform/Win32/EditorWindowManager.h index 4c85d758..3e0d6b7e 100644 --- a/new_editor/app/Platform/Win32/EditorWindowManager.h +++ b/new_editor/app/Platform/Win32/EditorWindowManager.h @@ -26,6 +26,8 @@ namespace XCEngine::UI::Editor::App { class EditorContext; class EditorWindow; +class EditorWindowHostRuntime; +class EditorWindowWorkspaceCoordinator; struct EditorWindowPanelTransferRequest; struct EditorWindowFrameTransferRequests; @@ -37,11 +39,6 @@ struct EditorWindowHostConfig { void* windowUserData = nullptr; }; -namespace Internal { -class EditorWindowHostRuntime; -class EditorWindowWorkspaceCoordinator; -} - class EditorWindowManager final { public: struct CreateParams { @@ -91,8 +88,8 @@ public: void RenderAllWindows(); private: - std::unique_ptr m_hostRuntime = {}; - std::unique_ptr m_workspaceCoordinator = {}; + std::unique_ptr m_hostRuntime = {}; + std::unique_ptr m_workspaceCoordinator = {}; }; } // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Platform/Win32/EditorWindowPlatformInternal.h b/new_editor/app/Platform/Win32/EditorWindowPlatformInternal.h deleted file mode 100644 index ac11f8e2..00000000 --- a/new_editor/app/Platform/Win32/EditorWindowPlatformInternal.h +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once - -#include "Platform/Win32/EditorWindowConstants.h" -#include "Internal/EmbeddedPngLoader.h" - -#include - -namespace XCEngine::UI::Editor::App::EditorWindowInternal { - -using App::Internal::LoadEmbeddedPngTexture; - -UINT QuerySystemDpi(); -UINT QueryWindowDpi(HWND hwnd); - -} // namespace XCEngine::UI::Editor::App::EditorWindowInternal diff --git a/new_editor/app/Platform/Win32/EditorWindowRuntimeController.cpp b/new_editor/app/Platform/Win32/EditorWindowRuntimeController.cpp index f2bff7f5..36398302 100644 --- a/new_editor/app/Platform/Win32/EditorWindowRuntimeController.cpp +++ b/new_editor/app/Platform/Win32/EditorWindowRuntimeController.cpp @@ -1,17 +1,25 @@ #include "Platform/Win32/EditorWindowRuntimeController.h" #include "Bootstrap/EditorResources.h" -#include "Internal/EmbeddedPngLoader.h" -#include "Platform/Win32/EditorWindowRuntimeInternal.h" #include "Composition/EditorContext.h" +#include "Platform/Win32/EditorWindowSupport.h" +#include "Support/EmbeddedPngLoader.h" #include +#include +#include #include -namespace XCEngine::UI::Editor::App::Internal { +namespace XCEngine::UI::Editor::App { -using App::Internal::LoadEmbeddedPngTexture; -using namespace EditorWindowInternal; +using App::LoadEmbeddedPngTexture; +using namespace EditorWindowSupport; + +namespace { + +constexpr float kFrameTimeSmoothingFactor = 0.12f; + +} EditorWindowRuntimeController::EditorWindowRuntimeController( UIEditorWorkspaceController workspaceController) @@ -135,6 +143,7 @@ bool EditorWindowRuntimeController::Initialize( editorContext.DescribeWorkspaceState( m_workspaceController, m_shellRuntime.GetShellInteractionState())); + ResetFrameTiming(); m_ready = true; m_autoScreenshot.Initialize(captureRoot); @@ -148,6 +157,7 @@ bool EditorWindowRuntimeController::Initialize( void EditorWindowRuntimeController::Shutdown() { m_ready = false; + ResetFrameTiming(); m_autoScreenshot.Shutdown(); m_shellRuntime.Shutdown(); m_renderer.ReleaseTexture(m_titleBarLogoIcon); @@ -158,6 +168,7 @@ void EditorWindowRuntimeController::Shutdown() { void EditorWindowRuntimeController::ResetInteractionState() { m_shellRuntime.ResetInteractionState(); + ResetFrameTiming(); } bool EditorWindowRuntimeController::ApplyResize(UINT width, UINT height) { @@ -181,7 +192,8 @@ bool EditorWindowRuntimeController::ApplyResize(UINT width, UINT height) { return resizeResult.hasViewportSurfacePresentation; } -Host::D3D12WindowRenderLoopFrameContext EditorWindowRuntimeController::BeginFrame() const { +Host::D3D12WindowRenderLoopFrameContext EditorWindowRuntimeController::BeginFrame() { + UpdateFrameTiming(); return m_windowRenderLoop.BeginFrame(); } @@ -223,4 +235,55 @@ std::string EditorWindowRuntimeController::BuildCaptureStatusText() const { return {}; } -} // namespace XCEngine::UI::Editor::App::Internal +std::string EditorWindowRuntimeController::BuildFrameRateText() const { + if (m_displayFps <= 0.0f || m_displayFrameTimeMs <= 0.0f) { + return {}; + } + + char buffer[48] = {}; + std::snprintf( + buffer, + sizeof(buffer), + "FPS %.1f | %.2f ms", + m_displayFps, + m_displayFrameTimeMs); + return buffer; +} + +void EditorWindowRuntimeController::ResetFrameTiming() { + m_lastFrameTime = {}; + m_hasLastFrameTime = false; + m_smoothedDeltaTimeSeconds = 0.0f; + m_displayFps = 0.0f; + m_displayFrameTimeMs = 0.0f; +} + +void EditorWindowRuntimeController::UpdateFrameTiming() { + const auto now = std::chrono::steady_clock::now(); + if (!m_hasLastFrameTime) { + m_lastFrameTime = now; + m_hasLastFrameTime = true; + return; + } + + const float deltaTime = std::chrono::duration(now - m_lastFrameTime).count(); + m_lastFrameTime = now; + if (deltaTime <= 0.0f) { + return; + } + + if (m_smoothedDeltaTimeSeconds <= 0.0f) { + m_smoothedDeltaTimeSeconds = deltaTime; + } else { + m_smoothedDeltaTimeSeconds += + (deltaTime - m_smoothedDeltaTimeSeconds) * kFrameTimeSmoothingFactor; + } + + m_displayFrameTimeMs = m_smoothedDeltaTimeSeconds * 1000.0f; + m_displayFps = m_smoothedDeltaTimeSeconds > 0.0f + ? 1.0f / m_smoothedDeltaTimeSeconds + : 0.0f; +} + +} // namespace XCEngine::UI::Editor::App + diff --git a/new_editor/app/Platform/Win32/EditorWindowRuntimeController.h b/new_editor/app/Platform/Win32/EditorWindowRuntimeController.h index 1b3507f1..17cc11dc 100644 --- a/new_editor/app/Platform/Win32/EditorWindowRuntimeController.h +++ b/new_editor/app/Platform/Win32/EditorWindowRuntimeController.h @@ -18,6 +18,7 @@ #include +#include #include #include @@ -36,8 +37,6 @@ namespace XCEngine::UI::Editor::App { class EditorContext; -namespace Internal { - class EditorWindowRuntimeController final { public: explicit EditorWindowRuntimeController(UIEditorWorkspaceController workspaceController); @@ -78,7 +77,7 @@ public: void ResetInteractionState(); bool ApplyResize(UINT width, UINT height); - Host::D3D12WindowRenderLoopFrameContext BeginFrame() const; + Host::D3D12WindowRenderLoopFrameContext BeginFrame(); Host::D3D12WindowRenderLoopPresentResult Present( const ::XCEngine::UI::UIDrawData& drawData) const; void CaptureIfRequested( @@ -89,8 +88,12 @@ public: void RequestManualScreenshot(std::string reason); std::string BuildCaptureStatusText() const; + std::string BuildFrameRateText() const; private: + void ResetFrameTiming(); + void UpdateFrameTiming(); + Host::NativeRenderer m_renderer = {}; Host::D3D12WindowRenderer m_windowRenderer = {}; Host::D3D12WindowRenderLoop m_windowRenderLoop = {}; @@ -98,8 +101,12 @@ private: ::XCEngine::UI::UITextureHandle m_titleBarLogoIcon = {}; UIEditorWorkspaceController m_workspaceController = {}; EditorShellRuntime m_shellRuntime = {}; + std::chrono::steady_clock::time_point m_lastFrameTime = {}; + bool m_hasLastFrameTime = false; + float m_smoothedDeltaTimeSeconds = 0.0f; + float m_displayFps = 0.0f; + float m_displayFrameTimeMs = 0.0f; bool m_ready = false; }; -} // namespace Internal } // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Platform/Win32/EditorWindowRuntimeInternal.h b/new_editor/app/Platform/Win32/EditorWindowRuntimeInternal.h deleted file mode 100644 index 25ad6364..00000000 --- a/new_editor/app/Platform/Win32/EditorWindowRuntimeInternal.h +++ /dev/null @@ -1,21 +0,0 @@ -#pragma once - -#include "Internal/EnvironmentFlags.h" -#include "Internal/StringEncoding.h" -#include "Internal/TextFormat.h" - -#include -#include - -#include - -namespace XCEngine::UI::Editor::App::EditorWindowInternal { - -bool ResolveVerboseRuntimeTraceEnabled(); -void LogRuntimeTrace(std::string_view channel, std::string_view message); -bool IsAutoCaptureOnStartupEnabled(); - -using App::Internal::TruncateText; -using App::Internal::WideToUtf8; - -} // namespace XCEngine::UI::Editor::App::EditorWindowInternal diff --git a/new_editor/app/Platform/Win32/EditorWindowInternalState.h b/new_editor/app/Platform/Win32/EditorWindowState.h similarity index 100% rename from new_editor/app/Platform/Win32/EditorWindowInternalState.h rename to new_editor/app/Platform/Win32/EditorWindowState.h diff --git a/new_editor/app/Platform/Win32/EditorWindowStyle.h b/new_editor/app/Platform/Win32/EditorWindowStyle.h deleted file mode 100644 index 7dbfd9d4..00000000 --- a/new_editor/app/Platform/Win32/EditorWindowStyle.h +++ /dev/null @@ -1,12 +0,0 @@ -#pragma once - -#include - -namespace XCEngine::UI::Editor::App::EditorWindowInternal { - -inline const ::XCEngine::UI::UIColor kShellSurfaceColor(0.10f, 0.10f, 0.10f, 1.0f); -inline const ::XCEngine::UI::UIColor kShellBorderColor(0.15f, 0.15f, 0.15f, 1.0f); -inline const ::XCEngine::UI::UIColor kShellTextColor(0.92f, 0.92f, 0.92f, 1.0f); -inline const ::XCEngine::UI::UIColor kShellMutedTextColor(0.70f, 0.70f, 0.70f, 1.0f); - -} // namespace XCEngine::UI::Editor::App::EditorWindowInternal diff --git a/new_editor/app/Platform/Win32/EditorWindowSupport.h b/new_editor/app/Platform/Win32/EditorWindowSupport.h new file mode 100644 index 00000000..229e19c5 --- /dev/null +++ b/new_editor/app/Platform/Win32/EditorWindowSupport.h @@ -0,0 +1,38 @@ +#pragma once + +#include "Support/EnvironmentFlags.h" +#include "Support/StringEncoding.h" +#include "Support/TextFormat.h" + +#include + +#include + +#ifndef NOMINMAX +#define NOMINMAX +#endif + +#include + +namespace XCEngine::UI::Editor::App::EditorWindowSupport { + +inline constexpr unsigned int kDefaultDpi = 96u; +inline constexpr float kBaseDpiScale = 96.0f; +inline constexpr float kBorderlessTitleBarHeightDips = 28.0f; +inline constexpr float kBorderlessTitleBarFontSize = 12.0f; + +inline const ::XCEngine::UI::UIColor kShellSurfaceColor(0.10f, 0.10f, 0.10f, 1.0f); +inline const ::XCEngine::UI::UIColor kShellBorderColor(0.15f, 0.15f, 0.15f, 1.0f); +inline const ::XCEngine::UI::UIColor kShellTextColor(0.92f, 0.92f, 0.92f, 1.0f); +inline const ::XCEngine::UI::UIColor kShellMutedTextColor(0.70f, 0.70f, 0.70f, 1.0f); + +UINT QuerySystemDpi(); +UINT QueryWindowDpi(HWND hwnd); +bool ResolveVerboseRuntimeTraceEnabled(); +void LogRuntimeTrace(std::string_view channel, std::string_view message); +bool IsAutoCaptureOnStartupEnabled(); + +using App::TruncateText; +using App::WideToUtf8; + +} // namespace XCEngine::UI::Editor::App::EditorWindowSupport diff --git a/new_editor/app/Platform/Win32/EditorWindowTitleBarInteraction.cpp b/new_editor/app/Platform/Win32/EditorWindowTitleBarInteraction.cpp deleted file mode 100644 index 4ae3ba32..00000000 --- a/new_editor/app/Platform/Win32/EditorWindowTitleBarInteraction.cpp +++ /dev/null @@ -1,68 +0,0 @@ -#include "Platform/Win32/EditorWindow.h" -#include "Platform/Win32/EditorWindowChromeController.h" -#include "Platform/Win32/EditorWindowConstants.h" -#include "Platform/Win32/EditorWindowInternalState.h" - -#include - -#include - -namespace XCEngine::UI::Editor::App { - -using namespace EditorWindowInternal; - -bool EditorWindow::UpdateBorderlessWindowChromeHover(LPARAM lParam) { - return m_chromeController->UpdateChromeHover(*this, lParam); -} - -bool EditorWindow::HandleBorderlessWindowChromeButtonDown(LPARAM lParam) { - return m_chromeController->HandleChromeButtonDown(*this, lParam); -} - -bool EditorWindow::HandleBorderlessWindowChromeButtonUp( - EditorContext& editorContext, - bool globalTabDragActive, - LPARAM lParam) { - return m_chromeController->HandleChromeButtonUp( - *this, - editorContext, - globalTabDragActive, - lParam); -} - -bool EditorWindow::HandleBorderlessWindowChromeDoubleClick( - EditorContext& editorContext, - bool globalTabDragActive, - LPARAM lParam) { - return m_chromeController->HandleChromeDoubleClick( - *this, - editorContext, - globalTabDragActive, - lParam); -} - -bool EditorWindow::HandleBorderlessWindowChromeDragRestorePointerMove( - EditorContext& editorContext, - bool globalTabDragActive) { - return m_chromeController->HandleChromeDragRestorePointerMove( - *this, - editorContext, - globalTabDragActive); -} - -void EditorWindow::ClearBorderlessWindowChromeDragRestoreState() { - m_chromeController->ClearChromeDragRestoreState(*this); -} - -void EditorWindow::ClearBorderlessWindowChromeState() { - m_chromeController->ClearChromeState(*this); -} - -void EditorWindow::ExecuteBorderlessWindowChromeAction( - EditorContext& editorContext, - bool globalTabDragActive, - Host::BorderlessWindowChromeHitTarget target) { - m_chromeController->ExecuteChromeAction(*this, editorContext, globalTabDragActive, target); -} - -} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Platform/Win32/EditorWindowTitleBarRendering.cpp b/new_editor/app/Platform/Win32/EditorWindowTitleBarRendering.cpp deleted file mode 100644 index 5e571787..00000000 --- a/new_editor/app/Platform/Win32/EditorWindowTitleBarRendering.cpp +++ /dev/null @@ -1,26 +0,0 @@ -#include "Platform/Win32/EditorWindow.h" -#include "Platform/Win32/EditorWindowChromeController.h" - -namespace XCEngine::UI::Editor::App { - -bool EditorWindow::ShouldUseDetachedTitleBarTabStrip() const { - return m_chromeController->ShouldUseDetachedTitleBarTabStrip(*this); -} - -Host::BorderlessWindowChromeHitTarget EditorWindow::HitTestBorderlessWindowChrome( - LPARAM lParam) const { - return m_chromeController->HitTestChrome(*this, lParam); -} - -Host::BorderlessWindowChromeLayout EditorWindow::ResolveBorderlessWindowChromeLayout( - float clientWidthDips) const { - return m_chromeController->ResolveChromeLayout(*this, clientWidthDips); -} - -void EditorWindow::AppendBorderlessWindowChrome( - ::XCEngine::UI::UIDrawList& drawList, - float clientWidthDips) const { - m_chromeController->AppendChrome(*this, drawList, clientWidthDips); -} - -} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Platform/Win32/EditorWindowTransferRequests.h b/new_editor/app/Platform/Win32/EditorWindowTransferRequests.h index b2994f12..fc97453d 100644 --- a/new_editor/app/Platform/Win32/EditorWindowTransferRequests.h +++ b/new_editor/app/Platform/Win32/EditorWindowTransferRequests.h @@ -21,12 +21,26 @@ struct EditorWindowPanelTransferRequest { } }; +struct EditorWindowOpenDetachedPanelRequest { + std::string panelId = {}; + POINT screenPoint = {}; + LONG preferredWidth = 0; + LONG preferredHeight = 0; + + bool IsValid() const { + return !panelId.empty(); + } +}; + struct EditorWindowFrameTransferRequests { std::optional beginGlobalTabDrag = {}; std::optional detachPanel = {}; + std::optional openDetachedPanel = {}; bool HasPendingRequests() const { - return beginGlobalTabDrag.has_value() || detachPanel.has_value(); + return beginGlobalTabDrag.has_value() || + detachPanel.has_value() || + openDetachedPanel.has_value(); } }; diff --git a/new_editor/app/Platform/Win32/Win32SystemInteractionHost.cpp b/new_editor/app/Platform/Win32/Win32SystemInteractionHost.cpp index 1d083b2f..5ec0cab8 100644 --- a/new_editor/app/Platform/Win32/Win32SystemInteractionHost.cpp +++ b/new_editor/app/Platform/Win32/Win32SystemInteractionHost.cpp @@ -1,6 +1,6 @@ #include "Platform/Win32/Win32SystemInteractionHost.h" -#include "Internal/StringEncoding.h" +#include "Support/StringEncoding.h" #include #include @@ -26,7 +26,7 @@ bool Win32SystemInteractionHost::CopyTextToClipboard(std::string_view text) { } const std::wstring wideText = - App::Internal::Utf8ToWide(std::string(text)); + App::Utf8ToWide(std::string(text)); const std::size_t byteCount = (wideText.size() + 1u) * sizeof(wchar_t); if (!OpenClipboard(nullptr)) { diff --git a/new_editor/app/Platform/Win32/WindowManager/Lifecycle.cpp b/new_editor/app/Platform/Win32/WindowManager/EditorWindowHostRuntime.cpp similarity index 56% rename from new_editor/app/Platform/Win32/WindowManager/Lifecycle.cpp rename to new_editor/app/Platform/Win32/WindowManager/EditorWindowHostRuntime.cpp index d0971cc0..764f70a6 100644 --- a/new_editor/app/Platform/Win32/WindowManager/Lifecycle.cpp +++ b/new_editor/app/Platform/Win32/WindowManager/EditorWindowHostRuntime.cpp @@ -1,121 +1,16 @@ -#include "Platform/Win32/WindowManager/Internal.h" -#include "Platform/Win32/WindowManager/EditorWindowMessageDispatcher.h" +#include "Platform/Win32/WindowManager/EditorWindowHostRuntime.h" #include "Bootstrap/EditorResources.h" #include "Composition/EditorContext.h" #include "Platform/Win32/EditorWindow.h" +#include "Platform/Win32/WindowManager/EditorWindowWorkspaceCoordinator.h" #include -#include -#include #include -#include namespace XCEngine::UI::Editor::App { -EditorWindowManager::EditorWindowManager( - EditorWindowHostConfig hostConfig, - std::filesystem::path repoRoot, - EditorContext& editorContext) - : m_hostRuntime(std::make_unique( - hostConfig, - std::move(repoRoot), - editorContext)) { - m_workspaceCoordinator = - std::make_unique(*m_hostRuntime); -} - -EditorWindowManager::~EditorWindowManager() = default; - -EditorWindow* EditorWindowManager::CreateEditorWindow( - UIEditorWorkspaceController workspaceController, - const CreateParams& params) { - EditorWindow* const window = - m_hostRuntime->CreateEditorWindow(std::move(workspaceController), params); - if (window != nullptr) { - m_workspaceCoordinator->RegisterExistingWindow(*window); - } - return window; -} - -void EditorWindowManager::HandlePendingNativeWindowCreated(HWND hwnd) { - m_hostRuntime->HandlePendingNativeWindowCreated(hwnd); -} - -void EditorWindowManager::Shutdown() { - m_workspaceCoordinator->EndGlobalTabDragSession(); - m_hostRuntime->Shutdown(); -} - -bool EditorWindowManager::TryDispatchWindowMessage( - HWND hwnd, - UINT message, - WPARAM wParam, - LPARAM lParam, - LRESULT& outResult) { - if (m_hostRuntime == nullptr || m_workspaceCoordinator == nullptr) { - return false; - } - - EditorWindow* const window = m_hostRuntime->FindWindow(hwnd); - if (window == nullptr) { - return false; - } - - return Internal::EditorWindowMessageDispatcher::TryDispatch( - hwnd, - *m_hostRuntime, - *m_workspaceCoordinator, - *window, - message, - wParam, - lParam, - outResult); -} - -EditorWindow* EditorWindowManager::FindWindow(HWND hwnd) { - return m_hostRuntime->FindWindow(hwnd); -} - -const EditorWindow* EditorWindowManager::FindWindow(HWND hwnd) const { - return m_hostRuntime->FindWindow(hwnd); -} - -EditorWindow* EditorWindowManager::FindWindow(std::string_view windowId) { - return m_hostRuntime->FindWindow(windowId); -} - -const EditorWindow* EditorWindowManager::FindWindow(std::string_view windowId) const { - return m_hostRuntime->FindWindow(windowId); -} - -EditorWindow* EditorWindowManager::FindPrimaryWindow() { - return m_hostRuntime->FindPrimaryWindow(); -} - -const EditorWindow* EditorWindowManager::FindPrimaryWindow() const { - return m_hostRuntime->FindPrimaryWindow(); -} - -bool EditorWindowManager::HasWindows() const { - return m_hostRuntime->HasWindows(); -} - -void EditorWindowManager::DestroyClosedWindows() { - m_hostRuntime->DestroyClosedWindows(); -} - -void EditorWindowManager::RenderAllWindows() { - m_hostRuntime->RenderAllWindows( - m_workspaceCoordinator->IsGlobalTabDragActive(), - *m_workspaceCoordinator); -} - -} // namespace XCEngine::UI::Editor::App - -namespace XCEngine::UI::Editor::App::Internal { - EditorWindowHostRuntime::EditorWindowHostRuntime( EditorWindowHostConfig hostConfig, std::filesystem::path repoRoot, @@ -381,102 +276,4 @@ void EditorWindowHostRuntime::LogRuntimeTrace( AppendUIEditorRuntimeTrace(channel, message); } -EditorWindowWorkspaceCoordinator::EditorWindowWorkspaceCoordinator( - EditorWindowHostRuntime& hostRuntime) - : m_hostRuntime(hostRuntime), - m_workspaceStore(hostRuntime.GetEditorContext().GetShellAsset().panelRegistry) {} - -EditorWindowWorkspaceCoordinator::~EditorWindowWorkspaceCoordinator() = default; - -void EditorWindowWorkspaceCoordinator::RegisterExistingWindow(EditorWindow& window) { - std::string error = {}; - if (!m_workspaceStore.RegisterWindowProjection( - window.GetWindowId(), - window.IsPrimary(), - window.GetWorkspaceController(), - error)) { - LogRuntimeTrace( - "window", - "failed to register window '" + std::string(window.GetWindowId()) + - "' in workspace store: " + error); - return; - } - - RefreshWindowTitle(window); -} - -void EditorWindowWorkspaceCoordinator::CommitWindowProjection(EditorWindow& window) { - std::string error = {}; - if (!m_workspaceStore.CommitWindowProjection( - window.GetWindowId(), - window.GetWorkspaceController(), - error)) { - if (m_workspaceStore.RegisterWindowProjection( - window.GetWindowId(), - window.IsPrimary(), - window.GetWorkspaceController(), - error)) { - RefreshWindowTitle(window); - return; - } - - LogRuntimeTrace( - "window", - "failed to commit window projection for '" + std::string(window.GetWindowId()) + - "': " + error); - return; - } - - RefreshWindowTitle(window); -} - -void EditorWindowWorkspaceCoordinator::HandleWindowDestroyed(const EditorWindow& window) { - m_workspaceStore.RemoveWindow(window.GetWindowId(), window.IsPrimary()); -} - -UIEditorWindowWorkspaceController -EditorWindowWorkspaceCoordinator::BuildWorkspaceMutationController() const { - return m_workspaceStore.BuildMutationController(); -} - -UIEditorWorkspaceController EditorWindowWorkspaceCoordinator::BuildWorkspaceControllerForWindow( - const UIEditorWindowWorkspaceState& windowState) const { - return UIEditorWorkspaceController( - m_workspaceStore.GetPanelRegistry(), - windowState.workspace, - windowState.session); -} - -void EditorWindowWorkspaceCoordinator::RefreshWindowTitle(EditorWindow& window) const { - if (window.IsPrimary()) { - return; - } - - const std::wstring title = BuildWindowTitle(window.GetWorkspaceController()); - if (title == window.GetTitle()) { - return; - } - - window.SetTitle(title); - if (window.GetHwnd() != nullptr) { - SetWindowTextW(window.GetHwnd(), window.GetTitle().c_str()); - } -} - -void EditorWindowWorkspaceCoordinator::HandleWindowFrameTransferRequests( - EditorWindow& sourceWindow, - EditorWindowFrameTransferRequests&& transferRequests) { - if (!m_globalTabDragSession.active && - transferRequests.beginGlobalTabDrag.has_value() && - transferRequests.beginGlobalTabDrag->IsValid()) { - TryStartGlobalTabDrag(sourceWindow, *transferRequests.beginGlobalTabDrag); - } - - if (!m_globalTabDragSession.active && - transferRequests.detachPanel.has_value() && - transferRequests.detachPanel->IsValid()) { - TryProcessDetachRequest(sourceWindow, *transferRequests.detachPanel); - } -} - -} // namespace XCEngine::UI::Editor::App::Internal +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Platform/Win32/WindowManager/EditorWindowHostRuntime.h b/new_editor/app/Platform/Win32/WindowManager/EditorWindowHostRuntime.h new file mode 100644 index 00000000..12e9ae77 --- /dev/null +++ b/new_editor/app/Platform/Win32/WindowManager/EditorWindowHostRuntime.h @@ -0,0 +1,82 @@ +#pragma once + +#include "Platform/Win32/EditorWindowManager.h" + +#include +#include +#include +#include + +namespace XCEngine::UI::Editor::App { + +class EditorContext; +class EditorWindow; +class EditorWindowWorkspaceCoordinator; + +class EditorWindowHostRuntime final { +public: + using CreateParams = EditorWindowManager::CreateParams; + + EditorWindowHostRuntime( + EditorWindowHostConfig hostConfig, + std::filesystem::path repoRoot, + EditorContext& editorContext); + ~EditorWindowHostRuntime(); + + EditorWindow* CreateEditorWindow( + UIEditorWorkspaceController workspaceController, + const CreateParams& params); + void HandlePendingNativeWindowCreated(HWND hwnd); + void Shutdown(); + + EditorWindow* FindWindow(HWND hwnd); + const EditorWindow* FindWindow(HWND hwnd) const; + EditorWindow* FindWindow(std::string_view windowId); + const EditorWindow* FindWindow(std::string_view windowId) const; + EditorWindow* FindPrimaryWindow(); + const EditorWindow* FindPrimaryWindow() const; + + bool HasWindows() const; + void DestroyClosedWindows(); + void RenderAllWindows( + bool globalTabDragActive, + EditorWindowWorkspaceCoordinator& workspaceCoordinator); + void HandleDestroyedWindow(HWND hwnd); + + EditorContext& GetEditorContext() { + return m_editorContext; + } + + const EditorContext& GetEditorContext() const { + return m_editorContext; + } + + const EditorWindowHostConfig& GetHostConfig() const { + return m_hostConfig; + } + + const std::filesystem::path& GetRepoRoot() const { + return m_repoRoot; + } + + std::vector>& GetWindows() { + return m_windows; + } + + const std::vector>& GetWindows() const { + return m_windows; + } + + void LogRuntimeTrace(std::string_view channel, std::string_view message) const; + +private: + void DestroyEditorWindow(EditorWindow& window); + + EditorWindowHostConfig m_hostConfig = {}; + std::filesystem::path m_repoRoot = {}; + EditorContext& m_editorContext; + std::vector> m_windows = {}; + EditorWindow* m_pendingCreateWindow = nullptr; +}; + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Platform/Win32/WindowManager/EditorWindowManager.cpp b/new_editor/app/Platform/Win32/WindowManager/EditorWindowManager.cpp new file mode 100644 index 00000000..950a6b7e --- /dev/null +++ b/new_editor/app/Platform/Win32/WindowManager/EditorWindowManager.cpp @@ -0,0 +1,110 @@ +#include "Platform/Win32/EditorWindowManager.h" + +#include "Platform/Win32/EditorWindow.h" +#include "Platform/Win32/WindowManager/EditorWindowHostRuntime.h" +#include "Platform/Win32/WindowManager/EditorWindowMessageDispatcher.h" +#include "Platform/Win32/WindowManager/EditorWindowWorkspaceCoordinator.h" + +#include + +namespace XCEngine::UI::Editor::App { + +EditorWindowManager::EditorWindowManager( + EditorWindowHostConfig hostConfig, + std::filesystem::path repoRoot, + EditorContext& editorContext) + : m_hostRuntime(std::make_unique( + hostConfig, + std::move(repoRoot), + editorContext)) { + m_workspaceCoordinator = + std::make_unique(*m_hostRuntime); +} + +EditorWindowManager::~EditorWindowManager() = default; + +EditorWindow* EditorWindowManager::CreateEditorWindow( + UIEditorWorkspaceController workspaceController, + const CreateParams& params) { + EditorWindow* const window = + m_hostRuntime->CreateEditorWindow(std::move(workspaceController), params); + if (window != nullptr) { + m_workspaceCoordinator->RegisterExistingWindow(*window); + } + return window; +} + +void EditorWindowManager::HandlePendingNativeWindowCreated(HWND hwnd) { + m_hostRuntime->HandlePendingNativeWindowCreated(hwnd); +} + +void EditorWindowManager::Shutdown() { + m_workspaceCoordinator->EndGlobalTabDragSession(); + m_hostRuntime->Shutdown(); +} + +bool EditorWindowManager::TryDispatchWindowMessage( + HWND hwnd, + UINT message, + WPARAM wParam, + LPARAM lParam, + LRESULT& outResult) { + if (m_hostRuntime == nullptr || m_workspaceCoordinator == nullptr) { + return false; + } + + EditorWindow* const window = m_hostRuntime->FindWindow(hwnd); + if (window == nullptr) { + return false; + } + + return EditorWindowMessageDispatcher::TryDispatch( + hwnd, + *m_hostRuntime, + *m_workspaceCoordinator, + *window, + message, + wParam, + lParam, + outResult); +} + +EditorWindow* EditorWindowManager::FindWindow(HWND hwnd) { + return m_hostRuntime->FindWindow(hwnd); +} + +const EditorWindow* EditorWindowManager::FindWindow(HWND hwnd) const { + return m_hostRuntime->FindWindow(hwnd); +} + +EditorWindow* EditorWindowManager::FindWindow(std::string_view windowId) { + return m_hostRuntime->FindWindow(windowId); +} + +const EditorWindow* EditorWindowManager::FindWindow(std::string_view windowId) const { + return m_hostRuntime->FindWindow(windowId); +} + +EditorWindow* EditorWindowManager::FindPrimaryWindow() { + return m_hostRuntime->FindPrimaryWindow(); +} + +const EditorWindow* EditorWindowManager::FindPrimaryWindow() const { + return m_hostRuntime->FindPrimaryWindow(); +} + +bool EditorWindowManager::HasWindows() const { + return m_hostRuntime->HasWindows(); +} + +void EditorWindowManager::DestroyClosedWindows() { + m_hostRuntime->DestroyClosedWindows(); +} + +void EditorWindowManager::RenderAllWindows() { + m_hostRuntime->RenderAllWindows( + m_workspaceCoordinator->IsGlobalTabDragActive(), + *m_workspaceCoordinator); +} + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Platform/Win32/WindowManager/EditorWindowMessageDispatcher.cpp b/new_editor/app/Platform/Win32/WindowManager/EditorWindowMessageDispatcher.cpp index f450f6c3..21f7385e 100644 --- a/new_editor/app/Platform/Win32/WindowManager/EditorWindowMessageDispatcher.cpp +++ b/new_editor/app/Platform/Win32/WindowManager/EditorWindowMessageDispatcher.cpp @@ -1,14 +1,15 @@ -#include "Platform/Win32/WindowManager/EditorWindowMessageDispatcher.h" +#include "Platform/Win32/WindowManager/EditorWindowMessageDispatcher.h" #include "Composition/EditorShellRuntime.h" #include "Platform/Win32/BorderlessWindowChrome.h" #include "Platform/Win32/EditorWindow.h" #include "Platform/Win32/EditorWindowPointerCapture.h" -#include "Platform/Win32/WindowManager/Internal.h" +#include "Platform/Win32/WindowManager/EditorWindowHostRuntime.h" +#include "Platform/Win32/WindowManager/EditorWindowWorkspaceCoordinator.h" #include -namespace XCEngine::UI::Editor::App::Internal { +namespace XCEngine::UI::Editor::App { namespace { @@ -164,7 +165,8 @@ bool EditorWindowMessageDispatcher::TryDispatchWindowPointerMessage( ::XCEngine::UI::UIInputEventType::PointerButtonDown, ::XCEngine::UI::UIPointerButton::Left, wParam, - lParam); + lParam, + true); outResult = 0; return true; case WM_RBUTTONDOWN: @@ -174,7 +176,8 @@ bool EditorWindowMessageDispatcher::TryDispatchWindowPointerMessage( ::XCEngine::UI::UIInputEventType::PointerButtonDown, ::XCEngine::UI::UIPointerButton::Right, wParam, - lParam); + lParam, + true); outResult = 0; return true; case WM_MBUTTONDOWN: @@ -184,7 +187,8 @@ bool EditorWindowMessageDispatcher::TryDispatchWindowPointerMessage( ::XCEngine::UI::UIInputEventType::PointerButtonDown, ::XCEngine::UI::UIPointerButton::Middle, wParam, - lParam); + lParam, + true); outResult = 0; return true; case WM_LBUTTONUP: @@ -490,4 +494,4 @@ bool EditorWindowMessageDispatcher::TryDispatch( TryDispatchWindowInputMessage(context, message, wParam, lParam, outResult); } -} // namespace XCEngine::UI::Editor::App::Internal +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Platform/Win32/WindowManager/EditorWindowMessageDispatcher.h b/new_editor/app/Platform/Win32/WindowManager/EditorWindowMessageDispatcher.h index 341ddce1..e233d3e5 100644 --- a/new_editor/app/Platform/Win32/WindowManager/EditorWindowMessageDispatcher.h +++ b/new_editor/app/Platform/Win32/WindowManager/EditorWindowMessageDispatcher.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once #ifndef NOMINMAX #define NOMINMAX @@ -10,7 +10,7 @@ namespace XCEngine::UI::Editor::App { class EditorWindow; } -namespace XCEngine::UI::Editor::App::Internal { +namespace XCEngine::UI::Editor::App { class EditorWindowHostRuntime; class EditorWindowWorkspaceCoordinator; @@ -62,4 +62,5 @@ private: LRESULT& outResult); }; -} // namespace XCEngine::UI::Editor::App::Internal +} // namespace XCEngine::UI::Editor::App + diff --git a/new_editor/app/Platform/Win32/WindowManager/TabDrag.cpp b/new_editor/app/Platform/Win32/WindowManager/EditorWindowWorkspaceCoordinator.cpp similarity index 52% rename from new_editor/app/Platform/Win32/WindowManager/TabDrag.cpp rename to new_editor/app/Platform/Win32/WindowManager/EditorWindowWorkspaceCoordinator.cpp index c91f90e2..1f5c6049 100644 --- a/new_editor/app/Platform/Win32/WindowManager/TabDrag.cpp +++ b/new_editor/app/Platform/Win32/WindowManager/EditorWindowWorkspaceCoordinator.cpp @@ -1,23 +1,29 @@ -#include "Platform/Win32/WindowManager/Internal.h" -#include "Platform/Win32/WindowManager/TabDragDropTarget.h" +#include "Platform/Win32/WindowManager/EditorWindowWorkspaceCoordinator.h" #include "Composition/EditorContext.h" #include "Platform/Win32/EditorWindow.h" +#include "Platform/Win32/WindowManager/EditorWindowHostRuntime.h" -#include -#include +#include #include +#include +#include #include #include #include -namespace XCEngine::UI::Editor::App::Internal { +namespace XCEngine::UI::Editor::App { namespace { +struct ExistingWindowSnapshot { + EditorWindow* window = nullptr; + UIEditorWorkspaceController workspaceController = {}; + std::wstring title = {}; +}; + using ::XCEngine::UI::UIPoint; -using ::XCEngine::UI::UIRect; constexpr LONG kFallbackDragHotspotX = 40; constexpr LONG kFallbackDragHotspotY = 12; @@ -29,15 +35,6 @@ POINT BuildFallbackGlobalTabDragHotspot() { return hotspot; } -float ResolveWindowDpiScale(HWND hwnd) { - if (hwnd == nullptr) { - return 1.0f; - } - - const UINT dpi = GetDpiForWindow(hwnd); - return dpi == 0u ? 1.0f : static_cast(dpi) / 96.0f; -} - bool CanStartGlobalTabDragFromWindow( const EditorWindow& sourceWindow, std::string_view sourceNodeId, @@ -63,11 +60,296 @@ bool IsLiveInteractiveWindow(const EditorWindow* window) { } // namespace +EditorWindowWorkspaceCoordinator::EditorWindowWorkspaceCoordinator( + EditorWindowHostRuntime& hostRuntime) + : m_hostRuntime(hostRuntime), + m_workspaceStore(hostRuntime.GetEditorContext().GetShellAsset().panelRegistry) {} + +EditorWindowWorkspaceCoordinator::~EditorWindowWorkspaceCoordinator() = default; + +void EditorWindowWorkspaceCoordinator::RegisterExistingWindow(EditorWindow& window) { + std::string error = {}; + if (!m_workspaceStore.RegisterWindowProjection( + window.GetWindowId(), + window.IsPrimary(), + window.GetWorkspaceController(), + error)) { + LogRuntimeTrace( + "window", + "failed to register window '" + std::string(window.GetWindowId()) + + "' in workspace store: " + error); + return; + } + + RefreshWindowTitle(window); +} + +void EditorWindowWorkspaceCoordinator::CommitWindowProjection(EditorWindow& window) { + std::string error = {}; + if (!m_workspaceStore.CommitWindowProjection( + window.GetWindowId(), + window.GetWorkspaceController(), + error)) { + if (m_workspaceStore.RegisterWindowProjection( + window.GetWindowId(), + window.IsPrimary(), + window.GetWorkspaceController(), + error)) { + RefreshWindowTitle(window); + return; + } + + LogRuntimeTrace( + "window", + "failed to commit window projection for '" + std::string(window.GetWindowId()) + + "': " + error); + return; + } + + RefreshWindowTitle(window); +} + +void EditorWindowWorkspaceCoordinator::HandleWindowDestroyed(const EditorWindow& window) { + m_workspaceStore.RemoveWindow(window.GetWindowId(), window.IsPrimary()); +} + +UIEditorWindowWorkspaceController +EditorWindowWorkspaceCoordinator::BuildWorkspaceMutationController() const { + return m_workspaceStore.BuildMutationController(); +} + +UIEditorWorkspaceController EditorWindowWorkspaceCoordinator::BuildWorkspaceControllerForWindow( + const UIEditorWindowWorkspaceState& windowState) const { + return UIEditorWorkspaceController( + m_workspaceStore.GetPanelRegistry(), + windowState.workspace, + windowState.session); +} + +std::wstring EditorWindowWorkspaceCoordinator::BuildWindowTitle( + const UIEditorWorkspaceController& workspaceController) const { + const std::string& activePanelId = workspaceController.GetWorkspace().activePanelId; + if (const UIEditorPanelDescriptor* descriptor = + FindUIEditorPanelDescriptor( + workspaceController.GetPanelRegistry(), + activePanelId); + descriptor != nullptr && + !descriptor->defaultTitle.empty()) { + const std::string titleText = descriptor->defaultTitle + " - XCEngine Editor"; + return std::wstring(titleText.begin(), titleText.end()); + } + + return std::wstring(L"XCEngine Editor"); +} + +void EditorWindowWorkspaceCoordinator::RefreshWindowTitle(EditorWindow& window) const { + if (window.IsPrimary()) { + return; + } + + const std::wstring title = BuildWindowTitle(window.GetWorkspaceController()); + if (title == window.GetTitle()) { + return; + } + + window.SetTitle(title); + if (window.GetHwnd() != nullptr) { + SetWindowTextW(window.GetHwnd(), window.GetTitle().c_str()); + } +} + +RECT EditorWindowWorkspaceCoordinator::BuildDetachedWindowRect( + const POINT& screenPoint, + LONG preferredWidth, + LONG preferredHeight) const { + const LONG resolvedWidth = preferredWidth > 0 ? preferredWidth : 960; + const LONG resolvedHeight = preferredHeight > 0 ? preferredHeight : 720; + const LONG left = screenPoint.x - resolvedWidth / 2; + const LONG top = screenPoint.y - 24; + RECT rect = { + left, + top, + left + resolvedWidth, + top + resolvedHeight + }; + + const HMONITOR monitor = MonitorFromPoint(screenPoint, MONITOR_DEFAULTTONEAREST); + MONITORINFO monitorInfo = {}; + monitorInfo.cbSize = sizeof(monitorInfo); + if (monitor != nullptr && GetMonitorInfoW(monitor, &monitorInfo)) { + const RECT& workArea = monitorInfo.rcWork; + const LONG width = rect.right - rect.left; + const LONG height = rect.bottom - rect.top; + rect.left = (std::max)(workArea.left, (std::min)(rect.left, workArea.right - width)); + rect.top = (std::max)(workArea.top, (std::min)(rect.top, workArea.bottom - height)); + rect.right = rect.left + width; + rect.bottom = rect.top + height; + } + + return rect; +} + +bool EditorWindowWorkspaceCoordinator::SynchronizeWindowsFromWindowSet( + const UIEditorWindowWorkspaceSet& windowSet, + std::string_view preferredNewWindowId, + const POINT& preferredScreenPoint, + LONG preferredWidth, + LONG preferredHeight) { + const auto restoreWindowSnapshot = [](const ExistingWindowSnapshot& snapshot) { + if (snapshot.window == nullptr) { + return; + } + + snapshot.window->ReplaceWorkspaceController(snapshot.workspaceController); + snapshot.window->SetTitle(snapshot.title); + snapshot.window->ResetInteractionState(); + if (snapshot.window->GetHwnd() != nullptr) { + SetWindowTextW(snapshot.window->GetHwnd(), snapshot.window->GetTitle().c_str()); + } + }; + + const auto destroyAndEraseWindowById = [this](std::string_view windowId) { + auto& windows = m_hostRuntime.GetWindows(); + const auto it = std::find_if( + windows.begin(), + windows.end(), + [windowId](const std::unique_ptr& candidate) { + return candidate != nullptr && candidate->GetWindowId() == windowId; + }); + if (it == windows.end() || *it == nullptr) { + return false; + } + + EditorWindow& window = *it->get(); + const HWND hwnd = window.GetHwnd(); + window.ForceReleasePointerCapture(); + + window.Shutdown(); + if (hwnd != nullptr && IsWindow(hwnd)) { + DestroyWindow(hwnd); + } + window.MarkDestroyed(); + windows.erase(it); + return true; + }; + + std::vector windowIdsInSet = {}; + windowIdsInSet.reserve(windowSet.windows.size()); + std::vector existingWindowSnapshots = {}; + existingWindowSnapshots.reserve(windowSet.windows.size()); + std::vector createdWindowIds = {}; + + for (const UIEditorWindowWorkspaceState& entry : windowSet.windows) { + windowIdsInSet.push_back(entry.windowId); + if (EditorWindow* existingWindow = m_hostRuntime.FindWindow(entry.windowId); + existingWindow != nullptr) { + existingWindow->ClearClosing(); + existingWindowSnapshots.push_back(ExistingWindowSnapshot{ + existingWindow, + existingWindow->GetWorkspaceController(), + existingWindow->GetTitle(), + }); + existingWindow->ReplaceWorkspaceController(BuildWorkspaceControllerForWindow(entry)); + existingWindow->ResetInteractionState(); + if (!existingWindow->IsPrimary()) { + existingWindow->SetTitle( + BuildWindowTitle(existingWindow->GetWorkspaceController())); + if (existingWindow->GetHwnd() != nullptr) { + SetWindowTextW(existingWindow->GetHwnd(), existingWindow->GetTitle().c_str()); + } + } + continue; + } + + EditorWindowHostRuntime::CreateParams createParams = {}; + createParams.windowId = entry.windowId; + createParams.primary = entry.windowId == windowSet.primaryWindowId; + createParams.title = + createParams.primary + ? std::wstring( + m_hostRuntime.GetHostConfig().primaryWindowTitle != nullptr && + m_hostRuntime.GetHostConfig().primaryWindowTitle[0] != L'\0' + ? m_hostRuntime.GetHostConfig().primaryWindowTitle + : L"XCEngine Editor") + : BuildWindowTitle(BuildWorkspaceControllerForWindow(entry)); + if (entry.windowId == preferredNewWindowId) { + const RECT detachedRect = BuildDetachedWindowRect( + preferredScreenPoint, + preferredWidth, + preferredHeight); + createParams.initialX = detachedRect.left; + createParams.initialY = detachedRect.top; + createParams.initialWidth = detachedRect.right - detachedRect.left; + createParams.initialHeight = detachedRect.bottom - detachedRect.top; + } + + if (m_hostRuntime.CreateEditorWindow( + BuildWorkspaceControllerForWindow(entry), + createParams) == nullptr) { + for (const ExistingWindowSnapshot& snapshot : existingWindowSnapshots) { + restoreWindowSnapshot(snapshot); + } + for (auto it = createdWindowIds.rbegin(); it != createdWindowIds.rend(); ++it) { + destroyAndEraseWindowById(*it); + } + return false; + } + createdWindowIds.push_back(entry.windowId); + } + + for (const std::unique_ptr& window : m_hostRuntime.GetWindows()) { + if (window == nullptr || + window->GetHwnd() == nullptr || + window->IsPrimary()) { + continue; + } + + const bool existsInWindowSet = + std::find( + windowIdsInSet.begin(), + windowIdsInSet.end(), + window->GetWindowId()) != windowIdsInSet.end(); + if (!existsInWindowSet) { + window->MarkClosing(); + PostMessageW(window->GetHwnd(), WM_CLOSE, 0, 0); + } + } + + return true; +} + +bool EditorWindowWorkspaceCoordinator::CommitWindowWorkspaceMutation( + const UIEditorWindowWorkspaceController& windowWorkspaceController, + std::string_view preferredNewWindowId, + const POINT& preferredScreenPoint, + LONG preferredWidth, + LONG preferredHeight) { + const UIEditorWindowWorkspaceSet nextWindowSet = windowWorkspaceController.GetWindowSet(); + std::string error = {}; + if (!m_workspaceStore.ValidateWindowSet(nextWindowSet, error)) { + LogRuntimeTrace("window", "workspace mutation validation failed: " + error); + return false; + } + + if (!SynchronizeWindowsFromWindowSet( + nextWindowSet, + preferredNewWindowId, + preferredScreenPoint, + preferredWidth, + preferredHeight)) { + return false; + } + + m_workspaceStore.ReplaceWindowSet(nextWindowSet); + return true; +} + bool EditorWindowWorkspaceCoordinator::IsGlobalTabDragActive() const { return m_globalTabDragSession.active; } -bool EditorWindowWorkspaceCoordinator::OwnsActiveGlobalTabDrag(std::string_view windowId) const { +bool EditorWindowWorkspaceCoordinator::OwnsActiveGlobalTabDrag( + std::string_view windowId) const { return m_globalTabDragSession.active && m_globalTabDragSession.panelWindowId == windowId; } @@ -92,45 +374,11 @@ bool EditorWindowWorkspaceCoordinator::TryResolveGlobalTabDragHotspot( std::string_view panelId, const POINT& screenPoint, POINT& outDragHotspot) const { - const HWND hwnd = sourceWindow.GetHwnd(); - if (hwnd == nullptr) { - return false; - } - - const auto& layout = - sourceWindow.GetShellFrame().workspaceInteractionFrame.dockHostFrame.layout; - for (const Widgets::UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) { - if (tabStack.nodeId != nodeId) { - continue; - } - - const std::size_t tabCount = - (std::min)(tabStack.items.size(), tabStack.tabStripLayout.tabHeaderRects.size()); - for (std::size_t index = 0u; index < tabCount; ++index) { - if (tabStack.items[index].panelId != panelId) { - continue; - } - - const ::XCEngine::UI::UIPoint clientPointDips = - sourceWindow.ConvertScreenPixelsToClientDips(screenPoint); - const ::XCEngine::UI::UIRect& tabRect = - tabStack.tabStripLayout.tabHeaderRects[index]; - const float dpiScale = ResolveWindowDpiScale(hwnd); - const float localOffsetXDips = (std::clamp)( - clientPointDips.x - tabRect.x, - 0.0f, - (std::max)(tabRect.width, 0.0f)); - const float localOffsetYDips = (std::clamp)( - clientPointDips.y - tabRect.y, - 0.0f, - (std::max)(tabRect.height, 0.0f)); - outDragHotspot.x = static_cast(std::lround(localOffsetXDips * dpiScale)); - outDragHotspot.y = static_cast(std::lround(localOffsetYDips * dpiScale)); - return true; - } - } - - return false; + return sourceWindow.TryResolveDockTabDragHotspot( + nodeId, + panelId, + screenPoint, + outDragHotspot); } void EditorWindowWorkspaceCoordinator::UpdateGlobalTabDragOwnerWindowPosition() { @@ -192,13 +440,10 @@ void EditorWindowWorkspaceCoordinator::UpdateGlobalTabDragDropPreview() { return; } - const ::XCEngine::UI::UIPoint targetPoint = - targetWindow->ConvertScreenPixelsToClientDips(m_globalTabDragSession.screenPoint); - const Widgets::UIEditorDockHostLayout& targetLayout = - targetWindow->GetShellFrame().workspaceInteractionFrame.dockHostFrame.layout; - const EditorWindowTabDragDropTarget dropTarget = - ResolveEditorWindowTabDragDropTarget(targetLayout, targetPoint); - if (!dropTarget.valid) { + UIEditorDockHostTabDropTarget dropTarget = {}; + if (!targetWindow->TryResolveDockTabDropTarget( + m_globalTabDragSession.screenPoint, + dropTarget)) { ClearGlobalTabDragDropPreview(); return; } @@ -288,13 +533,8 @@ bool EditorWindowWorkspaceCoordinator::HandleGlobalTabDragPointerButtonUp(HWND h return true; } - const UIPoint targetPoint = - targetWindow->ConvertScreenPixelsToClientDips(screenPoint); - const Widgets::UIEditorDockHostLayout& targetLayout = - targetWindow->GetShellFrame().workspaceInteractionFrame.dockHostFrame.layout; - const EditorWindowTabDragDropTarget dropTarget = - ResolveEditorWindowTabDragDropTarget(targetLayout, targetPoint); - if (!dropTarget.valid) { + UIEditorDockHostTabDropTarget dropTarget = {}; + if (!targetWindow->TryResolveDockTabDropTarget(screenPoint, dropTarget)) { return true; } @@ -484,6 +724,91 @@ bool EditorWindowWorkspaceCoordinator::TryProcessDetachRequest( return true; } +bool EditorWindowWorkspaceCoordinator::TryProcessOpenDetachedPanelRequest( + EditorWindow& sourceWindow, + const EditorWindowOpenDetachedPanelRequest& request) { + if (sourceWindow.IsClosing()) { + LogRuntimeTrace( + "window", + "open detached panel request rejected: source window is closing"); + return false; + } + + UIEditorWindowWorkspaceController windowWorkspaceController = + BuildWorkspaceMutationController(); + const UIEditorWindowWorkspaceOperationResult result = + windowWorkspaceController.OpenPanelInNewWindow(request.panelId); + if (result.status == UIEditorWindowWorkspaceOperationStatus::NoOp && + !result.targetWindowId.empty()) { + if (EditorWindow* existingWindow = m_hostRuntime.FindWindow(result.targetWindowId); + existingWindow != nullptr && + existingWindow->GetHwnd() != nullptr && + !existingWindow->IsClosing()) { + SetForegroundWindow(existingWindow->GetHwnd()); + } + + LogRuntimeTrace( + "window", + "open detached panel request reused existing window '" + + result.targetWindowId + "' for panel '" + request.panelId + "'"); + return true; + } + + if (result.status != UIEditorWindowWorkspaceOperationStatus::Changed) { + LogRuntimeTrace( + "window", + "open detached panel request rejected: " + result.message); + return false; + } + + if (!CommitWindowWorkspaceMutation( + windowWorkspaceController, + result.targetWindowId, + request.screenPoint, + request.preferredWidth, + request.preferredHeight)) { + LogRuntimeTrace("window", "failed to synchronize detached panel window state"); + return false; + } + + if (EditorWindow* detachedWindow = m_hostRuntime.FindWindow(result.targetWindowId); + detachedWindow != nullptr && + detachedWindow->GetHwnd() != nullptr && + !detachedWindow->IsClosing()) { + SetForegroundWindow(detachedWindow->GetHwnd()); + } + + LogRuntimeTrace( + "window", + "opened detached panel '" + request.panelId + "' in window '" + + result.targetWindowId + "'"); + return true; +} + +void EditorWindowWorkspaceCoordinator::HandleWindowFrameTransferRequests( + EditorWindow& sourceWindow, + EditorWindowFrameTransferRequests&& transferRequests) { + if (!m_globalTabDragSession.active && + transferRequests.beginGlobalTabDrag.has_value() && + transferRequests.beginGlobalTabDrag->IsValid()) { + TryStartGlobalTabDrag(sourceWindow, *transferRequests.beginGlobalTabDrag); + } + + if (!m_globalTabDragSession.active && + transferRequests.detachPanel.has_value() && + transferRequests.detachPanel->IsValid()) { + TryProcessDetachRequest(sourceWindow, *transferRequests.detachPanel); + } + + if (!m_globalTabDragSession.active && + transferRequests.openDetachedPanel.has_value() && + transferRequests.openDetachedPanel->IsValid()) { + TryProcessOpenDetachedPanelRequest( + sourceWindow, + *transferRequests.openDetachedPanel); + } +} + EditorWindow* EditorWindowWorkspaceCoordinator::FindTopmostWindowAtScreenPoint( const POINT& screenPoint, std::string_view excludedWindowId) { @@ -533,4 +858,4 @@ void EditorWindowWorkspaceCoordinator::LogRuntimeTrace( m_hostRuntime.LogRuntimeTrace(channel, message); } -} // namespace XCEngine::UI::Editor::App::Internal +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Platform/Win32/WindowManager/Internal.h b/new_editor/app/Platform/Win32/WindowManager/EditorWindowWorkspaceCoordinator.h similarity index 57% rename from new_editor/app/Platform/Win32/WindowManager/Internal.h rename to new_editor/app/Platform/Win32/WindowManager/EditorWindowWorkspaceCoordinator.h index c30727d1..0286fc2e 100644 --- a/new_editor/app/Platform/Win32/WindowManager/Internal.h +++ b/new_editor/app/Platform/Win32/WindowManager/EditorWindowWorkspaceCoordinator.h @@ -1,75 +1,21 @@ #pragma once +#ifndef NOMINMAX +#define NOMINMAX +#endif + #include "Composition/EditorWindowWorkspaceStore.h" -#include "Platform/Win32/EditorWindowManager.h" +#include "Platform/Win32/EditorWindowTransferRequests.h" -namespace XCEngine::UI::Editor::App::Internal { +#include -class EditorWindowHostRuntime final { -public: - using CreateParams = EditorWindowManager::CreateParams; +#include +#include - EditorWindowHostRuntime( - EditorWindowHostConfig hostConfig, - std::filesystem::path repoRoot, - EditorContext& editorContext); - ~EditorWindowHostRuntime(); +namespace XCEngine::UI::Editor::App { - EditorWindow* CreateEditorWindow( - UIEditorWorkspaceController workspaceController, - const CreateParams& params); - void HandlePendingNativeWindowCreated(HWND hwnd); - void Shutdown(); - - EditorWindow* FindWindow(HWND hwnd); - const EditorWindow* FindWindow(HWND hwnd) const; - EditorWindow* FindWindow(std::string_view windowId); - const EditorWindow* FindWindow(std::string_view windowId) const; - EditorWindow* FindPrimaryWindow(); - const EditorWindow* FindPrimaryWindow() const; - - bool HasWindows() const; - void DestroyClosedWindows(); - void RenderAllWindows( - bool globalTabDragActive, - EditorWindowWorkspaceCoordinator& workspaceCoordinator); - void HandleDestroyedWindow(HWND hwnd); - - EditorContext& GetEditorContext() { - return m_editorContext; - } - - const EditorContext& GetEditorContext() const { - return m_editorContext; - } - - const EditorWindowHostConfig& GetHostConfig() const { - return m_hostConfig; - } - - const std::filesystem::path& GetRepoRoot() const { - return m_repoRoot; - } - - std::vector>& GetWindows() { - return m_windows; - } - - const std::vector>& GetWindows() const { - return m_windows; - } - - void LogRuntimeTrace(std::string_view channel, std::string_view message) const; - -private: - void DestroyEditorWindow(EditorWindow& window); - - EditorWindowHostConfig m_hostConfig = {}; - std::filesystem::path m_repoRoot = {}; - EditorContext& m_editorContext; - std::vector> m_windows = {}; - EditorWindow* m_pendingCreateWindow = nullptr; -}; +class EditorWindow; +class EditorWindowHostRuntime; class EditorWindowWorkspaceCoordinator final { public: @@ -104,17 +50,24 @@ private: bool SynchronizeWindowsFromWindowSet( const UIEditorWindowWorkspaceSet& windowSet, std::string_view preferredNewWindowId, - const POINT& preferredScreenPoint); + const POINT& preferredScreenPoint, + LONG preferredWidth = 0, + LONG preferredHeight = 0); bool CommitWindowWorkspaceMutation( const UIEditorWindowWorkspaceController& windowWorkspaceController, std::string_view preferredNewWindowId, - const POINT& preferredScreenPoint); + const POINT& preferredScreenPoint, + LONG preferredWidth = 0, + LONG preferredHeight = 0); UIEditorWorkspaceController BuildWorkspaceControllerForWindow( const UIEditorWindowWorkspaceState& windowState) const; std::wstring BuildWindowTitle( const UIEditorWorkspaceController& workspaceController) const; void RefreshWindowTitle(EditorWindow& window) const; - RECT BuildDetachedWindowRect(const POINT& screenPoint) const; + RECT BuildDetachedWindowRect( + const POINT& screenPoint, + LONG preferredWidth = 0, + LONG preferredHeight = 0) const; void BeginGlobalTabDragSession( std::string_view panelWindowId, std::string_view sourceNodeId, @@ -142,6 +95,9 @@ private: bool TryProcessDetachRequest( EditorWindow& sourceWindow, const EditorWindowPanelTransferRequest& request); + bool TryProcessOpenDetachedPanelRequest( + EditorWindow& sourceWindow, + const EditorWindowOpenDetachedPanelRequest& request); void LogRuntimeTrace(std::string_view channel, std::string_view message) const; EditorWindowHostRuntime& m_hostRuntime; @@ -149,4 +105,4 @@ private: GlobalTabDragSession m_globalTabDragSession = {}; }; -} // namespace XCEngine::UI::Editor::App::Internal +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Platform/Win32/WindowManager/TabDragDropTarget.h b/new_editor/app/Platform/Win32/WindowManager/TabDragDropTarget.h deleted file mode 100644 index bc4f2aee..00000000 --- a/new_editor/app/Platform/Win32/WindowManager/TabDragDropTarget.h +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once - -#include - -#include - -#include -#include - -namespace XCEngine::UI::Editor::App::Internal { - -struct EditorWindowTabDragDropTarget { - bool valid = false; - std::string nodeId = {}; - UIEditorWorkspaceDockPlacement placement = UIEditorWorkspaceDockPlacement::Center; - std::size_t insertionIndex = Widgets::UIEditorTabStripInvalidIndex; -}; - -EditorWindowTabDragDropTarget ResolveEditorWindowTabDragDropTarget( - const Widgets::UIEditorDockHostLayout& layout, - const ::XCEngine::UI::UIPoint& point); - -} // namespace XCEngine::UI::Editor::App::Internal diff --git a/new_editor/app/Platform/Win32/WindowManager/WindowSync.cpp b/new_editor/app/Platform/Win32/WindowManager/WindowSync.cpp deleted file mode 100644 index c0f49281..00000000 --- a/new_editor/app/Platform/Win32/WindowManager/WindowSync.cpp +++ /dev/null @@ -1,207 +0,0 @@ -#include "Platform/Win32/WindowManager/Internal.h" - -#include "Composition/EditorContext.h" -#include "Platform/Win32/EditorWindow.h" - -#include -#include - -#include - -namespace XCEngine::UI::Editor::App::Internal { - -namespace { - -struct ExistingWindowSnapshot { - EditorWindow* window = nullptr; - UIEditorWorkspaceController workspaceController = {}; - std::wstring title = {}; -}; - -} // namespace - -std::wstring EditorWindowWorkspaceCoordinator::BuildWindowTitle( - const UIEditorWorkspaceController& workspaceController) const { - const std::string& activePanelId = workspaceController.GetWorkspace().activePanelId; - if (const UIEditorPanelDescriptor* descriptor = - FindUIEditorPanelDescriptor( - workspaceController.GetPanelRegistry(), - activePanelId); - descriptor != nullptr && - !descriptor->defaultTitle.empty()) { - const std::string titleText = descriptor->defaultTitle + " - XCEngine Editor"; - return std::wstring(titleText.begin(), titleText.end()); - } - - return std::wstring(L"XCEngine Editor"); -} - -RECT EditorWindowWorkspaceCoordinator::BuildDetachedWindowRect(const POINT& screenPoint) const { - RECT rect = { - screenPoint.x - 420, - screenPoint.y - 24, - screenPoint.x - 420 + 960, - screenPoint.y - 24 + 720 - }; - - const HMONITOR monitor = MonitorFromPoint(screenPoint, MONITOR_DEFAULTTONEAREST); - MONITORINFO monitorInfo = {}; - monitorInfo.cbSize = sizeof(monitorInfo); - if (monitor != nullptr && GetMonitorInfoW(monitor, &monitorInfo)) { - const RECT& workArea = monitorInfo.rcWork; - const LONG width = rect.right - rect.left; - const LONG height = rect.bottom - rect.top; - rect.left = (std::max)(workArea.left, (std::min)(rect.left, workArea.right - width)); - rect.top = (std::max)(workArea.top, (std::min)(rect.top, workArea.bottom - height)); - rect.right = rect.left + width; - rect.bottom = rect.top + height; - } - - return rect; -} - -bool EditorWindowWorkspaceCoordinator::SynchronizeWindowsFromWindowSet( - const UIEditorWindowWorkspaceSet& windowSet, - std::string_view preferredNewWindowId, - const POINT& preferredScreenPoint) { - const auto restoreWindowSnapshot = [](const ExistingWindowSnapshot& snapshot) { - if (snapshot.window == nullptr) { - return; - } - - snapshot.window->ReplaceWorkspaceController(snapshot.workspaceController); - snapshot.window->SetTitle(snapshot.title); - snapshot.window->ResetInteractionState(); - if (snapshot.window->GetHwnd() != nullptr) { - SetWindowTextW(snapshot.window->GetHwnd(), snapshot.window->GetTitle().c_str()); - } - }; - - const auto destroyAndEraseWindowById = [this](std::string_view windowId) { - auto& windows = m_hostRuntime.GetWindows(); - const auto it = std::find_if( - windows.begin(), - windows.end(), - [windowId](const std::unique_ptr& candidate) { - return candidate != nullptr && candidate->GetWindowId() == windowId; - }); - if (it == windows.end() || *it == nullptr) { - return false; - } - - EditorWindow& window = *it->get(); - const HWND hwnd = window.GetHwnd(); - window.ForceReleasePointerCapture(); - - window.Shutdown(); - if (hwnd != nullptr && IsWindow(hwnd)) { - DestroyWindow(hwnd); - } - window.MarkDestroyed(); - windows.erase(it); - return true; - }; - - std::vector windowIdsInSet = {}; - windowIdsInSet.reserve(windowSet.windows.size()); - std::vector existingWindowSnapshots = {}; - existingWindowSnapshots.reserve(windowSet.windows.size()); - std::vector createdWindowIds = {}; - - for (const UIEditorWindowWorkspaceState& entry : windowSet.windows) { - windowIdsInSet.push_back(entry.windowId); - if (EditorWindow* existingWindow = m_hostRuntime.FindWindow(entry.windowId); - existingWindow != nullptr) { - existingWindow->ClearClosing(); - existingWindowSnapshots.push_back(ExistingWindowSnapshot{ - existingWindow, - existingWindow->GetWorkspaceController(), - existingWindow->GetTitle(), - }); - existingWindow->ReplaceWorkspaceController(BuildWorkspaceControllerForWindow(entry)); - existingWindow->ResetInteractionState(); - if (!existingWindow->IsPrimary()) { - existingWindow->SetTitle( - BuildWindowTitle(existingWindow->GetWorkspaceController())); - if (existingWindow->GetHwnd() != nullptr) { - SetWindowTextW(existingWindow->GetHwnd(), existingWindow->GetTitle().c_str()); - } - } - continue; - } - - EditorWindowHostRuntime::CreateParams createParams = {}; - createParams.windowId = entry.windowId; - createParams.primary = entry.windowId == windowSet.primaryWindowId; - createParams.title = - createParams.primary - ? std::wstring( - m_hostRuntime.GetHostConfig().primaryWindowTitle != nullptr && - m_hostRuntime.GetHostConfig().primaryWindowTitle[0] != L'\0' - ? m_hostRuntime.GetHostConfig().primaryWindowTitle - : L"XCEngine Editor") - : BuildWindowTitle(BuildWorkspaceControllerForWindow(entry)); - if (entry.windowId == preferredNewWindowId) { - const RECT detachedRect = BuildDetachedWindowRect(preferredScreenPoint); - createParams.initialX = detachedRect.left; - createParams.initialY = detachedRect.top; - createParams.initialWidth = detachedRect.right - detachedRect.left; - createParams.initialHeight = detachedRect.bottom - detachedRect.top; - } - - if (m_hostRuntime.CreateEditorWindow(BuildWorkspaceControllerForWindow(entry), createParams) == nullptr) { - for (const ExistingWindowSnapshot& snapshot : existingWindowSnapshots) { - restoreWindowSnapshot(snapshot); - } - for (auto it = createdWindowIds.rbegin(); it != createdWindowIds.rend(); ++it) { - destroyAndEraseWindowById(*it); - } - return false; - } - createdWindowIds.push_back(entry.windowId); - } - - for (const std::unique_ptr& window : m_hostRuntime.GetWindows()) { - if (window == nullptr || - window->GetHwnd() == nullptr || - window->IsPrimary()) { - continue; - } - - const bool existsInWindowSet = - std::find( - windowIdsInSet.begin(), - windowIdsInSet.end(), - window->GetWindowId()) != windowIdsInSet.end(); - if (!existsInWindowSet) { - window->MarkClosing(); - PostMessageW(window->GetHwnd(), WM_CLOSE, 0, 0); - } - } - - return true; -} - -bool EditorWindowWorkspaceCoordinator::CommitWindowWorkspaceMutation( - const UIEditorWindowWorkspaceController& windowWorkspaceController, - std::string_view preferredNewWindowId, - const POINT& preferredScreenPoint) { - const UIEditorWindowWorkspaceSet nextWindowSet = windowWorkspaceController.GetWindowSet(); - std::string error = {}; - if (!m_workspaceStore.ValidateWindowSet(nextWindowSet, error)) { - LogRuntimeTrace("window", "workspace mutation validation failed: " + error); - return false; - } - - if (!SynchronizeWindowsFromWindowSet( - nextWindowSet, - preferredNewWindowId, - preferredScreenPoint)) { - return false; - } - - m_workspaceStore.ReplaceWindowSet(nextWindowSet); - return true; -} - -} // namespace XCEngine::UI::Editor::App::Internal diff --git a/new_editor/app/Ports/TexturePort.h b/new_editor/app/Ports/TexturePort.h index c12deca5..eed382f9 100644 --- a/new_editor/app/Ports/TexturePort.h +++ b/new_editor/app/Ports/TexturePort.h @@ -22,6 +22,12 @@ public: std::size_t size, ::XCEngine::UI::UITextureHandle& outTexture, std::string& outError) = 0; + virtual bool LoadTextureFromRgba( + const std::uint8_t* rgbaPixels, + std::uint32_t width, + std::uint32_t height, + ::XCEngine::UI::UITextureHandle& outTexture, + std::string& outError) = 0; virtual void ReleaseTexture(::XCEngine::UI::UITextureHandle& texture) = 0; }; diff --git a/new_editor/app/Rendering/Assets/BuiltInIcons.cpp b/new_editor/app/Rendering/Assets/BuiltInIcons.cpp index da2f6607..7b9d80a8 100644 --- a/new_editor/app/Rendering/Assets/BuiltInIcons.cpp +++ b/new_editor/app/Rendering/Assets/BuiltInIcons.cpp @@ -1,14 +1,53 @@ #include "BuiltInIcons.h" + #include "Bootstrap/EditorResources.h" #include "Ports/TexturePort.h" -#include "Internal/EmbeddedPngLoader.h" +#include "Support/EmbeddedPngLoader.h" +#include "Support/StringEncoding.h" +#include + +#include +#include +#include +#include +#include #include +#include +#include namespace XCEngine::UI::Editor::App { namespace { +struct PreviewDiskCacheEntry { + std::filesystem::path filePath = {}; + std::string relativePathKey = {}; +}; + +#pragma pack(push, 1) +struct ThumbnailCacheFileHeader { + char magic[8] = {}; + std::uint32_t version = 1u; + std::uint64_t fileSize = 0u; + std::int64_t lastWriteTimeTicks = 0; + std::uint32_t width = 0u; + std::uint32_t height = 0u; + std::uint32_t relativePathSize = 0u; + std::uint32_t pixelDataSize = 0u; +}; +#pragma pack(pop) + +constexpr std::size_t kMaxCachedAssetPreviews = 40u; +constexpr int kMaxPreviewLoadsPerFrame = 64; +constexpr std::size_t kMaxQueuedPreviewDecodeJobs = 64u; +constexpr int kMaxPreviewThumbnailExtent = 192; +constexpr std::size_t kPreviewWorkerCount = 2u; +constexpr int kPreviewSourceValidationIntervalFrames = 30; +constexpr std::uint32_t kThumbnailCacheVersion = 1u; +constexpr std::array kThumbnailCacheMagic = { 'X', 'C', 'T', 'H', 'M', 'B', '1', '\0' }; +constexpr std::uint32_t kMaxThumbnailCacheRelativePathBytes = 16u * 1024u; + void AppendLoadError( std::ostringstream& stream, std::string_view label, @@ -30,17 +69,436 @@ void LoadEmbeddedIconTexture( ::XCEngine::UI::UITextureHandle& outTexture, std::ostringstream& errorStream) { std::string error = {}; - if (!Internal::LoadEmbeddedPngTexture(renderer, resourceId, outTexture, error)) { + if (!LoadEmbeddedPngTexture(renderer, resourceId, outTexture, error)) { AppendLoadError(errorStream, label, error); } } +BuiltInIcons::SourceFileFingerprint MakeSourceFingerprint( + std::uint64_t fileSize, + std::int64_t lastWriteTimeTicks) { + BuiltInIcons::SourceFileFingerprint fingerprint = {}; + fingerprint.fileSize = fileSize; + fingerprint.lastWriteTimeTicks = lastWriteTimeTicks; + return fingerprint; +} + +bool QuerySourceFileFingerprint( + const std::filesystem::path& filePath, + BuiltInIcons::SourceFileFingerprint& outFingerprint) { + std::error_code errorCode = {}; + if (!std::filesystem::exists(filePath, errorCode) || errorCode) { + return false; + } + + errorCode.clear(); + if (!std::filesystem::is_regular_file(filePath, errorCode) || errorCode) { + return false; + } + + errorCode.clear(); + const auto fileSize = std::filesystem::file_size(filePath, errorCode); + if (errorCode || fileSize == static_cast(-1)) { + return false; + } + + errorCode.clear(); + const auto lastWriteTime = std::filesystem::last_write_time(filePath, errorCode); + if (errorCode) { + return false; + } + + outFingerprint.fileSize = static_cast(fileSize); + outFingerprint.lastWriteTimeTicks = + static_cast(lastWriteTime.time_since_epoch().count()); + return outFingerprint.IsValid(); +} + +std::string NormalizePathKey(const std::filesystem::path& path) { + std::wstring key = path.lexically_normal().generic_wstring(); + std::transform(key.begin(), key.end(), key.begin(), ::towlower); + return WideToUtf8(key); +} + +std::uint64_t ComputeFnv1a64(const std::string& value) { + constexpr std::uint64_t kOffsetBasis = 14695981039346656037ull; + constexpr std::uint64_t kPrime = 1099511628211ull; + + std::uint64_t hash = kOffsetBasis; + for (unsigned char character : value) { + hash ^= static_cast(character); + hash *= kPrime; + } + return hash; +} + +std::string FormatHashHex(std::uint64_t value) { + static constexpr char kHexDigits[] = "0123456789abcdef"; + + std::string result(16u, '0'); + for (int index = 15; index >= 0; --index) { + result[static_cast(index)] = kHexDigits[value & 0xFu]; + value >>= 4u; + } + return result; +} + +bool IsProjectRelativePath(const std::filesystem::path& relativePath) { + if (relativePath.empty()) { + return false; + } + + const std::wstring generic = relativePath.generic_wstring(); + return generic != L"." && generic != L".." && generic.rfind(L"../", 0) != 0; +} + +bool ResolvePreviewDiskCacheEntry( + const std::filesystem::path& projectRoot, + const std::filesystem::path& filePath, + PreviewDiskCacheEntry& outEntry) { + if (projectRoot.empty() || filePath.empty()) { + return false; + } + + std::error_code errorCode = {}; + const std::filesystem::path canonicalRoot = + std::filesystem::weakly_canonical(projectRoot, errorCode); + if (errorCode || canonicalRoot.empty()) { + return false; + } + + errorCode.clear(); + const std::filesystem::path canonicalAsset = + std::filesystem::weakly_canonical(filePath, errorCode); + if (errorCode || canonicalAsset.empty()) { + return false; + } + + errorCode.clear(); + std::filesystem::path relativePath = + std::filesystem::relative(canonicalAsset, canonicalRoot, errorCode); + if (errorCode) { + return false; + } + + relativePath = relativePath.lexically_normal(); + if (!IsProjectRelativePath(relativePath)) { + return false; + } + + outEntry.relativePathKey = NormalizePathKey(relativePath); + const std::string hashFileName = + FormatHashHex(ComputeFnv1a64(outEntry.relativePathKey)) + ".thumb"; + outEntry.filePath = + (canonicalRoot / L".xceditor" / L"thumbs" / + std::filesystem::path(Utf8ToWide(hashFileName))).lexically_normal(); + return true; +} + +bool ReadThumbnailCacheFile( + const std::filesystem::path& cacheFilePath, + const std::string& relativePathKey, + const BuiltInIcons::SourceFileFingerprint& sourceFingerprint, + BuiltInIcons::LoadedTexturePixels& outTexturePixels) { + std::ifstream stream(cacheFilePath, std::ios::binary | std::ios::in); + if (!stream.is_open()) { + return false; + } + + ThumbnailCacheFileHeader header = {}; + if (!stream.read(reinterpret_cast(&header), sizeof(header)).good()) { + return false; + } + + if (std::memcmp(header.magic, kThumbnailCacheMagic.data(), kThumbnailCacheMagic.size()) != 0 || + header.version != kThumbnailCacheVersion || + header.fileSize != sourceFingerprint.fileSize || + header.lastWriteTimeTicks != sourceFingerprint.lastWriteTimeTicks || + header.relativePathSize == 0u || + header.relativePathSize > kMaxThumbnailCacheRelativePathBytes || + header.width == 0u || + header.height == 0u || + header.width > static_cast(kMaxPreviewThumbnailExtent) || + header.height > static_cast(kMaxPreviewThumbnailExtent)) { + return false; + } + + const std::uint64_t expectedPixelBytes = + static_cast(header.width) * static_cast(header.height) * 4ull; + if (expectedPixelBytes == 0u || + expectedPixelBytes > static_cast((std::numeric_limits::max)()) || + header.pixelDataSize != expectedPixelBytes) { + return false; + } + + std::string cachedRelativePath(header.relativePathSize, '\0'); + if (!stream.read( + cachedRelativePath.data(), + static_cast(cachedRelativePath.size())).good()) { + return false; + } + if (cachedRelativePath != relativePathKey) { + return false; + } + + outTexturePixels.width = static_cast(header.width); + outTexturePixels.height = static_cast(header.height); + outTexturePixels.rgbaPixels.resize(static_cast(header.pixelDataSize)); + if (!stream.read( + reinterpret_cast(outTexturePixels.rgbaPixels.data()), + static_cast(outTexturePixels.rgbaPixels.size())).good()) { + outTexturePixels = {}; + return false; + } + + return true; +} + +void RemoveDiskCacheFile(const std::filesystem::path& cacheFilePath) { + std::error_code errorCode = {}; + std::filesystem::remove(cacheFilePath, errorCode); +} + +bool WriteThumbnailCacheFile( + const std::filesystem::path& cacheFilePath, + const std::string& relativePathKey, + const BuiltInIcons::SourceFileFingerprint& sourceFingerprint, + const BuiltInIcons::LoadedTexturePixels& texturePixels) { + if (cacheFilePath.empty() || + relativePathKey.empty() || + relativePathKey.size() > kMaxThumbnailCacheRelativePathBytes || + !sourceFingerprint.IsValid() || + texturePixels.width <= 0 || + texturePixels.height <= 0 || + texturePixels.rgbaPixels.empty()) { + return false; + } + + const std::uint64_t pixelDataSize = + static_cast(texturePixels.rgbaPixels.size()); + if (pixelDataSize != + static_cast(texturePixels.width) * + static_cast(texturePixels.height) * 4ull || + pixelDataSize > + static_cast((std::numeric_limits::max)())) { + return false; + } + + std::error_code errorCode = {}; + std::filesystem::create_directories(cacheFilePath.parent_path(), errorCode); + if (errorCode) { + return false; + } + + const std::filesystem::path tempPath = + cacheFilePath.parent_path() / (cacheFilePath.filename().wstring() + L".tmp"); + std::ofstream stream(tempPath, std::ios::binary | std::ios::out | std::ios::trunc); + if (!stream.is_open()) { + return false; + } + + ThumbnailCacheFileHeader header = {}; + std::memcpy(header.magic, kThumbnailCacheMagic.data(), kThumbnailCacheMagic.size()); + header.version = kThumbnailCacheVersion; + header.fileSize = sourceFingerprint.fileSize; + header.lastWriteTimeTicks = sourceFingerprint.lastWriteTimeTicks; + header.width = static_cast(texturePixels.width); + header.height = static_cast(texturePixels.height); + header.relativePathSize = static_cast(relativePathKey.size()); + header.pixelDataSize = static_cast(pixelDataSize); + + const bool writeSucceeded = + stream.write(reinterpret_cast(&header), sizeof(header)).good() && + stream.write( + relativePathKey.data(), + static_cast(relativePathKey.size())).good() && + stream.write( + reinterpret_cast(texturePixels.rgbaPixels.data()), + static_cast(texturePixels.rgbaPixels.size())).good(); + stream.close(); + if (!writeSucceeded) { + RemoveDiskCacheFile(tempPath); + return false; + } + + errorCode.clear(); + std::filesystem::remove(cacheFilePath, errorCode); + errorCode.clear(); + std::filesystem::rename(tempPath, cacheFilePath, errorCode); + if (errorCode) { + RemoveDiskCacheFile(tempPath); + return false; + } + + return true; +} + +bool ReadFileBytes( + const std::filesystem::path& filePath, + std::vector& bytes) { + std::ifstream stream(filePath, std::ios::binary | std::ios::ate); + if (!stream.is_open()) { + return false; + } + + const std::streamsize size = stream.tellg(); + if (size <= 0) { + return false; + } + + bytes.resize(static_cast(size)); + stream.seekg(0, std::ios::beg); + return stream.read(reinterpret_cast(bytes.data()), size).good(); +} + +void ResizeRgbaImageBilinear( + const std::uint8_t* srcPixels, + int srcWidth, + int srcHeight, + int dstWidth, + int dstHeight, + std::vector& dstPixels) { + dstPixels.resize( + static_cast(dstWidth) * static_cast(dstHeight) * 4u); + if (!srcPixels || + srcWidth <= 0 || + srcHeight <= 0 || + dstWidth <= 0 || + dstHeight <= 0) { + return; + } + + for (int dstY = 0; dstY < dstHeight; ++dstY) { + const float srcY = dstHeight > 1 + ? static_cast(dstY) * static_cast(srcHeight - 1) / + static_cast(dstHeight - 1) + : 0.0f; + const int y0 = static_cast(srcY); + const int y1 = (std::min)(y0 + 1, srcHeight - 1); + const float ty = srcY - static_cast(y0); + + for (int dstX = 0; dstX < dstWidth; ++dstX) { + const float srcX = dstWidth > 1 + ? static_cast(dstX) * static_cast(srcWidth - 1) / + static_cast(dstWidth - 1) + : 0.0f; + const int x0 = static_cast(srcX); + const int x1 = (std::min)(x0 + 1, srcWidth - 1); + const float tx = srcX - static_cast(x0); + + const std::size_t dstOffset = + (static_cast(dstY) * static_cast(dstWidth) + + static_cast(dstX)) * 4u; + const std::size_t srcOffset00 = + (static_cast(y0) * static_cast(srcWidth) + + static_cast(x0)) * 4u; + const std::size_t srcOffset10 = + (static_cast(y0) * static_cast(srcWidth) + + static_cast(x1)) * 4u; + const std::size_t srcOffset01 = + (static_cast(y1) * static_cast(srcWidth) + + static_cast(x0)) * 4u; + const std::size_t srcOffset11 = + (static_cast(y1) * static_cast(srcWidth) + + static_cast(x1)) * 4u; + + for (std::size_t channel = 0u; channel < 4u; ++channel) { + const float top = + static_cast(srcPixels[srcOffset00 + channel]) * (1.0f - tx) + + static_cast(srcPixels[srcOffset10 + channel]) * tx; + const float bottom = + static_cast(srcPixels[srcOffset01 + channel]) * (1.0f - tx) + + static_cast(srcPixels[srcOffset11 + channel]) * tx; + dstPixels[dstOffset + channel] = + static_cast(top * (1.0f - ty) + bottom * ty + 0.5f); + } + } + } +} + +bool DownscalePreviewTextureIfNeeded(BuiltInIcons::LoadedTexturePixels& texturePixels) { + if (texturePixels.width <= 0 || + texturePixels.height <= 0 || + texturePixels.rgbaPixels.empty()) { + return false; + } + + const int maxExtent = (std::max)(texturePixels.width, texturePixels.height); + if (maxExtent <= kMaxPreviewThumbnailExtent) { + return true; + } + + const float scale = + static_cast(kMaxPreviewThumbnailExtent) / static_cast(maxExtent); + const int dstWidth = + (std::max)(1, static_cast(static_cast(texturePixels.width) * scale + 0.5f)); + const int dstHeight = + (std::max)(1, static_cast(static_cast(texturePixels.height) * scale + 0.5f)); + + std::vector resizedPixels = {}; + ResizeRgbaImageBilinear( + texturePixels.rgbaPixels.data(), + texturePixels.width, + texturePixels.height, + dstWidth, + dstHeight, + resizedPixels); + if (resizedPixels.empty()) { + return false; + } + + texturePixels.rgbaPixels = std::move(resizedPixels); + texturePixels.width = dstWidth; + texturePixels.height = dstHeight; + return true; +} + +bool DecodeTextureFromFile( + const std::filesystem::path& filePath, + BuiltInIcons::LoadedTexturePixels& outTexturePixels) { + if (!std::filesystem::exists(filePath)) { + return false; + } + + std::vector fileData = {}; + if (!ReadFileBytes(filePath, fileData)) { + return false; + } + + int width = 0; + int height = 0; + int channels = 0; + stbi_uc* pixels = stbi_load_from_memory( + reinterpret_cast(fileData.data()), + static_cast(fileData.size()), + &width, + &height, + &channels, + STBI_rgb_alpha); + if (!pixels || width <= 0 || height <= 0) { + if (pixels != nullptr) { + stbi_image_free(pixels); + } + return false; + } + + outTexturePixels.width = width; + outTexturePixels.height = height; + outTexturePixels.rgbaPixels.assign( + pixels, + pixels + static_cast(width) * static_cast(height) * 4u); + stbi_image_free(pixels); + + return DownscalePreviewTextureIfNeeded(outTexturePixels); +} + } // namespace void BuiltInIcons::Initialize(Ports::TexturePort& renderer) { Shutdown(); m_renderer = &renderer; + StartPreviewWorkers(); std::ostringstream errorStream = {}; LoadEmbeddedIconTexture( @@ -61,21 +519,385 @@ void BuiltInIcons::Initialize(Ports::TexturePort& renderer) { "scene_icon.png", m_sceneIcon, errorStream); + LoadEmbeddedIconTexture( + renderer, + IDR_PNG_CAMERA_GIZMO_ICON, + "camera_gizmo.png", + m_cameraGizmoIcon, + errorStream); + LoadEmbeddedIconTexture( + renderer, + IDR_PNG_DIRECTIONAL_LIGHT_GIZMO_ICON, + "directional_light_gizmo.png", + m_directionalLightGizmoIcon, + errorStream); + LoadEmbeddedIconTexture( + renderer, + IDR_PNG_POINT_LIGHT_GIZMO_ICON, + "point_light_gizmo.png", + m_pointLightGizmoIcon, + errorStream); + LoadEmbeddedIconTexture( + renderer, + IDR_PNG_SPOT_LIGHT_GIZMO_ICON, + "spot_light_gizmo.png", + m_spotLightGizmoIcon, + errorStream); + LoadEmbeddedIconTexture( + renderer, + IDR_PNG_PLAY_BUTTON_ICON, + "play_button.png", + m_playButtonIcon, + errorStream); + LoadEmbeddedIconTexture( + renderer, + IDR_PNG_PAUSE_BUTTON_ICON, + "pause_button.png", + m_pauseButtonIcon, + errorStream); + LoadEmbeddedIconTexture( + renderer, + IDR_PNG_STEP_BUTTON_ICON, + "step_button.png", + m_stepButtonIcon, + errorStream); + m_frameIndex = 0; + m_previewLoadsThisFrame = 0; m_lastError = errorStream.str(); } void BuiltInIcons::Shutdown() { + StopPreviewWorkers(); + ResetAssetPreviewCache(); + if (m_renderer != nullptr) { m_renderer->ReleaseTexture(m_folderIcon); m_renderer->ReleaseTexture(m_gameObjectIcon); m_renderer->ReleaseTexture(m_sceneIcon); + m_renderer->ReleaseTexture(m_cameraGizmoIcon); + m_renderer->ReleaseTexture(m_directionalLightGizmoIcon); + m_renderer->ReleaseTexture(m_pointLightGizmoIcon); + m_renderer->ReleaseTexture(m_spotLightGizmoIcon); + m_renderer->ReleaseTexture(m_playButtonIcon); + m_renderer->ReleaseTexture(m_pauseButtonIcon); + m_renderer->ReleaseTexture(m_stepButtonIcon); } m_renderer = nullptr; + m_frameIndex = 0; + m_previewLoadsThisFrame = 0; m_lastError.clear(); } +void BuiltInIcons::BeginFrame() { + ++m_frameIndex; + m_previewLoadsThisFrame = 0; + MaintainPreviewRuntimeState(); + PruneAssetPreviewCache(); +} + +void BuiltInIcons::ResetAssetPreviewCache() { + if (m_renderer != nullptr) { + for (auto& entry : m_assetPreviews) { + m_renderer->ReleaseTexture(entry.second.texture); + } + } + m_assetPreviews.clear(); +} + +void BuiltInIcons::StartPreviewWorkers() { + if (m_previewWorkersRunning) { + return; + } + + m_previewWorkersRunning = true; + m_previewWorkers.reserve(kPreviewWorkerCount); + for (std::size_t workerIndex = 0u; workerIndex < kPreviewWorkerCount; ++workerIndex) { + m_previewWorkers.emplace_back([this]() { + PreviewWorkerMain(); + }); + } +} + +void BuiltInIcons::StopPreviewWorkers() { + { + std::lock_guard lock(m_previewQueueMutex); + m_previewWorkersRunning = false; + m_previewDecodeQueue.clear(); + m_previewDecodeResults.clear(); + m_pendingPreviewDecodeJobs = 0u; + } + m_previewQueueEvent.notify_all(); + + for (std::thread& worker : m_previewWorkers) { + if (worker.joinable()) { + worker.join(); + } + } + m_previewWorkers.clear(); +} + +void BuiltInIcons::PreviewWorkerMain() { + while (true) { + PreviewDecodeJob job = {}; + { + std::unique_lock lock(m_previewQueueMutex); + m_previewQueueEvent.wait(lock, [this]() { + return !m_previewWorkersRunning || !m_previewDecodeQueue.empty(); + }); + + if (!m_previewWorkersRunning && m_previewDecodeQueue.empty()) { + return; + } + + job = std::move(m_previewDecodeQueue.front()); + m_previewDecodeQueue.pop_front(); + } + + PreviewDecodeResult result = {}; + result.key = std::move(job.key); + result.fileSize = job.fileSize; + result.lastWriteTimeTicks = job.lastWriteTimeTicks; + + if (job.useDiskCache) { + result.success = ReadThumbnailCacheFile( + job.cacheFilePath, + job.relativePathKey, + MakeSourceFingerprint(job.fileSize, job.lastWriteTimeTicks), + result.pixels); + if (!result.success) { + RemoveDiskCacheFile(job.cacheFilePath); + } + } + + if (!result.success) { + result.success = DecodeTextureFromFile(job.filePath, result.pixels); + if (result.success && job.useDiskCache) { + WriteThumbnailCacheFile( + job.cacheFilePath, + job.relativePathKey, + MakeSourceFingerprint(job.fileSize, job.lastWriteTimeTicks), + result.pixels); + } + } + + { + std::lock_guard lock(m_previewQueueMutex); + m_previewDecodeResults.push_back(std::move(result)); + } + } +} + +void BuiltInIcons::DrainPreviewDecodeResults() { + std::deque completedResults = {}; + { + std::lock_guard lock(m_previewQueueMutex); + completedResults.swap(m_previewDecodeResults); + } + + for (PreviewDecodeResult& result : completedResults) { + if (m_pendingPreviewDecodeJobs > 0u) { + --m_pendingPreviewDecodeJobs; + } + + auto iterator = m_assetPreviews.find(result.key); + if (iterator == m_assetPreviews.end()) { + continue; + } + + CachedAssetPreview& preview = iterator->second; + preview.decodeQueued = false; + if (preview.sourceFingerprint != + MakeSourceFingerprint(result.fileSize, result.lastWriteTimeTicks)) { + continue; + } + + if (!result.success) { + preview.loadFailed = true; + preview.decodedPixels.reset(); + continue; + } + + preview.loadFailed = false; + preview.decodedPixels = + std::make_unique(std::move(result.pixels)); + } +} + +void BuiltInIcons::MaintainPreviewRuntimeState() { + DrainPreviewDecodeResults(); +} + +void BuiltInIcons::InvalidateAssetPreview(CachedAssetPreview& preview) { + if (m_renderer != nullptr) { + m_renderer->ReleaseTexture(preview.texture); + } else { + preview.texture = {}; + } + + preview.decodedPixels.reset(); + preview.decodeQueued = false; + preview.loadFailed = false; +} + +bool BuiltInIcons::RefreshAssetPreviewSourceFingerprint( + const std::filesystem::path& filePath, + CachedAssetPreview& preview) { + SourceFileFingerprint currentFingerprint = {}; + if (!QuerySourceFileFingerprint(filePath, currentFingerprint)) { + InvalidateAssetPreview(preview); + preview.sourceFingerprint = {}; + preview.loadFailed = true; + return false; + } + + if (preview.sourceFingerprint != currentFingerprint) { + InvalidateAssetPreview(preview); + preview.sourceFingerprint = currentFingerprint; + } + + return true; +} + +bool BuiltInIcons::QueuePreviewDecode( + const std::string& key, + const std::filesystem::path& filePath, + const SourceFileFingerprint& sourceFingerprint, + const std::filesystem::path& projectRoot) { + PreviewDiskCacheEntry diskCacheEntry = {}; + const bool useDiskCache = + ResolvePreviewDiskCacheEntry(projectRoot, filePath, diskCacheEntry); + + std::lock_guard lock(m_previewQueueMutex); + if (!m_previewWorkersRunning || + m_pendingPreviewDecodeJobs >= kMaxQueuedPreviewDecodeJobs) { + return false; + } + + PreviewDecodeJob job = {}; + job.key = key; + job.filePath = filePath; + job.fileSize = sourceFingerprint.fileSize; + job.lastWriteTimeTicks = sourceFingerprint.lastWriteTimeTicks; + if (useDiskCache) { + job.cacheFilePath = std::move(diskCacheEntry.filePath); + job.relativePathKey = std::move(diskCacheEntry.relativePathKey); + job.useDiskCache = true; + } + + m_previewDecodeQueue.push_back(std::move(job)); + ++m_pendingPreviewDecodeJobs; + m_previewQueueEvent.notify_one(); + return true; +} + +BuiltInIcons::CachedAssetPreview* BuiltInIcons::GetOrCreateAssetPreview( + const std::filesystem::path& assetPath, + const std::filesystem::path& projectRoot) { + if (m_renderer == nullptr || assetPath.empty()) { + return nullptr; + } + + if (m_frameIndex <= 0) { + BeginFrame(); + } else { + MaintainPreviewRuntimeState(); + } + + const std::filesystem::path normalizedAssetPath = assetPath.lexically_normal(); + const std::string key = NormalizePathKey(normalizedAssetPath); + auto [iterator, inserted] = m_assetPreviews.try_emplace(key); + CachedAssetPreview& preview = iterator->second; + preview.lastUsedFrame = m_frameIndex; + + if (inserted || + preview.lastSourceValidationFrame < 0 || + m_frameIndex - preview.lastSourceValidationFrame >= + kPreviewSourceValidationIntervalFrames) { + preview.lastSourceValidationFrame = m_frameIndex; + if (!RefreshAssetPreviewSourceFingerprint(normalizedAssetPath, preview)) { + return &preview; + } + } + + if (!preview.sourceFingerprint.IsValid()) { + return &preview; + } + + if (preview.texture.IsValid()) { + return &preview; + } + + if (preview.decodedPixels) { + std::string error = {}; + if (!m_renderer->LoadTextureFromRgba( + preview.decodedPixels->rgbaPixels.data(), + static_cast(preview.decodedPixels->width), + static_cast(preview.decodedPixels->height), + preview.texture, + error)) { + preview.decodedPixels.reset(); + preview.loadFailed = true; + return &preview; + } + + preview.decodedPixels.reset(); + preview.loadFailed = false; + return &preview; + } + + if (preview.decodeQueued || preview.loadFailed) { + return &preview; + } + + if (m_previewLoadsThisFrame >= kMaxPreviewLoadsPerFrame) { + return &preview; + } + + if (QueuePreviewDecode(key, normalizedAssetPath, preview.sourceFingerprint, projectRoot)) { + preview.decodeQueued = true; + ++m_previewLoadsThisFrame; + } + + return &preview; +} + +void BuiltInIcons::PruneAssetPreviewCache() { + if (m_assetPreviews.size() <= kMaxCachedAssetPreviews) { + return; + } + + std::vector> candidates = {}; + candidates.reserve(m_assetPreviews.size()); + for (const auto& entry : m_assetPreviews) { + if (entry.second.decodeQueued) { + continue; + } + candidates.emplace_back(entry.first, entry.second.lastUsedFrame); + } + + std::sort( + candidates.begin(), + candidates.end(), + [](const auto& lhs, const auto& rhs) { + return lhs.second < rhs.second; + }); + + const std::size_t removeCount = m_assetPreviews.size() - kMaxCachedAssetPreviews; + for (std::size_t index = 0u; index < removeCount && index < candidates.size(); ++index) { + auto iterator = m_assetPreviews.find(candidates[index].first); + if (iterator == m_assetPreviews.end()) { + continue; + } + + if (m_renderer != nullptr) { + m_renderer->ReleaseTexture(iterator->second.texture); + } + m_assetPreviews.erase(iterator); + } +} + const ::XCEngine::UI::UITextureHandle& BuiltInIcons::Resolve( BuiltInIconKind kind) const { switch (kind) { @@ -85,14 +907,39 @@ const ::XCEngine::UI::UITextureHandle& BuiltInIcons::Resolve( return m_gameObjectIcon; case BuiltInIconKind::Scene: return m_sceneIcon; + case BuiltInIconKind::CameraGizmo: + return m_cameraGizmoIcon; + case BuiltInIconKind::DirectionalLightGizmo: + return m_directionalLightGizmoIcon; + case BuiltInIconKind::PointLightGizmo: + return m_pointLightGizmoIcon; + case BuiltInIconKind::SpotLightGizmo: + return m_spotLightGizmoIcon; + case BuiltInIconKind::PlayButton: + return m_playButtonIcon; + case BuiltInIconKind::PauseButton: + return m_pauseButtonIcon; + case BuiltInIconKind::StepButton: + return m_stepButtonIcon; default: return m_folderIcon; } } +const ::XCEngine::UI::UITextureHandle* BuiltInIcons::ResolveAssetPreview( + const std::filesystem::path& assetPath, + const std::filesystem::path& projectRoot) { + CachedAssetPreview* preview = GetOrCreateAssetPreview(assetPath, projectRoot); + if (preview == nullptr || !preview->texture.IsValid()) { + return nullptr; + } + + PruneAssetPreviewCache(); + return &preview->texture; +} + const std::string& BuiltInIcons::GetLastError() const { return m_lastError; } } // namespace XCEngine::UI::Editor::App - diff --git a/new_editor/app/Rendering/Assets/BuiltInIcons.h b/new_editor/app/Rendering/Assets/BuiltInIcons.h index 12a26404..08c0563e 100644 --- a/new_editor/app/Rendering/Assets/BuiltInIcons.h +++ b/new_editor/app/Rendering/Assets/BuiltInIcons.h @@ -4,30 +4,131 @@ #include +#include #include +#include +#include +#include +#include #include +#include +#include +#include namespace XCEngine::UI::Editor::App { enum class BuiltInIconKind : std::uint8_t { Folder = 0, GameObject, - Scene + Scene, + CameraGizmo, + DirectionalLightGizmo, + PointLightGizmo, + SpotLightGizmo, + PlayButton, + PauseButton, + StepButton }; class BuiltInIcons { public: void Initialize(Ports::TexturePort& renderer); void Shutdown(); + void BeginFrame(); const ::XCEngine::UI::UITextureHandle& Resolve(BuiltInIconKind kind) const; + const ::XCEngine::UI::UITextureHandle* ResolveAssetPreview( + const std::filesystem::path& assetPath, + const std::filesystem::path& projectRoot); const std::string& GetLastError() const; + struct LoadedTexturePixels { + std::vector rgbaPixels = {}; + int width = 0; + int height = 0; + }; + + struct SourceFileFingerprint { + std::uint64_t fileSize = 0; + std::int64_t lastWriteTimeTicks = 0; + + bool IsValid() const { + return fileSize > 0 && lastWriteTimeTicks != 0; + } + + bool operator==(const SourceFileFingerprint&) const = default; + }; + + struct PreviewDecodeJob { + std::string key = {}; + std::filesystem::path filePath = {}; + std::filesystem::path cacheFilePath = {}; + std::string relativePathKey = {}; + std::uint64_t fileSize = 0; + std::int64_t lastWriteTimeTicks = 0; + bool useDiskCache = false; + }; + + struct PreviewDecodeResult { + std::string key = {}; + LoadedTexturePixels pixels = {}; + std::uint64_t fileSize = 0; + std::int64_t lastWriteTimeTicks = 0; + bool success = false; + }; + + struct CachedAssetPreview { + ::XCEngine::UI::UITextureHandle texture = {}; + std::unique_ptr decodedPixels = {}; + SourceFileFingerprint sourceFingerprint = {}; + bool decodeQueued = false; + bool loadFailed = false; + std::int64_t lastUsedFrame = -1; + std::int64_t lastSourceValidationFrame = -1; + }; + private: + void ResetAssetPreviewCache(); + void StartPreviewWorkers(); + void StopPreviewWorkers(); + void PreviewWorkerMain(); + void DrainPreviewDecodeResults(); + void MaintainPreviewRuntimeState(); + void InvalidateAssetPreview(CachedAssetPreview& preview); + bool RefreshAssetPreviewSourceFingerprint( + const std::filesystem::path& filePath, + CachedAssetPreview& preview); + bool QueuePreviewDecode( + const std::string& key, + const std::filesystem::path& filePath, + const SourceFileFingerprint& sourceFingerprint, + const std::filesystem::path& projectRoot); + CachedAssetPreview* GetOrCreateAssetPreview( + const std::filesystem::path& assetPath, + const std::filesystem::path& projectRoot); + void PruneAssetPreviewCache(); + Ports::TexturePort* m_renderer = nullptr; ::XCEngine::UI::UITextureHandle m_folderIcon = {}; ::XCEngine::UI::UITextureHandle m_gameObjectIcon = {}; ::XCEngine::UI::UITextureHandle m_sceneIcon = {}; + ::XCEngine::UI::UITextureHandle m_cameraGizmoIcon = {}; + ::XCEngine::UI::UITextureHandle m_directionalLightGizmoIcon = {}; + ::XCEngine::UI::UITextureHandle m_pointLightGizmoIcon = {}; + ::XCEngine::UI::UITextureHandle m_spotLightGizmoIcon = {}; + ::XCEngine::UI::UITextureHandle m_playButtonIcon = {}; + ::XCEngine::UI::UITextureHandle m_pauseButtonIcon = {}; + ::XCEngine::UI::UITextureHandle m_stepButtonIcon = {}; + std::unordered_map m_assetPreviews = {}; + std::vector m_previewWorkers = {}; + std::deque m_previewDecodeQueue = {}; + std::deque m_previewDecodeResults = {}; + std::mutex m_previewQueueMutex = {}; + std::condition_variable m_previewQueueEvent = {}; + bool m_previewWorkersRunning = false; + std::size_t m_pendingPreviewDecodeJobs = 0u; + std::int64_t m_frameIndex = 0; + int m_previewLoadsThisFrame = 0; std::string m_lastError = {}; }; diff --git a/new_editor/app/Rendering/D3D12/D3D12HostDeviceLifecycle.cpp b/new_editor/app/Rendering/D3D12/D3D12HostDevice.cpp similarity index 51% rename from new_editor/app/Rendering/D3D12/D3D12HostDeviceLifecycle.cpp rename to new_editor/app/Rendering/D3D12/D3D12HostDevice.cpp index 2093cbb3..a82ff795 100644 --- a/new_editor/app/Rendering/D3D12/D3D12HostDeviceLifecycle.cpp +++ b/new_editor/app/Rendering/D3D12/D3D12HostDevice.cpp @@ -1,5 +1,4 @@ -#include "D3D12HostDevice.h" - +#include "D3D12HostDevice.h" #include namespace XCEngine::UI::Editor::Host { @@ -152,3 +151,157 @@ D3D12CommandList* D3D12HostDevice::GetD3D12CommandList(std::uint32_t frameIndex) } } // namespace XCEngine::UI::Editor::Host + +namespace XCEngine::UI::Editor::Host { + +using ::XCEngine::RHI::D3D12CommandList; +using ::XCEngine::RHI::RHICommandList; + +bool D3D12HostDevice::BeginFrame(std::uint32_t frameIndex) { + WaitForFrame(frameIndex); + + RHICommandList* commandList = GetCommandList(frameIndex); + D3D12CommandList* d3d12CommandList = GetD3D12CommandList(frameIndex); + if (commandList == nullptr || d3d12CommandList == nullptr) { + m_lastError = "BeginFrame could not resolve the active command list."; + return false; + } + + commandList->Reset(); + m_lastError.clear(); + return true; +} + +bool D3D12HostDevice::SubmitFrame(std::uint32_t frameIndex) { + RHICommandList* commandList = GetCommandList(frameIndex); + if (commandList == nullptr || m_commandQueue == nullptr) { + m_lastError = "SubmitFrame requires an initialized command list and queue."; + return false; + } + + commandList->Close(); + void* commandLists[] = { commandList }; + m_commandQueue->ExecuteCommandLists(1, commandLists); + m_lastError.clear(); + return true; +} + +bool D3D12HostDevice::SignalFrameCompletion(std::uint32_t frameIndex) { + ID3D12CommandQueue* commandQueue = GetCommandQueue(); + if (commandQueue == nullptr || + m_frameCompletionFence == nullptr || + frameIndex >= m_frameFenceValues.size()) { + return false; + } + + ++m_lastSubmittedFrameValue; + const HRESULT hr = commandQueue->Signal( + m_frameCompletionFence.Get(), + m_lastSubmittedFrameValue); + if (SUCCEEDED(hr)) { + m_frameFenceValues[frameIndex] = m_lastSubmittedFrameValue; + m_lastError.clear(); + } + return SUCCEEDED(hr); +} + +void D3D12HostDevice::WaitForFrame(std::uint32_t frameIndex) { + if (m_frameCompletionFence == nullptr || + m_frameCompletionEvent == nullptr || + frameIndex >= m_frameFenceValues.size()) { + return; + } + + const std::uint64_t fenceValue = m_frameFenceValues[frameIndex]; + if (fenceValue == 0u || + m_frameCompletionFence->GetCompletedValue() >= fenceValue) { + return; + } + + const HRESULT hr = m_frameCompletionFence->SetEventOnCompletion( + fenceValue, + m_frameCompletionEvent); + if (SUCCEEDED(hr)) { + WaitForSingleObject(m_frameCompletionEvent, INFINITE); + } +} + +void D3D12HostDevice::WaitForGpuIdle() { + ID3D12CommandQueue* commandQueue = GetCommandQueue(); + if (commandQueue == nullptr || + m_frameCompletionFence == nullptr || + m_frameCompletionEvent == nullptr) { + if (m_commandQueue != nullptr) { + m_commandQueue->WaitForIdle(); + } + return; + } + + ++m_lastSubmittedFrameValue; + const std::uint64_t fenceValue = m_lastSubmittedFrameValue; + const HRESULT signalHr = commandQueue->Signal( + m_frameCompletionFence.Get(), + fenceValue); + if (FAILED(signalHr)) { + return; + } + + if (m_frameCompletionFence->GetCompletedValue() >= fenceValue) { + return; + } + + const HRESULT waitHr = m_frameCompletionFence->SetEventOnCompletion( + fenceValue, + m_frameCompletionEvent); + if (SUCCEEDED(waitHr)) { + WaitForSingleObject(m_frameCompletionEvent, INFINITE); + } +} + +void D3D12HostDevice::ResetFrameTracking() { + m_frameFenceValues.fill(0u); +} + +} // namespace XCEngine::UI::Editor::Host + +namespace XCEngine::UI::Editor::Host { + +bool D3D12HostDevice::InitializeFrameCompletionFence() { + ReleaseFrameCompletionFence(); + + ID3D12Device* device = GetDevice(); + if (device == nullptr) { + return false; + } + + const HRESULT hr = device->CreateFence( + 0u, + D3D12_FENCE_FLAG_NONE, + IID_PPV_ARGS(m_frameCompletionFence.ReleaseAndGetAddressOf())); + if (FAILED(hr)) { + return false; + } + + m_frameCompletionEvent = CreateEventW(nullptr, FALSE, FALSE, nullptr); + if (m_frameCompletionEvent == nullptr) { + m_frameCompletionFence.Reset(); + return false; + } + + m_lastSubmittedFrameValue = 0u; + return true; +} + +void D3D12HostDevice::ReleaseFrameCompletionFence() { + if (m_frameCompletionEvent != nullptr) { + CloseHandle(m_frameCompletionEvent); + m_frameCompletionEvent = nullptr; + } + + m_frameCompletionFence.Reset(); + m_frameFenceValues.fill(0u); + m_lastSubmittedFrameValue = 0u; +} + +} // namespace XCEngine::UI::Editor::Host + diff --git a/new_editor/app/Rendering/D3D12/D3D12HostDeviceFence.cpp b/new_editor/app/Rendering/D3D12/D3D12HostDeviceFence.cpp deleted file mode 100644 index bf36fd4e..00000000 --- a/new_editor/app/Rendering/D3D12/D3D12HostDeviceFence.cpp +++ /dev/null @@ -1,42 +0,0 @@ -#include "D3D12HostDevice.h" - -namespace XCEngine::UI::Editor::Host { - -bool D3D12HostDevice::InitializeFrameCompletionFence() { - ReleaseFrameCompletionFence(); - - ID3D12Device* device = GetDevice(); - if (device == nullptr) { - return false; - } - - const HRESULT hr = device->CreateFence( - 0u, - D3D12_FENCE_FLAG_NONE, - IID_PPV_ARGS(m_frameCompletionFence.ReleaseAndGetAddressOf())); - if (FAILED(hr)) { - return false; - } - - m_frameCompletionEvent = CreateEventW(nullptr, FALSE, FALSE, nullptr); - if (m_frameCompletionEvent == nullptr) { - m_frameCompletionFence.Reset(); - return false; - } - - m_lastSubmittedFrameValue = 0u; - return true; -} - -void D3D12HostDevice::ReleaseFrameCompletionFence() { - if (m_frameCompletionEvent != nullptr) { - CloseHandle(m_frameCompletionEvent); - m_frameCompletionEvent = nullptr; - } - - m_frameCompletionFence.Reset(); - m_frameFenceValues.fill(0u); - m_lastSubmittedFrameValue = 0u; -} - -} // namespace XCEngine::UI::Editor::Host diff --git a/new_editor/app/Rendering/D3D12/D3D12HostDeviceFrame.cpp b/new_editor/app/Rendering/D3D12/D3D12HostDeviceFrame.cpp deleted file mode 100644 index 69b660d2..00000000 --- a/new_editor/app/Rendering/D3D12/D3D12HostDeviceFrame.cpp +++ /dev/null @@ -1,113 +0,0 @@ -#include "D3D12HostDevice.h" - -namespace XCEngine::UI::Editor::Host { - -using ::XCEngine::RHI::D3D12CommandList; -using ::XCEngine::RHI::RHICommandList; - -bool D3D12HostDevice::BeginFrame(std::uint32_t frameIndex) { - WaitForFrame(frameIndex); - - RHICommandList* commandList = GetCommandList(frameIndex); - D3D12CommandList* d3d12CommandList = GetD3D12CommandList(frameIndex); - if (commandList == nullptr || d3d12CommandList == nullptr) { - m_lastError = "BeginFrame could not resolve the active command list."; - return false; - } - - commandList->Reset(); - m_lastError.clear(); - return true; -} - -bool D3D12HostDevice::SubmitFrame(std::uint32_t frameIndex) { - RHICommandList* commandList = GetCommandList(frameIndex); - if (commandList == nullptr || m_commandQueue == nullptr) { - m_lastError = "SubmitFrame requires an initialized command list and queue."; - return false; - } - - commandList->Close(); - void* commandLists[] = { commandList }; - m_commandQueue->ExecuteCommandLists(1, commandLists); - m_lastError.clear(); - return true; -} - -bool D3D12HostDevice::SignalFrameCompletion(std::uint32_t frameIndex) { - ID3D12CommandQueue* commandQueue = GetCommandQueue(); - if (commandQueue == nullptr || - m_frameCompletionFence == nullptr || - frameIndex >= m_frameFenceValues.size()) { - return false; - } - - ++m_lastSubmittedFrameValue; - const HRESULT hr = commandQueue->Signal( - m_frameCompletionFence.Get(), - m_lastSubmittedFrameValue); - if (SUCCEEDED(hr)) { - m_frameFenceValues[frameIndex] = m_lastSubmittedFrameValue; - m_lastError.clear(); - } - return SUCCEEDED(hr); -} - -void D3D12HostDevice::WaitForFrame(std::uint32_t frameIndex) { - if (m_frameCompletionFence == nullptr || - m_frameCompletionEvent == nullptr || - frameIndex >= m_frameFenceValues.size()) { - return; - } - - const std::uint64_t fenceValue = m_frameFenceValues[frameIndex]; - if (fenceValue == 0u || - m_frameCompletionFence->GetCompletedValue() >= fenceValue) { - return; - } - - const HRESULT hr = m_frameCompletionFence->SetEventOnCompletion( - fenceValue, - m_frameCompletionEvent); - if (SUCCEEDED(hr)) { - WaitForSingleObject(m_frameCompletionEvent, INFINITE); - } -} - -void D3D12HostDevice::WaitForGpuIdle() { - ID3D12CommandQueue* commandQueue = GetCommandQueue(); - if (commandQueue == nullptr || - m_frameCompletionFence == nullptr || - m_frameCompletionEvent == nullptr) { - if (m_commandQueue != nullptr) { - m_commandQueue->WaitForIdle(); - } - return; - } - - ++m_lastSubmittedFrameValue; - const std::uint64_t fenceValue = m_lastSubmittedFrameValue; - const HRESULT signalHr = commandQueue->Signal( - m_frameCompletionFence.Get(), - fenceValue); - if (FAILED(signalHr)) { - return; - } - - if (m_frameCompletionFence->GetCompletedValue() >= fenceValue) { - return; - } - - const HRESULT waitHr = m_frameCompletionFence->SetEventOnCompletion( - fenceValue, - m_frameCompletionEvent); - if (SUCCEEDED(waitHr)) { - WaitForSingleObject(m_frameCompletionEvent, INFINITE); - } -} - -void D3D12HostDevice::ResetFrameTracking() { - m_frameFenceValues.fill(0u); -} - -} // namespace XCEngine::UI::Editor::Host diff --git a/new_editor/app/Rendering/D3D12/D3D12WindowInteropContext.cpp b/new_editor/app/Rendering/D3D12/D3D12WindowInteropContext.cpp index 02f9f4d4..9ba1fb86 100644 --- a/new_editor/app/Rendering/D3D12/D3D12WindowInteropContext.cpp +++ b/new_editor/app/Rendering/D3D12/D3D12WindowInteropContext.cpp @@ -1,8 +1,12 @@ -#include "D3D12WindowInteropInternal.h" +#include "D3D12WindowInteropHelpers.h" +#include +#include + +#include #include -namespace XCEngine::UI::Editor::Host::D3D12WindowInteropInternal { +namespace XCEngine::UI::Editor::Host::D3D12WindowInteropHelpers { std::string HrToInteropString(const char* operation, HRESULT hr) { char buffer[128] = {}; @@ -42,10 +46,12 @@ void CollectInteropTextureHandles( } } -} // namespace XCEngine::UI::Editor::Host::D3D12WindowInteropInternal +} // namespace XCEngine::UI::Editor::Host::D3D12WindowInteropHelpers namespace XCEngine::UI::Editor::Host { +using namespace D3D12WindowInteropHelpers; + bool D3D12WindowInteropContext::Attach( D3D12WindowRenderer& windowRenderer, ID2D1Factory1& d2dFactory) { @@ -97,4 +103,345 @@ const std::string& D3D12WindowInteropContext::GetLastError() const { return m_lastError; } +bool D3D12WindowInteropContext::EnsureInterop() { + if (m_windowRenderer == nullptr) { + m_lastError = "EnsureInterop requires an attached D3D12 window renderer."; + return false; + } + if (m_d2dFactory == nullptr) { + m_lastError = "EnsureInterop requires an initialized D2D factory."; + return false; + } + if (m_d3d11On12Device != nullptr && + m_d2dDeviceContext != nullptr && + m_interopBrush != nullptr) { + return true; + } + + ReleaseInteropState(); + + ID3D12Device* d3d12Device = m_windowRenderer->GetDevice(); + ID3D12CommandQueue* d3d12CommandQueue = m_windowRenderer->GetCommandQueue(); + if (d3d12Device == nullptr || d3d12CommandQueue == nullptr) { + m_lastError = "The attached D3D12 window renderer does not expose a native device/queue."; + return false; + } + + const std::array featureLevels = { + D3D_FEATURE_LEVEL_12_1, + D3D_FEATURE_LEVEL_12_0, + D3D_FEATURE_LEVEL_11_1, + D3D_FEATURE_LEVEL_11_0 + }; + const std::array commandQueues = { + reinterpret_cast(d3d12CommandQueue) + }; + + UINT createFlags = D3D11_CREATE_DEVICE_BGRA_SUPPORT; +#ifdef _DEBUG + createFlags |= D3D11_CREATE_DEVICE_DEBUG; +#endif + + D3D_FEATURE_LEVEL actualFeatureLevel = D3D_FEATURE_LEVEL_11_0; + HRESULT hr = D3D11On12CreateDevice( + d3d12Device, + createFlags, + featureLevels.data(), + static_cast(featureLevels.size()), + commandQueues.data(), + static_cast(commandQueues.size()), + 0u, + m_d3d11Device.ReleaseAndGetAddressOf(), + m_d3d11DeviceContext.ReleaseAndGetAddressOf(), + &actualFeatureLevel); +#ifdef _DEBUG + if (FAILED(hr)) { + createFlags &= ~D3D11_CREATE_DEVICE_DEBUG; + hr = D3D11On12CreateDevice( + d3d12Device, + createFlags, + featureLevels.data(), + static_cast(featureLevels.size()), + commandQueues.data(), + static_cast(commandQueues.size()), + 0u, + m_d3d11Device.ReleaseAndGetAddressOf(), + m_d3d11DeviceContext.ReleaseAndGetAddressOf(), + &actualFeatureLevel); + } +#endif + if (FAILED(hr) || m_d3d11Device == nullptr || m_d3d11DeviceContext == nullptr) { + m_lastError = HrToInteropString("D3D11On12CreateDevice", hr); + ReleaseInteropState(); + return false; + } + + hr = m_d3d11Device.As(&m_d3d11On12Device); + if (FAILED(hr) || m_d3d11On12Device == nullptr) { + m_lastError = HrToInteropString("ID3D11Device::QueryInterface(ID3D11On12Device)", hr); + ReleaseInteropState(); + return false; + } + + Microsoft::WRL::ComPtr dxgiDevice = {}; + hr = m_d3d11Device.As(&dxgiDevice); + if (FAILED(hr) || dxgiDevice == nullptr) { + m_lastError = HrToInteropString("ID3D11Device::QueryInterface(IDXGIDevice)", hr); + ReleaseInteropState(); + return false; + } + + hr = m_d2dFactory->CreateDevice(dxgiDevice.Get(), m_d2dDevice.ReleaseAndGetAddressOf()); + if (FAILED(hr) || m_d2dDevice == nullptr) { + m_lastError = HrToInteropString("ID2D1Factory1::CreateDevice", hr); + ReleaseInteropState(); + return false; + } + + hr = m_d2dDevice->CreateDeviceContext( + D2D1_DEVICE_CONTEXT_OPTIONS_NONE, + m_d2dDeviceContext.ReleaseAndGetAddressOf()); + if (FAILED(hr) || m_d2dDeviceContext == nullptr) { + m_lastError = HrToInteropString("ID2D1Device::CreateDeviceContext", hr); + ReleaseInteropState(); + return false; + } + + hr = m_d2dDeviceContext->CreateSolidColorBrush( + D2D1::ColorF(1.0f, 1.0f, 1.0f, 1.0f), + m_interopBrush.ReleaseAndGetAddressOf()); + if (FAILED(hr) || m_interopBrush == nullptr) { + m_lastError = HrToInteropString("ID2D1DeviceContext::CreateSolidColorBrush", hr); + ReleaseInteropState(); + return false; + } + + m_d2dDeviceContext->SetDpi(96.0f, 96.0f); + m_d2dDeviceContext->SetTextAntialiasMode(D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE); + m_lastError.clear(); + return true; +} + +void D3D12WindowInteropContext::ReleaseInteropState() { + ReleaseBackBufferTargets(); + m_interopBrush.Reset(); + m_d2dDeviceContext.Reset(); + m_d2dDevice.Reset(); + m_d3d11On12Device.Reset(); + m_d3d11DeviceContext.Reset(); + m_d3d11Device.Reset(); +} + +void D3D12WindowInteropContext::ReleaseBackBufferTargets() { + ClearSourceTextures(); + if (m_d2dDeviceContext != nullptr) { + m_d2dDeviceContext->SetTarget(nullptr); + } + if (m_d3d11DeviceContext != nullptr) { + m_d3d11DeviceContext->ClearState(); + } + m_backBufferTargets.clear(); + if (m_d2dDeviceContext != nullptr) { + D2D1_TAG firstTag = 0u; + D2D1_TAG secondTag = 0u; + m_d2dDeviceContext->Flush(&firstTag, &secondTag); + } + if (m_d3d11DeviceContext != nullptr) { + m_d3d11DeviceContext->Flush(); + } +} + +bool D3D12WindowInteropContext::RebuildBackBufferTargets() { + m_backBufferTargets.clear(); + if (m_windowRenderer == nullptr || m_d3d11On12Device == nullptr || m_d2dDeviceContext == nullptr) { + return false; + } + + const std::uint32_t backBufferCount = m_windowRenderer->GetBackBufferCount(); + m_backBufferTargets.resize(backBufferCount); + for (std::uint32_t index = 0u; index < backBufferCount; ++index) { + const ::XCEngine::RHI::D3D12Texture* backBufferTexture = + m_windowRenderer->GetBackBufferTexture(index); + if (backBufferTexture == nullptr || backBufferTexture->GetResource() == nullptr) { + m_lastError = "Failed to resolve a D3D12 swap chain back buffer."; + m_backBufferTargets.clear(); + return false; + } + + D3D11_RESOURCE_FLAGS resourceFlags = {}; + resourceFlags.BindFlags = D3D11_BIND_RENDER_TARGET; + HRESULT hr = m_d3d11On12Device->CreateWrappedResource( + backBufferTexture->GetResource(), + &resourceFlags, + D3D12_RESOURCE_STATE_RENDER_TARGET, + D3D12_RESOURCE_STATE_PRESENT, + IID_PPV_ARGS(m_backBufferTargets[index].wrappedResource.ReleaseAndGetAddressOf())); + if (FAILED(hr) || m_backBufferTargets[index].wrappedResource == nullptr) { + m_lastError = HrToInteropString("ID3D11On12Device::CreateWrappedResource(backbuffer)", hr); + m_backBufferTargets.clear(); + return false; + } + + Microsoft::WRL::ComPtr dxgiSurface = {}; + hr = m_backBufferTargets[index].wrappedResource.As(&dxgiSurface); + if (FAILED(hr) || dxgiSurface == nullptr) { + m_lastError = HrToInteropString("ID3D11Resource::QueryInterface(IDXGISurface)", hr); + m_backBufferTargets.clear(); + return false; + } + + const D2D1_BITMAP_PROPERTIES1 bitmapProperties = + BuildD2DBitmapProperties( + backBufferTexture->GetDesc().Format, + D2D1_BITMAP_OPTIONS_TARGET | D2D1_BITMAP_OPTIONS_CANNOT_DRAW); + hr = m_d2dDeviceContext->CreateBitmapFromDxgiSurface( + dxgiSurface.Get(), + &bitmapProperties, + m_backBufferTargets[index].targetBitmap.ReleaseAndGetAddressOf()); + if (FAILED(hr) || m_backBufferTargets[index].targetBitmap == nullptr) { + m_lastError = HrToInteropString( + "ID2D1DeviceContext::CreateBitmapFromDxgiSurface(backbuffer)", + hr); + m_backBufferTargets.clear(); + return false; + } + } + + m_lastError.clear(); + return true; +} + +bool D3D12WindowInteropContext::HasAttachedWindowRenderer() const { + return m_windowRenderer != nullptr && + m_d3d11On12Device != nullptr && + m_d2dDeviceContext != nullptr && + !m_backBufferTargets.empty(); +} + +bool D3D12WindowInteropContext::HasBackBufferTargets() const { + return !m_backBufferTargets.empty(); +} + +void D3D12WindowInteropContext::BuildAcquiredResources( + std::uint32_t backBufferIndex, + std::vector& outResources) const { + outResources.clear(); + + ID3D11Resource* backBufferResource = GetWrappedBackBufferResource(backBufferIndex); + if (backBufferResource != nullptr) { + outResources.push_back(backBufferResource); + } + + for (const SourceTextureResource& resource : m_activeSourceTextures) { + if (resource.wrappedResource != nullptr) { + outResources.push_back(resource.wrappedResource.Get()); + } + } +} + +ID3D11Resource* D3D12WindowInteropContext::GetWrappedBackBufferResource(std::uint32_t index) const { + return index < m_backBufferTargets.size() ? m_backBufferTargets[index].wrappedResource.Get() : nullptr; +} + +ID2D1Bitmap1* D3D12WindowInteropContext::GetBackBufferTargetBitmap(std::uint32_t index) const { + return index < m_backBufferTargets.size() ? m_backBufferTargets[index].targetBitmap.Get() : nullptr; +} + +std::uint32_t D3D12WindowInteropContext::GetCurrentBackBufferIndex() const { + return m_windowRenderer != nullptr && m_windowRenderer->GetSwapChain() != nullptr + ? m_windowRenderer->GetSwapChain()->GetCurrentBackBufferIndex() + : 0u; +} + +bool D3D12WindowInteropContext::PrepareSourceTextures( + const ::XCEngine::UI::UIDrawData& drawData) { + ClearSourceTextures(); + if (m_windowRenderer == nullptr || m_d3d11On12Device == nullptr || m_d2dDeviceContext == nullptr) { + return false; + } + + std::vector<::XCEngine::UI::UITextureHandle> textureHandles = {}; + CollectInteropTextureHandles(drawData, textureHandles); + m_activeSourceTextures.reserve(textureHandles.size()); + + for (const ::XCEngine::UI::UITextureHandle& textureHandle : textureHandles) { + auto* texture = + reinterpret_cast<::XCEngine::RHI::RHITexture*>(textureHandle.resourceHandle); + auto* nativeTexture = dynamic_cast<::XCEngine::RHI::D3D12Texture*>(texture); + if (nativeTexture == nullptr || nativeTexture->GetResource() == nullptr) { + m_lastError = "Failed to resolve a D3D12 source texture for UI composition."; + ClearSourceTextures(); + return false; + } + + D3D11_RESOURCE_FLAGS resourceFlags = {}; + resourceFlags.BindFlags = D3D11_BIND_SHADER_RESOURCE; + + SourceTextureResource resource = {}; + resource.key = textureHandle.resourceHandle; + HRESULT hr = m_d3d11On12Device->CreateWrappedResource( + nativeTexture->GetResource(), + &resourceFlags, + D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, + D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, + IID_PPV_ARGS(resource.wrappedResource.ReleaseAndGetAddressOf())); + if (FAILED(hr) || resource.wrappedResource == nullptr) { + m_lastError = HrToInteropString("ID3D11On12Device::CreateWrappedResource(source)", hr); + ClearSourceTextures(); + return false; + } + + Microsoft::WRL::ComPtr dxgiSurface = {}; + hr = resource.wrappedResource.As(&dxgiSurface); + if (FAILED(hr) || dxgiSurface == nullptr) { + m_lastError = HrToInteropString("ID3D11Resource::QueryInterface(IDXGISurface)", hr); + ClearSourceTextures(); + return false; + } + + const D2D1_BITMAP_PROPERTIES1 bitmapProperties = + BuildD2DBitmapProperties( + nativeTexture->GetDesc().Format, + D2D1_BITMAP_OPTIONS_NONE); + hr = m_d2dDeviceContext->CreateBitmapFromDxgiSurface( + dxgiSurface.Get(), + &bitmapProperties, + resource.bitmap.ReleaseAndGetAddressOf()); + if (FAILED(hr) || resource.bitmap == nullptr) { + m_lastError = + HrToInteropString("ID2D1DeviceContext::CreateBitmapFromDxgiSurface(source)", hr); + ClearSourceTextures(); + return false; + } + + m_activeBitmaps.emplace(resource.key, resource.bitmap); + m_activeSourceTextures.push_back(std::move(resource)); + } + + m_lastError.clear(); + return true; +} + +void D3D12WindowInteropContext::ClearSourceTextures() { + m_activeBitmaps.clear(); + m_activeSourceTextures.clear(); +} + +bool D3D12WindowInteropContext::ResolveInteropBitmap( + const ::XCEngine::UI::UITextureHandle& texture, + Microsoft::WRL::ComPtr& outBitmap) const { + outBitmap.Reset(); + if (!IsInteropTextureHandle(texture)) { + return false; + } + + const auto found = m_activeBitmaps.find(texture.resourceHandle); + if (found == m_activeBitmaps.end() || found->second == nullptr) { + return false; + } + + outBitmap = found->second; + return true; +} + } // namespace XCEngine::UI::Editor::Host diff --git a/new_editor/app/Rendering/D3D12/D3D12WindowInteropDevice.cpp b/new_editor/app/Rendering/D3D12/D3D12WindowInteropDevice.cpp deleted file mode 100644 index e43c7ea2..00000000 --- a/new_editor/app/Rendering/D3D12/D3D12WindowInteropDevice.cpp +++ /dev/null @@ -1,138 +0,0 @@ -#include "D3D12WindowInteropInternal.h" - -#include - -namespace XCEngine::UI::Editor::Host { - -using namespace D3D12WindowInteropInternal; - -bool D3D12WindowInteropContext::EnsureInterop() { - if (m_windowRenderer == nullptr) { - m_lastError = "EnsureInterop requires an attached D3D12 window renderer."; - return false; - } - if (m_d2dFactory == nullptr) { - m_lastError = "EnsureInterop requires an initialized D2D factory."; - return false; - } - if (m_d3d11On12Device != nullptr && - m_d2dDeviceContext != nullptr && - m_interopBrush != nullptr) { - return true; - } - - ReleaseInteropState(); - - ID3D12Device* d3d12Device = m_windowRenderer->GetDevice(); - ID3D12CommandQueue* d3d12CommandQueue = m_windowRenderer->GetCommandQueue(); - if (d3d12Device == nullptr || d3d12CommandQueue == nullptr) { - m_lastError = "The attached D3D12 window renderer does not expose a native device/queue."; - return false; - } - - const std::array featureLevels = { - D3D_FEATURE_LEVEL_12_1, - D3D_FEATURE_LEVEL_12_0, - D3D_FEATURE_LEVEL_11_1, - D3D_FEATURE_LEVEL_11_0 - }; - const std::array commandQueues = { - reinterpret_cast(d3d12CommandQueue) - }; - - UINT createFlags = D3D11_CREATE_DEVICE_BGRA_SUPPORT; -#ifdef _DEBUG - createFlags |= D3D11_CREATE_DEVICE_DEBUG; -#endif - - D3D_FEATURE_LEVEL actualFeatureLevel = D3D_FEATURE_LEVEL_11_0; - HRESULT hr = D3D11On12CreateDevice( - d3d12Device, - createFlags, - featureLevels.data(), - static_cast(featureLevels.size()), - commandQueues.data(), - static_cast(commandQueues.size()), - 0u, - m_d3d11Device.ReleaseAndGetAddressOf(), - m_d3d11DeviceContext.ReleaseAndGetAddressOf(), - &actualFeatureLevel); -#ifdef _DEBUG - if (FAILED(hr)) { - createFlags &= ~D3D11_CREATE_DEVICE_DEBUG; - hr = D3D11On12CreateDevice( - d3d12Device, - createFlags, - featureLevels.data(), - static_cast(featureLevels.size()), - commandQueues.data(), - static_cast(commandQueues.size()), - 0u, - m_d3d11Device.ReleaseAndGetAddressOf(), - m_d3d11DeviceContext.ReleaseAndGetAddressOf(), - &actualFeatureLevel); - } -#endif - if (FAILED(hr) || m_d3d11Device == nullptr || m_d3d11DeviceContext == nullptr) { - m_lastError = HrToInteropString("D3D11On12CreateDevice", hr); - ReleaseInteropState(); - return false; - } - - hr = m_d3d11Device.As(&m_d3d11On12Device); - if (FAILED(hr) || m_d3d11On12Device == nullptr) { - m_lastError = HrToInteropString("ID3D11Device::QueryInterface(ID3D11On12Device)", hr); - ReleaseInteropState(); - return false; - } - - Microsoft::WRL::ComPtr dxgiDevice = {}; - hr = m_d3d11Device.As(&dxgiDevice); - if (FAILED(hr) || dxgiDevice == nullptr) { - m_lastError = HrToInteropString("ID3D11Device::QueryInterface(IDXGIDevice)", hr); - ReleaseInteropState(); - return false; - } - - hr = m_d2dFactory->CreateDevice(dxgiDevice.Get(), m_d2dDevice.ReleaseAndGetAddressOf()); - if (FAILED(hr) || m_d2dDevice == nullptr) { - m_lastError = HrToInteropString("ID2D1Factory1::CreateDevice", hr); - ReleaseInteropState(); - return false; - } - - hr = m_d2dDevice->CreateDeviceContext( - D2D1_DEVICE_CONTEXT_OPTIONS_NONE, - m_d2dDeviceContext.ReleaseAndGetAddressOf()); - if (FAILED(hr) || m_d2dDeviceContext == nullptr) { - m_lastError = HrToInteropString("ID2D1Device::CreateDeviceContext", hr); - ReleaseInteropState(); - return false; - } - - hr = m_d2dDeviceContext->CreateSolidColorBrush( - D2D1::ColorF(1.0f, 1.0f, 1.0f, 1.0f), - m_interopBrush.ReleaseAndGetAddressOf()); - if (FAILED(hr) || m_interopBrush == nullptr) { - m_lastError = HrToInteropString("ID2D1DeviceContext::CreateSolidColorBrush", hr); - ReleaseInteropState(); - return false; - } - - m_d2dDeviceContext->SetDpi(96.0f, 96.0f); - m_d2dDeviceContext->SetTextAntialiasMode(D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE); - m_lastError.clear(); - return true; -} - -void D3D12WindowInteropContext::ReleaseInteropState() { - ReleaseBackBufferTargets(); - m_interopBrush.Reset(); - m_d2dDeviceContext.Reset(); - m_d2dDevice.Reset(); - m_d3d11On12Device.Reset(); - m_d3d11DeviceContext.Reset(); - m_d3d11Device.Reset(); -} - -} // namespace XCEngine::UI::Editor::Host diff --git a/new_editor/app/Rendering/D3D12/D3D12WindowInteropInternal.h b/new_editor/app/Rendering/D3D12/D3D12WindowInteropHelpers.h similarity index 76% rename from new_editor/app/Rendering/D3D12/D3D12WindowInteropInternal.h rename to new_editor/app/Rendering/D3D12/D3D12WindowInteropHelpers.h index c8326964..962749d5 100644 --- a/new_editor/app/Rendering/D3D12/D3D12WindowInteropInternal.h +++ b/new_editor/app/Rendering/D3D12/D3D12WindowInteropHelpers.h @@ -2,7 +2,7 @@ #include "D3D12WindowInteropContext.h" -namespace XCEngine::UI::Editor::Host::D3D12WindowInteropInternal { +namespace XCEngine::UI::Editor::Host::D3D12WindowInteropHelpers { std::string HrToInteropString(const char* operation, HRESULT hr); @@ -16,4 +16,4 @@ void CollectInteropTextureHandles( const ::XCEngine::UI::UIDrawData& drawData, std::vector<::XCEngine::UI::UITextureHandle>& outTextures); -} // namespace XCEngine::UI::Editor::Host::D3D12WindowInteropInternal +} // namespace XCEngine::UI::Editor::Host::D3D12WindowInteropHelpers diff --git a/new_editor/app/Rendering/D3D12/D3D12WindowInteropResources.cpp b/new_editor/app/Rendering/D3D12/D3D12WindowInteropResources.cpp deleted file mode 100644 index 2080d895..00000000 --- a/new_editor/app/Rendering/D3D12/D3D12WindowInteropResources.cpp +++ /dev/null @@ -1,128 +0,0 @@ -#include "D3D12WindowInteropInternal.h" - -namespace XCEngine::UI::Editor::Host { - -using namespace D3D12WindowInteropInternal; - -void D3D12WindowInteropContext::ReleaseBackBufferTargets() { - ClearSourceTextures(); - if (m_d2dDeviceContext != nullptr) { - m_d2dDeviceContext->SetTarget(nullptr); - } - if (m_d3d11DeviceContext != nullptr) { - m_d3d11DeviceContext->ClearState(); - } - m_backBufferTargets.clear(); - if (m_d2dDeviceContext != nullptr) { - D2D1_TAG firstTag = 0u; - D2D1_TAG secondTag = 0u; - m_d2dDeviceContext->Flush(&firstTag, &secondTag); - } - if (m_d3d11DeviceContext != nullptr) { - m_d3d11DeviceContext->Flush(); - } -} - -bool D3D12WindowInteropContext::RebuildBackBufferTargets() { - m_backBufferTargets.clear(); - if (m_windowRenderer == nullptr || m_d3d11On12Device == nullptr || m_d2dDeviceContext == nullptr) { - return false; - } - - const std::uint32_t backBufferCount = m_windowRenderer->GetBackBufferCount(); - m_backBufferTargets.resize(backBufferCount); - for (std::uint32_t index = 0u; index < backBufferCount; ++index) { - const ::XCEngine::RHI::D3D12Texture* backBufferTexture = - m_windowRenderer->GetBackBufferTexture(index); - if (backBufferTexture == nullptr || backBufferTexture->GetResource() == nullptr) { - m_lastError = "Failed to resolve a D3D12 swap chain back buffer."; - m_backBufferTargets.clear(); - return false; - } - - D3D11_RESOURCE_FLAGS resourceFlags = {}; - resourceFlags.BindFlags = D3D11_BIND_RENDER_TARGET; - HRESULT hr = m_d3d11On12Device->CreateWrappedResource( - backBufferTexture->GetResource(), - &resourceFlags, - D3D12_RESOURCE_STATE_RENDER_TARGET, - D3D12_RESOURCE_STATE_PRESENT, - IID_PPV_ARGS(m_backBufferTargets[index].wrappedResource.ReleaseAndGetAddressOf())); - if (FAILED(hr) || m_backBufferTargets[index].wrappedResource == nullptr) { - m_lastError = HrToInteropString("ID3D11On12Device::CreateWrappedResource(backbuffer)", hr); - m_backBufferTargets.clear(); - return false; - } - - Microsoft::WRL::ComPtr dxgiSurface = {}; - hr = m_backBufferTargets[index].wrappedResource.As(&dxgiSurface); - if (FAILED(hr) || dxgiSurface == nullptr) { - m_lastError = HrToInteropString("ID3D11Resource::QueryInterface(IDXGISurface)", hr); - m_backBufferTargets.clear(); - return false; - } - - const D2D1_BITMAP_PROPERTIES1 bitmapProperties = - BuildD2DBitmapProperties( - backBufferTexture->GetDesc().Format, - D2D1_BITMAP_OPTIONS_TARGET | D2D1_BITMAP_OPTIONS_CANNOT_DRAW); - hr = m_d2dDeviceContext->CreateBitmapFromDxgiSurface( - dxgiSurface.Get(), - &bitmapProperties, - m_backBufferTargets[index].targetBitmap.ReleaseAndGetAddressOf()); - if (FAILED(hr) || m_backBufferTargets[index].targetBitmap == nullptr) { - m_lastError = HrToInteropString( - "ID2D1DeviceContext::CreateBitmapFromDxgiSurface(backbuffer)", - hr); - m_backBufferTargets.clear(); - return false; - } - } - - m_lastError.clear(); - return true; -} - -bool D3D12WindowInteropContext::HasAttachedWindowRenderer() const { - return m_windowRenderer != nullptr && - m_d3d11On12Device != nullptr && - m_d2dDeviceContext != nullptr && - !m_backBufferTargets.empty(); -} - -bool D3D12WindowInteropContext::HasBackBufferTargets() const { - return !m_backBufferTargets.empty(); -} - -void D3D12WindowInteropContext::BuildAcquiredResources( - std::uint32_t backBufferIndex, - std::vector& outResources) const { - outResources.clear(); - - ID3D11Resource* backBufferResource = GetWrappedBackBufferResource(backBufferIndex); - if (backBufferResource != nullptr) { - outResources.push_back(backBufferResource); - } - - for (const SourceTextureResource& resource : m_activeSourceTextures) { - if (resource.wrappedResource != nullptr) { - outResources.push_back(resource.wrappedResource.Get()); - } - } -} - -ID3D11Resource* D3D12WindowInteropContext::GetWrappedBackBufferResource(std::uint32_t index) const { - return index < m_backBufferTargets.size() ? m_backBufferTargets[index].wrappedResource.Get() : nullptr; -} - -ID2D1Bitmap1* D3D12WindowInteropContext::GetBackBufferTargetBitmap(std::uint32_t index) const { - return index < m_backBufferTargets.size() ? m_backBufferTargets[index].targetBitmap.Get() : nullptr; -} - -std::uint32_t D3D12WindowInteropContext::GetCurrentBackBufferIndex() const { - return m_windowRenderer != nullptr && m_windowRenderer->GetSwapChain() != nullptr - ? m_windowRenderer->GetSwapChain()->GetCurrentBackBufferIndex() - : 0u; -} - -} // namespace XCEngine::UI::Editor::Host diff --git a/new_editor/app/Rendering/D3D12/D3D12WindowInteropSourceTextures.cpp b/new_editor/app/Rendering/D3D12/D3D12WindowInteropSourceTextures.cpp deleted file mode 100644 index e7eda6d6..00000000 --- a/new_editor/app/Rendering/D3D12/D3D12WindowInteropSourceTextures.cpp +++ /dev/null @@ -1,100 +0,0 @@ -#include "D3D12WindowInteropInternal.h" - -#include -#include - -namespace XCEngine::UI::Editor::Host { - -using namespace D3D12WindowInteropInternal; - -bool D3D12WindowInteropContext::PrepareSourceTextures( - const ::XCEngine::UI::UIDrawData& drawData) { - ClearSourceTextures(); - if (m_windowRenderer == nullptr || m_d3d11On12Device == nullptr || m_d2dDeviceContext == nullptr) { - return false; - } - - std::vector<::XCEngine::UI::UITextureHandle> textureHandles = {}; - CollectInteropTextureHandles(drawData, textureHandles); - m_activeSourceTextures.reserve(textureHandles.size()); - - for (const ::XCEngine::UI::UITextureHandle& textureHandle : textureHandles) { - auto* texture = - reinterpret_cast<::XCEngine::RHI::RHITexture*>(textureHandle.resourceHandle); - auto* nativeTexture = dynamic_cast<::XCEngine::RHI::D3D12Texture*>(texture); - if (nativeTexture == nullptr || nativeTexture->GetResource() == nullptr) { - m_lastError = "Failed to resolve a D3D12 source texture for UI composition."; - ClearSourceTextures(); - return false; - } - - D3D11_RESOURCE_FLAGS resourceFlags = {}; - resourceFlags.BindFlags = D3D11_BIND_SHADER_RESOURCE; - - SourceTextureResource resource = {}; - resource.key = textureHandle.resourceHandle; - HRESULT hr = m_d3d11On12Device->CreateWrappedResource( - nativeTexture->GetResource(), - &resourceFlags, - D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, - D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, - IID_PPV_ARGS(resource.wrappedResource.ReleaseAndGetAddressOf())); - if (FAILED(hr) || resource.wrappedResource == nullptr) { - m_lastError = HrToInteropString("ID3D11On12Device::CreateWrappedResource(source)", hr); - ClearSourceTextures(); - return false; - } - - Microsoft::WRL::ComPtr dxgiSurface = {}; - hr = resource.wrappedResource.As(&dxgiSurface); - if (FAILED(hr) || dxgiSurface == nullptr) { - m_lastError = HrToInteropString("ID3D11Resource::QueryInterface(IDXGISurface)", hr); - ClearSourceTextures(); - return false; - } - - const D2D1_BITMAP_PROPERTIES1 bitmapProperties = - BuildD2DBitmapProperties( - nativeTexture->GetDesc().Format, - D2D1_BITMAP_OPTIONS_NONE); - hr = m_d2dDeviceContext->CreateBitmapFromDxgiSurface( - dxgiSurface.Get(), - &bitmapProperties, - resource.bitmap.ReleaseAndGetAddressOf()); - if (FAILED(hr) || resource.bitmap == nullptr) { - m_lastError = HrToInteropString("ID2D1DeviceContext::CreateBitmapFromDxgiSurface(source)", hr); - ClearSourceTextures(); - return false; - } - - m_activeBitmaps.emplace(resource.key, resource.bitmap); - m_activeSourceTextures.push_back(std::move(resource)); - } - - m_lastError.clear(); - return true; -} - -void D3D12WindowInteropContext::ClearSourceTextures() { - m_activeBitmaps.clear(); - m_activeSourceTextures.clear(); -} - -bool D3D12WindowInteropContext::ResolveInteropBitmap( - const ::XCEngine::UI::UITextureHandle& texture, - Microsoft::WRL::ComPtr& outBitmap) const { - outBitmap.Reset(); - if (!IsInteropTextureHandle(texture)) { - return false; - } - - const auto found = m_activeBitmaps.find(texture.resourceHandle); - if (found == m_activeBitmaps.end() || found->second == nullptr) { - return false; - } - - outBitmap = found->second; - return true; -} - -} // namespace XCEngine::UI::Editor::Host diff --git a/new_editor/app/Rendering/D3D12/D3D12WindowSwapChainPresenter.cpp b/new_editor/app/Rendering/D3D12/D3D12WindowSwapChainPresenter.cpp new file mode 100644 index 00000000..c07a73ff --- /dev/null +++ b/new_editor/app/Rendering/D3D12/D3D12WindowSwapChainPresenter.cpp @@ -0,0 +1,390 @@ +#include "D3D12WindowSwapChainPresenter.h" + +#include + +namespace XCEngine::UI::Editor::Host { + +using ::XCEngine::RHI::D3D12SwapChain; +using ::XCEngine::RHI::D3D12Texture; +using ::XCEngine::RHI::Format; +using ::XCEngine::RHI::RHISwapChain; +using ::XCEngine::RHI::ResourceStates; +using ::XCEngine::RHI::ResourceViewDesc; +using ::XCEngine::RHI::ResourceViewDimension; +using ::XCEngine::RHI::SwapChainDesc; + +bool D3D12WindowSwapChainPresenter::Initialize( + D3D12HostDevice& hostDevice, + HWND hwnd, + int width, + int height) { + Shutdown(); + + if (hwnd == nullptr || width <= 0 || height <= 0) { + m_lastError = "Initialize rejected an invalid hwnd or size."; + return false; + } + + if (hostDevice.GetRHIDevice() == nullptr || hostDevice.GetRHICommandQueue() == nullptr) { + m_lastError = "Initialize requires an initialized host D3D12 device."; + return false; + } + + m_hwnd = hwnd; + m_width = width; + m_height = height; + m_hostDevice = &hostDevice; + + if (!CreateSwapChain(width, height)) { + Shutdown(); + return false; + } + + m_lastError.clear(); + return true; +} + +bool D3D12WindowSwapChainPresenter::CreateSwapChain(int width, int height) { + if (m_hostDevice == nullptr || + m_hostDevice->GetRHIDevice() == nullptr || + m_hostDevice->GetRHICommandQueue() == nullptr) { + m_lastError = "CreateSwapChain requires an initialized host D3D12 device."; + return false; + } + + SwapChainDesc swapChainDesc = {}; + swapChainDesc.windowHandle = m_hwnd; + swapChainDesc.width = static_cast(width); + swapChainDesc.height = static_cast(height); + swapChainDesc.bufferCount = kSwapChainBufferCount; + m_swapChain = m_hostDevice->GetRHIDevice()->CreateSwapChain( + swapChainDesc, + m_hostDevice->GetRHICommandQueue()); + if (m_swapChain == nullptr || GetD3D12SwapChain() == nullptr) { + m_lastError = "Failed to create the D3D12 swap chain."; + return false; + } + + ConfigureFrameLatency(); + + if (!RecreateBackBufferViews()) { + m_lastError = "Failed to create swap chain back buffer views."; + return false; + } + + m_width = width; + m_height = height; + return true; +} + +void D3D12WindowSwapChainPresenter::ConfigureFrameLatency() { + D3D12SwapChain* d3d12SwapChain = GetD3D12SwapChain(); + if (d3d12SwapChain == nullptr) { + return; + } + + auto* nativeSwapChain = + static_cast(d3d12SwapChain->GetNativeHandle()); + if (nativeSwapChain == nullptr) { + return; + } + + nativeSwapChain->SetMaximumFrameLatency(1u); +} + +void D3D12WindowSwapChainPresenter::DestroySwapChain() { + ReleaseBackBufferViews(); + + if (m_swapChain != nullptr) { + m_swapChain->Shutdown(); + delete m_swapChain; + m_swapChain = nullptr; + } +} + +bool D3D12WindowSwapChainPresenter::RecreateSwapChain(int width, int height) { + DestroySwapChain(); + m_hostDevice->ResetFrameTracking(); + return CreateSwapChain(width, height); +} + +void D3D12WindowSwapChainPresenter::Shutdown() { + if (m_hostDevice != nullptr) { + m_hostDevice->WaitForGpuIdle(); + } + + DestroySwapChain(); + + m_hwnd = nullptr; + m_width = 0; + m_height = 0; + m_hostDevice = nullptr; + m_lastError.clear(); +} + +const std::string& D3D12WindowSwapChainPresenter::GetLastError() const { + return m_lastError; +} + +RHISwapChain* D3D12WindowSwapChainPresenter::GetSwapChain() const { + return m_swapChain; +} + +const ::XCEngine::Rendering::RenderSurface* +D3D12WindowSwapChainPresenter::GetCurrentRenderSurface() const { + if (m_swapChain == nullptr) { + return nullptr; + } + + const std::uint32_t backBufferIndex = m_swapChain->GetCurrentBackBufferIndex(); + if (backBufferIndex >= m_backBufferSurfaces.size()) { + return nullptr; + } + + return &m_backBufferSurfaces[backBufferIndex]; +} + +const D3D12Texture* D3D12WindowSwapChainPresenter::GetCurrentBackBufferTexture() const { + if (m_swapChain == nullptr) { + return nullptr; + } + + return GetBackBufferTexture(m_swapChain->GetCurrentBackBufferIndex()); +} + +const D3D12Texture* D3D12WindowSwapChainPresenter::GetBackBufferTexture(std::uint32_t index) const { + const D3D12SwapChain* d3d12SwapChain = GetD3D12SwapChain(); + if (d3d12SwapChain == nullptr) { + return nullptr; + } + + return d3d12SwapChain->TryGetBackBuffer(index); +} + +std::uint32_t D3D12WindowSwapChainPresenter::GetBackBufferCount() const { + return kSwapChainBufferCount; +} + +std::uint32_t D3D12WindowSwapChainPresenter::GetCurrentBackBufferIndex() const { + return m_swapChain != nullptr ? m_swapChain->GetCurrentBackBufferIndex() : 0u; +} + +D3D12SwapChain* D3D12WindowSwapChainPresenter::GetD3D12SwapChain() const { + return m_swapChain != nullptr ? static_cast(m_swapChain) : nullptr; +} + +void D3D12WindowSwapChainPresenter::ReleaseBackBufferCommandReferences() { + if (m_hostDevice == nullptr) { + return; + } + + for (std::uint32_t frameIndex = 0u; + frameIndex < D3D12HostDevice::kFrameContextCount; + ++frameIndex) { + auto* commandList = m_hostDevice->GetCommandList(frameIndex); + if (commandList == nullptr) { + continue; + } + + commandList->Reset(); + commandList->Close(); + } +} + +void D3D12WindowSwapChainPresenter::ReleaseBackBufferViews() { + for (auto* view : m_backBufferViews) { + if (view != nullptr) { + view->Shutdown(); + delete view; + } + } + + m_backBufferViews.clear(); + m_backBufferSurfaces.clear(); +} + +bool D3D12WindowSwapChainPresenter::RecreateBackBufferViews() { + D3D12SwapChain* d3d12SwapChain = GetD3D12SwapChain(); + if (m_hostDevice == nullptr || + m_hostDevice->GetRHIDevice() == nullptr || + d3d12SwapChain == nullptr) { + return false; + } + + m_backBufferViews.resize(kSwapChainBufferCount, nullptr); + m_backBufferSurfaces.resize(kSwapChainBufferCount); + + ResourceViewDesc viewDesc = {}; + viewDesc.format = static_cast(Format::R8G8B8A8_UNorm); + viewDesc.dimension = ResourceViewDimension::Texture2D; + + for (std::uint32_t backBufferIndex = 0u; + backBufferIndex < kSwapChainBufferCount; + ++backBufferIndex) { + D3D12Texture* backBufferTexture = d3d12SwapChain->TryGetBackBuffer(backBufferIndex); + if (backBufferTexture == nullptr) { + ReleaseBackBufferViews(); + m_lastError = "RecreateBackBufferViews could not resolve swap chain back buffer " + + std::to_string(backBufferIndex) + "."; + return false; + } + + m_backBufferViews[backBufferIndex] = m_hostDevice->GetRHIDevice()->CreateRenderTargetView( + backBufferTexture, + viewDesc); + if (m_backBufferViews[backBufferIndex] == nullptr) { + ReleaseBackBufferViews(); + m_lastError = + "RecreateBackBufferViews failed to create RTV for swap chain back buffer " + + std::to_string(backBufferIndex) + "."; + return false; + } + + ::XCEngine::Rendering::RenderSurface& surface = m_backBufferSurfaces[backBufferIndex]; + surface = ::XCEngine::Rendering::RenderSurface( + static_cast(m_width), + static_cast(m_height)); + surface.SetColorAttachment(m_backBufferViews[backBufferIndex]); + surface.SetAutoTransitionEnabled(false); + surface.SetColorStateBefore(ResourceStates::RenderTarget); + surface.SetColorStateAfter(ResourceStates::RenderTarget); + } + + m_lastError.clear(); + return true; +} + +bool D3D12WindowSwapChainPresenter::PreparePresentSurface( + const ::XCEngine::Rendering::RenderContext& renderContext) { + if (!renderContext.IsValid() || renderContext.commandList == nullptr) { + m_lastError = "PreparePresentSurface requires a valid render context."; + return false; + } + + if (m_swapChain == nullptr) { + m_lastError = "PreparePresentSurface requires an initialized swap chain."; + return false; + } + + const std::uint32_t backBufferIndex = m_swapChain->GetCurrentBackBufferIndex(); + if (backBufferIndex >= m_backBufferViews.size() || + m_backBufferViews[backBufferIndex] == nullptr) { + std::ostringstream error = {}; + error << "PreparePresentSurface could not find the current swap chain RTV. index=" + << backBufferIndex + << " rtvCount=" + << m_backBufferViews.size(); + m_lastError = error.str(); + return false; + } + + renderContext.commandList->TransitionBarrier( + m_backBufferViews[backBufferIndex], + ::XCEngine::RHI::ResourceStates::Present, + ::XCEngine::RHI::ResourceStates::RenderTarget); + m_lastError.clear(); + return true; +} + +bool D3D12WindowSwapChainPresenter::PresentFrame() { + if (m_swapChain == nullptr) { + m_lastError = "PresentFrame requires an initialized swap chain."; + return false; + } + + m_swapChain->Present(0, 0); + m_lastError.clear(); + return true; +} + +bool D3D12WindowSwapChainPresenter::Resize(int width, int height) { + if (width <= 0 || height <= 0 || m_swapChain == nullptr || m_hostDevice == nullptr) { + return false; + } + + if (m_width == width && m_height == height) { + m_lastError.clear(); + return true; + } + + m_hostDevice->WaitForGpuIdle(); + ReleaseBackBufferCommandReferences(); + ReleaseBackBufferViews(); + + D3D12SwapChain* d3d12SwapChain = GetD3D12SwapChain(); + if (d3d12SwapChain == nullptr) { + m_lastError = "Resize could not resolve the native D3D12 swap chain."; + return false; + } + + d3d12SwapChain->Resize( + static_cast(width), + static_cast(height)); + const HRESULT resizeHr = d3d12SwapChain->GetLastResizeResult(); + if (FAILED(resizeHr)) { + if (RecreateSwapChain(width, height)) { + m_lastError.clear(); + return true; + } + + std::ostringstream error = {}; + error << "ResizeBuffers failed. requested=" + << width + << 'x' + << height + << " hr=0x" + << std::uppercase + << std::hex + << static_cast(resizeHr); + m_lastError = error.str(); + return false; + } + + const D3D12Texture* backBufferTexture = GetBackBufferTexture(0u); + if (backBufferTexture == nullptr) { + if (RecreateSwapChain(width, height)) { + m_lastError.clear(); + return true; + } + + m_lastError = "Resize failed to restore swap chain back buffers after ResizeBuffers."; + return false; + } + + m_width = static_cast(backBufferTexture->GetWidth()); + m_height = static_cast(backBufferTexture->GetHeight()); + m_hostDevice->ResetFrameTracking(); + if (!RecreateBackBufferViews()) { + if (RecreateSwapChain(width, height)) { + m_lastError.clear(); + return true; + } + + m_lastError = "Resize failed to recreate swap chain back buffer views."; + return false; + } + + if (m_width != width || m_height != height) { + if (RecreateSwapChain(width, height)) { + m_lastError.clear(); + return true; + } + + std::ostringstream error = {}; + error << "Resize ended with an unexpected swap chain size. requested=" + << width + << 'x' + << height + << " actual=" + << m_width + << 'x' + << m_height; + m_lastError = error.str(); + return false; + } + + m_lastError.clear(); + return true; +} + +} // namespace XCEngine::UI::Editor::Host diff --git a/new_editor/app/Rendering/D3D12/SwapChainPresenter/BackBuffers.cpp b/new_editor/app/Rendering/D3D12/SwapChainPresenter/BackBuffers.cpp deleted file mode 100644 index f5d7afe6..00000000 --- a/new_editor/app/Rendering/D3D12/SwapChainPresenter/BackBuffers.cpp +++ /dev/null @@ -1,92 +0,0 @@ -#include "Rendering/D3D12/D3D12WindowSwapChainPresenter.h" - -namespace XCEngine::UI::Editor::Host { - -using ::XCEngine::RHI::D3D12SwapChain; -using ::XCEngine::RHI::D3D12Texture; -using ::XCEngine::RHI::Format; -using ::XCEngine::RHI::ResourceStates; -using ::XCEngine::RHI::ResourceViewDesc; -using ::XCEngine::RHI::ResourceViewDimension; - -void D3D12WindowSwapChainPresenter::ReleaseBackBufferCommandReferences() { - if (m_hostDevice == nullptr) { - return; - } - - for (std::uint32_t frameIndex = 0u; - frameIndex < D3D12HostDevice::kFrameContextCount; - ++frameIndex) { - auto* commandList = m_hostDevice->GetCommandList(frameIndex); - if (commandList == nullptr) { - continue; - } - - commandList->Reset(); - commandList->Close(); - } -} - -void D3D12WindowSwapChainPresenter::ReleaseBackBufferViews() { - for (auto* view : m_backBufferViews) { - if (view != nullptr) { - view->Shutdown(); - delete view; - } - } - - m_backBufferViews.clear(); - m_backBufferSurfaces.clear(); -} - -bool D3D12WindowSwapChainPresenter::RecreateBackBufferViews() { - D3D12SwapChain* d3d12SwapChain = GetD3D12SwapChain(); - if (m_hostDevice == nullptr || - m_hostDevice->GetRHIDevice() == nullptr || - d3d12SwapChain == nullptr) { - return false; - } - - m_backBufferViews.resize(kSwapChainBufferCount, nullptr); - m_backBufferSurfaces.resize(kSwapChainBufferCount); - - ResourceViewDesc viewDesc = {}; - viewDesc.format = static_cast(Format::R8G8B8A8_UNorm); - viewDesc.dimension = ResourceViewDimension::Texture2D; - - for (std::uint32_t backBufferIndex = 0u; - backBufferIndex < kSwapChainBufferCount; - ++backBufferIndex) { - D3D12Texture* backBufferTexture = d3d12SwapChain->TryGetBackBuffer(backBufferIndex); - if (backBufferTexture == nullptr) { - ReleaseBackBufferViews(); - m_lastError = "RecreateBackBufferViews could not resolve swap chain back buffer " + - std::to_string(backBufferIndex) + "."; - return false; - } - - m_backBufferViews[backBufferIndex] = m_hostDevice->GetRHIDevice()->CreateRenderTargetView( - backBufferTexture, - viewDesc); - if (m_backBufferViews[backBufferIndex] == nullptr) { - ReleaseBackBufferViews(); - m_lastError = "RecreateBackBufferViews failed to create RTV for swap chain back buffer " + - std::to_string(backBufferIndex) + "."; - return false; - } - - ::XCEngine::Rendering::RenderSurface& surface = m_backBufferSurfaces[backBufferIndex]; - surface = ::XCEngine::Rendering::RenderSurface( - static_cast(m_width), - static_cast(m_height)); - surface.SetColorAttachment(m_backBufferViews[backBufferIndex]); - surface.SetAutoTransitionEnabled(false); - surface.SetColorStateBefore(ResourceStates::RenderTarget); - surface.SetColorStateAfter(ResourceStates::RenderTarget); - } - - m_lastError.clear(); - return true; -} - -} // namespace XCEngine::UI::Editor::Host diff --git a/new_editor/app/Rendering/D3D12/SwapChainPresenter/Lifecycle.cpp b/new_editor/app/Rendering/D3D12/SwapChainPresenter/Lifecycle.cpp deleted file mode 100644 index 9e17bc2f..00000000 --- a/new_editor/app/Rendering/D3D12/SwapChainPresenter/Lifecycle.cpp +++ /dev/null @@ -1,170 +0,0 @@ -#include "Rendering/D3D12/D3D12WindowSwapChainPresenter.h" - -namespace XCEngine::UI::Editor::Host { - -using ::XCEngine::RHI::D3D12SwapChain; -using ::XCEngine::RHI::D3D12Texture; -using ::XCEngine::RHI::RHISwapChain; -using ::XCEngine::RHI::SwapChainDesc; - -bool D3D12WindowSwapChainPresenter::Initialize( - D3D12HostDevice& hostDevice, - HWND hwnd, - int width, - int height) { - Shutdown(); - - if (hwnd == nullptr || width <= 0 || height <= 0) { - m_lastError = "Initialize rejected an invalid hwnd or size."; - return false; - } - - if (hostDevice.GetRHIDevice() == nullptr || hostDevice.GetRHICommandQueue() == nullptr) { - m_lastError = "Initialize requires an initialized host D3D12 device."; - return false; - } - - m_hwnd = hwnd; - m_width = width; - m_height = height; - m_hostDevice = &hostDevice; - - if (!CreateSwapChain(width, height)) { - Shutdown(); - return false; - } - - m_lastError.clear(); - return true; -} - -bool D3D12WindowSwapChainPresenter::CreateSwapChain(int width, int height) { - if (m_hostDevice == nullptr || - m_hostDevice->GetRHIDevice() == nullptr || - m_hostDevice->GetRHICommandQueue() == nullptr) { - m_lastError = "CreateSwapChain requires an initialized host D3D12 device."; - return false; - } - - SwapChainDesc swapChainDesc = {}; - swapChainDesc.windowHandle = m_hwnd; - swapChainDesc.width = static_cast(width); - swapChainDesc.height = static_cast(height); - swapChainDesc.bufferCount = kSwapChainBufferCount; - m_swapChain = m_hostDevice->GetRHIDevice()->CreateSwapChain( - swapChainDesc, - m_hostDevice->GetRHICommandQueue()); - if (m_swapChain == nullptr || GetD3D12SwapChain() == nullptr) { - m_lastError = "Failed to create the D3D12 swap chain."; - return false; - } - - ConfigureFrameLatency(); - - if (!RecreateBackBufferViews()) { - m_lastError = "Failed to create swap chain back buffer views."; - return false; - } - - m_width = width; - m_height = height; - return true; -} - -void D3D12WindowSwapChainPresenter::ConfigureFrameLatency() { - D3D12SwapChain* d3d12SwapChain = GetD3D12SwapChain(); - if (d3d12SwapChain == nullptr) { - return; - } - - auto* nativeSwapChain = - static_cast(d3d12SwapChain->GetNativeHandle()); - if (nativeSwapChain == nullptr) { - return; - } - - nativeSwapChain->SetMaximumFrameLatency(1u); -} - -void D3D12WindowSwapChainPresenter::DestroySwapChain() { - ReleaseBackBufferViews(); - - if (m_swapChain != nullptr) { - m_swapChain->Shutdown(); - delete m_swapChain; - m_swapChain = nullptr; - } -} - -bool D3D12WindowSwapChainPresenter::RecreateSwapChain(int width, int height) { - DestroySwapChain(); - m_hostDevice->ResetFrameTracking(); - return CreateSwapChain(width, height); -} - -void D3D12WindowSwapChainPresenter::Shutdown() { - if (m_hostDevice != nullptr) { - m_hostDevice->WaitForGpuIdle(); - } - - DestroySwapChain(); - - m_hwnd = nullptr; - m_width = 0; - m_height = 0; - m_hostDevice = nullptr; - m_lastError.clear(); -} - -const std::string& D3D12WindowSwapChainPresenter::GetLastError() const { - return m_lastError; -} - -RHISwapChain* D3D12WindowSwapChainPresenter::GetSwapChain() const { - return m_swapChain; -} - -const ::XCEngine::Rendering::RenderSurface* -D3D12WindowSwapChainPresenter::GetCurrentRenderSurface() const { - if (m_swapChain == nullptr) { - return nullptr; - } - - const std::uint32_t backBufferIndex = m_swapChain->GetCurrentBackBufferIndex(); - if (backBufferIndex >= m_backBufferSurfaces.size()) { - return nullptr; - } - - return &m_backBufferSurfaces[backBufferIndex]; -} - -const D3D12Texture* D3D12WindowSwapChainPresenter::GetCurrentBackBufferTexture() const { - if (m_swapChain == nullptr) { - return nullptr; - } - - return GetBackBufferTexture(m_swapChain->GetCurrentBackBufferIndex()); -} - -const D3D12Texture* D3D12WindowSwapChainPresenter::GetBackBufferTexture(std::uint32_t index) const { - const D3D12SwapChain* d3d12SwapChain = GetD3D12SwapChain(); - if (d3d12SwapChain == nullptr) { - return nullptr; - } - - return d3d12SwapChain->TryGetBackBuffer(index); -} - -std::uint32_t D3D12WindowSwapChainPresenter::GetBackBufferCount() const { - return kSwapChainBufferCount; -} - -std::uint32_t D3D12WindowSwapChainPresenter::GetCurrentBackBufferIndex() const { - return m_swapChain != nullptr ? m_swapChain->GetCurrentBackBufferIndex() : 0u; -} - -D3D12SwapChain* D3D12WindowSwapChainPresenter::GetD3D12SwapChain() const { - return m_swapChain != nullptr ? static_cast(m_swapChain) : nullptr; -} - -} // namespace XCEngine::UI::Editor::Host diff --git a/new_editor/app/Rendering/D3D12/SwapChainPresenter/Presentation.cpp b/new_editor/app/Rendering/D3D12/SwapChainPresenter/Presentation.cpp deleted file mode 100644 index e8ab4723..00000000 --- a/new_editor/app/Rendering/D3D12/SwapChainPresenter/Presentation.cpp +++ /dev/null @@ -1,50 +0,0 @@ -#include "Rendering/D3D12/D3D12WindowSwapChainPresenter.h" - -#include - -namespace XCEngine::UI::Editor::Host { - -bool D3D12WindowSwapChainPresenter::PreparePresentSurface( - const ::XCEngine::Rendering::RenderContext& renderContext) { - if (!renderContext.IsValid() || renderContext.commandList == nullptr) { - m_lastError = "PreparePresentSurface requires a valid render context."; - return false; - } - - if (m_swapChain == nullptr) { - m_lastError = "PreparePresentSurface requires an initialized swap chain."; - return false; - } - - const std::uint32_t backBufferIndex = m_swapChain->GetCurrentBackBufferIndex(); - if (backBufferIndex >= m_backBufferViews.size() || - m_backBufferViews[backBufferIndex] == nullptr) { - std::ostringstream error = {}; - error << "PreparePresentSurface could not find the current swap chain RTV. index=" - << backBufferIndex - << " rtvCount=" - << m_backBufferViews.size(); - m_lastError = error.str(); - return false; - } - - renderContext.commandList->TransitionBarrier( - m_backBufferViews[backBufferIndex], - ::XCEngine::RHI::ResourceStates::Present, - ::XCEngine::RHI::ResourceStates::RenderTarget); - m_lastError.clear(); - return true; -} - -bool D3D12WindowSwapChainPresenter::PresentFrame() { - if (m_swapChain == nullptr) { - m_lastError = "PresentFrame requires an initialized swap chain."; - return false; - } - - m_swapChain->Present(0, 0); - m_lastError.clear(); - return true; -} - -} // namespace XCEngine::UI::Editor::Host diff --git a/new_editor/app/Rendering/D3D12/SwapChainPresenter/Resize.cpp b/new_editor/app/Rendering/D3D12/SwapChainPresenter/Resize.cpp deleted file mode 100644 index 7b25aa87..00000000 --- a/new_editor/app/Rendering/D3D12/SwapChainPresenter/Resize.cpp +++ /dev/null @@ -1,100 +0,0 @@ -#include "Rendering/D3D12/D3D12WindowSwapChainPresenter.h" - -#include - -namespace XCEngine::UI::Editor::Host { - -using ::XCEngine::RHI::D3D12SwapChain; -using ::XCEngine::RHI::D3D12Texture; - -bool D3D12WindowSwapChainPresenter::Resize(int width, int height) { - if (width <= 0 || height <= 0 || m_swapChain == nullptr || m_hostDevice == nullptr) { - return false; - } - - if (m_width == width && m_height == height) { - m_lastError.clear(); - return true; - } - - m_hostDevice->WaitForGpuIdle(); - ReleaseBackBufferCommandReferences(); - ReleaseBackBufferViews(); - - D3D12SwapChain* d3d12SwapChain = GetD3D12SwapChain(); - if (d3d12SwapChain == nullptr) { - m_lastError = "Resize could not resolve the native D3D12 swap chain."; - return false; - } - - d3d12SwapChain->Resize( - static_cast(width), - static_cast(height)); - const HRESULT resizeHr = d3d12SwapChain->GetLastResizeResult(); - if (FAILED(resizeHr)) { - if (RecreateSwapChain(width, height)) { - m_lastError.clear(); - return true; - } - - std::ostringstream error = {}; - error << "ResizeBuffers failed. requested=" - << width - << 'x' - << height - << " hr=0x" - << std::uppercase - << std::hex - << static_cast(resizeHr); - m_lastError = error.str(); - return false; - } - - const D3D12Texture* backBufferTexture = GetBackBufferTexture(0u); - if (backBufferTexture == nullptr) { - if (RecreateSwapChain(width, height)) { - m_lastError.clear(); - return true; - } - - m_lastError = "Resize failed to restore swap chain back buffers after ResizeBuffers."; - return false; - } - - m_width = static_cast(backBufferTexture->GetWidth()); - m_height = static_cast(backBufferTexture->GetHeight()); - m_hostDevice->ResetFrameTracking(); - if (!RecreateBackBufferViews()) { - if (RecreateSwapChain(width, height)) { - m_lastError.clear(); - return true; - } - - m_lastError = "Resize failed to recreate swap chain back buffer views."; - return false; - } - - if (m_width != width || m_height != height) { - if (RecreateSwapChain(width, height)) { - m_lastError.clear(); - return true; - } - - std::ostringstream error = {}; - error << "Resize ended with an unexpected swap chain size. requested=" - << width - << 'x' - << height - << " actual=" - << m_width - << 'x' - << m_height; - m_lastError = error.str(); - return false; - } - - m_lastError.clear(); - return true; -} - -} // namespace XCEngine::UI::Editor::Host diff --git a/new_editor/app/Rendering/Native/AutoScreenshot.cpp b/new_editor/app/Rendering/Native/AutoScreenshot.cpp index 4f583838..33af907e 100644 --- a/new_editor/app/Rendering/Native/AutoScreenshot.cpp +++ b/new_editor/app/Rendering/Native/AutoScreenshot.cpp @@ -1,8 +1,8 @@ #include "AutoScreenshot.h" #include "NativeRenderer.h" -#include "Internal/EnvironmentFlags.h" -#include "Internal/ExecutablePath.h" +#include "Support/EnvironmentFlags.h" +#include "Support/ExecutablePath.h" #include #include @@ -14,7 +14,7 @@ namespace XCEngine::UI::Editor::Host { namespace { std::filesystem::path ResolveBuildCaptureRoot(const std::filesystem::path& requestedCaptureRoot) { - std::filesystem::path captureRoot = App::Internal::GetExecutableDirectory() / "captures"; + std::filesystem::path captureRoot = App::GetExecutableDirectory() / "captures"; const std::filesystem::path scenarioPath = requestedCaptureRoot.parent_path().filename(); if (!scenarioPath.empty() && scenarioPath != "captures") { captureRoot /= scenarioPath; @@ -34,7 +34,7 @@ void AutoScreenshotController::Initialize(const std::filesystem::path& captureRo m_pendingReason.clear(); m_lastCaptureSummary = "Output: " + m_captureRoot.string(); m_lastCaptureError.clear(); - if (App::Internal::IsEnvironmentFlagEnabled("XCUI_AUTO_CAPTURE_ON_STARTUP")) { + if (App::IsEnvironmentFlagEnabled("XCUI_AUTO_CAPTURE_ON_STARTUP")) { RequestCapture("startup"); } } diff --git a/new_editor/app/Rendering/Native/NativeRenderer.cpp b/new_editor/app/Rendering/Native/NativeRenderer.cpp new file mode 100644 index 00000000..620295ae --- /dev/null +++ b/new_editor/app/Rendering/Native/NativeRenderer.cpp @@ -0,0 +1,1344 @@ +#include "NativeRendererHelpers.h" +#include +#include +#include +#include +#include + +namespace XCEngine::UI::Editor::Host { + +using namespace NativeRendererHelpers; + +bool NativeRenderer::Initialize(HWND hwnd) { + Shutdown(); + + if (hwnd == nullptr) { + m_lastRenderError = "Initialize rejected a null hwnd."; + return false; + } + + m_hwnd = hwnd; + D2D1_FACTORY_OPTIONS factoryOptions = {}; +#ifdef _DEBUG + factoryOptions.debugLevel = D2D1_DEBUG_LEVEL_INFORMATION; +#endif + HRESULT hr = D2D1CreateFactory( + D2D1_FACTORY_TYPE_SINGLE_THREADED, + __uuidof(ID2D1Factory1), + &factoryOptions, + reinterpret_cast(m_d2dFactory.ReleaseAndGetAddressOf())); + if (FAILED(hr)) { + m_lastRenderError = HrToString("D2D1CreateFactory", hr); + Shutdown(); + return false; + } + + hr = DWriteCreateFactory( + DWRITE_FACTORY_TYPE_SHARED, + __uuidof(IDWriteFactory), + reinterpret_cast(m_dwriteFactory.ReleaseAndGetAddressOf())); + if (FAILED(hr)) { + m_lastRenderError = HrToString("DWriteCreateFactory", hr); + Shutdown(); + return false; + } + + m_lastRenderError.clear(); + return true; +} + +void NativeRenderer::Shutdown() { + DetachWindowRenderer(); + while (!m_liveTextures.empty()) { + auto it = m_liveTextures.begin(); + delete *it; + m_liveTextures.erase(it); + } + m_textFormats.clear(); + m_solidBrush.Reset(); + m_renderTarget.Reset(); + m_wicFactory.Reset(); + m_dwriteFactory.Reset(); + m_d2dFactory.Reset(); + if (m_wicComInitialized) { + CoUninitialize(); + m_wicComInitialized = false; + } + m_hwnd = nullptr; +} + +void NativeRenderer::SetDpiScale(float dpiScale) { + m_dpiScale = ClampDpiScale(dpiScale); + if (m_renderTarget) { + m_renderTarget->SetDpi(kBaseDpi, kBaseDpi); + } +} + +float NativeRenderer::GetDpiScale() const { + return m_dpiScale; +} + +const std::string& NativeRenderer::GetLastRenderError() const { + return m_lastRenderError; +} + +bool NativeRenderer::EnsureWicFactory(std::string& outError) { + outError.clear(); + if (m_wicFactory) { + return true; + } + + const HRESULT initHr = CoInitializeEx(nullptr, COINIT_MULTITHREADED); + if (FAILED(initHr) && initHr != RPC_E_CHANGED_MODE) { + outError = HrToString("CoInitializeEx", initHr); + return false; + } + if (SUCCEEDED(initHr)) { + m_wicComInitialized = true; + } + + const HRESULT factoryHr = CoCreateInstance( + CLSID_WICImagingFactory, + nullptr, + CLSCTX_INPROC_SERVER, + IID_PPV_ARGS(m_wicFactory.ReleaseAndGetAddressOf())); + if (FAILED(factoryHr)) { + outError = HrToString("CoCreateInstance(CLSID_WICImagingFactory)", factoryHr); + return false; + } + + return true; +} + +} // namespace XCEngine::UI::Editor::Host + +namespace XCEngine::UI::Editor::Host { + +using namespace NativeRendererHelpers; + +bool NativeRenderer::Render(const ::XCEngine::UI::UIDrawData& drawData) { + if (!EnsureRenderTarget()) { + if (m_lastRenderError.empty()) { + m_lastRenderError = "EnsureRenderTarget failed."; + } + return false; + } + + const bool rendered = RenderToTarget(*m_renderTarget.Get(), *m_solidBrush.Get(), drawData); + const HRESULT hr = m_renderTarget->EndDraw(); + if (hr == D2DERR_RECREATE_TARGET) { + m_lastRenderError = HrToString("ID2D1HwndRenderTarget::EndDraw", hr); + DiscardRenderTarget(); + return false; + } + + if (!rendered || FAILED(hr)) { + m_lastRenderError = HrToString("ID2D1HwndRenderTarget::EndDraw", hr); + return false; + } + + m_lastRenderError.clear(); + return true; +} + +bool NativeRenderer::RenderToWindowRenderer(const ::XCEngine::UI::UIDrawData& drawData) { + if (!EnsureWindowRendererInterop()) { + if (m_lastRenderError.empty()) { + m_lastRenderError = "Window renderer interop is not available."; + } + return false; + } + + if (!m_windowInterop.HasBackBufferTargets() && + !m_windowInterop.RebuildBackBufferTargets()) { + if (m_lastRenderError.empty()) { + m_lastRenderError = "Window renderer back buffer interop targets are unavailable."; + } + return false; + } + + ID3D11On12Device* d3d11On12Device = m_windowInterop.GetD3D11On12Device(); + ID3D11DeviceContext* d3d11DeviceContext = m_windowInterop.GetD3D11DeviceContext(); + ID2D1DeviceContext* d2dDeviceContext = m_windowInterop.GetD2DDeviceContext(); + ID2D1SolidColorBrush* interopBrush = m_windowInterop.GetInteropBrush(); + if (d3d11On12Device == nullptr || + d3d11DeviceContext == nullptr || + d2dDeviceContext == nullptr || + interopBrush == nullptr) { + m_lastRenderError = "Window renderer interop resources are incomplete."; + return false; + } + + const std::uint32_t backBufferIndex = m_windowInterop.GetCurrentBackBufferIndex(); + if (m_windowInterop.GetWrappedBackBufferResource(backBufferIndex) == nullptr || + m_windowInterop.GetBackBufferTargetBitmap(backBufferIndex) == nullptr) { + m_lastRenderError = "Back buffer interop target index is out of range."; + return false; + } + + if (!m_windowRenderer->PreparePresentSurface()) { + m_lastRenderError = "Failed to prepare the D3D12 present surface: " + + m_windowRenderer->GetLastError(); + return false; + } + + if (!m_windowRenderer->SubmitFrame(false)) { + m_lastRenderError = "Failed to submit the D3D12 frame before UI composition."; + return false; + } + + if (!m_windowInterop.PrepareSourceTextures(drawData)) { + ID3D11Resource* backBufferResource = + m_windowInterop.GetWrappedBackBufferResource(backBufferIndex); + if (backBufferResource != nullptr) { + d3d11On12Device->AcquireWrappedResources(&backBufferResource, 1u); + d3d11On12Device->ReleaseWrappedResources(&backBufferResource, 1u); + } + d3d11DeviceContext->Flush(); + m_windowInterop.ClearSourceTextures(); + const bool signaled = m_windowRenderer->SignalFrameCompletion(); + ReleaseWindowRendererInterop(); + if (!signaled) { + m_lastRenderError = + "Failed to signal D3D12 frame completion after interop preparation failed."; + } + return false; + } + + std::vector acquiredResources = {}; + m_windowInterop.BuildAcquiredResources(backBufferIndex, acquiredResources); + if (acquiredResources.empty()) { + m_lastRenderError = "No wrapped interop resources were prepared for UI composition."; + return false; + } + + d3d11On12Device->AcquireWrappedResources( + acquiredResources.data(), + static_cast(acquiredResources.size())); + + d2dDeviceContext->SetTarget(m_windowInterop.GetBackBufferTargetBitmap(backBufferIndex)); + const bool rendered = RenderToTarget(*d2dDeviceContext, *interopBrush, drawData); + const HRESULT hr = d2dDeviceContext->EndDraw(); + + d3d11On12Device->ReleaseWrappedResources( + acquiredResources.data(), + static_cast(acquiredResources.size())); + d3d11DeviceContext->Flush(); + d2dDeviceContext->SetTarget(nullptr); + m_windowInterop.ClearSourceTextures(); + + if (!rendered || FAILED(hr)) { + m_lastRenderError = FAILED(hr) + ? HrToString("ID2D1DeviceContext::EndDraw", hr) + : "RenderToTarget failed during D3D11On12 composition."; + const bool signaled = m_windowRenderer->SignalFrameCompletion(); + if (hr == D2DERR_RECREATE_TARGET) { + ReleaseWindowRendererBackBufferTargets(); + } else { + ReleaseWindowRendererInterop(); + } + if (!signaled) { + m_lastRenderError = + "Failed to signal D3D12 frame completion after UI composition failed."; + } + return false; + } + + if (!m_windowRenderer->SignalFrameCompletion()) { + m_lastRenderError = "Failed to signal D3D12 frame completion after UI composition."; + ReleaseWindowRendererInterop(); + return false; + } + if (!m_windowRenderer->PresentFrame()) { + m_lastRenderError = "Failed to present the D3D12 swap chain."; + ReleaseWindowRendererInterop(); + return false; + } + + m_lastRenderError.clear(); + return true; +} + +} // namespace XCEngine::UI::Editor::Host + +namespace XCEngine::UI::Editor::Host { + +using namespace NativeRendererHelpers; + +void NativeRenderer::Resize(UINT width, UINT height) { + if (!m_renderTarget || width == 0 || height == 0) { + return; + } + + const HRESULT hr = m_renderTarget->Resize(D2D1::SizeU(width, height)); + if (hr == D2DERR_RECREATE_TARGET) { + DiscardRenderTarget(); + } +} + +bool NativeRenderer::EnsureRenderTarget() { + if (!m_hwnd || !m_d2dFactory || !m_dwriteFactory) { + m_lastRenderError = "EnsureRenderTarget requires hwnd, D2D factory, and DWrite factory."; + return false; + } + + return CreateDeviceResources(); +} + +void NativeRenderer::DiscardRenderTarget() { + InvalidateCachedTextureBitmaps(m_renderTarget.Get()); + m_solidBrush.Reset(); + m_renderTarget.Reset(); +} + +bool NativeRenderer::CreateDeviceResources() { + if (m_renderTarget) { + return true; + } + + RECT clientRect = {}; + GetClientRect(m_hwnd, &clientRect); + const UINT width = static_cast((std::max)(clientRect.right - clientRect.left, 1L)); + const UINT height = static_cast((std::max)(clientRect.bottom - clientRect.top, 1L)); + + const D2D1_RENDER_TARGET_PROPERTIES renderTargetProps = D2D1::RenderTargetProperties( + D2D1_RENDER_TARGET_TYPE_DEFAULT, + D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED), + kBaseDpi, + kBaseDpi); + const D2D1_HWND_RENDER_TARGET_PROPERTIES hwndProps = D2D1::HwndRenderTargetProperties( + m_hwnd, + D2D1::SizeU(width, height)); + + const HRESULT renderTargetHr = m_d2dFactory->CreateHwndRenderTarget( + renderTargetProps, + hwndProps, + m_renderTarget.ReleaseAndGetAddressOf()); + if (FAILED(renderTargetHr)) { + m_lastRenderError = HrToString("ID2D1Factory::CreateHwndRenderTarget", renderTargetHr); + return false; + } + + const HRESULT brushHr = m_renderTarget->CreateSolidColorBrush( + D2D1::ColorF(1.0f, 1.0f, 1.0f, 1.0f), + m_solidBrush.ReleaseAndGetAddressOf()); + if (FAILED(brushHr)) { + m_lastRenderError = HrToString("ID2D1HwndRenderTarget::CreateSolidColorBrush", brushHr); + DiscardRenderTarget(); + return false; + } + + m_renderTarget->SetDpi(kBaseDpi, kBaseDpi); + m_renderTarget->SetTextAntialiasMode(D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE); + m_lastRenderError.clear(); + return true; +} + +void NativeRenderer::InvalidateCachedTextureBitmaps(const ID2D1RenderTarget* renderTarget) { + for (NativeTextureResource* texture : m_liveTextures) { + if (texture == nullptr) { + continue; + } + + if (renderTarget == nullptr || texture->cachedTarget == renderTarget) { + texture->cachedBitmap.Reset(); + texture->cachedTarget = nullptr; + } + } +} + +} // namespace XCEngine::UI::Editor::Host + +namespace XCEngine::UI::Editor::Host { + +using namespace NativeRendererHelpers; + +bool NativeRenderer::RenderToTarget( + ID2D1RenderTarget& renderTarget, + ID2D1SolidColorBrush& solidBrush, + const ::XCEngine::UI::UIDrawData& drawData) { + renderTarget.SetDpi(kBaseDpi, kBaseDpi); + renderTarget.SetTextAntialiasMode(D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE); + renderTarget.BeginDraw(); + renderTarget.Clear(D2D1::ColorF(0.04f, 0.05f, 0.06f, 1.0f)); + + std::vector clipStack = {}; + for (const ::XCEngine::UI::UIDrawList& drawList : drawData.GetDrawLists()) { + for (const ::XCEngine::UI::UIDrawCommand& command : drawList.GetCommands()) { + RenderCommand(renderTarget, solidBrush, command, clipStack); + } + } + + while (!clipStack.empty()) { + renderTarget.PopAxisAlignedClip(); + clipStack.pop_back(); + } + + return true; +} + +void NativeRenderer::RenderCommand( + ID2D1RenderTarget& renderTarget, + ID2D1SolidColorBrush& solidBrush, + const ::XCEngine::UI::UIDrawCommand& command, + std::vector& clipStack) { + solidBrush.SetColor(ToD2DColor(command.color)); + + switch (command.type) { + case ::XCEngine::UI::UIDrawCommandType::FilledRect: + RenderFilledRectCommand(renderTarget, solidBrush, command); + break; + case ::XCEngine::UI::UIDrawCommandType::FilledRectLinearGradient: + RenderFilledRectLinearGradientCommand(renderTarget, command); + break; + case ::XCEngine::UI::UIDrawCommandType::RectOutline: + RenderRectOutlineCommand(renderTarget, solidBrush, command); + break; + case ::XCEngine::UI::UIDrawCommandType::Line: + RenderLineCommand(renderTarget, solidBrush, command); + break; + case ::XCEngine::UI::UIDrawCommandType::FilledTriangle: + RenderFilledTriangleCommand(renderTarget, solidBrush, command); + break; + case ::XCEngine::UI::UIDrawCommandType::FilledCircle: + RenderFilledCircleCommand(renderTarget, solidBrush, command); + break; + case ::XCEngine::UI::UIDrawCommandType::CircleOutline: + RenderCircleOutlineCommand(renderTarget, solidBrush, command); + break; + case ::XCEngine::UI::UIDrawCommandType::Text: + RenderTextCommand(renderTarget, solidBrush, command); + break; + case ::XCEngine::UI::UIDrawCommandType::Image: + RenderImageCommand(renderTarget, solidBrush, command); + break; + case ::XCEngine::UI::UIDrawCommandType::PushClipRect: { + const float dpiScale = ClampDpiScale(m_dpiScale); + const D2D1_RECT_F rect = ToD2DRect(command.rect, dpiScale); + renderTarget.PushAxisAlignedClip(rect, D2D1_ANTIALIAS_MODE_PER_PRIMITIVE); + clipStack.push_back(rect); + break; + } + case ::XCEngine::UI::UIDrawCommandType::PopClipRect: { + if (!clipStack.empty()) { + renderTarget.PopAxisAlignedClip(); + clipStack.pop_back(); + } + break; + } + default: + break; + } +} + +} // namespace XCEngine::UI::Editor::Host + +namespace XCEngine::UI::Editor::Host { + +using namespace NativeRendererHelpers; + +void NativeRenderer::RenderFilledRectCommand( + ID2D1RenderTarget& renderTarget, + ID2D1SolidColorBrush& solidBrush, + const ::XCEngine::UI::UIDrawCommand& command) { + const float dpiScale = ClampDpiScale(m_dpiScale); + const D2D1_RECT_F rect = ToD2DRect(command.rect, dpiScale); + const float rounding = command.rounding > 0.0f ? command.rounding * dpiScale : 0.0f; + if (command.rounding > 0.0f) { + renderTarget.FillRoundedRectangle( + D2D1::RoundedRect(rect, rounding, rounding), + &solidBrush); + return; + } + + renderTarget.FillRectangle(rect, &solidBrush); +} + +void NativeRenderer::RenderFilledRectLinearGradientCommand( + ID2D1RenderTarget& renderTarget, + const ::XCEngine::UI::UIDrawCommand& command) { + const float dpiScale = ClampDpiScale(m_dpiScale); + const D2D1_RECT_F rect = ToD2DRect(command.rect, dpiScale); + const float rounding = command.rounding > 0.0f ? command.rounding * dpiScale : 0.0f; + + const D2D1_GRADIENT_STOP stops[2] = { + D2D1::GradientStop(0.0f, ToD2DColor(command.color)), + D2D1::GradientStop(1.0f, ToD2DColor(command.secondaryColor)) + }; + + Microsoft::WRL::ComPtr stopCollection; + HRESULT hr = renderTarget.CreateGradientStopCollection( + stops, + 2u, + stopCollection.ReleaseAndGetAddressOf()); + if (FAILED(hr) || !stopCollection) { + return; + } + + const D2D1_POINT_2F startPoint = + command.gradientDirection == ::XCEngine::UI::UILinearGradientDirection::Vertical + ? D2D1::Point2F((rect.left + rect.right) * 0.5f, rect.top) + : D2D1::Point2F(rect.left, (rect.top + rect.bottom) * 0.5f); + const D2D1_POINT_2F endPoint = + command.gradientDirection == ::XCEngine::UI::UILinearGradientDirection::Vertical + ? D2D1::Point2F((rect.left + rect.right) * 0.5f, rect.bottom) + : D2D1::Point2F(rect.right, (rect.top + rect.bottom) * 0.5f); + + Microsoft::WRL::ComPtr gradientBrush; + hr = renderTarget.CreateLinearGradientBrush( + D2D1::LinearGradientBrushProperties(startPoint, endPoint), + stopCollection.Get(), + gradientBrush.ReleaseAndGetAddressOf()); + if (FAILED(hr) || !gradientBrush) { + return; + } + + if (command.rounding > 0.0f) { + renderTarget.FillRoundedRectangle( + D2D1::RoundedRect(rect, rounding, rounding), + gradientBrush.Get()); + return; + } + + renderTarget.FillRectangle(rect, gradientBrush.Get()); +} + +void NativeRenderer::RenderRectOutlineCommand( + ID2D1RenderTarget& renderTarget, + ID2D1SolidColorBrush& solidBrush, + const ::XCEngine::UI::UIDrawCommand& command) { + const float dpiScale = ClampDpiScale(m_dpiScale); + const D2D1_RECT_F rect = ToD2DRect(command.rect, dpiScale); + const float thickness = (command.thickness > 0.0f ? command.thickness : 1.0f) * dpiScale; + const float rounding = command.rounding > 0.0f ? command.rounding * dpiScale : 0.0f; + if (command.rounding > 0.0f) { + renderTarget.DrawRoundedRectangle( + D2D1::RoundedRect(rect, rounding, rounding), + &solidBrush, + thickness); + return; + } + + renderTarget.DrawRectangle(rect, &solidBrush, thickness); +} + +void NativeRenderer::RenderLineCommand( + ID2D1RenderTarget& renderTarget, + ID2D1SolidColorBrush& solidBrush, + const ::XCEngine::UI::UIDrawCommand& command) { + const float dpiScale = ClampDpiScale(m_dpiScale); + const float thickness = (command.thickness > 0.0f ? command.thickness : 1.0f) * dpiScale; + const float pixelOffset = ResolveStrokePixelOffset(thickness); + const D2D1_POINT_2F start = ToD2DPoint(command.position, dpiScale, pixelOffset); + const D2D1_POINT_2F end = ToD2DPoint(command.uvMin, dpiScale, pixelOffset); + renderTarget.DrawLine(start, end, &solidBrush, thickness); +} + +void NativeRenderer::RenderFilledTriangleCommand( + ID2D1RenderTarget& renderTarget, + ID2D1SolidColorBrush& solidBrush, + const ::XCEngine::UI::UIDrawCommand& command) { + Microsoft::WRL::ComPtr geometry; + HRESULT hr = m_d2dFactory->CreatePathGeometry(geometry.ReleaseAndGetAddressOf()); + if (FAILED(hr) || !geometry) { + return; + } + + Microsoft::WRL::ComPtr sink; + hr = geometry->Open(sink.ReleaseAndGetAddressOf()); + if (FAILED(hr) || !sink) { + return; + } + + const float dpiScale = ClampDpiScale(m_dpiScale); + const D2D1_POINT_2F points[3] = { + ToD2DPoint(command.position, dpiScale), + ToD2DPoint(command.uvMin, dpiScale), + ToD2DPoint(command.uvMax, dpiScale) + }; + + sink->BeginFigure(points[0], D2D1_FIGURE_BEGIN_FILLED); + sink->AddLines(points + 1, 2u); + sink->EndFigure(D2D1_FIGURE_END_CLOSED); + hr = sink->Close(); + if (FAILED(hr)) { + return; + } + + renderTarget.FillGeometry(geometry.Get(), &solidBrush); +} + +void NativeRenderer::RenderFilledCircleCommand( + ID2D1RenderTarget& renderTarget, + ID2D1SolidColorBrush& solidBrush, + const ::XCEngine::UI::UIDrawCommand& command) { + const float dpiScale = ClampDpiScale(m_dpiScale); + const float radius = command.radius * dpiScale; + renderTarget.FillEllipse( + D2D1::Ellipse(ToD2DPoint(command.position, dpiScale), radius, radius), + &solidBrush); +} + +void NativeRenderer::RenderCircleOutlineCommand( + ID2D1RenderTarget& renderTarget, + ID2D1SolidColorBrush& solidBrush, + const ::XCEngine::UI::UIDrawCommand& command) { + const float dpiScale = ClampDpiScale(m_dpiScale); + const float radius = command.radius * dpiScale; + const float thickness = (command.thickness > 0.0f ? command.thickness : 1.0f) * dpiScale; + renderTarget.DrawEllipse( + D2D1::Ellipse(ToD2DPoint(command.position, dpiScale), radius, radius), + &solidBrush, + thickness); +} + +} // namespace XCEngine::UI::Editor::Host + +namespace XCEngine::UI::Editor::Host { + +using namespace NativeRendererHelpers; + +void NativeRenderer::RenderTextCommand( + ID2D1RenderTarget& renderTarget, + ID2D1SolidColorBrush& solidBrush, + const ::XCEngine::UI::UIDrawCommand& command) { + if (command.text.empty()) { + return; + } + + const float dpiScale = ClampDpiScale(m_dpiScale); + const float fontSize = ResolveFontSize(command.fontSize); + const float scaledFontSize = fontSize * dpiScale; + IDWriteTextFormat* textFormat = GetTextFormat(scaledFontSize); + if (textFormat == nullptr) { + return; + } + + const std::wstring text = Utf8ToWide(command.text); + if (text.empty()) { + return; + } + + const D2D1_SIZE_F targetSize = renderTarget.GetSize(); + const float originX = SnapToPixel(command.position.x, dpiScale); + const float originY = SnapToPixel(command.position.y, dpiScale); + const float lineHeight = std::ceil(scaledFontSize * 1.6f); + const D2D1_RECT_F layoutRect = D2D1::RectF( + originX, + originY, + targetSize.width, + originY + lineHeight); + renderTarget.DrawTextW( + text.c_str(), + static_cast(text.size()), + textFormat, + layoutRect, + &solidBrush, + D2D1_DRAW_TEXT_OPTIONS_CLIP, + DWRITE_MEASURING_MODE_GDI_NATURAL); +} + +void NativeRenderer::RenderImageCommand( + ID2D1RenderTarget& renderTarget, + ID2D1SolidColorBrush& solidBrush, + const ::XCEngine::UI::UIDrawCommand& command) { + if (!command.texture.IsValid()) { + return; + } + + const float dpiScale = ClampDpiScale(m_dpiScale); + Microsoft::WRL::ComPtr bitmap; + if (command.texture.kind == ::XCEngine::UI::UITextureHandleKind::ShaderResourceView) { + if (!ResolveInteropBitmap(command.texture, bitmap) || !bitmap) { + const D2D1_RECT_F rect = ToD2DRect(command.rect, dpiScale); + renderTarget.DrawRectangle(rect, &solidBrush, 1.0f); + return; + } + } else { + auto* texture = reinterpret_cast(command.texture.nativeHandle); + if (texture == nullptr || m_liveTextures.find(texture) == m_liveTextures.end()) { + const D2D1_RECT_F rect = ToD2DRect(command.rect, dpiScale); + renderTarget.DrawRectangle(rect, &solidBrush, 1.0f); + return; + } + + if (!ResolveTextureBitmap(renderTarget, *texture, bitmap) || !bitmap) { + return; + } + } + + const D2D1_RECT_F rect = ToD2DRect(command.rect, dpiScale); + const float sourceWidth = static_cast(command.texture.width); + const float sourceHeight = static_cast(command.texture.height); + const float sourceLeft = sourceWidth * std::clamp(command.uvMin.x, 0.0f, 1.0f); + const float sourceTop = sourceHeight * std::clamp(command.uvMin.y, 0.0f, 1.0f); + const float sourceRight = sourceWidth * std::clamp(command.uvMax.x, 0.0f, 1.0f); + const float sourceBottom = sourceHeight * std::clamp(command.uvMax.y, 0.0f, 1.0f); + renderTarget.DrawBitmap( + bitmap.Get(), + rect, + std::clamp(command.color.a, 0.0f, 1.0f), + D2D1_BITMAP_INTERPOLATION_MODE_LINEAR, + D2D1::RectF(sourceLeft, sourceTop, sourceRight, sourceBottom)); +} + +} // namespace XCEngine::UI::Editor::Host + +namespace XCEngine::UI::Editor::Host { + +using namespace NativeRendererHelpers; + +float NativeRenderer::MeasureTextWidth( + const ::XCEngine::UI::Editor::UIEditorTextMeasureRequest& request) const { + if (!m_dwriteFactory || request.text.empty()) { + return 0.0f; + } + + const std::wstring text = Utf8ToWide(request.text); + if (text.empty()) { + return 0.0f; + } + + const float dpiScale = ClampDpiScale(m_dpiScale); + const float scaledFontSize = ResolveFontSize(request.fontSize) * dpiScale; + IDWriteTextFormat* textFormat = GetTextFormat(scaledFontSize); + if (textFormat == nullptr) { + return 0.0f; + } + + Microsoft::WRL::ComPtr textLayout; + HRESULT hr = m_dwriteFactory->CreateTextLayout( + text.c_str(), + static_cast(text.size()), + textFormat, + 4096.0f, + scaledFontSize * 2.0f, + textLayout.ReleaseAndGetAddressOf()); + if (FAILED(hr) || !textLayout) { + return 0.0f; + } + + DWRITE_TEXT_METRICS textMetrics = {}; + hr = textLayout->GetMetrics(&textMetrics); + if (FAILED(hr)) { + return 0.0f; + } + + DWRITE_OVERHANG_METRICS overhangMetrics = {}; + float width = textMetrics.widthIncludingTrailingWhitespace; + if (SUCCEEDED(textLayout->GetOverhangMetrics(&overhangMetrics))) { + width += (std::max)(overhangMetrics.left, 0.0f); + width += (std::max)(overhangMetrics.right, 0.0f); + } + + return std::ceil(width) / dpiScale; +} + +IDWriteTextFormat* NativeRenderer::GetTextFormat(float fontSize) const { + if (!m_dwriteFactory) { + return nullptr; + } + + const float resolvedFontSize = ResolveFontSize(fontSize); + const int key = static_cast(std::lround(resolvedFontSize * 10.0f)); + const auto found = m_textFormats.find(key); + if (found != m_textFormats.end()) { + return found->second.Get(); + } + + Microsoft::WRL::ComPtr textFormat; + const HRESULT hr = m_dwriteFactory->CreateTextFormat( + L"Segoe UI", + nullptr, + DWRITE_FONT_WEIGHT_REGULAR, + DWRITE_FONT_STYLE_NORMAL, + DWRITE_FONT_STRETCH_NORMAL, + resolvedFontSize, + L"", + textFormat.ReleaseAndGetAddressOf()); + if (FAILED(hr)) { + return nullptr; + } + + textFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_LEADING); + textFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_NEAR); + textFormat->SetWordWrapping(DWRITE_WORD_WRAPPING_NO_WRAP); + + IDWriteTextFormat* result = textFormat.Get(); + m_textFormats.emplace(key, std::move(textFormat)); + return result; +} + +D2D1_COLOR_F NativeRenderer::ToD2DColor(const ::XCEngine::UI::UIColor& color) { + return D2D1::ColorF(color.r, color.g, color.b, color.a); +} + +std::wstring NativeRenderer::Utf8ToWide(std::string_view text) { + if (text.empty()) { + return {}; + } + + const int sizeNeeded = MultiByteToWideChar( + CP_UTF8, + 0, + text.data(), + static_cast(text.size()), + nullptr, + 0); + if (sizeNeeded <= 0) { + return {}; + } + + std::wstring wideText(static_cast(sizeNeeded), L'\0'); + MultiByteToWideChar( + CP_UTF8, + 0, + text.data(), + static_cast(text.size()), + wideText.data(), + sizeNeeded); + return wideText; +} + +} // namespace XCEngine::UI::Editor::Host + +namespace XCEngine::UI::Editor::Host { + +using namespace NativeRendererHelpers; + +bool NativeRenderer::DecodeTextureFile( + const std::filesystem::path& path, + NativeTextureResource& outTexture, + std::string& outError) { + outError.clear(); + if (!EnsureWicFactory(outError)) { + return false; + } + + const std::wstring widePath = path.wstring(); + Microsoft::WRL::ComPtr decoder; + HRESULT hr = m_wicFactory->CreateDecoderFromFilename( + widePath.c_str(), + nullptr, + GENERIC_READ, + WICDecodeMetadataCacheOnLoad, + decoder.ReleaseAndGetAddressOf()); + if (FAILED(hr) || !decoder) { + outError = HrToString("IWICImagingFactory::CreateDecoderFromFilename", hr); + return false; + } + + Microsoft::WRL::ComPtr frame; + hr = decoder->GetFrame(0u, frame.ReleaseAndGetAddressOf()); + if (FAILED(hr) || !frame) { + outError = HrToString("IWICBitmapDecoder::GetFrame", hr); + return false; + } + + return DecodeTextureFrame(*frame.Get(), outTexture, outError); +} + +bool NativeRenderer::DecodeTextureMemory( + const std::uint8_t* data, + std::size_t size, + NativeTextureResource& outTexture, + std::string& outError) { + outError.clear(); + if (data == nullptr || size == 0u) { + outError = "DecodeTextureMemory rejected an empty image payload."; + return false; + } + + if (size > static_cast((std::numeric_limits::max)())) { + outError = "DecodeTextureMemory payload exceeds WIC stream limits."; + return false; + } + + if (!EnsureWicFactory(outError)) { + return false; + } + + Microsoft::WRL::ComPtr stream; + HRESULT hr = m_wicFactory->CreateStream(stream.ReleaseAndGetAddressOf()); + if (FAILED(hr) || !stream) { + outError = HrToString("IWICImagingFactory::CreateStream", hr); + return false; + } + + hr = stream->InitializeFromMemory( + const_cast(reinterpret_cast(data)), + static_cast(size)); + if (FAILED(hr)) { + outError = HrToString("IWICStream::InitializeFromMemory", hr); + return false; + } + + Microsoft::WRL::ComPtr decoder; + hr = m_wicFactory->CreateDecoderFromStream( + stream.Get(), + nullptr, + WICDecodeMetadataCacheOnLoad, + decoder.ReleaseAndGetAddressOf()); + if (FAILED(hr) || !decoder) { + outError = HrToString("IWICImagingFactory::CreateDecoderFromStream", hr); + return false; + } + + Microsoft::WRL::ComPtr frame; + hr = decoder->GetFrame(0u, frame.ReleaseAndGetAddressOf()); + if (FAILED(hr) || !frame) { + outError = HrToString("IWICBitmapDecoder::GetFrame", hr); + return false; + } + + return DecodeTextureFrame(*frame.Get(), outTexture, outError); +} + +bool NativeRenderer::DecodeTextureFrame( + IWICBitmapSource& source, + NativeTextureResource& outTexture, + std::string& outError) { + outError.clear(); + + Microsoft::WRL::ComPtr converter; + HRESULT hr = m_wicFactory->CreateFormatConverter(converter.ReleaseAndGetAddressOf()); + if (FAILED(hr) || !converter) { + outError = HrToString("IWICImagingFactory::CreateFormatConverter", hr); + return false; + } + + hr = converter->Initialize( + &source, + GUID_WICPixelFormat32bppPBGRA, + WICBitmapDitherTypeNone, + nullptr, + 0.0f, + WICBitmapPaletteTypeCustom); + if (FAILED(hr)) { + outError = HrToString("IWICFormatConverter::Initialize", hr); + return false; + } + + UINT width = 0u; + UINT height = 0u; + hr = converter->GetSize(&width, &height); + if (FAILED(hr) || width == 0u || height == 0u) { + outError = HrToString("IWICBitmapSource::GetSize", hr); + return false; + } + + std::vector pixels( + static_cast(width) * static_cast(height) * 4u); + hr = converter->CopyPixels( + nullptr, + width * 4u, + static_cast(pixels.size()), + pixels.data()); + if (FAILED(hr)) { + outError = HrToString("IWICBitmapSource::CopyPixels", hr); + return false; + } + + outTexture.pixels = std::move(pixels); + outTexture.width = width; + outTexture.height = height; + outTexture.cachedBitmap.Reset(); + outTexture.cachedTarget = nullptr; + return true; +} + +} // namespace XCEngine::UI::Editor::Host + +namespace XCEngine::UI::Editor::Host { + +using namespace NativeRendererHelpers; + +bool NativeRenderer::LoadTextureFromFile( + const std::filesystem::path& path, + ::XCEngine::UI::UITextureHandle& outTexture, + std::string& outError) { + outError.clear(); + ReleaseTexture(outTexture); + + auto texture = std::make_unique(); + if (!DecodeTextureFile(path, *texture, outError)) { + outTexture = {}; + return false; + } + + outTexture.nativeHandle = reinterpret_cast(texture.get()); + outTexture.width = texture->width; + outTexture.height = texture->height; + outTexture.kind = ::XCEngine::UI::UITextureHandleKind::DescriptorHandle; + m_liveTextures.insert(texture.get()); + texture.release(); + return true; +} + +bool NativeRenderer::LoadTextureFromMemory( + const std::uint8_t* data, + std::size_t size, + ::XCEngine::UI::UITextureHandle& outTexture, + std::string& outError) { + outError.clear(); + ReleaseTexture(outTexture); + + auto texture = std::make_unique(); + if (!DecodeTextureMemory(data, size, *texture, outError)) { + outTexture = {}; + return false; + } + + outTexture.nativeHandle = reinterpret_cast(texture.get()); + outTexture.width = texture->width; + outTexture.height = texture->height; + outTexture.kind = ::XCEngine::UI::UITextureHandleKind::DescriptorHandle; + m_liveTextures.insert(texture.get()); + texture.release(); + return true; +} + +bool NativeRenderer::LoadTextureFromRgba( + const std::uint8_t* rgbaPixels, + std::uint32_t width, + std::uint32_t height, + ::XCEngine::UI::UITextureHandle& outTexture, + std::string& outError) { + outError.clear(); + ReleaseTexture(outTexture); + + if (rgbaPixels == nullptr || width == 0u || height == 0u) { + outError = "LoadTextureFromRgba rejected an empty RGBA payload."; + return false; + } + + const std::uint64_t pixelCount = + static_cast(width) * static_cast(height); + if (pixelCount == 0u || + pixelCount > + static_cast((std::numeric_limits::max)() / 4u)) { + outError = "LoadTextureFromRgba payload exceeds supported limits."; + return false; + } + + auto texture = std::make_unique(); + texture->pixels.resize(static_cast(pixelCount) * 4u); + for (std::uint64_t pixelIndex = 0u; pixelIndex < pixelCount; ++pixelIndex) { + const std::size_t srcOffset = static_cast(pixelIndex) * 4u; + const std::uint8_t red = rgbaPixels[srcOffset + 0u]; + const std::uint8_t green = rgbaPixels[srcOffset + 1u]; + const std::uint8_t blue = rgbaPixels[srcOffset + 2u]; + const std::uint8_t alpha = rgbaPixels[srcOffset + 3u]; + + texture->pixels[srcOffset + 0u] = + static_cast((static_cast(blue) * alpha + 127u) / 255u); + texture->pixels[srcOffset + 1u] = + static_cast((static_cast(green) * alpha + 127u) / 255u); + texture->pixels[srcOffset + 2u] = + static_cast((static_cast(red) * alpha + 127u) / 255u); + texture->pixels[srcOffset + 3u] = alpha; + } + + texture->width = width; + texture->height = height; + texture->cachedBitmap.Reset(); + texture->cachedTarget = nullptr; + + outTexture.nativeHandle = reinterpret_cast(texture.get()); + outTexture.width = texture->width; + outTexture.height = texture->height; + outTexture.kind = ::XCEngine::UI::UITextureHandleKind::DescriptorHandle; + m_liveTextures.insert(texture.get()); + texture.release(); + return true; +} + +void NativeRenderer::ReleaseTexture(::XCEngine::UI::UITextureHandle& texture) { + if (!texture.IsValid()) { + texture = {}; + return; + } + + auto* resource = reinterpret_cast(texture.nativeHandle); + if (resource != nullptr) { + const auto found = m_liveTextures.find(resource); + if (found != m_liveTextures.end()) { + m_liveTextures.erase(found); + delete resource; + } + } + + texture = {}; +} + +bool NativeRenderer::ResolveTextureBitmap( + ID2D1RenderTarget& renderTarget, + NativeTextureResource& texture, + Microsoft::WRL::ComPtr& outBitmap) { + outBitmap.Reset(); + if (texture.width == 0u || texture.height == 0u || texture.pixels.empty()) { + return false; + } + + if (texture.cachedBitmap && texture.cachedTarget == &renderTarget) { + outBitmap = texture.cachedBitmap; + return true; + } + + Microsoft::WRL::ComPtr bitmap; + const D2D1_BITMAP_PROPERTIES properties = D2D1::BitmapProperties( + D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED), + kBaseDpi, + kBaseDpi); + const HRESULT hr = renderTarget.CreateBitmap( + D2D1::SizeU(texture.width, texture.height), + texture.pixels.data(), + texture.width * 4u, + &properties, + bitmap.ReleaseAndGetAddressOf()); + if (FAILED(hr) || !bitmap) { + return false; + } + + if (&renderTarget == m_renderTarget.Get()) { + texture.cachedBitmap = bitmap; + texture.cachedTarget = &renderTarget; + } + + outBitmap = std::move(bitmap); + return true; +} + +bool NativeRenderer::ResolveInteropBitmap( + const ::XCEngine::UI::UITextureHandle& texture, + Microsoft::WRL::ComPtr& outBitmap) const { + return m_windowInterop.ResolveInteropBitmap(texture, outBitmap); +} + +} // namespace XCEngine::UI::Editor::Host + +namespace XCEngine::UI::Editor::Host { + +using namespace NativeRendererHelpers; + +bool NativeRenderer::AttachWindowRenderer(D3D12WindowRenderer& windowRenderer) { + if (m_windowRenderer != &windowRenderer) { + ReleaseWindowRendererInterop(); + m_windowRenderer = &windowRenderer; + } + + if (!EnsureWindowRendererInterop()) { + return false; + } + + DiscardRenderTarget(); + + if (m_windowInterop.HasBackBufferTargets()) { + return true; + } + + return m_windowInterop.RebuildBackBufferTargets(); +} + +void NativeRenderer::DetachWindowRenderer() { + ReleaseWindowRendererInterop(); + m_windowRenderer = nullptr; +} + +void NativeRenderer::ReleaseWindowRendererBackBufferTargets() { + m_windowInterop.ReleaseBackBufferTargets(); +} + +bool NativeRenderer::RebuildWindowRendererBackBufferTargets() { + if (!EnsureWindowRendererInterop()) { + return false; + } + + DiscardRenderTarget(); + ReleaseWindowRendererBackBufferTargets(); + return m_windowInterop.RebuildBackBufferTargets(); +} + +bool NativeRenderer::HasAttachedWindowRenderer() const { + return m_windowInterop.HasAttachedWindowRenderer(); +} + +bool NativeRenderer::EnsureWindowRendererInterop() { + if (m_windowRenderer == nullptr) { + m_lastRenderError = "EnsureWindowRendererInterop requires an attached D3D12 window renderer."; + return false; + } + if (m_d2dFactory == nullptr || m_dwriteFactory == nullptr) { + m_lastRenderError = "EnsureWindowRendererInterop requires initialized D2D and DWrite factories."; + return false; + } + + const bool attached = m_windowInterop.Attach(*m_windowRenderer, *m_d2dFactory.Get()); + if (!attached) { + m_lastRenderError = m_windowInterop.GetLastError(); + } else { + m_lastRenderError.clear(); + } + return attached; +} + +void NativeRenderer::ReleaseWindowRendererInterop() { + m_windowInterop.Detach(); +} + +} // namespace XCEngine::UI::Editor::Host + +namespace XCEngine::UI::Editor::Host { + +using namespace NativeRendererHelpers; + +bool NativeRenderer::CaptureToPng( + const ::XCEngine::UI::UIDrawData& drawData, + UINT width, + UINT height, + const std::filesystem::path& outputPath, + std::string& outError) { + outError.clear(); + if (width == 0 || height == 0) { + outError = "CaptureToPng rejected an empty render size."; + return false; + } + + if (!m_d2dFactory || !m_dwriteFactory) { + outError = "CaptureToPng requires an initialized NativeRenderer."; + return false; + } + + if (!EnsureWicFactory(outError)) { + return false; + } + + std::error_code errorCode = {}; + std::filesystem::create_directories(outputPath.parent_path(), errorCode); + if (errorCode) { + outError = "Failed to create screenshot directory: " + outputPath.parent_path().string(); + return false; + } + + Microsoft::WRL::ComPtr bitmap; + HRESULT hr = m_wicFactory->CreateBitmap( + width, + height, + GUID_WICPixelFormat32bppPBGRA, + WICBitmapCacheOnLoad, + bitmap.ReleaseAndGetAddressOf()); + if (FAILED(hr)) { + outError = HrToString("IWICImagingFactory::CreateBitmap", hr); + return false; + } + + const D2D1_RENDER_TARGET_PROPERTIES renderTargetProperties = D2D1::RenderTargetProperties( + D2D1_RENDER_TARGET_TYPE_DEFAULT, + D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED), + kBaseDpi, + kBaseDpi); + + Microsoft::WRL::ComPtr offscreenRenderTarget; + hr = m_d2dFactory->CreateWicBitmapRenderTarget( + bitmap.Get(), + renderTargetProperties, + offscreenRenderTarget.ReleaseAndGetAddressOf()); + if (FAILED(hr)) { + outError = HrToString("ID2D1Factory::CreateWicBitmapRenderTarget", hr); + return false; + } + + Microsoft::WRL::ComPtr offscreenBrush; + hr = offscreenRenderTarget->CreateSolidColorBrush( + D2D1::ColorF(1.0f, 1.0f, 1.0f, 1.0f), + offscreenBrush.ReleaseAndGetAddressOf()); + if (FAILED(hr)) { + outError = HrToString("ID2D1RenderTarget::CreateSolidColorBrush", hr); + return false; + } + + const bool rendered = RenderToTarget(*offscreenRenderTarget.Get(), *offscreenBrush.Get(), drawData); + hr = offscreenRenderTarget->EndDraw(); + if (!rendered || FAILED(hr)) { + outError = HrToString("ID2D1RenderTarget::EndDraw", hr); + return false; + } + + const std::wstring wideOutputPath = outputPath.wstring(); + Microsoft::WRL::ComPtr stream; + hr = m_wicFactory->CreateStream(stream.ReleaseAndGetAddressOf()); + if (FAILED(hr)) { + outError = HrToString("IWICImagingFactory::CreateStream", hr); + return false; + } + + hr = stream->InitializeFromFilename(wideOutputPath.c_str(), GENERIC_WRITE); + if (FAILED(hr)) { + outError = HrToString("IWICStream::InitializeFromFilename", hr); + return false; + } + + Microsoft::WRL::ComPtr encoder; + hr = m_wicFactory->CreateEncoder(GUID_ContainerFormatPng, nullptr, encoder.ReleaseAndGetAddressOf()); + if (FAILED(hr)) { + outError = HrToString("IWICImagingFactory::CreateEncoder", hr); + return false; + } + + hr = encoder->Initialize(stream.Get(), WICBitmapEncoderNoCache); + if (FAILED(hr)) { + outError = HrToString("IWICBitmapEncoder::Initialize", hr); + return false; + } + + Microsoft::WRL::ComPtr frame; + Microsoft::WRL::ComPtr propertyBag; + hr = encoder->CreateNewFrame(frame.ReleaseAndGetAddressOf(), propertyBag.ReleaseAndGetAddressOf()); + if (FAILED(hr)) { + outError = HrToString("IWICBitmapEncoder::CreateNewFrame", hr); + return false; + } + + hr = frame->Initialize(propertyBag.Get()); + if (FAILED(hr)) { + outError = HrToString("IWICBitmapFrameEncode::Initialize", hr); + return false; + } + + hr = frame->SetSize(width, height); + if (FAILED(hr)) { + outError = HrToString("IWICBitmapFrameEncode::SetSize", hr); + return false; + } + + WICPixelFormatGUID pixelFormat = GUID_WICPixelFormat32bppPBGRA; + hr = frame->SetPixelFormat(&pixelFormat); + if (FAILED(hr)) { + outError = HrToString("IWICBitmapFrameEncode::SetPixelFormat", hr); + return false; + } + + hr = frame->WriteSource(bitmap.Get(), nullptr); + if (FAILED(hr)) { + outError = HrToString("IWICBitmapFrameEncode::WriteSource", hr); + return false; + } + + hr = frame->Commit(); + if (FAILED(hr)) { + outError = HrToString("IWICBitmapFrameEncode::Commit", hr); + return false; + } + + hr = encoder->Commit(); + if (FAILED(hr)) { + outError = HrToString("IWICBitmapEncoder::Commit", hr); + return false; + } + + return true; +} + +} // namespace XCEngine::UI::Editor::Host + + diff --git a/new_editor/app/Rendering/Native/NativeRenderer.h b/new_editor/app/Rendering/Native/NativeRenderer.h index 9ddaae02..623f439f 100644 --- a/new_editor/app/Rendering/Native/NativeRenderer.h +++ b/new_editor/app/Rendering/Native/NativeRenderer.h @@ -57,6 +57,12 @@ public: std::size_t size, ::XCEngine::UI::UITextureHandle& outTexture, std::string& outError) override; + bool LoadTextureFromRgba( + const std::uint8_t* rgbaPixels, + std::uint32_t width, + std::uint32_t height, + ::XCEngine::UI::UITextureHandle& outTexture, + std::string& outError) override; void ReleaseTexture(::XCEngine::UI::UITextureHandle& texture) override; float MeasureTextWidth( const ::XCEngine::UI::Editor::UIEditorTextMeasureRequest& request) const override; diff --git a/new_editor/app/Rendering/Native/NativeRendererCapture.cpp b/new_editor/app/Rendering/Native/NativeRendererCapture.cpp deleted file mode 100644 index de17d980..00000000 --- a/new_editor/app/Rendering/Native/NativeRendererCapture.cpp +++ /dev/null @@ -1,154 +0,0 @@ -#include "NativeRendererInternal.h" - -namespace XCEngine::UI::Editor::Host { - -using namespace NativeRendererInternal; - -bool NativeRenderer::CaptureToPng( - const ::XCEngine::UI::UIDrawData& drawData, - UINT width, - UINT height, - const std::filesystem::path& outputPath, - std::string& outError) { - outError.clear(); - if (width == 0 || height == 0) { - outError = "CaptureToPng rejected an empty render size."; - return false; - } - - if (!m_d2dFactory || !m_dwriteFactory) { - outError = "CaptureToPng requires an initialized NativeRenderer."; - return false; - } - - if (!EnsureWicFactory(outError)) { - return false; - } - - std::error_code errorCode = {}; - std::filesystem::create_directories(outputPath.parent_path(), errorCode); - if (errorCode) { - outError = "Failed to create screenshot directory: " + outputPath.parent_path().string(); - return false; - } - - Microsoft::WRL::ComPtr bitmap; - HRESULT hr = m_wicFactory->CreateBitmap( - width, - height, - GUID_WICPixelFormat32bppPBGRA, - WICBitmapCacheOnLoad, - bitmap.ReleaseAndGetAddressOf()); - if (FAILED(hr)) { - outError = HrToString("IWICImagingFactory::CreateBitmap", hr); - return false; - } - - const D2D1_RENDER_TARGET_PROPERTIES renderTargetProperties = D2D1::RenderTargetProperties( - D2D1_RENDER_TARGET_TYPE_DEFAULT, - D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED), - kBaseDpi, - kBaseDpi); - - Microsoft::WRL::ComPtr offscreenRenderTarget; - hr = m_d2dFactory->CreateWicBitmapRenderTarget( - bitmap.Get(), - renderTargetProperties, - offscreenRenderTarget.ReleaseAndGetAddressOf()); - if (FAILED(hr)) { - outError = HrToString("ID2D1Factory::CreateWicBitmapRenderTarget", hr); - return false; - } - - Microsoft::WRL::ComPtr offscreenBrush; - hr = offscreenRenderTarget->CreateSolidColorBrush( - D2D1::ColorF(1.0f, 1.0f, 1.0f, 1.0f), - offscreenBrush.ReleaseAndGetAddressOf()); - if (FAILED(hr)) { - outError = HrToString("ID2D1RenderTarget::CreateSolidColorBrush", hr); - return false; - } - - const bool rendered = RenderToTarget(*offscreenRenderTarget.Get(), *offscreenBrush.Get(), drawData); - hr = offscreenRenderTarget->EndDraw(); - if (!rendered || FAILED(hr)) { - outError = HrToString("ID2D1RenderTarget::EndDraw", hr); - return false; - } - - const std::wstring wideOutputPath = outputPath.wstring(); - Microsoft::WRL::ComPtr stream; - hr = m_wicFactory->CreateStream(stream.ReleaseAndGetAddressOf()); - if (FAILED(hr)) { - outError = HrToString("IWICImagingFactory::CreateStream", hr); - return false; - } - - hr = stream->InitializeFromFilename(wideOutputPath.c_str(), GENERIC_WRITE); - if (FAILED(hr)) { - outError = HrToString("IWICStream::InitializeFromFilename", hr); - return false; - } - - Microsoft::WRL::ComPtr encoder; - hr = m_wicFactory->CreateEncoder(GUID_ContainerFormatPng, nullptr, encoder.ReleaseAndGetAddressOf()); - if (FAILED(hr)) { - outError = HrToString("IWICImagingFactory::CreateEncoder", hr); - return false; - } - - hr = encoder->Initialize(stream.Get(), WICBitmapEncoderNoCache); - if (FAILED(hr)) { - outError = HrToString("IWICBitmapEncoder::Initialize", hr); - return false; - } - - Microsoft::WRL::ComPtr frame; - Microsoft::WRL::ComPtr propertyBag; - hr = encoder->CreateNewFrame(frame.ReleaseAndGetAddressOf(), propertyBag.ReleaseAndGetAddressOf()); - if (FAILED(hr)) { - outError = HrToString("IWICBitmapEncoder::CreateNewFrame", hr); - return false; - } - - hr = frame->Initialize(propertyBag.Get()); - if (FAILED(hr)) { - outError = HrToString("IWICBitmapFrameEncode::Initialize", hr); - return false; - } - - hr = frame->SetSize(width, height); - if (FAILED(hr)) { - outError = HrToString("IWICBitmapFrameEncode::SetSize", hr); - return false; - } - - WICPixelFormatGUID pixelFormat = GUID_WICPixelFormat32bppPBGRA; - hr = frame->SetPixelFormat(&pixelFormat); - if (FAILED(hr)) { - outError = HrToString("IWICBitmapFrameEncode::SetPixelFormat", hr); - return false; - } - - hr = frame->WriteSource(bitmap.Get(), nullptr); - if (FAILED(hr)) { - outError = HrToString("IWICBitmapFrameEncode::WriteSource", hr); - return false; - } - - hr = frame->Commit(); - if (FAILED(hr)) { - outError = HrToString("IWICBitmapFrameEncode::Commit", hr); - return false; - } - - hr = encoder->Commit(); - if (FAILED(hr)) { - outError = HrToString("IWICBitmapEncoder::Commit", hr); - return false; - } - - return true; -} - -} // namespace XCEngine::UI::Editor::Host diff --git a/new_editor/app/Rendering/Native/NativeRendererDraw.cpp b/new_editor/app/Rendering/Native/NativeRendererDraw.cpp deleted file mode 100644 index 35d79051..00000000 --- a/new_editor/app/Rendering/Native/NativeRendererDraw.cpp +++ /dev/null @@ -1,85 +0,0 @@ -#include "NativeRendererInternal.h" - -namespace XCEngine::UI::Editor::Host { - -using namespace NativeRendererInternal; - -bool NativeRenderer::RenderToTarget( - ID2D1RenderTarget& renderTarget, - ID2D1SolidColorBrush& solidBrush, - const ::XCEngine::UI::UIDrawData& drawData) { - renderTarget.SetDpi(kBaseDpi, kBaseDpi); - renderTarget.SetTextAntialiasMode(D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE); - renderTarget.BeginDraw(); - renderTarget.Clear(D2D1::ColorF(0.04f, 0.05f, 0.06f, 1.0f)); - - std::vector clipStack = {}; - for (const ::XCEngine::UI::UIDrawList& drawList : drawData.GetDrawLists()) { - for (const ::XCEngine::UI::UIDrawCommand& command : drawList.GetCommands()) { - RenderCommand(renderTarget, solidBrush, command, clipStack); - } - } - - while (!clipStack.empty()) { - renderTarget.PopAxisAlignedClip(); - clipStack.pop_back(); - } - - return true; -} - -void NativeRenderer::RenderCommand( - ID2D1RenderTarget& renderTarget, - ID2D1SolidColorBrush& solidBrush, - const ::XCEngine::UI::UIDrawCommand& command, - std::vector& clipStack) { - solidBrush.SetColor(ToD2DColor(command.color)); - - switch (command.type) { - case ::XCEngine::UI::UIDrawCommandType::FilledRect: - RenderFilledRectCommand(renderTarget, solidBrush, command); - break; - case ::XCEngine::UI::UIDrawCommandType::FilledRectLinearGradient: - RenderFilledRectLinearGradientCommand(renderTarget, command); - break; - case ::XCEngine::UI::UIDrawCommandType::RectOutline: - RenderRectOutlineCommand(renderTarget, solidBrush, command); - break; - case ::XCEngine::UI::UIDrawCommandType::Line: - RenderLineCommand(renderTarget, solidBrush, command); - break; - case ::XCEngine::UI::UIDrawCommandType::FilledTriangle: - RenderFilledTriangleCommand(renderTarget, solidBrush, command); - break; - case ::XCEngine::UI::UIDrawCommandType::FilledCircle: - RenderFilledCircleCommand(renderTarget, solidBrush, command); - break; - case ::XCEngine::UI::UIDrawCommandType::CircleOutline: - RenderCircleOutlineCommand(renderTarget, solidBrush, command); - break; - case ::XCEngine::UI::UIDrawCommandType::Text: - RenderTextCommand(renderTarget, solidBrush, command); - break; - case ::XCEngine::UI::UIDrawCommandType::Image: - RenderImageCommand(renderTarget, solidBrush, command); - break; - case ::XCEngine::UI::UIDrawCommandType::PushClipRect: { - const float dpiScale = ClampDpiScale(m_dpiScale); - const D2D1_RECT_F rect = ToD2DRect(command.rect, dpiScale); - renderTarget.PushAxisAlignedClip(rect, D2D1_ANTIALIAS_MODE_PER_PRIMITIVE); - clipStack.push_back(rect); - break; - } - case ::XCEngine::UI::UIDrawCommandType::PopClipRect: { - if (!clipStack.empty()) { - renderTarget.PopAxisAlignedClip(); - clipStack.pop_back(); - } - break; - } - default: - break; - } -} - -} // namespace XCEngine::UI::Editor::Host diff --git a/new_editor/app/Rendering/Native/NativeRendererDrawContent.cpp b/new_editor/app/Rendering/Native/NativeRendererDrawContent.cpp deleted file mode 100644 index 92165e45..00000000 --- a/new_editor/app/Rendering/Native/NativeRendererDrawContent.cpp +++ /dev/null @@ -1,94 +0,0 @@ -#include "NativeRendererInternal.h" - -#include -#include - -namespace XCEngine::UI::Editor::Host { - -using namespace NativeRendererInternal; - -void NativeRenderer::RenderTextCommand( - ID2D1RenderTarget& renderTarget, - ID2D1SolidColorBrush& solidBrush, - const ::XCEngine::UI::UIDrawCommand& command) { - if (command.text.empty()) { - return; - } - - const float dpiScale = ClampDpiScale(m_dpiScale); - const float fontSize = ResolveFontSize(command.fontSize); - const float scaledFontSize = fontSize * dpiScale; - IDWriteTextFormat* textFormat = GetTextFormat(scaledFontSize); - if (textFormat == nullptr) { - return; - } - - const std::wstring text = Utf8ToWide(command.text); - if (text.empty()) { - return; - } - - const D2D1_SIZE_F targetSize = renderTarget.GetSize(); - const float originX = SnapToPixel(command.position.x, dpiScale); - const float originY = SnapToPixel(command.position.y, dpiScale); - const float lineHeight = std::ceil(scaledFontSize * 1.6f); - const D2D1_RECT_F layoutRect = D2D1::RectF( - originX, - originY, - targetSize.width, - originY + lineHeight); - renderTarget.DrawTextW( - text.c_str(), - static_cast(text.size()), - textFormat, - layoutRect, - &solidBrush, - D2D1_DRAW_TEXT_OPTIONS_CLIP, - DWRITE_MEASURING_MODE_GDI_NATURAL); -} - -void NativeRenderer::RenderImageCommand( - ID2D1RenderTarget& renderTarget, - ID2D1SolidColorBrush& solidBrush, - const ::XCEngine::UI::UIDrawCommand& command) { - if (!command.texture.IsValid()) { - return; - } - - const float dpiScale = ClampDpiScale(m_dpiScale); - Microsoft::WRL::ComPtr bitmap; - if (command.texture.kind == ::XCEngine::UI::UITextureHandleKind::ShaderResourceView) { - if (!ResolveInteropBitmap(command.texture, bitmap) || !bitmap) { - const D2D1_RECT_F rect = ToD2DRect(command.rect, dpiScale); - renderTarget.DrawRectangle(rect, &solidBrush, 1.0f); - return; - } - } else { - auto* texture = reinterpret_cast(command.texture.nativeHandle); - if (texture == nullptr || m_liveTextures.find(texture) == m_liveTextures.end()) { - const D2D1_RECT_F rect = ToD2DRect(command.rect, dpiScale); - renderTarget.DrawRectangle(rect, &solidBrush, 1.0f); - return; - } - - if (!ResolveTextureBitmap(renderTarget, *texture, bitmap) || !bitmap) { - return; - } - } - - const D2D1_RECT_F rect = ToD2DRect(command.rect, dpiScale); - const float sourceWidth = static_cast(command.texture.width); - const float sourceHeight = static_cast(command.texture.height); - const float sourceLeft = sourceWidth * std::clamp(command.uvMin.x, 0.0f, 1.0f); - const float sourceTop = sourceHeight * std::clamp(command.uvMin.y, 0.0f, 1.0f); - const float sourceRight = sourceWidth * std::clamp(command.uvMax.x, 0.0f, 1.0f); - const float sourceBottom = sourceHeight * std::clamp(command.uvMax.y, 0.0f, 1.0f); - renderTarget.DrawBitmap( - bitmap.Get(), - rect, - std::clamp(command.color.a, 0.0f, 1.0f), - D2D1_BITMAP_INTERPOLATION_MODE_LINEAR, - D2D1::RectF(sourceLeft, sourceTop, sourceRight, sourceBottom)); -} - -} // namespace XCEngine::UI::Editor::Host diff --git a/new_editor/app/Rendering/Native/NativeRendererDrawGeometry.cpp b/new_editor/app/Rendering/Native/NativeRendererDrawGeometry.cpp deleted file mode 100644 index ea991eea..00000000 --- a/new_editor/app/Rendering/Native/NativeRendererDrawGeometry.cpp +++ /dev/null @@ -1,162 +0,0 @@ -#include "NativeRendererInternal.h" - -namespace XCEngine::UI::Editor::Host { - -using namespace NativeRendererInternal; - -void NativeRenderer::RenderFilledRectCommand( - ID2D1RenderTarget& renderTarget, - ID2D1SolidColorBrush& solidBrush, - const ::XCEngine::UI::UIDrawCommand& command) { - const float dpiScale = ClampDpiScale(m_dpiScale); - const D2D1_RECT_F rect = ToD2DRect(command.rect, dpiScale); - const float rounding = command.rounding > 0.0f ? command.rounding * dpiScale : 0.0f; - if (command.rounding > 0.0f) { - renderTarget.FillRoundedRectangle( - D2D1::RoundedRect(rect, rounding, rounding), - &solidBrush); - return; - } - - renderTarget.FillRectangle(rect, &solidBrush); -} - -void NativeRenderer::RenderFilledRectLinearGradientCommand( - ID2D1RenderTarget& renderTarget, - const ::XCEngine::UI::UIDrawCommand& command) { - const float dpiScale = ClampDpiScale(m_dpiScale); - const D2D1_RECT_F rect = ToD2DRect(command.rect, dpiScale); - const float rounding = command.rounding > 0.0f ? command.rounding * dpiScale : 0.0f; - - const D2D1_GRADIENT_STOP stops[2] = { - D2D1::GradientStop(0.0f, ToD2DColor(command.color)), - D2D1::GradientStop(1.0f, ToD2DColor(command.secondaryColor)) - }; - - Microsoft::WRL::ComPtr stopCollection; - HRESULT hr = renderTarget.CreateGradientStopCollection( - stops, - 2u, - stopCollection.ReleaseAndGetAddressOf()); - if (FAILED(hr) || !stopCollection) { - return; - } - - const D2D1_POINT_2F startPoint = - command.gradientDirection == ::XCEngine::UI::UILinearGradientDirection::Vertical - ? D2D1::Point2F((rect.left + rect.right) * 0.5f, rect.top) - : D2D1::Point2F(rect.left, (rect.top + rect.bottom) * 0.5f); - const D2D1_POINT_2F endPoint = - command.gradientDirection == ::XCEngine::UI::UILinearGradientDirection::Vertical - ? D2D1::Point2F((rect.left + rect.right) * 0.5f, rect.bottom) - : D2D1::Point2F(rect.right, (rect.top + rect.bottom) * 0.5f); - - Microsoft::WRL::ComPtr gradientBrush; - hr = renderTarget.CreateLinearGradientBrush( - D2D1::LinearGradientBrushProperties(startPoint, endPoint), - stopCollection.Get(), - gradientBrush.ReleaseAndGetAddressOf()); - if (FAILED(hr) || !gradientBrush) { - return; - } - - if (command.rounding > 0.0f) { - renderTarget.FillRoundedRectangle( - D2D1::RoundedRect(rect, rounding, rounding), - gradientBrush.Get()); - return; - } - - renderTarget.FillRectangle(rect, gradientBrush.Get()); -} - -void NativeRenderer::RenderRectOutlineCommand( - ID2D1RenderTarget& renderTarget, - ID2D1SolidColorBrush& solidBrush, - const ::XCEngine::UI::UIDrawCommand& command) { - const float dpiScale = ClampDpiScale(m_dpiScale); - const D2D1_RECT_F rect = ToD2DRect(command.rect, dpiScale); - const float thickness = (command.thickness > 0.0f ? command.thickness : 1.0f) * dpiScale; - const float rounding = command.rounding > 0.0f ? command.rounding * dpiScale : 0.0f; - if (command.rounding > 0.0f) { - renderTarget.DrawRoundedRectangle( - D2D1::RoundedRect(rect, rounding, rounding), - &solidBrush, - thickness); - return; - } - - renderTarget.DrawRectangle(rect, &solidBrush, thickness); -} - -void NativeRenderer::RenderLineCommand( - ID2D1RenderTarget& renderTarget, - ID2D1SolidColorBrush& solidBrush, - const ::XCEngine::UI::UIDrawCommand& command) { - const float dpiScale = ClampDpiScale(m_dpiScale); - const float thickness = (command.thickness > 0.0f ? command.thickness : 1.0f) * dpiScale; - const float pixelOffset = ResolveStrokePixelOffset(thickness); - const D2D1_POINT_2F start = ToD2DPoint(command.position, dpiScale, pixelOffset); - const D2D1_POINT_2F end = ToD2DPoint(command.uvMin, dpiScale, pixelOffset); - renderTarget.DrawLine(start, end, &solidBrush, thickness); -} - -void NativeRenderer::RenderFilledTriangleCommand( - ID2D1RenderTarget& renderTarget, - ID2D1SolidColorBrush& solidBrush, - const ::XCEngine::UI::UIDrawCommand& command) { - Microsoft::WRL::ComPtr geometry; - HRESULT hr = m_d2dFactory->CreatePathGeometry(geometry.ReleaseAndGetAddressOf()); - if (FAILED(hr) || !geometry) { - return; - } - - Microsoft::WRL::ComPtr sink; - hr = geometry->Open(sink.ReleaseAndGetAddressOf()); - if (FAILED(hr) || !sink) { - return; - } - - const float dpiScale = ClampDpiScale(m_dpiScale); - const D2D1_POINT_2F points[3] = { - ToD2DPoint(command.position, dpiScale), - ToD2DPoint(command.uvMin, dpiScale), - ToD2DPoint(command.uvMax, dpiScale) - }; - - sink->BeginFigure(points[0], D2D1_FIGURE_BEGIN_FILLED); - sink->AddLines(points + 1, 2u); - sink->EndFigure(D2D1_FIGURE_END_CLOSED); - hr = sink->Close(); - if (FAILED(hr)) { - return; - } - - renderTarget.FillGeometry(geometry.Get(), &solidBrush); -} - -void NativeRenderer::RenderFilledCircleCommand( - ID2D1RenderTarget& renderTarget, - ID2D1SolidColorBrush& solidBrush, - const ::XCEngine::UI::UIDrawCommand& command) { - const float dpiScale = ClampDpiScale(m_dpiScale); - const float radius = command.radius * dpiScale; - renderTarget.FillEllipse( - D2D1::Ellipse(ToD2DPoint(command.position, dpiScale), radius, radius), - &solidBrush); -} - -void NativeRenderer::RenderCircleOutlineCommand( - ID2D1RenderTarget& renderTarget, - ID2D1SolidColorBrush& solidBrush, - const ::XCEngine::UI::UIDrawCommand& command) { - const float dpiScale = ClampDpiScale(m_dpiScale); - const float radius = command.radius * dpiScale; - const float thickness = (command.thickness > 0.0f ? command.thickness : 1.0f) * dpiScale; - renderTarget.DrawEllipse( - D2D1::Ellipse(ToD2DPoint(command.position, dpiScale), radius, radius), - &solidBrush, - thickness); -} - -} // namespace XCEngine::UI::Editor::Host diff --git a/new_editor/app/Rendering/Native/NativeRendererInternal.h b/new_editor/app/Rendering/Native/NativeRendererHelpers.h similarity index 95% rename from new_editor/app/Rendering/Native/NativeRendererInternal.h rename to new_editor/app/Rendering/Native/NativeRendererHelpers.h index 423d751b..322039fc 100644 --- a/new_editor/app/Rendering/Native/NativeRendererInternal.h +++ b/new_editor/app/Rendering/Native/NativeRendererHelpers.h @@ -7,7 +7,7 @@ #include #include -namespace XCEngine::UI::Editor::Host::NativeRendererInternal { +namespace XCEngine::UI::Editor::Host::NativeRendererHelpers { inline constexpr float kBaseDpi = 96.0f; inline constexpr float kDefaultFontSize = 16.0f; @@ -86,4 +86,4 @@ inline void CollectInteropTextureHandles( } } -} // namespace XCEngine::UI::Editor::Host::NativeRendererInternal +} // namespace XCEngine::UI::Editor::Host::NativeRendererHelpers diff --git a/new_editor/app/Rendering/Native/NativeRendererLifecycle.cpp b/new_editor/app/Rendering/Native/NativeRendererLifecycle.cpp deleted file mode 100644 index 0a4bfbfb..00000000 --- a/new_editor/app/Rendering/Native/NativeRendererLifecycle.cpp +++ /dev/null @@ -1,108 +0,0 @@ -#include "NativeRendererInternal.h" - -namespace XCEngine::UI::Editor::Host { - -using namespace NativeRendererInternal; - -bool NativeRenderer::Initialize(HWND hwnd) { - Shutdown(); - - if (hwnd == nullptr) { - m_lastRenderError = "Initialize rejected a null hwnd."; - return false; - } - - m_hwnd = hwnd; - D2D1_FACTORY_OPTIONS factoryOptions = {}; -#ifdef _DEBUG - factoryOptions.debugLevel = D2D1_DEBUG_LEVEL_INFORMATION; -#endif - HRESULT hr = D2D1CreateFactory( - D2D1_FACTORY_TYPE_SINGLE_THREADED, - __uuidof(ID2D1Factory1), - &factoryOptions, - reinterpret_cast(m_d2dFactory.ReleaseAndGetAddressOf())); - if (FAILED(hr)) { - m_lastRenderError = HrToString("D2D1CreateFactory", hr); - Shutdown(); - return false; - } - - hr = DWriteCreateFactory( - DWRITE_FACTORY_TYPE_SHARED, - __uuidof(IDWriteFactory), - reinterpret_cast(m_dwriteFactory.ReleaseAndGetAddressOf())); - if (FAILED(hr)) { - m_lastRenderError = HrToString("DWriteCreateFactory", hr); - Shutdown(); - return false; - } - - m_lastRenderError.clear(); - return true; -} - -void NativeRenderer::Shutdown() { - DetachWindowRenderer(); - while (!m_liveTextures.empty()) { - auto it = m_liveTextures.begin(); - delete *it; - m_liveTextures.erase(it); - } - m_textFormats.clear(); - m_solidBrush.Reset(); - m_renderTarget.Reset(); - m_wicFactory.Reset(); - m_dwriteFactory.Reset(); - m_d2dFactory.Reset(); - if (m_wicComInitialized) { - CoUninitialize(); - m_wicComInitialized = false; - } - m_hwnd = nullptr; -} - -void NativeRenderer::SetDpiScale(float dpiScale) { - m_dpiScale = ClampDpiScale(dpiScale); - if (m_renderTarget) { - m_renderTarget->SetDpi(kBaseDpi, kBaseDpi); - } -} - -float NativeRenderer::GetDpiScale() const { - return m_dpiScale; -} - -const std::string& NativeRenderer::GetLastRenderError() const { - return m_lastRenderError; -} - -bool NativeRenderer::EnsureWicFactory(std::string& outError) { - outError.clear(); - if (m_wicFactory) { - return true; - } - - const HRESULT initHr = CoInitializeEx(nullptr, COINIT_MULTITHREADED); - if (FAILED(initHr) && initHr != RPC_E_CHANGED_MODE) { - outError = HrToString("CoInitializeEx", initHr); - return false; - } - if (SUCCEEDED(initHr)) { - m_wicComInitialized = true; - } - - const HRESULT factoryHr = CoCreateInstance( - CLSID_WICImagingFactory, - nullptr, - CLSCTX_INPROC_SERVER, - IID_PPV_ARGS(m_wicFactory.ReleaseAndGetAddressOf())); - if (FAILED(factoryHr)) { - outError = HrToString("CoCreateInstance(CLSID_WICImagingFactory)", factoryHr); - return false; - } - - return true; -} - -} // namespace XCEngine::UI::Editor::Host diff --git a/new_editor/app/Rendering/Native/NativeRendererRenderTarget.cpp b/new_editor/app/Rendering/Native/NativeRendererRenderTarget.cpp deleted file mode 100644 index dc9b9c71..00000000 --- a/new_editor/app/Rendering/Native/NativeRendererRenderTarget.cpp +++ /dev/null @@ -1,89 +0,0 @@ -#include "NativeRendererInternal.h" - -namespace XCEngine::UI::Editor::Host { - -using namespace NativeRendererInternal; - -void NativeRenderer::Resize(UINT width, UINT height) { - if (!m_renderTarget || width == 0 || height == 0) { - return; - } - - const HRESULT hr = m_renderTarget->Resize(D2D1::SizeU(width, height)); - if (hr == D2DERR_RECREATE_TARGET) { - DiscardRenderTarget(); - } -} - -bool NativeRenderer::EnsureRenderTarget() { - if (!m_hwnd || !m_d2dFactory || !m_dwriteFactory) { - m_lastRenderError = "EnsureRenderTarget requires hwnd, D2D factory, and DWrite factory."; - return false; - } - - return CreateDeviceResources(); -} - -void NativeRenderer::DiscardRenderTarget() { - InvalidateCachedTextureBitmaps(m_renderTarget.Get()); - m_solidBrush.Reset(); - m_renderTarget.Reset(); -} - -bool NativeRenderer::CreateDeviceResources() { - if (m_renderTarget) { - return true; - } - - RECT clientRect = {}; - GetClientRect(m_hwnd, &clientRect); - const UINT width = static_cast((std::max)(clientRect.right - clientRect.left, 1L)); - const UINT height = static_cast((std::max)(clientRect.bottom - clientRect.top, 1L)); - - const D2D1_RENDER_TARGET_PROPERTIES renderTargetProps = D2D1::RenderTargetProperties( - D2D1_RENDER_TARGET_TYPE_DEFAULT, - D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED), - kBaseDpi, - kBaseDpi); - const D2D1_HWND_RENDER_TARGET_PROPERTIES hwndProps = D2D1::HwndRenderTargetProperties( - m_hwnd, - D2D1::SizeU(width, height)); - - const HRESULT renderTargetHr = m_d2dFactory->CreateHwndRenderTarget( - renderTargetProps, - hwndProps, - m_renderTarget.ReleaseAndGetAddressOf()); - if (FAILED(renderTargetHr)) { - m_lastRenderError = HrToString("ID2D1Factory::CreateHwndRenderTarget", renderTargetHr); - return false; - } - - const HRESULT brushHr = m_renderTarget->CreateSolidColorBrush( - D2D1::ColorF(1.0f, 1.0f, 1.0f, 1.0f), - m_solidBrush.ReleaseAndGetAddressOf()); - if (FAILED(brushHr)) { - m_lastRenderError = HrToString("ID2D1HwndRenderTarget::CreateSolidColorBrush", brushHr); - DiscardRenderTarget(); - return false; - } - - m_renderTarget->SetDpi(kBaseDpi, kBaseDpi); - m_renderTarget->SetTextAntialiasMode(D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE); - m_lastRenderError.clear(); - return true; -} - -void NativeRenderer::InvalidateCachedTextureBitmaps(const ID2D1RenderTarget* renderTarget) { - for (NativeTextureResource* texture : m_liveTextures) { - if (texture == nullptr) { - continue; - } - - if (renderTarget == nullptr || texture->cachedTarget == renderTarget) { - texture->cachedBitmap.Reset(); - texture->cachedTarget = nullptr; - } - } -} - -} // namespace XCEngine::UI::Editor::Host diff --git a/new_editor/app/Rendering/Native/NativeRendererRendering.cpp b/new_editor/app/Rendering/Native/NativeRendererRendering.cpp deleted file mode 100644 index fc0687bb..00000000 --- a/new_editor/app/Rendering/Native/NativeRendererRendering.cpp +++ /dev/null @@ -1,150 +0,0 @@ -#include "NativeRendererInternal.h" - -namespace XCEngine::UI::Editor::Host { - -using namespace NativeRendererInternal; - -bool NativeRenderer::Render(const ::XCEngine::UI::UIDrawData& drawData) { - if (!EnsureRenderTarget()) { - if (m_lastRenderError.empty()) { - m_lastRenderError = "EnsureRenderTarget failed."; - } - return false; - } - - const bool rendered = RenderToTarget(*m_renderTarget.Get(), *m_solidBrush.Get(), drawData); - const HRESULT hr = m_renderTarget->EndDraw(); - if (hr == D2DERR_RECREATE_TARGET) { - m_lastRenderError = HrToString("ID2D1HwndRenderTarget::EndDraw", hr); - DiscardRenderTarget(); - return false; - } - - if (!rendered || FAILED(hr)) { - m_lastRenderError = HrToString("ID2D1HwndRenderTarget::EndDraw", hr); - return false; - } - - m_lastRenderError.clear(); - return true; -} - -bool NativeRenderer::RenderToWindowRenderer(const ::XCEngine::UI::UIDrawData& drawData) { - if (!EnsureWindowRendererInterop()) { - if (m_lastRenderError.empty()) { - m_lastRenderError = "Window renderer interop is not available."; - } - return false; - } - - if (!m_windowInterop.HasBackBufferTargets() && - !m_windowInterop.RebuildBackBufferTargets()) { - if (m_lastRenderError.empty()) { - m_lastRenderError = "Window renderer back buffer interop targets are unavailable."; - } - return false; - } - - ID3D11On12Device* d3d11On12Device = m_windowInterop.GetD3D11On12Device(); - ID3D11DeviceContext* d3d11DeviceContext = m_windowInterop.GetD3D11DeviceContext(); - ID2D1DeviceContext* d2dDeviceContext = m_windowInterop.GetD2DDeviceContext(); - ID2D1SolidColorBrush* interopBrush = m_windowInterop.GetInteropBrush(); - if (d3d11On12Device == nullptr || - d3d11DeviceContext == nullptr || - d2dDeviceContext == nullptr || - interopBrush == nullptr) { - m_lastRenderError = "Window renderer interop resources are incomplete."; - return false; - } - - const std::uint32_t backBufferIndex = m_windowInterop.GetCurrentBackBufferIndex(); - if (m_windowInterop.GetWrappedBackBufferResource(backBufferIndex) == nullptr || - m_windowInterop.GetBackBufferTargetBitmap(backBufferIndex) == nullptr) { - m_lastRenderError = "Back buffer interop target index is out of range."; - return false; - } - - if (!m_windowRenderer->PreparePresentSurface()) { - m_lastRenderError = "Failed to prepare the D3D12 present surface: " + - m_windowRenderer->GetLastError(); - return false; - } - - if (!m_windowRenderer->SubmitFrame(false)) { - m_lastRenderError = "Failed to submit the D3D12 frame before UI composition."; - return false; - } - - if (!m_windowInterop.PrepareSourceTextures(drawData)) { - ID3D11Resource* backBufferResource = - m_windowInterop.GetWrappedBackBufferResource(backBufferIndex); - if (backBufferResource != nullptr) { - d3d11On12Device->AcquireWrappedResources(&backBufferResource, 1u); - d3d11On12Device->ReleaseWrappedResources(&backBufferResource, 1u); - } - d3d11DeviceContext->Flush(); - m_windowInterop.ClearSourceTextures(); - const bool signaled = m_windowRenderer->SignalFrameCompletion(); - ReleaseWindowRendererInterop(); - if (!signaled) { - m_lastRenderError = - "Failed to signal D3D12 frame completion after interop preparation failed."; - } - return false; - } - - std::vector acquiredResources = {}; - m_windowInterop.BuildAcquiredResources(backBufferIndex, acquiredResources); - if (acquiredResources.empty()) { - m_lastRenderError = "No wrapped interop resources were prepared for UI composition."; - return false; - } - - d3d11On12Device->AcquireWrappedResources( - acquiredResources.data(), - static_cast(acquiredResources.size())); - - d2dDeviceContext->SetTarget(m_windowInterop.GetBackBufferTargetBitmap(backBufferIndex)); - const bool rendered = RenderToTarget(*d2dDeviceContext, *interopBrush, drawData); - const HRESULT hr = d2dDeviceContext->EndDraw(); - - d3d11On12Device->ReleaseWrappedResources( - acquiredResources.data(), - static_cast(acquiredResources.size())); - d3d11DeviceContext->Flush(); - d2dDeviceContext->SetTarget(nullptr); - m_windowInterop.ClearSourceTextures(); - - if (!rendered || FAILED(hr)) { - m_lastRenderError = FAILED(hr) - ? HrToString("ID2D1DeviceContext::EndDraw", hr) - : "RenderToTarget failed during D3D11On12 composition."; - const bool signaled = m_windowRenderer->SignalFrameCompletion(); - if (hr == D2DERR_RECREATE_TARGET) { - ReleaseWindowRendererBackBufferTargets(); - } else { - ReleaseWindowRendererInterop(); - } - if (!signaled) { - m_lastRenderError = - "Failed to signal D3D12 frame completion after UI composition failed."; - } - return false; - } - - if (!m_windowRenderer->SignalFrameCompletion()) { - m_lastRenderError = "Failed to signal D3D12 frame completion after UI composition."; - ReleaseWindowRendererInterop(); - return false; - } - if (!m_windowRenderer->PresentFrame()) { - m_lastRenderError = "Failed to present the D3D12 swap chain."; - ReleaseWindowRendererInterop(); - return false; - } - - m_lastRenderError.clear(); - return true; -} - -} // namespace XCEngine::UI::Editor::Host diff --git a/new_editor/app/Rendering/Native/NativeRendererText.cpp b/new_editor/app/Rendering/Native/NativeRendererText.cpp deleted file mode 100644 index bbb59bad..00000000 --- a/new_editor/app/Rendering/Native/NativeRendererText.cpp +++ /dev/null @@ -1,122 +0,0 @@ -#include "NativeRendererInternal.h" - -#include -#include - -namespace XCEngine::UI::Editor::Host { - -using namespace NativeRendererInternal; - -float NativeRenderer::MeasureTextWidth( - const ::XCEngine::UI::Editor::UIEditorTextMeasureRequest& request) const { - if (!m_dwriteFactory || request.text.empty()) { - return 0.0f; - } - - const std::wstring text = Utf8ToWide(request.text); - if (text.empty()) { - return 0.0f; - } - - const float dpiScale = ClampDpiScale(m_dpiScale); - const float scaledFontSize = ResolveFontSize(request.fontSize) * dpiScale; - IDWriteTextFormat* textFormat = GetTextFormat(scaledFontSize); - if (textFormat == nullptr) { - return 0.0f; - } - - Microsoft::WRL::ComPtr textLayout; - HRESULT hr = m_dwriteFactory->CreateTextLayout( - text.c_str(), - static_cast(text.size()), - textFormat, - 4096.0f, - scaledFontSize * 2.0f, - textLayout.ReleaseAndGetAddressOf()); - if (FAILED(hr) || !textLayout) { - return 0.0f; - } - - DWRITE_TEXT_METRICS textMetrics = {}; - hr = textLayout->GetMetrics(&textMetrics); - if (FAILED(hr)) { - return 0.0f; - } - - DWRITE_OVERHANG_METRICS overhangMetrics = {}; - float width = textMetrics.widthIncludingTrailingWhitespace; - if (SUCCEEDED(textLayout->GetOverhangMetrics(&overhangMetrics))) { - width += (std::max)(overhangMetrics.left, 0.0f); - width += (std::max)(overhangMetrics.right, 0.0f); - } - - return std::ceil(width) / dpiScale; -} - -IDWriteTextFormat* NativeRenderer::GetTextFormat(float fontSize) const { - if (!m_dwriteFactory) { - return nullptr; - } - - const float resolvedFontSize = ResolveFontSize(fontSize); - const int key = static_cast(std::lround(resolvedFontSize * 10.0f)); - const auto found = m_textFormats.find(key); - if (found != m_textFormats.end()) { - return found->second.Get(); - } - - Microsoft::WRL::ComPtr textFormat; - const HRESULT hr = m_dwriteFactory->CreateTextFormat( - L"Segoe UI", - nullptr, - DWRITE_FONT_WEIGHT_REGULAR, - DWRITE_FONT_STYLE_NORMAL, - DWRITE_FONT_STRETCH_NORMAL, - resolvedFontSize, - L"", - textFormat.ReleaseAndGetAddressOf()); - if (FAILED(hr)) { - return nullptr; - } - - textFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_LEADING); - textFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_NEAR); - textFormat->SetWordWrapping(DWRITE_WORD_WRAPPING_NO_WRAP); - - IDWriteTextFormat* result = textFormat.Get(); - m_textFormats.emplace(key, std::move(textFormat)); - return result; -} - -D2D1_COLOR_F NativeRenderer::ToD2DColor(const ::XCEngine::UI::UIColor& color) { - return D2D1::ColorF(color.r, color.g, color.b, color.a); -} - -std::wstring NativeRenderer::Utf8ToWide(std::string_view text) { - if (text.empty()) { - return {}; - } - - const int sizeNeeded = MultiByteToWideChar( - CP_UTF8, - 0, - text.data(), - static_cast(text.size()), - nullptr, - 0); - if (sizeNeeded <= 0) { - return {}; - } - - std::wstring wideText(static_cast(sizeNeeded), L'\0'); - MultiByteToWideChar( - CP_UTF8, - 0, - text.data(), - static_cast(text.size()), - wideText.data(), - sizeNeeded); - return wideText; -} - -} // namespace XCEngine::UI::Editor::Host diff --git a/new_editor/app/Rendering/Native/NativeRendererTextureDecoding.cpp b/new_editor/app/Rendering/Native/NativeRendererTextureDecoding.cpp deleted file mode 100644 index 032fe7fe..00000000 --- a/new_editor/app/Rendering/Native/NativeRendererTextureDecoding.cpp +++ /dev/null @@ -1,150 +0,0 @@ -#include "NativeRendererInternal.h" - -#include - -namespace XCEngine::UI::Editor::Host { - -using namespace NativeRendererInternal; - -bool NativeRenderer::DecodeTextureFile( - const std::filesystem::path& path, - NativeTextureResource& outTexture, - std::string& outError) { - outError.clear(); - if (!EnsureWicFactory(outError)) { - return false; - } - - const std::wstring widePath = path.wstring(); - Microsoft::WRL::ComPtr decoder; - HRESULT hr = m_wicFactory->CreateDecoderFromFilename( - widePath.c_str(), - nullptr, - GENERIC_READ, - WICDecodeMetadataCacheOnLoad, - decoder.ReleaseAndGetAddressOf()); - if (FAILED(hr) || !decoder) { - outError = HrToString("IWICImagingFactory::CreateDecoderFromFilename", hr); - return false; - } - - Microsoft::WRL::ComPtr frame; - hr = decoder->GetFrame(0u, frame.ReleaseAndGetAddressOf()); - if (FAILED(hr) || !frame) { - outError = HrToString("IWICBitmapDecoder::GetFrame", hr); - return false; - } - - return DecodeTextureFrame(*frame.Get(), outTexture, outError); -} - -bool NativeRenderer::DecodeTextureMemory( - const std::uint8_t* data, - std::size_t size, - NativeTextureResource& outTexture, - std::string& outError) { - outError.clear(); - if (data == nullptr || size == 0u) { - outError = "DecodeTextureMemory rejected an empty image payload."; - return false; - } - - if (size > static_cast((std::numeric_limits::max)())) { - outError = "DecodeTextureMemory payload exceeds WIC stream limits."; - return false; - } - - if (!EnsureWicFactory(outError)) { - return false; - } - - Microsoft::WRL::ComPtr stream; - HRESULT hr = m_wicFactory->CreateStream(stream.ReleaseAndGetAddressOf()); - if (FAILED(hr) || !stream) { - outError = HrToString("IWICImagingFactory::CreateStream", hr); - return false; - } - - hr = stream->InitializeFromMemory( - const_cast(reinterpret_cast(data)), - static_cast(size)); - if (FAILED(hr)) { - outError = HrToString("IWICStream::InitializeFromMemory", hr); - return false; - } - - Microsoft::WRL::ComPtr decoder; - hr = m_wicFactory->CreateDecoderFromStream( - stream.Get(), - nullptr, - WICDecodeMetadataCacheOnLoad, - decoder.ReleaseAndGetAddressOf()); - if (FAILED(hr) || !decoder) { - outError = HrToString("IWICImagingFactory::CreateDecoderFromStream", hr); - return false; - } - - Microsoft::WRL::ComPtr frame; - hr = decoder->GetFrame(0u, frame.ReleaseAndGetAddressOf()); - if (FAILED(hr) || !frame) { - outError = HrToString("IWICBitmapDecoder::GetFrame", hr); - return false; - } - - return DecodeTextureFrame(*frame.Get(), outTexture, outError); -} - -bool NativeRenderer::DecodeTextureFrame( - IWICBitmapSource& source, - NativeTextureResource& outTexture, - std::string& outError) { - outError.clear(); - - Microsoft::WRL::ComPtr converter; - HRESULT hr = m_wicFactory->CreateFormatConverter(converter.ReleaseAndGetAddressOf()); - if (FAILED(hr) || !converter) { - outError = HrToString("IWICImagingFactory::CreateFormatConverter", hr); - return false; - } - - hr = converter->Initialize( - &source, - GUID_WICPixelFormat32bppPBGRA, - WICBitmapDitherTypeNone, - nullptr, - 0.0f, - WICBitmapPaletteTypeCustom); - if (FAILED(hr)) { - outError = HrToString("IWICFormatConverter::Initialize", hr); - return false; - } - - UINT width = 0u; - UINT height = 0u; - hr = converter->GetSize(&width, &height); - if (FAILED(hr) || width == 0u || height == 0u) { - outError = HrToString("IWICBitmapSource::GetSize", hr); - return false; - } - - std::vector pixels( - static_cast(width) * static_cast(height) * 4u); - hr = converter->CopyPixels( - nullptr, - width * 4u, - static_cast(pixels.size()), - pixels.data()); - if (FAILED(hr)) { - outError = HrToString("IWICBitmapSource::CopyPixels", hr); - return false; - } - - outTexture.pixels = std::move(pixels); - outTexture.width = width; - outTexture.height = height; - outTexture.cachedBitmap.Reset(); - outTexture.cachedTarget = nullptr; - return true; -} - -} // namespace XCEngine::UI::Editor::Host diff --git a/new_editor/app/Rendering/Native/NativeRendererTextures.cpp b/new_editor/app/Rendering/Native/NativeRendererTextures.cpp deleted file mode 100644 index c6a13c82..00000000 --- a/new_editor/app/Rendering/Native/NativeRendererTextures.cpp +++ /dev/null @@ -1,117 +0,0 @@ -#include "NativeRendererInternal.h" - -#include -#include - -namespace XCEngine::UI::Editor::Host { - -using namespace NativeRendererInternal; - -bool NativeRenderer::LoadTextureFromFile( - const std::filesystem::path& path, - ::XCEngine::UI::UITextureHandle& outTexture, - std::string& outError) { - outError.clear(); - ReleaseTexture(outTexture); - - auto texture = std::make_unique(); - if (!DecodeTextureFile(path, *texture, outError)) { - outTexture = {}; - return false; - } - - outTexture.nativeHandle = reinterpret_cast(texture.get()); - outTexture.width = texture->width; - outTexture.height = texture->height; - outTexture.kind = ::XCEngine::UI::UITextureHandleKind::DescriptorHandle; - m_liveTextures.insert(texture.get()); - texture.release(); - return true; -} - -bool NativeRenderer::LoadTextureFromMemory( - const std::uint8_t* data, - std::size_t size, - ::XCEngine::UI::UITextureHandle& outTexture, - std::string& outError) { - outError.clear(); - ReleaseTexture(outTexture); - - auto texture = std::make_unique(); - if (!DecodeTextureMemory(data, size, *texture, outError)) { - outTexture = {}; - return false; - } - - outTexture.nativeHandle = reinterpret_cast(texture.get()); - outTexture.width = texture->width; - outTexture.height = texture->height; - outTexture.kind = ::XCEngine::UI::UITextureHandleKind::DescriptorHandle; - m_liveTextures.insert(texture.get()); - texture.release(); - return true; -} - -void NativeRenderer::ReleaseTexture(::XCEngine::UI::UITextureHandle& texture) { - if (!texture.IsValid()) { - texture = {}; - return; - } - - auto* resource = reinterpret_cast(texture.nativeHandle); - if (resource != nullptr) { - const auto found = m_liveTextures.find(resource); - if (found != m_liveTextures.end()) { - m_liveTextures.erase(found); - delete resource; - } - } - - texture = {}; -} - -bool NativeRenderer::ResolveTextureBitmap( - ID2D1RenderTarget& renderTarget, - NativeTextureResource& texture, - Microsoft::WRL::ComPtr& outBitmap) { - outBitmap.Reset(); - if (texture.width == 0u || texture.height == 0u || texture.pixels.empty()) { - return false; - } - - if (texture.cachedBitmap && texture.cachedTarget == &renderTarget) { - outBitmap = texture.cachedBitmap; - return true; - } - - Microsoft::WRL::ComPtr bitmap; - const D2D1_BITMAP_PROPERTIES properties = D2D1::BitmapProperties( - D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED), - kBaseDpi, - kBaseDpi); - const HRESULT hr = renderTarget.CreateBitmap( - D2D1::SizeU(texture.width, texture.height), - texture.pixels.data(), - texture.width * 4u, - &properties, - bitmap.ReleaseAndGetAddressOf()); - if (FAILED(hr) || !bitmap) { - return false; - } - - if (&renderTarget == m_renderTarget.Get()) { - texture.cachedBitmap = bitmap; - texture.cachedTarget = &renderTarget; - } - - outBitmap = std::move(bitmap); - return true; -} - -bool NativeRenderer::ResolveInteropBitmap( - const ::XCEngine::UI::UITextureHandle& texture, - Microsoft::WRL::ComPtr& outBitmap) const { - return m_windowInterop.ResolveInteropBitmap(texture, outBitmap); -} - -} // namespace XCEngine::UI::Editor::Host diff --git a/new_editor/app/Rendering/Native/NativeRendererWindowInterop.cpp b/new_editor/app/Rendering/Native/NativeRendererWindowInterop.cpp deleted file mode 100644 index 84552d79..00000000 --- a/new_editor/app/Rendering/Native/NativeRendererWindowInterop.cpp +++ /dev/null @@ -1,72 +0,0 @@ -#include "NativeRendererInternal.h" - -namespace XCEngine::UI::Editor::Host { - -using namespace NativeRendererInternal; - -bool NativeRenderer::AttachWindowRenderer(D3D12WindowRenderer& windowRenderer) { - if (m_windowRenderer != &windowRenderer) { - ReleaseWindowRendererInterop(); - m_windowRenderer = &windowRenderer; - } - - if (!EnsureWindowRendererInterop()) { - return false; - } - - DiscardRenderTarget(); - - if (m_windowInterop.HasBackBufferTargets()) { - return true; - } - - return m_windowInterop.RebuildBackBufferTargets(); -} - -void NativeRenderer::DetachWindowRenderer() { - ReleaseWindowRendererInterop(); - m_windowRenderer = nullptr; -} - -void NativeRenderer::ReleaseWindowRendererBackBufferTargets() { - m_windowInterop.ReleaseBackBufferTargets(); -} - -bool NativeRenderer::RebuildWindowRendererBackBufferTargets() { - if (!EnsureWindowRendererInterop()) { - return false; - } - - DiscardRenderTarget(); - ReleaseWindowRendererBackBufferTargets(); - return m_windowInterop.RebuildBackBufferTargets(); -} - -bool NativeRenderer::HasAttachedWindowRenderer() const { - return m_windowInterop.HasAttachedWindowRenderer(); -} - -bool NativeRenderer::EnsureWindowRendererInterop() { - if (m_windowRenderer == nullptr) { - m_lastRenderError = "EnsureWindowRendererInterop requires an attached D3D12 window renderer."; - return false; - } - if (m_d2dFactory == nullptr || m_dwriteFactory == nullptr) { - m_lastRenderError = "EnsureWindowRendererInterop requires initialized D2D and DWrite factories."; - return false; - } - - const bool attached = m_windowInterop.Attach(*m_windowRenderer, *m_d2dFactory.Get()); - if (!attached) { - m_lastRenderError = m_windowInterop.GetLastError(); - } else { - m_lastRenderError.clear(); - } - return attached; -} - -void NativeRenderer::ReleaseWindowRendererInterop() { - m_windowInterop.Detach(); -} - -} // namespace XCEngine::UI::Editor::Host diff --git a/new_editor/app/Rendering/Viewport/Passes/SceneViewportGridPass.cpp b/new_editor/app/Rendering/Viewport/Passes/SceneViewportGridPass.cpp index c7fb9caf..502282d7 100644 --- a/new_editor/app/Rendering/Viewport/Passes/SceneViewportGridPass.cpp +++ b/new_editor/app/Rendering/Viewport/Passes/SceneViewportGridPass.cpp @@ -2,8 +2,8 @@ #include "Rendering/Viewport/SceneViewportResourcePaths.h" -#include -#include +#include +#include #include #include @@ -55,7 +55,7 @@ const ::XCEngine::Resources::ShaderPass* FindInfiniteGridCompatiblePass( const ::XCEngine::Resources::ShaderPass* gridPass = shader.FindPass("InfiniteGrid"); if (gridPass != nullptr && - ::XCEngine::Rendering::Internal::ShaderPassHasGraphicsVariants( + ::XCEngine::Rendering::ShaderPassHasGraphicsVariants( shader, gridPass->name, backend)) { @@ -63,7 +63,7 @@ const ::XCEngine::Resources::ShaderPass* FindInfiniteGridCompatiblePass( } if (shader.GetPassCount() > 0 && - ::XCEngine::Rendering::Internal::ShaderPassHasGraphicsVariants( + ::XCEngine::Rendering::ShaderPassHasGraphicsVariants( shader, shader.GetPasses()[0].name, backend)) { @@ -83,12 +83,12 @@ const ::XCEngine::Resources::ShaderPass* FindInfiniteGridCompatiblePass( pipelineDesc.pipelineLayout = pipelineLayout; pipelineDesc.topologyType = static_cast( ::XCEngine::RHI::PrimitiveTopologyType::Triangle); - ::XCEngine::Rendering::Internal:: + ::XCEngine::Rendering:: ApplySingleColorAttachmentPropertiesToGraphicsPipelineDesc( surface, pipelineDesc); pipelineDesc.depthStencilFormat = static_cast( - ::XCEngine::Rendering::Internal::ResolveSurfaceDepthFormat(surface)); + ::XCEngine::Rendering::ResolveSurfaceDepthFormat(surface)); const ::XCEngine::Resources::ShaderPass* shaderPass = shader.FindPass(passName); @@ -125,14 +125,14 @@ const ::XCEngine::Resources::ShaderPass* FindInfiniteGridCompatiblePass( } const ::XCEngine::Resources::ShaderBackend backend = - ::XCEngine::Rendering::Internal::ToShaderBackend(backendType); + ::XCEngine::Rendering::ToShaderBackend(backendType); if (const ::XCEngine::Resources::ShaderStageVariant* vertexVariant = shader.FindVariant( passName, ::XCEngine::Resources::ShaderType::Vertex, backend)) { if (shaderPass != nullptr) { - ::XCEngine::Rendering::Internal::ApplyShaderStageVariant( + ::XCEngine::Rendering::ApplyShaderStageVariant( shader.GetPath(), *shaderPass, backend, @@ -146,7 +146,7 @@ const ::XCEngine::Resources::ShaderPass* FindInfiniteGridCompatiblePass( ::XCEngine::Resources::ShaderType::Fragment, backend)) { if (shaderPass != nullptr) { - ::XCEngine::Rendering::Internal::ApplyShaderStageVariant( + ::XCEngine::Rendering::ApplyShaderStageVariant( shader.GetPath(), *shaderPass, backend, @@ -301,7 +301,7 @@ public: const std::vector<::XCEngine::RHI::RHIResourceView*>& colorAttachments = surface.GetColorAttachments(); - if (!::XCEngine::Rendering::Internal::HasSingleColorAttachment(surface) || + if (!::XCEngine::Rendering::HasSingleColorAttachment(surface) || colorAttachments.empty() || colorAttachments[0] == nullptr || surface.GetDepthAttachment() == nullptr) { @@ -436,13 +436,13 @@ private: const ::XCEngine::Rendering::RenderContext& renderContext, const ::XCEngine::Rendering::RenderSurface& surface) { const ::XCEngine::RHI::Format renderTargetFormat = - ::XCEngine::Rendering::Internal::ResolveSurfaceColorFormat( + ::XCEngine::Rendering::ResolveSurfaceColorFormat( surface, 0u); const ::XCEngine::RHI::Format depthFormat = - ::XCEngine::Rendering::Internal::ResolveSurfaceDepthFormat(surface); + ::XCEngine::Rendering::ResolveSurfaceDepthFormat(surface); const std::uint32_t renderTargetSampleCount = - ::XCEngine::Rendering::Internal::ResolveSurfaceSampleCount(surface); + ::XCEngine::Rendering::ResolveSurfaceSampleCount(surface); if (m_pipelineState != nullptr && m_pipelineLayout != nullptr && m_constantPool != nullptr && @@ -466,11 +466,11 @@ private: return false; } - if (!::XCEngine::Rendering::Internal::HasSingleColorAttachment(surface) || - ::XCEngine::Rendering::Internal::ResolveSurfaceColorFormat( + if (!::XCEngine::Rendering::HasSingleColorAttachment(surface) || + ::XCEngine::Rendering::ResolveSurfaceColorFormat( surface, 0u) == ::XCEngine::RHI::Format::Unknown || - ::XCEngine::Rendering::Internal::ResolveSurfaceDepthFormat(surface) == + ::XCEngine::Rendering::ResolveSurfaceDepthFormat(surface) == ::XCEngine::RHI::Format::Unknown) { return false; } @@ -495,7 +495,7 @@ private: } const ::XCEngine::Resources::ShaderBackend backend = - ::XCEngine::Rendering::Internal::ToShaderBackend(m_backendType); + ::XCEngine::Rendering::ToShaderBackend(m_backendType); const ::XCEngine::Resources::ShaderPass* infiniteGridPass = FindInfiniteGridCompatiblePass(*m_shader.Get(), backend); if (infiniteGridPass == nullptr) { @@ -556,13 +556,13 @@ private: } m_renderTargetFormat = - ::XCEngine::Rendering::Internal::ResolveSurfaceColorFormat( + ::XCEngine::Rendering::ResolveSurfaceColorFormat( surface, 0u); m_depthStencilFormat = - ::XCEngine::Rendering::Internal::ResolveSurfaceDepthFormat(surface); + ::XCEngine::Rendering::ResolveSurfaceDepthFormat(surface); m_renderTargetSampleCount = - ::XCEngine::Rendering::Internal::ResolveSurfaceSampleCount(surface); + ::XCEngine::Rendering::ResolveSurfaceSampleCount(surface); return true; } diff --git a/new_editor/app/Rendering/Viewport/Passes/SceneViewportSelectedHelpersPass.cpp b/new_editor/app/Rendering/Viewport/Passes/SceneViewportSelectedHelpersPass.cpp new file mode 100644 index 00000000..592f0d86 --- /dev/null +++ b/new_editor/app/Rendering/Viewport/Passes/SceneViewportSelectedHelpersPass.cpp @@ -0,0 +1,948 @@ +#include "Rendering/Viewport/Passes/SceneViewportSelectedHelpersPass.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace XCEngine::UI::Editor::App { + +namespace { + +using ::XCEngine::Components::CameraComponent; +using ::XCEngine::Components::GameObject; +using ::XCEngine::Components::LightComponent; +using ::XCEngine::Math::RectInt; +using ::XCEngine::RHI::RHIResourceView; +using ::XCEngine::Rendering::RenderPass; +using ::XCEngine::Rendering::RenderPassContext; + +constexpr std::uint64_t kMinDynamicVertexBufferBytes = 4096u; +constexpr ::XCEngine::Math::Color + kSelectedCameraHelperColor(1.0f, 1.0f, 1.0f, 1.0f); +constexpr ::XCEngine::Math::Color + kSelectedLightHelperColor(1.0f, 0.92f, 0.24f, 1.0f); +constexpr std::size_t kSelectedLightHelperSegmentCount = 32u; + +const char kSelectedHelpersLineHlsl[] = R"( +cbuffer OverlayConstants : register(b0) { + float4x4 gViewProjectionMatrix; +}; + +struct VSInput { + float3 position : POSITION; + float4 color : COLOR0; +}; + +struct VSOutput { + float4 position : SV_POSITION; + float4 color : COLOR0; +}; + +VSOutput MainVS(VSInput input) { + VSOutput output; + output.position = mul(gViewProjectionMatrix, float4(input.position, 1.0)); + output.color = input.color; + return output; +} + +float4 MainPS(VSOutput input) : SV_TARGET0 { + return input.color; +} +)"; + +struct SelectedHelpersCameraData { + bool valid = false; + ::XCEngine::Math::Matrix4x4 viewProjection = + ::XCEngine::Math::Matrix4x4::Identity(); + ::XCEngine::Math::Vector3 cameraPosition = + ::XCEngine::Math::Vector3::Zero(); + ::XCEngine::Math::Vector3 cameraForward = + ::XCEngine::Math::Vector3::Forward(); + float verticalFovRadians = 60.0f * ::XCEngine::Math::DEG_TO_RAD; + float nearClipPlane = 0.03f; + float farClipPlane = 2000.0f; +}; + +struct HelperLine { + ::XCEngine::Math::Vector3 startWorld = ::XCEngine::Math::Vector3::Zero(); + ::XCEngine::Math::Vector3 endWorld = ::XCEngine::Math::Vector3::Zero(); + ::XCEngine::Math::Color color = ::XCEngine::Math::Color::White(); +}; + +struct OverlayConstants { + ::XCEngine::Math::Matrix4x4 viewProjection = + ::XCEngine::Math::Matrix4x4::Identity(); +}; + +struct OverlayLineVertex { + ::XCEngine::Math::Vector3 position = ::XCEngine::Math::Vector3::Zero(); + ::XCEngine::Math::Color color = ::XCEngine::Math::Color::White(); +}; + +bool CanBuildHelpersForGameObject( + const ::XCEngine::Components::GameObject* gameObject) { + return gameObject != nullptr && + gameObject->GetTransform() != nullptr && + gameObject->IsActiveInHierarchy(); +} + +SelectedHelpersCameraData BuildCameraData( + const ::XCEngine::Rendering::RenderSceneData& sceneData) { + SelectedHelpersCameraData data = {}; + if (!sceneData.HasCamera()) { + return data; + } + + data.valid = true; + data.viewProjection = sceneData.cameraData.viewProjection; + data.cameraPosition = sceneData.cameraData.worldPosition; + data.cameraForward = sceneData.cameraData.worldForward; + data.verticalFovRadians = sceneData.cameraData.verticalFovRadians; + data.nearClipPlane = sceneData.cameraData.nearClipPlane; + data.farClipPlane = sceneData.cameraData.farClipPlane; + return data; +} + +float ResolveCameraAspect( + const ::XCEngine::Components::CameraComponent& camera, + float viewportWidth, + float viewportHeight) { + const ::XCEngine::Math::Rect viewportRect = camera.GetViewportRect(); + const float resolvedWidth = viewportWidth * + (viewportRect.width > ::XCEngine::Math::EPSILON ? viewportRect.width : 1.0f); + const float resolvedHeight = viewportHeight * + (viewportRect.height > ::XCEngine::Math::EPSILON ? viewportRect.height : 1.0f); + return resolvedHeight > ::XCEngine::Math::EPSILON + ? resolvedWidth / resolvedHeight + : 1.0f; +} + +float ComputeWorldUnitsPerPixel( + const SelectedHelpersCameraData& overlay, + const ::XCEngine::Math::Vector3& worldPoint, + float viewportHeight) { + if (!overlay.valid || viewportHeight <= 1.0f) { + return 0.0f; + } + + const ::XCEngine::Math::Vector3 cameraForward = + overlay.cameraForward.Normalized(); + const float depth = ::XCEngine::Math::Vector3::Dot( + worldPoint - overlay.cameraPosition, + cameraForward); + if (depth <= ::XCEngine::Math::EPSILON) { + return 0.0f; + } + + return 2.0f * depth * + std::tan(overlay.verticalFovRadians * 0.5f) / + viewportHeight; +} + +void AppendWorldLine( + std::vector& outLines, + const ::XCEngine::Math::Vector3& startWorld, + const ::XCEngine::Math::Vector3& endWorld, + const ::XCEngine::Math::Color& color) { + HelperLine line = {}; + line.startWorld = startWorld; + line.endWorld = endWorld; + line.color = color; + outLines.push_back(std::move(line)); +} + +void AppendWireCircle( + std::vector& outLines, + const ::XCEngine::Math::Vector3& center, + const ::XCEngine::Math::Vector3& basisA, + const ::XCEngine::Math::Vector3& basisB, + float radius, + const ::XCEngine::Math::Color& color) { + const ::XCEngine::Math::Vector3 axisA = basisA.Normalized(); + const ::XCEngine::Math::Vector3 axisB = basisB.Normalized(); + if (radius <= ::XCEngine::Math::EPSILON || + axisA.SqrMagnitude() <= ::XCEngine::Math::EPSILON || + axisB.SqrMagnitude() <= ::XCEngine::Math::EPSILON) { + return; + } + + for (std::size_t segmentIndex = 0; segmentIndex < kSelectedLightHelperSegmentCount; ++segmentIndex) { + const float angle0 = + static_cast(segmentIndex) / static_cast(kSelectedLightHelperSegmentCount) * + ::XCEngine::Math::PI * 2.0f; + const float angle1 = + static_cast(segmentIndex + 1u) / static_cast(kSelectedLightHelperSegmentCount) * + ::XCEngine::Math::PI * 2.0f; + const ::XCEngine::Math::Vector3 p0 = + center + axisA * std::cos(angle0) * radius + axisB * std::sin(angle0) * radius; + const ::XCEngine::Math::Vector3 p1 = + center + axisA * std::cos(angle1) * radius + axisB * std::sin(angle1) * radius; + AppendWorldLine(outLines, p0, p1, color); + } +} + +void AppendSelectedCameraHelper( + std::vector& outLines, + const ::XCEngine::Components::CameraComponent& camera, + const ::XCEngine::Components::GameObject& gameObject, + float viewportWidth, + float viewportHeight) { + const ::XCEngine::Components::TransformComponent* transform = + gameObject.GetTransform(); + if (transform == nullptr) { + return; + } + + const ::XCEngine::Math::Vector3 position = transform->GetPosition(); + const ::XCEngine::Math::Vector3 forward = transform->GetForward().Normalized(); + const ::XCEngine::Math::Vector3 right = transform->GetRight().Normalized(); + const ::XCEngine::Math::Vector3 up = transform->GetUp().Normalized(); + if (forward.SqrMagnitude() <= ::XCEngine::Math::EPSILON || + right.SqrMagnitude() <= ::XCEngine::Math::EPSILON || + up.SqrMagnitude() <= ::XCEngine::Math::EPSILON) { + return; + } + + const float nearClip = (std::max)(camera.GetNearClipPlane(), 0.01f); + const float farClip = (std::max)(camera.GetFarClipPlane(), nearClip + 0.01f); + const float aspect = ResolveCameraAspect(camera, viewportWidth, viewportHeight); + + float nearHalfHeight = 0.0f; + float nearHalfWidth = 0.0f; + float farHalfHeight = 0.0f; + float farHalfWidth = 0.0f; + if (camera.GetProjectionType() == + ::XCEngine::Components::CameraProjectionType::Perspective) { + const float halfFovRadians = + std::clamp(camera.GetFieldOfView(), 1.0f, 179.0f) * + ::XCEngine::Math::DEG_TO_RAD * 0.5f; + nearHalfHeight = std::tan(halfFovRadians) * nearClip; + nearHalfWidth = nearHalfHeight * aspect; + farHalfHeight = std::tan(halfFovRadians) * farClip; + farHalfWidth = farHalfHeight * aspect; + } else { + const float halfHeight = (std::max)(camera.GetOrthographicSize(), 0.01f); + const float halfWidth = halfHeight * aspect; + nearHalfHeight = halfHeight; + nearHalfWidth = halfWidth; + farHalfHeight = halfHeight; + farHalfWidth = halfWidth; + } + + const ::XCEngine::Math::Vector3 nearCenter = position + forward * nearClip; + const ::XCEngine::Math::Vector3 farCenter = position + forward * farClip; + const std::array<::XCEngine::Math::Vector3, 8> corners = {{ + nearCenter + up * nearHalfHeight - right * nearHalfWidth, + nearCenter + up * nearHalfHeight + right * nearHalfWidth, + nearCenter - up * nearHalfHeight + right * nearHalfWidth, + nearCenter - up * nearHalfHeight - right * nearHalfWidth, + farCenter + up * farHalfHeight - right * farHalfWidth, + farCenter + up * farHalfHeight + right * farHalfWidth, + farCenter - up * farHalfHeight + right * farHalfWidth, + farCenter - up * farHalfHeight - right * farHalfWidth + }}; + static constexpr std::array, 12> kFrustumEdges = {{ + { 0u, 1u }, { 1u, 2u }, { 2u, 3u }, { 3u, 0u }, + { 4u, 5u }, { 5u, 6u }, { 6u, 7u }, { 7u, 4u }, + { 0u, 4u }, { 1u, 5u }, { 2u, 6u }, { 3u, 7u } + }}; + for (const auto& edge : kFrustumEdges) { + AppendWorldLine( + outLines, + corners[edge.first], + corners[edge.second], + kSelectedCameraHelperColor); + } +} + +void AppendSelectedDirectionalLightHelper( + std::vector& outLines, + const SelectedHelpersCameraData& overlay, + const ::XCEngine::Components::GameObject& gameObject, + float viewportHeight) { + const ::XCEngine::Components::TransformComponent* transform = + gameObject.GetTransform(); + if (transform == nullptr) { + return; + } + + const ::XCEngine::Math::Vector3 position = transform->GetPosition(); + const ::XCEngine::Math::Vector3 forward = transform->GetForward().Normalized(); + const ::XCEngine::Math::Vector3 right = transform->GetRight().Normalized(); + const ::XCEngine::Math::Vector3 up = transform->GetUp().Normalized(); + if (forward.SqrMagnitude() <= ::XCEngine::Math::EPSILON || + right.SqrMagnitude() <= ::XCEngine::Math::EPSILON || + up.SqrMagnitude() <= ::XCEngine::Math::EPSILON) { + return; + } + + const float worldUnitsPerPixel = + ComputeWorldUnitsPerPixel(overlay, position, viewportHeight); + if (worldUnitsPerPixel <= ::XCEngine::Math::EPSILON) { + return; + } + + constexpr std::array kRayAngles = {{ + 0.0f, + ::XCEngine::Math::PI / 3.0f, + ::XCEngine::Math::PI * 2.0f / 3.0f, + ::XCEngine::Math::PI, + ::XCEngine::Math::PI * 4.0f / 3.0f, + ::XCEngine::Math::PI * 5.0f / 3.0f + }}; + const float ringRadius = worldUnitsPerPixel * 26.0f; + const float ringOffset = worldUnitsPerPixel * 54.0f; + const float innerRayRadius = ringRadius * 0.52f; + const float rayLength = worldUnitsPerPixel * 96.0f; + const ::XCEngine::Math::Vector3 ringCenter = position + forward * ringOffset; + + AppendWireCircle( + outLines, + ringCenter, + right, + up, + ringRadius, + kSelectedLightHelperColor); + AppendWorldLine(outLines, position, ringCenter, kSelectedLightHelperColor); + AppendWorldLine( + outLines, + ringCenter, + ringCenter + forward * rayLength, + kSelectedLightHelperColor); + + for (float angle : kRayAngles) { + const ::XCEngine::Math::Vector3 rayStart = + ringCenter + right * std::cos(angle) * innerRayRadius + + up * std::sin(angle) * innerRayRadius; + AppendWorldLine( + outLines, + rayStart, + rayStart + forward * rayLength, + kSelectedLightHelperColor); + } +} + +void AppendSelectedPointLightHelper( + std::vector& outLines, + const ::XCEngine::Components::GameObject& gameObject, + const ::XCEngine::Components::LightComponent& light) { + const ::XCEngine::Components::TransformComponent* transform = + gameObject.GetTransform(); + if (transform == nullptr) { + return; + } + + const ::XCEngine::Math::Vector3 position = transform->GetPosition(); + const ::XCEngine::Math::Vector3 right = transform->GetRight().Normalized(); + const ::XCEngine::Math::Vector3 up = transform->GetUp().Normalized(); + const ::XCEngine::Math::Vector3 forward = transform->GetForward().Normalized(); + if (right.SqrMagnitude() <= ::XCEngine::Math::EPSILON || + up.SqrMagnitude() <= ::XCEngine::Math::EPSILON || + forward.SqrMagnitude() <= ::XCEngine::Math::EPSILON) { + return; + } + + const float range = (std::max)(light.GetRange(), 0.001f); + AppendWireCircle( + outLines, + position, + right, + up, + range, + kSelectedLightHelperColor); + AppendWireCircle( + outLines, + position, + right, + forward, + range, + kSelectedLightHelperColor); + AppendWireCircle( + outLines, + position, + up, + forward, + range, + kSelectedLightHelperColor); +} + +void AppendSelectedSpotLightHelper( + std::vector& outLines, + const ::XCEngine::Components::GameObject& gameObject, + const ::XCEngine::Components::LightComponent& light) { + const ::XCEngine::Components::TransformComponent* transform = + gameObject.GetTransform(); + if (transform == nullptr) { + return; + } + + const ::XCEngine::Math::Vector3 position = transform->GetPosition(); + const ::XCEngine::Math::Vector3 forward = transform->GetForward().Normalized(); + const ::XCEngine::Math::Vector3 right = transform->GetRight().Normalized(); + const ::XCEngine::Math::Vector3 up = transform->GetUp().Normalized(); + if (forward.SqrMagnitude() <= ::XCEngine::Math::EPSILON || + right.SqrMagnitude() <= ::XCEngine::Math::EPSILON || + up.SqrMagnitude() <= ::XCEngine::Math::EPSILON) { + return; + } + + const float range = (std::max)(light.GetRange(), 0.001f); + const float halfAngleRadians = + std::clamp(light.GetSpotAngle(), 1.0f, 179.0f) * + ::XCEngine::Math::DEG_TO_RAD * 0.5f; + const float coneRadius = std::tan(halfAngleRadians) * range; + const ::XCEngine::Math::Vector3 coneBaseCenter = position + forward * range; + + AppendWorldLine( + outLines, + position, + coneBaseCenter, + kSelectedLightHelperColor); + AppendWireCircle( + outLines, + coneBaseCenter, + right, + up, + coneRadius, + kSelectedLightHelperColor); + + static constexpr std::array kConeEdgeAngles = {{ + 0.0f, + ::XCEngine::Math::PI * 0.5f, + ::XCEngine::Math::PI, + ::XCEngine::Math::PI * 1.5f + }}; + for (float angle : kConeEdgeAngles) { + const ::XCEngine::Math::Vector3 coneEdgePoint = + coneBaseCenter + right * std::cos(angle) * coneRadius + + up * std::sin(angle) * coneRadius; + AppendWorldLine( + outLines, + position, + coneEdgePoint, + kSelectedLightHelperColor); + } +} + +std::vector BuildHelperLines( + const SceneViewportRenderRequest& request, + const SelectedHelpersCameraData& overlay, + float viewportWidth, + float viewportHeight) { + std::vector lines = {}; + if (!request.IsValid() || request.scene == nullptr || request.selectedObjectIds.empty()) { + return lines; + } + + if (!overlay.valid) { + return lines; + } + + for (const std::uint64_t entityId : request.selectedObjectIds) { + if (entityId == 0) { + continue; + } + + GameObject* gameObject = request.scene->FindByID(entityId); + if (!CanBuildHelpersForGameObject(gameObject)) { + continue; + } + + if (CameraComponent* camera = gameObject->GetComponent(); + camera != nullptr && camera->IsEnabled()) { + AppendSelectedCameraHelper( + lines, + *camera, + *gameObject, + viewportWidth, + viewportHeight); + } + + if (LightComponent* light = gameObject->GetComponent(); + light != nullptr && light->IsEnabled()) { + switch (light->GetLightType()) { + case ::XCEngine::Components::LightType::Directional: + AppendSelectedDirectionalLightHelper( + lines, + overlay, + *gameObject, + viewportHeight); + break; + case ::XCEngine::Components::LightType::Point: + AppendSelectedPointLightHelper(lines, *gameObject, *light); + break; + case ::XCEngine::Components::LightType::Spot: + AppendSelectedSpotLightHelper(lines, *gameObject, *light); + break; + default: + break; + } + } + } + + return lines; +} + +::XCEngine::RHI::GraphicsPipelineDesc BuildLinePipelineDesc( + ::XCEngine::RHI::RHIPipelineLayout* pipelineLayout, + const ::XCEngine::Rendering::RenderSurface& surface) { + ::XCEngine::RHI::GraphicsPipelineDesc pipelineDesc = {}; + pipelineDesc.pipelineLayout = pipelineLayout; + pipelineDesc.topologyType = + static_cast(::XCEngine::RHI::PrimitiveTopologyType::Line); + ::XCEngine::Rendering::ApplySingleColorAttachmentPropertiesToGraphicsPipelineDesc( + surface, + pipelineDesc); + pipelineDesc.depthStencilFormat = + static_cast(::XCEngine::Rendering::ResolveSurfaceDepthFormat(surface)); + + pipelineDesc.inputLayout.elements = { + { "POSITION", 0, static_cast(::XCEngine::RHI::Format::R32G32B32_Float), 0, 0, 0, 0 }, + { "COLOR", 0, static_cast(::XCEngine::RHI::Format::R32G32B32A32_Float), 0, 12, 0, 0 } + }; + + pipelineDesc.rasterizerState.fillMode = + static_cast(::XCEngine::RHI::FillMode::Solid); + pipelineDesc.rasterizerState.cullMode = + static_cast(::XCEngine::RHI::CullMode::None); + pipelineDesc.rasterizerState.frontFace = + static_cast(::XCEngine::RHI::FrontFace::CounterClockwise); + pipelineDesc.rasterizerState.depthClipEnable = true; + + pipelineDesc.blendState.blendEnable = true; + pipelineDesc.blendState.srcBlend = + static_cast(::XCEngine::RHI::BlendFactor::SrcAlpha); + pipelineDesc.blendState.dstBlend = + static_cast(::XCEngine::RHI::BlendFactor::InvSrcAlpha); + pipelineDesc.blendState.srcBlendAlpha = + static_cast(::XCEngine::RHI::BlendFactor::One); + pipelineDesc.blendState.dstBlendAlpha = + static_cast(::XCEngine::RHI::BlendFactor::InvSrcAlpha); + pipelineDesc.blendState.blendOp = + static_cast(::XCEngine::RHI::BlendOp::Add); + pipelineDesc.blendState.blendOpAlpha = + static_cast(::XCEngine::RHI::BlendOp::Add); + pipelineDesc.blendState.colorWriteMask = + static_cast(::XCEngine::RHI::ColorWriteMask::All); + + pipelineDesc.depthStencilState.depthTestEnable = false; + pipelineDesc.depthStencilState.depthWriteEnable = false; + pipelineDesc.depthStencilState.depthFunc = + static_cast(::XCEngine::RHI::ComparisonFunc::LessEqual); + + pipelineDesc.vertexShader.source.assign( + kSelectedHelpersLineHlsl, + kSelectedHelpersLineHlsl + std::strlen(kSelectedHelpersLineHlsl)); + pipelineDesc.vertexShader.sourceLanguage = ::XCEngine::RHI::ShaderLanguage::HLSL; + pipelineDesc.vertexShader.entryPoint = L"MainVS"; + pipelineDesc.vertexShader.profile = L"vs_5_0"; + + pipelineDesc.fragmentShader.source.assign( + kSelectedHelpersLineHlsl, + kSelectedHelpersLineHlsl + std::strlen(kSelectedHelpersLineHlsl)); + pipelineDesc.fragmentShader.sourceLanguage = ::XCEngine::RHI::ShaderLanguage::HLSL; + pipelineDesc.fragmentShader.entryPoint = L"MainPS"; + pipelineDesc.fragmentShader.profile = L"ps_5_0"; + return pipelineDesc; +} + +class SceneViewportSelectedHelpersPass final : public RenderPass { +public: + SceneViewportSelectedHelpersPass( + SceneViewportSelectedHelpersPassRenderer& renderer, + SceneViewportRenderRequest request) + : m_renderer(renderer) + , m_request(std::move(request)) { + } + + const char* GetName() const override { + return "SceneViewportSelectedHelpers"; + } + + bool Execute(const RenderPassContext& context) override { + return m_renderer.Render(context, m_request); + } + +private: + SceneViewportSelectedHelpersPassRenderer& m_renderer; + SceneViewportRenderRequest m_request = {}; +}; + +} // namespace + +class SceneViewportSelectedHelpersPassRenderer::Impl { +public: + void Shutdown() { + DestroyResources(); + } + + bool Render( + const ::XCEngine::Rendering::RenderPassContext& context, + const SceneViewportRenderRequest& request) { + const ::XCEngine::Rendering::RenderContext& renderContext = + context.renderContext; + const ::XCEngine::Rendering::RenderSurface& surface = context.surface; + if (!request.IsValid() || + !renderContext.IsValid() || + renderContext.commandList == nullptr) { + return true; + } + + const std::vector& colorAttachments = surface.GetColorAttachments(); + if (!::XCEngine::Rendering::HasSingleColorAttachment(surface) || + colorAttachments.empty() || + colorAttachments[0] == nullptr || + surface.GetDepthAttachment() == nullptr) { + return false; + } + + const RectInt renderArea = surface.GetRenderArea(); + if (renderArea.width <= 0 || renderArea.height <= 0) { + return false; + } + + const SelectedHelpersCameraData cameraData = + BuildCameraData(context.sceneData); + const std::vector lines = + BuildHelperLines( + request, + cameraData, + static_cast(renderArea.width), + static_cast(renderArea.height)); + if (lines.empty()) { + return true; + } + + if (!EnsureInitialized(renderContext, surface) || + !EnsureLineBufferCapacity(lines.size() * 2u)) { + return false; + } + + std::vector vertices = {}; + vertices.reserve(lines.size() * 2u); + for (const HelperLine& line : lines) { + vertices.push_back({ line.startWorld, line.color }); + vertices.push_back({ line.endWorld, line.color }); + } + m_lineVertexBuffer->SetData( + vertices.data(), + vertices.size() * sizeof(OverlayLineVertex)); + + OverlayConstants constants = {}; + constants.viewProjection = cameraData.viewProjection; + m_constantSet->WriteConstant(0, &constants, sizeof(constants)); + + ::XCEngine::RHI::RHICommandList* commandList = renderContext.commandList; + ::XCEngine::RHI::RHIResourceView* renderTarget = colorAttachments[0]; + ::XCEngine::RHI::RHIResourceView* depthAttachment = + surface.GetDepthAttachment(); + if (surface.IsAutoTransitionEnabled()) { + commandList->TransitionBarrier( + renderTarget, + surface.GetColorStateAfter(), + ::XCEngine::RHI::ResourceStates::RenderTarget); + commandList->TransitionBarrier( + depthAttachment, + surface.GetDepthStateAfter(), + ::XCEngine::RHI::ResourceStates::DepthWrite); + } + + commandList->SetRenderTargets(1, &renderTarget, depthAttachment); + const ::XCEngine::RHI::Viewport viewport = { + static_cast(renderArea.x), + static_cast(renderArea.y), + static_cast(renderArea.width), + static_cast(renderArea.height), + 0.0f, + 1.0f + }; + const ::XCEngine::RHI::Rect scissorRect = { + renderArea.x, + renderArea.y, + renderArea.x + renderArea.width, + renderArea.y + renderArea.height + }; + commandList->SetViewport(viewport); + commandList->SetScissorRect(scissorRect); + commandList->SetPrimitiveTopology( + ::XCEngine::RHI::PrimitiveTopology::LineList); + + ::XCEngine::RHI::RHIResourceView* vertexBuffers[] = { + m_lineVertexBufferView + }; + const std::uint64_t offsets[] = { 0u }; + const std::uint32_t strides[] = { + static_cast(sizeof(OverlayLineVertex)) + }; + commandList->SetVertexBuffers(0, 1, vertexBuffers, offsets, strides); + commandList->SetPipelineState(m_pipelineState); + + ::XCEngine::RHI::RHIDescriptorSet* descriptorSets[] = { + m_constantSet + }; + commandList->SetGraphicsDescriptorSets( + 0, + 1, + descriptorSets, + m_pipelineLayout); + commandList->Draw(static_cast(vertices.size()), 1, 0, 0); + commandList->EndRenderPass(); + + if (surface.IsAutoTransitionEnabled()) { + commandList->TransitionBarrier( + renderTarget, + ::XCEngine::RHI::ResourceStates::RenderTarget, + surface.GetColorStateAfter()); + commandList->TransitionBarrier( + depthAttachment, + ::XCEngine::RHI::ResourceStates::DepthWrite, + surface.GetDepthStateAfter()); + } + return true; + } + +private: + bool EnsureInitialized( + const ::XCEngine::Rendering::RenderContext& renderContext, + const ::XCEngine::Rendering::RenderSurface& surface) { + const ::XCEngine::RHI::Format renderTargetFormat = + ::XCEngine::Rendering::ResolveSurfaceColorFormat(surface, 0u); + const ::XCEngine::RHI::Format depthStencilFormat = + ::XCEngine::Rendering::ResolveSurfaceDepthFormat(surface); + const std::uint32_t renderTargetSampleCount = + ::XCEngine::Rendering::ResolveSurfaceSampleCount(surface); + if (m_device == renderContext.device && + m_backendType == renderContext.backendType && + m_pipelineLayout != nullptr && + m_pipelineState != nullptr && + m_constantPool != nullptr && + m_constantSet != nullptr && + m_renderTargetFormat == renderTargetFormat && + m_depthStencilFormat == depthStencilFormat && + m_renderTargetSampleCount == renderTargetSampleCount) { + return true; + } + + DestroyResources(); + return CreateResources(renderContext, surface); + } + + bool CreateResources( + const ::XCEngine::Rendering::RenderContext& renderContext, + const ::XCEngine::Rendering::RenderSurface& surface) { + if (!renderContext.IsValid()) { + return false; + } + + if (!::XCEngine::Rendering::HasSingleColorAttachment(surface) || + ::XCEngine::Rendering::ResolveSurfaceColorFormat(surface, 0u) == + ::XCEngine::RHI::Format::Unknown || + ::XCEngine::Rendering::ResolveSurfaceDepthFormat(surface) == + ::XCEngine::RHI::Format::Unknown) { + return false; + } + + m_device = renderContext.device; + m_backendType = renderContext.backendType; + m_renderTargetFormat = + ::XCEngine::Rendering::ResolveSurfaceColorFormat(surface, 0u); + m_depthStencilFormat = + ::XCEngine::Rendering::ResolveSurfaceDepthFormat(surface); + m_renderTargetSampleCount = + ::XCEngine::Rendering::ResolveSurfaceSampleCount(surface); + + ::XCEngine::RHI::DescriptorSetLayoutBinding constantBinding = {}; + constantBinding.binding = 0; + constantBinding.type = + static_cast(::XCEngine::RHI::DescriptorType::CBV); + constantBinding.count = 1; + + ::XCEngine::RHI::DescriptorSetLayoutDesc constantLayout = {}; + constantLayout.bindings = &constantBinding; + constantLayout.bindingCount = 1; + + ::XCEngine::RHI::RHIPipelineLayoutDesc pipelineLayoutDesc = {}; + pipelineLayoutDesc.setLayouts = &constantLayout; + pipelineLayoutDesc.setLayoutCount = 1; + m_pipelineLayout = m_device->CreatePipelineLayout(pipelineLayoutDesc); + if (m_pipelineLayout == nullptr) { + DestroyResources(); + return false; + } + + ::XCEngine::RHI::DescriptorPoolDesc constantPoolDesc = {}; + constantPoolDesc.type = ::XCEngine::RHI::DescriptorHeapType::CBV_SRV_UAV; + constantPoolDesc.descriptorCount = 1; + constantPoolDesc.shaderVisible = false; + m_constantPool = m_device->CreateDescriptorPool(constantPoolDesc); + if (m_constantPool == nullptr) { + DestroyResources(); + return false; + } + + m_constantSet = m_constantPool->AllocateSet(constantLayout); + if (m_constantSet == nullptr) { + DestroyResources(); + return false; + } + + m_pipelineState = m_device->CreatePipelineState( + BuildLinePipelineDesc(m_pipelineLayout, surface)); + if (m_pipelineState == nullptr || !m_pipelineState->IsValid()) { + DestroyResources(); + return false; + } + + return true; + } + + bool EnsureLineBufferCapacity(std::size_t requiredVertexCount) { + const std::uint64_t requiredBytes = + static_cast(requiredVertexCount * sizeof(OverlayLineVertex)); + if (requiredBytes == 0u) { + return true; + } + + if (m_lineVertexBuffer != nullptr && m_lineVertexBufferCapacity >= requiredBytes) { + return true; + } + + if (m_lineVertexBufferView != nullptr) { + m_lineVertexBufferView->Shutdown(); + delete m_lineVertexBufferView; + m_lineVertexBufferView = nullptr; + } + if (m_lineVertexBuffer != nullptr) { + m_lineVertexBuffer->Shutdown(); + delete m_lineVertexBuffer; + m_lineVertexBuffer = nullptr; + } + + m_lineVertexBufferCapacity = + (std::max)(requiredBytes, kMinDynamicVertexBufferBytes); + ::XCEngine::RHI::BufferDesc bufferDesc = {}; + bufferDesc.size = m_lineVertexBufferCapacity; + bufferDesc.stride = static_cast(sizeof(OverlayLineVertex)); + bufferDesc.bufferType = + static_cast(::XCEngine::RHI::BufferType::Vertex); + m_lineVertexBuffer = m_device->CreateBuffer(bufferDesc); + if (m_lineVertexBuffer == nullptr) { + m_lineVertexBufferCapacity = 0u; + return false; + } + + m_lineVertexBuffer->SetStride(bufferDesc.stride); + m_lineVertexBuffer->SetBufferType(::XCEngine::RHI::BufferType::Vertex); + + ::XCEngine::RHI::ResourceViewDesc viewDesc = {}; + viewDesc.dimension = ::XCEngine::RHI::ResourceViewDimension::Buffer; + viewDesc.structureByteStride = bufferDesc.stride; + m_lineVertexBufferView = + m_device->CreateVertexBufferView(m_lineVertexBuffer, viewDesc); + return m_lineVertexBufferView != nullptr; + } + + void DestroyResources() { + if (m_lineVertexBufferView != nullptr) { + m_lineVertexBufferView->Shutdown(); + delete m_lineVertexBufferView; + m_lineVertexBufferView = nullptr; + } + if (m_lineVertexBuffer != nullptr) { + m_lineVertexBuffer->Shutdown(); + delete m_lineVertexBuffer; + m_lineVertexBuffer = nullptr; + } + if (m_pipelineState != nullptr) { + m_pipelineState->Shutdown(); + delete m_pipelineState; + m_pipelineState = nullptr; + } + if (m_constantSet != nullptr) { + m_constantSet->Shutdown(); + delete m_constantSet; + m_constantSet = nullptr; + } + if (m_constantPool != nullptr) { + m_constantPool->Shutdown(); + delete m_constantPool; + m_constantPool = nullptr; + } + if (m_pipelineLayout != nullptr) { + m_pipelineLayout->Shutdown(); + delete m_pipelineLayout; + m_pipelineLayout = nullptr; + } + + m_lineVertexBufferCapacity = 0u; + m_renderTargetFormat = ::XCEngine::RHI::Format::Unknown; + m_depthStencilFormat = ::XCEngine::RHI::Format::Unknown; + m_renderTargetSampleCount = 1u; + m_device = nullptr; + m_backendType = ::XCEngine::RHI::RHIType::D3D12; + } + + ::XCEngine::RHI::RHIDevice* m_device = nullptr; + ::XCEngine::RHI::RHIType m_backendType = + ::XCEngine::RHI::RHIType::D3D12; + ::XCEngine::RHI::RHIPipelineLayout* m_pipelineLayout = nullptr; + ::XCEngine::RHI::RHIPipelineState* m_pipelineState = nullptr; + ::XCEngine::RHI::RHIDescriptorPool* m_constantPool = nullptr; + ::XCEngine::RHI::RHIDescriptorSet* m_constantSet = nullptr; + ::XCEngine::RHI::RHIBuffer* m_lineVertexBuffer = nullptr; + ::XCEngine::RHI::RHIResourceView* m_lineVertexBufferView = nullptr; + std::uint64_t m_lineVertexBufferCapacity = 0u; + ::XCEngine::RHI::Format m_renderTargetFormat = + ::XCEngine::RHI::Format::Unknown; + ::XCEngine::RHI::Format m_depthStencilFormat = + ::XCEngine::RHI::Format::Unknown; + std::uint32_t m_renderTargetSampleCount = 1u; +}; + +SceneViewportSelectedHelpersPassRenderer::SceneViewportSelectedHelpersPassRenderer() + : m_impl(std::make_unique()) { +} + +SceneViewportSelectedHelpersPassRenderer::~SceneViewportSelectedHelpersPassRenderer() = + default; + +void SceneViewportSelectedHelpersPassRenderer::Shutdown() { + m_impl->Shutdown(); +} + +bool SceneViewportSelectedHelpersPassRenderer::Render( + const ::XCEngine::Rendering::RenderPassContext& context, + const SceneViewportRenderRequest& request) { + return m_impl->Render(context, request); +} + +std::unique_ptr<::XCEngine::Rendering::RenderPass> +CreateSceneViewportSelectedHelpersPass( + SceneViewportSelectedHelpersPassRenderer& renderer, + const SceneViewportRenderRequest& request) { + return std::make_unique(renderer, request); +} + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Rendering/Viewport/Passes/SceneViewportSelectedHelpersPass.h b/new_editor/app/Rendering/Viewport/Passes/SceneViewportSelectedHelpersPass.h new file mode 100644 index 00000000..2e4f7095 --- /dev/null +++ b/new_editor/app/Rendering/Viewport/Passes/SceneViewportSelectedHelpersPass.h @@ -0,0 +1,40 @@ +#pragma once + +#include "Rendering/Viewport/SceneViewportRenderRequest.h" + +#include + +#include + +namespace XCEngine::UI::Editor::App { + +class SceneViewportSelectedHelpersPassRenderer { +public: + SceneViewportSelectedHelpersPassRenderer(); + ~SceneViewportSelectedHelpersPassRenderer(); + SceneViewportSelectedHelpersPassRenderer( + const SceneViewportSelectedHelpersPassRenderer&) = delete; + SceneViewportSelectedHelpersPassRenderer& operator=( + const SceneViewportSelectedHelpersPassRenderer&) = delete; + SceneViewportSelectedHelpersPassRenderer( + SceneViewportSelectedHelpersPassRenderer&&) = delete; + SceneViewportSelectedHelpersPassRenderer& operator=( + SceneViewportSelectedHelpersPassRenderer&&) = delete; + + void Shutdown(); + + bool Render( + const ::XCEngine::Rendering::RenderPassContext& context, + const SceneViewportRenderRequest& request); + +private: + class Impl; + std::unique_ptr m_impl = {}; +}; + +std::unique_ptr<::XCEngine::Rendering::RenderPass> +CreateSceneViewportSelectedHelpersPass( + SceneViewportSelectedHelpersPassRenderer& renderer, + const SceneViewportRenderRequest& request); + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Rendering/Viewport/Passes/SceneViewportSelectionOutlinePass.cpp b/new_editor/app/Rendering/Viewport/Passes/SceneViewportSelectionOutlinePass.cpp index da00feb9..e46cd4f1 100644 --- a/new_editor/app/Rendering/Viewport/Passes/SceneViewportSelectionOutlinePass.cpp +++ b/new_editor/app/Rendering/Viewport/Passes/SceneViewportSelectionOutlinePass.cpp @@ -2,8 +2,8 @@ #include "Rendering/Viewport/ViewportRenderTargets.h" -#include -#include +#include +#include #include #include @@ -105,7 +105,7 @@ const ::XCEngine::Resources::ShaderPass* FindSelectionOutlineCompatiblePass( const ::XCEngine::Resources::ShaderPass* outlinePass = shader.FindPass("SelectionOutline"); if (outlinePass != nullptr && - ::XCEngine::Rendering::Internal::ShaderPassHasGraphicsVariants( + ::XCEngine::Rendering::ShaderPassHasGraphicsVariants( shader, outlinePass->name, backend)) { @@ -115,7 +115,7 @@ const ::XCEngine::Resources::ShaderPass* FindSelectionOutlineCompatiblePass( const ::XCEngine::Resources::ShaderPass* editorOutlinePass = shader.FindPass("EditorSelectionOutline"); if (editorOutlinePass != nullptr && - ::XCEngine::Rendering::Internal::ShaderPassHasGraphicsVariants( + ::XCEngine::Rendering::ShaderPassHasGraphicsVariants( shader, editorOutlinePass->name, backend)) { @@ -123,7 +123,7 @@ const ::XCEngine::Resources::ShaderPass* FindSelectionOutlineCompatiblePass( } if (shader.GetPassCount() > 0 && - ::XCEngine::Rendering::Internal::ShaderPassHasGraphicsVariants( + ::XCEngine::Rendering::ShaderPassHasGraphicsVariants( shader, shader.GetPasses()[0].name, backend)) { @@ -144,7 +144,7 @@ const ::XCEngine::Resources::ShaderPass* FindSelectionOutlineCompatiblePass( pipelineDesc.topologyType = static_cast( ::XCEngine::RHI::PrimitiveTopologyType::Triangle); - ::XCEngine::Rendering::Internal:: + ::XCEngine::Rendering:: ApplySingleColorAttachmentPropertiesToGraphicsPipelineDesc( surface, pipelineDesc); @@ -186,14 +186,14 @@ const ::XCEngine::Resources::ShaderPass* FindSelectionOutlineCompatiblePass( } const ::XCEngine::Resources::ShaderBackend backend = - ::XCEngine::Rendering::Internal::ToShaderBackend(backendType); + ::XCEngine::Rendering::ToShaderBackend(backendType); if (const ::XCEngine::Resources::ShaderStageVariant* vertexVariant = shader.FindVariant( passName, ::XCEngine::Resources::ShaderType::Vertex, backend)) { if (shaderPass != nullptr) { - ::XCEngine::Rendering::Internal::ApplyShaderStageVariant( + ::XCEngine::Rendering::ApplyShaderStageVariant( shader.GetPath(), *shaderPass, backend, @@ -207,7 +207,7 @@ const ::XCEngine::Resources::ShaderPass* FindSelectionOutlineCompatiblePass( ::XCEngine::Resources::ShaderType::Fragment, backend)) { if (shaderPass != nullptr) { - ::XCEngine::Rendering::Internal::ApplyShaderStageVariant( + ::XCEngine::Rendering::ApplyShaderStageVariant( shader.GetPath(), *shaderPass, backend, @@ -331,7 +331,7 @@ private: const std::vector<::XCEngine::RHI::RHIResourceView*>& colorAttachments = surface.GetColorAttachments(); - if (!::XCEngine::Rendering::Internal::HasSingleColorAttachment(surface) || + if (!::XCEngine::Rendering::HasSingleColorAttachment(surface) || colorAttachments.empty() || colorAttachments[0] == nullptr) { return false; @@ -446,11 +446,11 @@ private: const ::XCEngine::Rendering::RenderContext& renderContext, const ::XCEngine::Rendering::RenderSurface& surface) { const ::XCEngine::RHI::Format renderTargetFormat = - ::XCEngine::Rendering::Internal::ResolveSurfaceColorFormat( + ::XCEngine::Rendering::ResolveSurfaceColorFormat( surface, 0u); const std::uint32_t renderTargetSampleCount = - ::XCEngine::Rendering::Internal::ResolveSurfaceSampleCount( + ::XCEngine::Rendering::ResolveSurfaceSampleCount( surface); if (m_pipelineLayout != nullptr && m_pipelineState != nullptr && @@ -478,8 +478,8 @@ private: return false; } - if (!::XCEngine::Rendering::Internal::HasSingleColorAttachment(surface) || - ::XCEngine::Rendering::Internal::ResolveSurfaceColorFormat( + if (!::XCEngine::Rendering::HasSingleColorAttachment(surface) || + ::XCEngine::Rendering::ResolveSurfaceColorFormat( surface, 0u) == ::XCEngine::RHI::Format::Unknown) { return false; @@ -509,7 +509,7 @@ private: m_shader.emplace(std::move(shader)); const ::XCEngine::Resources::ShaderBackend backend = - ::XCEngine::Rendering::Internal::ToShaderBackend(m_backendType); + ::XCEngine::Rendering::ToShaderBackend(m_backendType); const ::XCEngine::Resources::ShaderPass* outlinePass = FindSelectionOutlineCompatiblePass(*m_shader->Get(), backend); if (outlinePass == nullptr) { @@ -605,11 +605,11 @@ private: } m_renderTargetFormat = - ::XCEngine::Rendering::Internal::ResolveSurfaceColorFormat( + ::XCEngine::Rendering::ResolveSurfaceColorFormat( surface, 0u); m_renderTargetSampleCount = - ::XCEngine::Rendering::Internal::ResolveSurfaceSampleCount( + ::XCEngine::Rendering::ResolveSurfaceSampleCount( surface); return true; } diff --git a/new_editor/app/Rendering/Viewport/SceneViewportRenderPassBundle.cpp b/new_editor/app/Rendering/Viewport/SceneViewportRenderPassBundle.cpp index 26382a2d..818b60fa 100644 --- a/new_editor/app/Rendering/Viewport/SceneViewportRenderPassBundle.cpp +++ b/new_editor/app/Rendering/Viewport/SceneViewportRenderPassBundle.cpp @@ -5,6 +5,7 @@ namespace XCEngine::UI::Editor::App { void SceneViewportRenderPassBundle::Shutdown() { m_gridRenderer.Shutdown(); m_selectionOutlineRenderer.Shutdown(); + m_selectedHelpersRenderer.Shutdown(); } SceneViewportRenderPlanBuildResult @@ -26,6 +27,11 @@ SceneViewportRenderPassBundle::BuildRenderPlan( outlineTargets, selectedObjectIds, style); + }, + [this](const SceneViewportRenderRequest& requestData) { + return CreateSceneViewportSelectedHelpersPass( + m_selectedHelpersRenderer, + requestData); }); } diff --git a/new_editor/app/Rendering/Viewport/SceneViewportRenderPassBundle.h b/new_editor/app/Rendering/Viewport/SceneViewportRenderPassBundle.h index 68caae5a..6047fe9c 100644 --- a/new_editor/app/Rendering/Viewport/SceneViewportRenderPassBundle.h +++ b/new_editor/app/Rendering/Viewport/SceneViewportRenderPassBundle.h @@ -2,6 +2,7 @@ #include "Rendering/Viewport/Passes/SceneViewportGridPass.h" #include "Rendering/Viewport/Passes/SceneViewportSelectionOutlinePass.h" +#include "Rendering/Viewport/Passes/SceneViewportSelectedHelpersPass.h" #include "Rendering/Viewport/SceneViewportRenderPlan.h" namespace XCEngine::UI::Editor::App { @@ -19,6 +20,7 @@ public: private: SceneViewportGridPassRenderer m_gridRenderer = {}; SceneViewportSelectionOutlinePassRenderer m_selectionOutlineRenderer = {}; + SceneViewportSelectedHelpersPassRenderer m_selectedHelpersRenderer = {}; }; } // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Rendering/Viewport/SceneViewportRenderPlan.h b/new_editor/app/Rendering/Viewport/SceneViewportRenderPlan.h index 6bfd9d7f..53636aa4 100644 --- a/new_editor/app/Rendering/Viewport/SceneViewportRenderPlan.h +++ b/new_editor/app/Rendering/Viewport/SceneViewportRenderPlan.h @@ -37,6 +37,9 @@ using SceneViewportSelectionOutlinePassFactory = std::function< ViewportRenderTargets*, const std::vector&, const SceneViewportSelectionOutlineStyle&)>; +using SceneViewportSelectedHelpersPassFactory = std::function< + std::unique_ptr<::XCEngine::Rendering::RenderPass>( + const SceneViewportRenderRequest&)>; struct SceneViewportRenderPlanBuildResult { SceneViewportRenderPlan plan = {}; @@ -84,7 +87,8 @@ inline SceneViewportRenderPlanBuildResult BuildSceneViewportRenderPlan( ViewportRenderTargets& targets, const SceneViewportRenderRequest& request, const SceneViewportGridPassFactory& gridPassFactory, - const SceneViewportSelectionOutlinePassFactory& selectionOutlinePassFactory) { + const SceneViewportSelectionOutlinePassFactory& selectionOutlinePassFactory, + const SceneViewportSelectedHelpersPassFactory& selectedHelpersPassFactory) { SceneViewportRenderPlanBuildResult result = {}; const SceneViewportGridPassData gridPassData = BuildSceneViewportGridPassData(request); @@ -101,24 +105,43 @@ inline SceneViewportRenderPlanBuildResult BuildSceneViewportRenderPlan( return result; } - if (!CanRenderSceneViewportSelectionOutline(targets) || - selectionOutlinePassFactory == nullptr) { + if (selectionOutlinePassFactory == nullptr) { + if (selectedHelpersPassFactory == nullptr) { + result.warningStatusText = + "Scene selection helper passes are unavailable"; + } + } else if (!CanRenderSceneViewportSelectionOutline(targets)) { result.warningStatusText = "Scene selection outline resources are unavailable"; + } else { + std::unique_ptr<::XCEngine::Rendering::RenderPass> selectionOutlinePass = + selectionOutlinePassFactory( + &targets, + request.selectedObjectIds, + BuildSceneViewportSelectionOutlineStyle( + request.debugSelectionMask)); + if (selectionOutlinePass != nullptr) { + result.plan.postScenePasses.AddPass(std::move(selectionOutlinePass)); + result.plan.usesSelectionOutline = true; + } else { + result.warningStatusText = "Scene selection outline pass creation failed"; + } + } + + if (selectedHelpersPassFactory == nullptr) { return result; } - std::unique_ptr<::XCEngine::Rendering::RenderPass> selectionOutlinePass = - selectionOutlinePassFactory( - &targets, - request.selectedObjectIds, - BuildSceneViewportSelectionOutlineStyle(request.debugSelectionMask)); - if (selectionOutlinePass != nullptr) { - result.plan.postScenePasses.AddPass(std::move(selectionOutlinePass)); - result.plan.usesSelectionOutline = true; + std::unique_ptr<::XCEngine::Rendering::RenderPass> selectedHelpersPass = + selectedHelpersPassFactory(request); + if (selectedHelpersPass != nullptr) { + result.plan.postScenePasses.AddPass(std::move(selectedHelpersPass)); return result; } - result.warningStatusText = "Scene selection outline pass creation failed"; + if (result.warningStatusText == nullptr) { + result.warningStatusText = + "Scene selected helper pass creation failed"; + } return result; } diff --git a/new_editor/app/Rendering/Viewport/SceneViewportResourcePaths.h b/new_editor/app/Rendering/Viewport/SceneViewportResourcePaths.h index 002801a0..841f6c17 100644 --- a/new_editor/app/Rendering/Viewport/SceneViewportResourcePaths.h +++ b/new_editor/app/Rendering/Viewport/SceneViewportResourcePaths.h @@ -6,8 +6,6 @@ namespace XCEngine::UI::Editor::App { -namespace Internal { - inline ::XCEngine::Containers::String NormalizeSceneViewportResourcePath( const std::filesystem::path& path) { return ::XCEngine::Containers::String( @@ -27,19 +25,17 @@ inline std::filesystem::path GetSceneViewportResourceRepoRootPath() { #endif } -inline ::XCEngine::Containers::String BuildSceneViewportEditorResourcePath( +inline ::XCEngine::Containers::String BuildSceneViewportResourcePath( const std::filesystem::path& relativePath) { return NormalizeSceneViewportResourcePath( GetSceneViewportResourceRepoRootPath() / - "editor" / + "new_editor" / "resources" / relativePath); } -} // namespace Internal - inline ::XCEngine::Containers::String GetSceneViewportInfiniteGridShaderPath() { - return Internal::BuildSceneViewportEditorResourcePath( + return BuildSceneViewportResourcePath( std::filesystem::path("shaders") / "scene-viewport" / "infinite-grid" / diff --git a/new_editor/app/Rendering/Viewport/ViewportRenderTargetInternal.cpp b/new_editor/app/Rendering/Viewport/ViewportRenderTargetUtils.cpp similarity index 98% rename from new_editor/app/Rendering/Viewport/ViewportRenderTargetInternal.cpp rename to new_editor/app/Rendering/Viewport/ViewportRenderTargetUtils.cpp index 31811a69..48043f2c 100644 --- a/new_editor/app/Rendering/Viewport/ViewportRenderTargetInternal.cpp +++ b/new_editor/app/Rendering/Viewport/ViewportRenderTargetUtils.cpp @@ -1,4 +1,4 @@ -#include "Rendering/Viewport/ViewportRenderTargetInternal.h" +#include "Rendering/Viewport/ViewportRenderTargetUtils.h" #include #include diff --git a/new_editor/app/Rendering/Viewport/ViewportRenderTargetInternal.h b/new_editor/app/Rendering/Viewport/ViewportRenderTargetUtils.h similarity index 100% rename from new_editor/app/Rendering/Viewport/ViewportRenderTargetInternal.h rename to new_editor/app/Rendering/Viewport/ViewportRenderTargetUtils.h diff --git a/new_editor/app/Rendering/Viewport/ViewportRenderTargets.h b/new_editor/app/Rendering/Viewport/ViewportRenderTargets.h index 43bd3308..e69ba122 100644 --- a/new_editor/app/Rendering/Viewport/ViewportRenderTargets.h +++ b/new_editor/app/Rendering/Viewport/ViewportRenderTargets.h @@ -1,7 +1,7 @@ #pragma once #include "Ports/PortFwd.h" -#include "ViewportRenderTargetInternal.h" +#include "ViewportRenderTargetUtils.h" #include #include diff --git a/new_editor/app/Scene/EditorSceneRuntime.cpp b/new_editor/app/Scene/EditorSceneRuntime.cpp index c6fd777f..62663223 100644 --- a/new_editor/app/Scene/EditorSceneRuntime.cpp +++ b/new_editor/app/Scene/EditorSceneRuntime.cpp @@ -174,6 +174,7 @@ bool EditorSceneRuntime::Initialize(const std::filesystem::path& projectRoot) { m_projectRoot = projectRoot; m_ownedSelectionService = {}; m_selectionService = &m_ownedSelectionService; + m_inspectorRevision = 0u; m_startupSceneResult = EnsureEditorStartupScene(projectRoot); EnsureSceneViewCamera(); ResetTransformEditHistory(); @@ -334,6 +335,10 @@ std::uint64_t EditorSceneRuntime::GetSelectionStamp() const { return SelectionService().GetStamp(); } +std::uint64_t EditorSceneRuntime::GetInspectorRevision() const { + return m_inspectorRevision; +} + bool EditorSceneRuntime::SetSelection(std::string_view itemId) { const std::optional gameObjectId = ParseEditorGameObjectItemId(itemId); @@ -393,6 +398,7 @@ bool EditorSceneRuntime::OpenSceneAsset(const std::filesystem::path& scenePath) ResetTransformEditHistory(); ResetToolInteractionState(); SelectionService().ClearSelection(); + IncrementInspectorRevision(); RefreshScene(); EnsureSceneSelection(); return true; @@ -406,6 +412,9 @@ bool EditorSceneRuntime::RenameGameObject( std::string_view itemId, std::string_view newName) { const bool renamed = RenameEditorGameObject(itemId, newName); + if (renamed) { + IncrementInspectorRevision(); + } RefreshScene(); return renamed; } @@ -413,6 +422,9 @@ bool EditorSceneRuntime::RenameGameObject( bool EditorSceneRuntime::DeleteGameObject(std::string_view itemId) { ResetTransformEditHistory(); const bool deleted = DeleteEditorGameObject(itemId); + if (deleted) { + IncrementInspectorRevision(); + } RefreshScene(); EnsureSceneSelection(); return deleted; @@ -422,6 +434,7 @@ std::string EditorSceneRuntime::DuplicateGameObject(std::string_view itemId) { ResetTransformEditHistory(); const std::string duplicatedItemId = DuplicateEditorGameObject(itemId); if (!duplicatedItemId.empty()) { + IncrementInspectorRevision(); SetSelection(duplicatedItemId); } else { RefreshScene(); @@ -435,6 +448,9 @@ bool EditorSceneRuntime::ReparentGameObject( ResetTransformEditHistory(); const bool reparented = ReparentEditorGameObject(itemId, parentItemId); + if (reparented) { + IncrementInspectorRevision(); + } RefreshScene(); return reparented; } @@ -442,6 +458,9 @@ bool EditorSceneRuntime::ReparentGameObject( bool EditorSceneRuntime::MoveGameObjectToRoot(std::string_view itemId) { ResetTransformEditHistory(); const bool moved = MoveEditorGameObjectToRoot(itemId); + if (moved) { + IncrementInspectorRevision(); + } RefreshScene(); return moved; } @@ -475,6 +494,9 @@ bool EditorSceneRuntime::RemoveSelectedComponent(std::string_view componentId) { ResetTransformEditHistory(); const bool removed = gameObject->RemoveComponent(component); + if (removed) { + IncrementInspectorRevision(); + } RefreshScene(); return removed; } @@ -500,7 +522,10 @@ bool EditorSceneRuntime::SetSelectedTransformLocalPosition( SceneTransformSnapshot afterSnapshot = {}; CaptureSelectedTransformSnapshot(afterSnapshot); - RecordTransformEdit(beforeSnapshot, afterSnapshot); + if (!TransformSnapshotsMatch(beforeSnapshot, afterSnapshot)) { + RecordTransformEdit(beforeSnapshot, afterSnapshot); + IncrementInspectorRevision(); + } return true; } @@ -525,7 +550,10 @@ bool EditorSceneRuntime::SetSelectedTransformLocalEulerAngles( SceneTransformSnapshot afterSnapshot = {}; CaptureSelectedTransformSnapshot(afterSnapshot); - RecordTransformEdit(beforeSnapshot, afterSnapshot); + if (!TransformSnapshotsMatch(beforeSnapshot, afterSnapshot)) { + RecordTransformEdit(beforeSnapshot, afterSnapshot); + IncrementInspectorRevision(); + } return true; } @@ -550,7 +578,10 @@ bool EditorSceneRuntime::SetSelectedTransformLocalScale( SceneTransformSnapshot afterSnapshot = {}; CaptureSelectedTransformSnapshot(afterSnapshot); - RecordTransformEdit(beforeSnapshot, afterSnapshot); + if (!TransformSnapshotsMatch(beforeSnapshot, afterSnapshot)) { + RecordTransformEdit(beforeSnapshot, afterSnapshot); + IncrementInspectorRevision(); + } return true; } @@ -568,7 +599,12 @@ bool EditorSceneRuntime::ApplySelectedComponentMutation( return false; } - return mutation(*component); + if (!mutation(*component)) { + return false; + } + + IncrementInspectorRevision(); + return true; } const SceneToolState& EditorSceneRuntime::GetToolState() const { @@ -682,6 +718,7 @@ bool EditorSceneRuntime::ApplyTransformSnapshot( transform->SetPosition(snapshot.position); transform->SetRotation(snapshot.rotation); transform->SetScale(snapshot.scale); + IncrementInspectorRevision(); return true; } @@ -939,6 +976,10 @@ void EditorSceneRuntime::ResetTransformEditHistory() { m_transformRedoStack.clear(); } +void EditorSceneRuntime::IncrementInspectorRevision() { + ++m_inspectorRevision; +} + void EditorSceneRuntime::ResetToolInteractionTransientState() { m_toolState.hoveredHandle = SceneToolHandle::None; m_toolState.activeHandle = SceneToolHandle::None; diff --git a/new_editor/app/Scene/EditorSceneRuntime.h b/new_editor/app/Scene/EditorSceneRuntime.h index bde537da..abecd66f 100644 --- a/new_editor/app/Scene/EditorSceneRuntime.h +++ b/new_editor/app/Scene/EditorSceneRuntime.h @@ -70,6 +70,7 @@ public: const ::XCEngine::Components::GameObject* GetSelectedGameObject() const; std::vector GetSelectedComponents() const; std::uint64_t GetSelectionStamp() const; + std::uint64_t GetInspectorRevision() const; bool SetSelection(std::string_view itemId); bool SetSelection(::XCEngine::Components::GameObject::ID id); @@ -157,6 +158,7 @@ private: void ResetTransformEditHistory(); void ResetToolInteractionTransientState(); void ClearInvalidToolInteractionState(); + void IncrementInspectorRevision(); std::filesystem::path m_projectRoot = {}; EditorStartupSceneResult m_startupSceneResult = {}; @@ -166,6 +168,7 @@ private: SceneToolState m_toolState = {}; std::vector m_transformUndoStack = {}; std::vector m_transformRedoStack = {}; + std::uint64_t m_inspectorRevision = 0u; }; } // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/State/EditorColorPickerToolState.cpp b/new_editor/app/State/EditorColorPickerToolState.cpp new file mode 100644 index 00000000..a09bb904 --- /dev/null +++ b/new_editor/app/State/EditorColorPickerToolState.cpp @@ -0,0 +1,67 @@ +#include "State/EditorColorPickerToolState.h" + +#include + +namespace XCEngine::UI::Editor::App { + +namespace { + +bool AreColorsEqual( + const ::XCEngine::UI::UIColor& lhs, + const ::XCEngine::UI::UIColor& rhs) { + return lhs.r == rhs.r && + lhs.g == rhs.g && + lhs.b == rhs.b && + lhs.a == rhs.a; +} + +} // namespace + +void ResetEditorColorPickerToolState(EditorColorPickerToolState& state) { + state = {}; +} + +void OpenEditorColorPickerToolForInspectorField( + EditorColorPickerToolState& state, + std::string_view subjectKey, + std::string_view fieldId, + const ::XCEngine::UI::UIColor& color, + bool showAlpha) { + state.active = true; + state.showAlpha = showAlpha; + state.requestOpenDetachedPanel = true; + state.color = color; + state.inspectorTarget.subjectKey = std::string(subjectKey); + state.inspectorTarget.fieldId = std::string(fieldId); + ++state.revision; +} + +bool ConsumeEditorColorPickerOpenDetachedPanelRequest( + EditorColorPickerToolState& state) { + const bool requested = state.requestOpenDetachedPanel; + state.requestOpenDetachedPanel = false; + return requested; +} + +bool UpdateEditorColorPickerToolColor( + EditorColorPickerToolState& state, + const ::XCEngine::UI::UIColor& color) { + if (!state.active || AreColorsEqual(state.color, color)) { + return false; + } + + state.color = color; + ++state.revision; + return true; +} + +bool IsEditorColorPickerToolTarget( + const EditorColorPickerToolState& state, + std::string_view subjectKey, + std::string_view fieldId) { + return state.active && + state.inspectorTarget.subjectKey == subjectKey && + state.inspectorTarget.fieldId == fieldId; +} + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/State/EditorColorPickerToolState.h b/new_editor/app/State/EditorColorPickerToolState.h new file mode 100644 index 00000000..07eb93cf --- /dev/null +++ b/new_editor/app/State/EditorColorPickerToolState.h @@ -0,0 +1,42 @@ +#pragma once + +#include + +#include +#include +#include + +namespace XCEngine::UI::Editor::App { + +struct EditorColorPickerInspectorTarget { + std::string subjectKey = {}; + std::string fieldId = {}; +}; + +struct EditorColorPickerToolState { + bool active = false; + bool showAlpha = true; + bool requestOpenDetachedPanel = false; + ::XCEngine::UI::UIColor color = {}; + EditorColorPickerInspectorTarget inspectorTarget = {}; + std::uint64_t revision = 0u; +}; + +void ResetEditorColorPickerToolState(EditorColorPickerToolState& state); +void OpenEditorColorPickerToolForInspectorField( + EditorColorPickerToolState& state, + std::string_view subjectKey, + std::string_view fieldId, + const ::XCEngine::UI::UIColor& color, + bool showAlpha); +bool ConsumeEditorColorPickerOpenDetachedPanelRequest( + EditorColorPickerToolState& state); +bool UpdateEditorColorPickerToolColor( + EditorColorPickerToolState& state, + const ::XCEngine::UI::UIColor& color); +bool IsEditorColorPickerToolTarget( + const EditorColorPickerToolState& state, + std::string_view subjectKey, + std::string_view fieldId); + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Internal/EmbeddedPngLoader.cpp b/new_editor/app/Support/EmbeddedPngLoader.cpp similarity index 92% rename from new_editor/app/Internal/EmbeddedPngLoader.cpp rename to new_editor/app/Support/EmbeddedPngLoader.cpp index e11596f8..8dbcaa91 100644 --- a/new_editor/app/Internal/EmbeddedPngLoader.cpp +++ b/new_editor/app/Support/EmbeddedPngLoader.cpp @@ -1,8 +1,8 @@ -#include "Internal/EmbeddedPngLoader.h" +#include "Support/EmbeddedPngLoader.h" #include "Ports/TexturePort.h" -namespace XCEngine::UI::Editor::App::Internal { +namespace XCEngine::UI::Editor::App { bool LoadEmbeddedPngBytes( UINT resourceId, @@ -62,4 +62,4 @@ bool LoadEmbeddedPngTexture( return renderer.LoadTextureFromMemory(bytes, byteCount, outTexture, outError); } -} // namespace XCEngine::UI::Editor::App::Internal +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Internal/EmbeddedPngLoader.h b/new_editor/app/Support/EmbeddedPngLoader.h similarity index 83% rename from new_editor/app/Internal/EmbeddedPngLoader.h rename to new_editor/app/Support/EmbeddedPngLoader.h index 5dc374c7..34422e9b 100644 --- a/new_editor/app/Internal/EmbeddedPngLoader.h +++ b/new_editor/app/Support/EmbeddedPngLoader.h @@ -14,7 +14,7 @@ #include -namespace XCEngine::UI::Editor::App::Internal { +namespace XCEngine::UI::Editor::App { bool LoadEmbeddedPngBytes( UINT resourceId, @@ -28,4 +28,4 @@ bool LoadEmbeddedPngTexture( ::XCEngine::UI::UITextureHandle& outTexture, std::string& outError); -} // namespace XCEngine::UI::Editor::App::Internal +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Internal/EnvironmentFlags.h b/new_editor/app/Support/EnvironmentFlags.h similarity index 92% rename from new_editor/app/Internal/EnvironmentFlags.h rename to new_editor/app/Support/EnvironmentFlags.h index cb9c083c..d477c9d9 100644 --- a/new_editor/app/Internal/EnvironmentFlags.h +++ b/new_editor/app/Support/EnvironmentFlags.h @@ -6,7 +6,7 @@ #include #include -namespace XCEngine::UI::Editor::App::Internal { +namespace XCEngine::UI::Editor::App { inline bool IsEnvironmentFlagEnabled(const char* envName) { if (envName == nullptr || envName[0] == '\0') { @@ -51,4 +51,4 @@ inline std::optional TryGetEnvironmentInt(const char* envName) { return static_cast(parsed); } -} // namespace XCEngine::UI::Editor::App::Internal +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Internal/ExecutablePath.h b/new_editor/app/Support/ExecutablePath.h similarity index 87% rename from new_editor/app/Internal/ExecutablePath.h rename to new_editor/app/Support/ExecutablePath.h index be0244d5..fafd11bb 100644 --- a/new_editor/app/Internal/ExecutablePath.h +++ b/new_editor/app/Support/ExecutablePath.h @@ -5,7 +5,7 @@ #include -namespace XCEngine::UI::Editor::App::Internal { +namespace XCEngine::UI::Editor::App { inline std::filesystem::path GetExecutableDirectory() { std::vector buffer(MAX_PATH); @@ -28,4 +28,4 @@ inline std::filesystem::path GetExecutableDirectory() { } } -} // namespace XCEngine::UI::Editor::App::Internal +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Internal/StringEncoding.h b/new_editor/app/Support/StringEncoding.h similarity index 94% rename from new_editor/app/Internal/StringEncoding.h rename to new_editor/app/Support/StringEncoding.h index b1628e66..de2ab2c7 100644 --- a/new_editor/app/Internal/StringEncoding.h +++ b/new_editor/app/Support/StringEncoding.h @@ -5,7 +5,7 @@ #include -namespace XCEngine::UI::Editor::App::Internal { +namespace XCEngine::UI::Editor::App { inline std::wstring Utf8ToWide(std::string_view text) { if (text.empty()) { @@ -75,4 +75,4 @@ inline std::string WideToUtf8(std::wstring_view text) { return utf8; } -} // namespace XCEngine::UI::Editor::App::Internal +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Internal/TextFormat.h b/new_editor/app/Support/TextFormat.h similarity index 75% rename from new_editor/app/Internal/TextFormat.h rename to new_editor/app/Support/TextFormat.h index 4c191f43..6ec34ca4 100644 --- a/new_editor/app/Internal/TextFormat.h +++ b/new_editor/app/Support/TextFormat.h @@ -2,7 +2,7 @@ #include -namespace XCEngine::UI::Editor::App::Internal { +namespace XCEngine::UI::Editor::App { inline std::string TruncateText(const std::string& text, std::size_t maxLength) { if (text.size() <= maxLength) { @@ -16,4 +16,4 @@ inline std::string TruncateText(const std::string& text, std::size_t maxLength) return text.substr(0u, maxLength - 3u) + "..."; } -} // namespace XCEngine::UI::Editor::App::Internal +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/include/XCEditor/Docking/UIEditorDockHostTransfer.h b/new_editor/include/XCEditor/Docking/UIEditorDockHostTransfer.h new file mode 100644 index 00000000..e79c8d74 --- /dev/null +++ b/new_editor/include/XCEditor/Docking/UIEditorDockHostTransfer.h @@ -0,0 +1,30 @@ +#pragma once + +#include + +#include + +#include +#include + +namespace XCEngine::UI::Editor { + +struct UIEditorDockHostTabDropTarget { + bool valid = false; + std::string nodeId = {}; + UIEditorWorkspaceDockPlacement placement = UIEditorWorkspaceDockPlacement::Center; + std::size_t insertionIndex = Widgets::UIEditorTabStripInvalidIndex; +}; + +bool TryResolveUIEditorDockHostTabDragHotspot( + const Widgets::UIEditorDockHostLayout& layout, + std::string_view nodeId, + std::string_view panelId, + const ::XCEngine::UI::UIPoint& point, + ::XCEngine::UI::UIPoint& outHotspot); + +UIEditorDockHostTabDropTarget ResolveUIEditorDockHostTabDropTarget( + const Widgets::UIEditorDockHostLayout& layout, + const ::XCEngine::UI::UIPoint& point); + +} // namespace XCEngine::UI::Editor diff --git a/new_editor/include/XCEditor/Fields/UIEditorColorField.h b/new_editor/include/XCEditor/Fields/UIEditorColorField.h index 08979e54..cd7ec6f6 100644 --- a/new_editor/include/XCEditor/Fields/UIEditorColorField.h +++ b/new_editor/include/XCEditor/Fields/UIEditorColorField.h @@ -27,6 +27,7 @@ struct UIEditorColorFieldSpec { ::XCEngine::UI::UIColor value = {}; bool showAlpha = true; bool readOnly = false; + bool embeddedEditor = false; }; struct UIEditorColorFieldState { @@ -140,6 +141,7 @@ struct UIEditorColorFieldPalette { struct UIEditorColorFieldLayout { ::XCEngine::UI::UIRect bounds = {}; + bool embeddedEditor = false; ::XCEngine::UI::UIRect labelRect = {}; ::XCEngine::UI::UIRect controlRect = {}; ::XCEngine::UI::UIRect swatchRect = {}; diff --git a/new_editor/include/XCEditor/Fields/UIEditorEditableFieldCore.h b/new_editor/include/XCEditor/Fields/UIEditorEditableFieldCore.h new file mode 100644 index 00000000..04d084cd --- /dev/null +++ b/new_editor/include/XCEditor/Fields/UIEditorEditableFieldCore.h @@ -0,0 +1,107 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include + +namespace XCEngine::UI::Editor { + +inline constexpr std::size_t UIEditorEditableFieldInvalidComponentIndex = + static_cast(-1); +inline constexpr std::uint64_t + UIEditorEditableFieldDoubleClickIntervalNanoseconds = 500000000ull; +inline constexpr float UIEditorEditableFieldDoubleClickDistanceSquared = 16.0f; +inline constexpr float UIEditorEditableFieldDragActivationDistanceSquared = 9.0f; + +struct UIEditorEditableFieldSession { + std::string activeFieldId = {}; + std::string lastClickFieldId = {}; + ::XCEngine::UI::Text::UITextInputState textInputState = {}; + ::XCEngine::UI::UIPoint pointerPosition = {}; + ::XCEngine::UI::UIPoint pointerDownPosition = {}; + ::XCEngine::UI::UIPoint lastClickPosition = {}; + std::size_t activeComponentIndex = + UIEditorEditableFieldInvalidComponentIndex; + std::size_t lastClickComponentIndex = + UIEditorEditableFieldInvalidComponentIndex; + std::uint64_t lastClickTimestampNanoseconds = 0u; + std::uint64_t caretBlinkStartNanoseconds = 0u; + double dragStartValue = 0.0; + bool focused = false; + bool editing = false; + bool dragArmed = false; + bool dragActive = false; + bool hasPointerPosition = false; +}; + +bool ShouldUseUIEditorEditableFieldPointerPosition( + const ::XCEngine::UI::UIInputEvent& event); + +void UpdateUIEditorEditableFieldPointerPosition( + UIEditorEditableFieldSession& session, + const ::XCEngine::UI::UIInputEvent& event); + +float ComputeUIEditorEditableFieldDistanceSquared( + const ::XCEngine::UI::UIPoint& a, + const ::XCEngine::UI::UIPoint& b); + +void RestartUIEditorEditableFieldCaretBlink( + UIEditorEditableFieldSession& session); + +bool BeginUIEditorEditableFieldEdit( + UIEditorEditableFieldSession& session, + std::string_view fieldId, + std::size_t componentIndex, + std::string_view baselineText, + bool clearText); + +void CommitUIEditorEditableFieldEdit( + UIEditorEditableFieldSession& session); + +void CancelUIEditorEditableFieldEdit( + UIEditorEditableFieldSession& session); + +void ArmUIEditorEditableFieldDrag( + UIEditorEditableFieldSession& session, + std::string_view fieldId, + std::size_t componentIndex, + double startValue); + +bool TryActivateUIEditorEditableFieldDrag( + UIEditorEditableFieldSession& session, + const ::XCEngine::UI::UIInputEvent& event, + float activationDistanceSquared = + UIEditorEditableFieldDragActivationDistanceSquared); + +double ResolveUIEditorEditableFieldDragDelta( + const UIEditorEditableFieldSession& session, + double sensitivity); + +void EndUIEditorEditableFieldDrag(UIEditorEditableFieldSession& session); + +bool IsUIEditorEditableFieldDoubleClick( + const UIEditorEditableFieldSession& session, + std::string_view fieldId, + const ::XCEngine::UI::UIInputEvent& event, + std::size_t componentIndex); + +void RecordUIEditorEditableFieldClick( + UIEditorEditableFieldSession& session, + std::string_view fieldId, + const ::XCEngine::UI::UIInputEvent& event, + std::size_t componentIndex); + +::XCEngine::UI::Text::UITextInputEditResult HandleUIEditorEditableFieldKeyDown( + UIEditorEditableFieldSession& session, + std::int32_t keyCode, + const ::XCEngine::UI::UIInputModifiers& modifiers); + +bool InsertUIEditorEditableFieldCharacter( + UIEditorEditableFieldSession& session, + std::uint32_t character); + +} // namespace XCEngine::UI::Editor diff --git a/new_editor/include/XCEditor/Fields/UIEditorNumberField.h b/new_editor/include/XCEditor/Fields/UIEditorNumberField.h index 6e9c1742..814da810 100644 --- a/new_editor/include/XCEditor/Fields/UIEditorNumberField.h +++ b/new_editor/include/XCEditor/Fields/UIEditorNumberField.h @@ -31,6 +31,8 @@ struct UIEditorNumberFieldState { UIEditorNumberFieldHitTargetKind activeTarget = UIEditorNumberFieldHitTargetKind::None; bool focused = false; bool editing = false; + std::size_t caretOffset = 0u; + std::uint64_t caretBlinkStartNanoseconds = 0u; std::string displayText = {}; }; diff --git a/new_editor/include/XCEditor/Fields/UIEditorNumberFieldInteraction.h b/new_editor/include/XCEditor/Fields/UIEditorNumberFieldInteraction.h index 2063d1cf..ba1499c4 100644 --- a/new_editor/include/XCEditor/Fields/UIEditorNumberFieldInteraction.h +++ b/new_editor/include/XCEditor/Fields/UIEditorNumberFieldInteraction.h @@ -1,22 +1,16 @@ #pragma once +#include #include -#include -#include -#include - #include #include namespace XCEngine::UI::Editor { struct UIEditorNumberFieldInteractionState { + UIEditorEditableFieldSession session = {}; Widgets::UIEditorNumberFieldState numberFieldState = {}; - ::XCEngine::UI::Text::UITextInputState textInputState = {}; - ::XCEngine::UI::Widgets::UIPropertyEditModel editModel = {}; - ::XCEngine::UI::UIPoint pointerPosition = {}; - bool hasPointerPosition = false; }; struct UIEditorNumberFieldInteractionResult { diff --git a/new_editor/include/XCEditor/Fields/UIEditorPropertyGrid.h b/new_editor/include/XCEditor/Fields/UIEditorPropertyGrid.h index e1259754..f78e3158 100644 --- a/new_editor/include/XCEditor/Fields/UIEditorPropertyGrid.h +++ b/new_editor/include/XCEditor/Fields/UIEditorPropertyGrid.h @@ -7,6 +7,8 @@ #include #include +#include +#include #include #include #include @@ -141,9 +143,14 @@ struct UIEditorPropertyGridSection { float desiredHeaderHeight = 0.0f; }; -struct UIEditorPropertyGridColorFieldVisualState { +struct UIEditorPropertyGridNumberFieldVisualState { std::string fieldId = {}; - UIEditorColorFieldState state = {}; + UIEditorNumberFieldState state = {}; +}; + +struct UIEditorPropertyGridTextFieldVisualState { + std::string fieldId = {}; + UIEditorTextFieldState state = {}; }; struct UIEditorPropertyGridAssetFieldVisualState { @@ -174,8 +181,9 @@ struct UIEditorPropertyGridState { std::string pressedFieldId = {}; std::string popupFieldId = {}; std::size_t popupHighlightedIndex = UIEditorPropertyGridInvalidIndex; + std::vector numberFieldStates = {}; + std::vector textFieldStates = {}; std::vector assetFieldStates = {}; - std::vector colorFieldStates = {}; std::vector vector2FieldStates = {}; std::vector vector3FieldStates = {}; std::vector vector4FieldStates = {}; @@ -188,6 +196,7 @@ struct UIEditorPropertyGridMetrics { float fieldRowHeight = 32.0f; float rowGap = 2.0f; float horizontalPadding = 12.0f; + float sectionHeaderHorizontalPadding = 6.0f; float controlColumnStart = 236.0f; float labelControlGap = 20.0f; float disclosureExtent = 12.0f; @@ -320,6 +329,7 @@ void AppendUIEditorPropertyGridForeground( ::XCEngine::UI::UIDrawList& drawList, const UIEditorPropertyGridLayout& layout, const std::vector& sections, + const ::XCEngine::UI::Widgets::UISelectionModel& selectionModel, const UIEditorPropertyGridState& state, const ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel, const UIEditorPropertyGridPalette& palette = {}, diff --git a/new_editor/include/XCEditor/Fields/UIEditorPropertyGridInteraction.h b/new_editor/include/XCEditor/Fields/UIEditorPropertyGridInteraction.h index 072e9eaf..059b88a3 100644 --- a/new_editor/include/XCEditor/Fields/UIEditorPropertyGridInteraction.h +++ b/new_editor/include/XCEditor/Fields/UIEditorPropertyGridInteraction.h @@ -1,7 +1,10 @@ #pragma once +#include #include +#include #include +#include #include #include #include @@ -23,32 +26,14 @@ struct UIEditorPropertyGridAssetFieldInteractionEntry { UIEditorAssetFieldInteractionState state = {}; }; -struct UIEditorPropertyGridVector2FieldInteractionEntry { - std::string fieldId = {}; - UIEditorVector2FieldInteractionState state = {}; -}; - -struct UIEditorPropertyGridVector3FieldInteractionEntry { - std::string fieldId = {}; - UIEditorVector3FieldInteractionState state = {}; -}; - -struct UIEditorPropertyGridVector4FieldInteractionEntry { - std::string fieldId = {}; - UIEditorVector4FieldInteractionState state = {}; -}; - struct UIEditorPropertyGridInteractionState { Widgets::UIEditorPropertyGridState propertyGridState = {}; ::XCEngine::UI::Widgets::UIKeyboardNavigationModel keyboardNavigation = {}; - ::XCEngine::UI::Text::UITextInputState textInputState = {}; + UIEditorEditableFieldSession editableFieldSession = {}; ::XCEngine::UI::UIPoint pointerPosition = {}; std::size_t pressedPopupIndex = Widgets::UIEditorPropertyGridInvalidIndex; bool hasPointerPosition = false; std::vector assetFieldInteractionStates = {}; - std::vector vector2FieldInteractionStates = {}; - std::vector vector3FieldInteractionStates = {}; - std::vector vector4FieldInteractionStates = {}; }; struct UIEditorPropertyGridInteractionResult { diff --git a/new_editor/include/XCEditor/Fields/UIEditorTextField.h b/new_editor/include/XCEditor/Fields/UIEditorTextField.h index a428fc3f..c4c2d4e9 100644 --- a/new_editor/include/XCEditor/Fields/UIEditorTextField.h +++ b/new_editor/include/XCEditor/Fields/UIEditorTextField.h @@ -26,6 +26,8 @@ struct UIEditorTextFieldState { UIEditorTextFieldHitTargetKind activeTarget = UIEditorTextFieldHitTargetKind::None; bool focused = false; bool editing = false; + std::size_t caretOffset = 0u; + std::uint64_t caretBlinkStartNanoseconds = 0u; std::string displayText = {}; }; diff --git a/new_editor/include/XCEditor/Fields/UIEditorTextFieldInteraction.h b/new_editor/include/XCEditor/Fields/UIEditorTextFieldInteraction.h index 330ab820..88d95f98 100644 --- a/new_editor/include/XCEditor/Fields/UIEditorTextFieldInteraction.h +++ b/new_editor/include/XCEditor/Fields/UIEditorTextFieldInteraction.h @@ -1,22 +1,16 @@ #pragma once +#include #include -#include -#include -#include - #include #include namespace XCEngine::UI::Editor { struct UIEditorTextFieldInteractionState { + UIEditorEditableFieldSession session = {}; Widgets::UIEditorTextFieldState textFieldState = {}; - ::XCEngine::UI::Text::UITextInputState textInputState = {}; - ::XCEngine::UI::Widgets::UIPropertyEditModel editModel = {}; - ::XCEngine::UI::UIPoint pointerPosition = {}; - bool hasPointerPosition = false; }; struct UIEditorTextFieldInteractionResult { diff --git a/new_editor/include/XCEditor/Fields/UIEditorVector2Field.h b/new_editor/include/XCEditor/Fields/UIEditorVector2Field.h index fab34537..2435fc77 100644 --- a/new_editor/include/XCEditor/Fields/UIEditorVector2Field.h +++ b/new_editor/include/XCEditor/Fields/UIEditorVector2Field.h @@ -37,6 +37,8 @@ struct UIEditorVector2FieldState { std::size_t selectedComponentIndex = UIEditorVector2FieldInvalidComponentIndex; bool focused = false; bool editing = false; + std::size_t caretOffset = 0u; + std::uint64_t caretBlinkStartNanoseconds = 0u; std::array displayTexts = { std::string(), std::string() }; }; diff --git a/new_editor/include/XCEditor/Fields/UIEditorVector2FieldInteraction.h b/new_editor/include/XCEditor/Fields/UIEditorVector2FieldInteraction.h index 965979ef..bc8878cd 100644 --- a/new_editor/include/XCEditor/Fields/UIEditorVector2FieldInteraction.h +++ b/new_editor/include/XCEditor/Fields/UIEditorVector2FieldInteraction.h @@ -1,22 +1,16 @@ #pragma once +#include #include -#include -#include -#include - #include #include namespace XCEngine::UI::Editor { struct UIEditorVector2FieldInteractionState { + UIEditorEditableFieldSession session = {}; Widgets::UIEditorVector2FieldState vector2FieldState = {}; - ::XCEngine::UI::Text::UITextInputState textInputState = {}; - ::XCEngine::UI::Widgets::UIPropertyEditModel editModel = {}; - ::XCEngine::UI::UIPoint pointerPosition = {}; - bool hasPointerPosition = false; }; struct UIEditorVector2FieldInteractionResult { diff --git a/new_editor/include/XCEditor/Fields/UIEditorVector3Field.h b/new_editor/include/XCEditor/Fields/UIEditorVector3Field.h index 0ea428bf..aff0bafe 100644 --- a/new_editor/include/XCEditor/Fields/UIEditorVector3Field.h +++ b/new_editor/include/XCEditor/Fields/UIEditorVector3Field.h @@ -37,6 +37,8 @@ struct UIEditorVector3FieldState { std::size_t selectedComponentIndex = UIEditorVector3FieldInvalidComponentIndex; bool focused = false; bool editing = false; + std::size_t caretOffset = 0u; + std::uint64_t caretBlinkStartNanoseconds = 0u; std::array displayTexts = { std::string(), std::string(), std::string() }; }; diff --git a/new_editor/include/XCEditor/Fields/UIEditorVector3FieldInteraction.h b/new_editor/include/XCEditor/Fields/UIEditorVector3FieldInteraction.h index ce79df86..f8c1462e 100644 --- a/new_editor/include/XCEditor/Fields/UIEditorVector3FieldInteraction.h +++ b/new_editor/include/XCEditor/Fields/UIEditorVector3FieldInteraction.h @@ -1,22 +1,16 @@ #pragma once +#include #include -#include -#include -#include - #include #include namespace XCEngine::UI::Editor { struct UIEditorVector3FieldInteractionState { + UIEditorEditableFieldSession session = {}; Widgets::UIEditorVector3FieldState vector3FieldState = {}; - ::XCEngine::UI::Text::UITextInputState textInputState = {}; - ::XCEngine::UI::Widgets::UIPropertyEditModel editModel = {}; - ::XCEngine::UI::UIPoint pointerPosition = {}; - bool hasPointerPosition = false; }; struct UIEditorVector3FieldInteractionResult { diff --git a/new_editor/include/XCEditor/Fields/UIEditorVector4Field.h b/new_editor/include/XCEditor/Fields/UIEditorVector4Field.h index 4ccd4105..8961c587 100644 --- a/new_editor/include/XCEditor/Fields/UIEditorVector4Field.h +++ b/new_editor/include/XCEditor/Fields/UIEditorVector4Field.h @@ -43,6 +43,8 @@ struct UIEditorVector4FieldState { std::size_t selectedComponentIndex = UIEditorVector4FieldInvalidComponentIndex; bool focused = false; bool editing = false; + std::size_t caretOffset = 0u; + std::uint64_t caretBlinkStartNanoseconds = 0u; std::array displayTexts = { std::string(), std::string(), diff --git a/new_editor/include/XCEditor/Fields/UIEditorVector4FieldInteraction.h b/new_editor/include/XCEditor/Fields/UIEditorVector4FieldInteraction.h index 7081d3c8..33d03e93 100644 --- a/new_editor/include/XCEditor/Fields/UIEditorVector4FieldInteraction.h +++ b/new_editor/include/XCEditor/Fields/UIEditorVector4FieldInteraction.h @@ -1,22 +1,16 @@ #pragma once +#include #include -#include -#include -#include - #include #include namespace XCEngine::UI::Editor { struct UIEditorVector4FieldInteractionState { + UIEditorEditableFieldSession session = {}; Widgets::UIEditorVector4FieldState vector4FieldState = {}; - ::XCEngine::UI::Text::UITextInputState textInputState = {}; - ::XCEngine::UI::Widgets::UIPropertyEditModel editModel = {}; - ::XCEngine::UI::UIPoint pointerPosition = {}; - bool hasPointerPosition = false; }; struct UIEditorVector4FieldInteractionResult { diff --git a/new_editor/include/XCEditor/Panels/UIEditorPanelRegistry.h b/new_editor/include/XCEditor/Panels/UIEditorPanelRegistry.h index e5b61021..5e1bb647 100644 --- a/new_editor/include/XCEditor/Panels/UIEditorPanelRegistry.h +++ b/new_editor/include/XCEditor/Panels/UIEditorPanelRegistry.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -22,6 +23,9 @@ struct UIEditorPanelDescriptor { bool placeholder = true; bool canHide = true; bool canClose = true; + bool toolWindow = false; + ::XCEngine::UI::UISize preferredDetachedWindowSize = {}; + ::XCEngine::UI::UISize minimumDetachedWindowSize = {}; UIEditorViewportShellSpec viewportShellSpec = {}; }; diff --git a/new_editor/include/XCEditor/Shell/UIEditorShellCompose.h b/new_editor/include/XCEditor/Shell/UIEditorShellCompose.h index 098a6b8f..68fb8efc 100644 --- a/new_editor/include/XCEditor/Shell/UIEditorShellCompose.h +++ b/new_editor/include/XCEditor/Shell/UIEditorShellCompose.h @@ -4,17 +4,15 @@ #include #include -namespace XCEngine::UI::Editor { +namespace XCEngine::UI::Editor::App { +class BuiltInIcons; +} -enum class UIEditorShellToolbarGlyph : std::uint8_t { - Play = 0, - Pause, - Step -}; +namespace XCEngine::UI::Editor { struct UIEditorShellToolbarButton { std::string buttonId = {}; - UIEditorShellToolbarGlyph glyph = UIEditorShellToolbarGlyph::Play; + std::uint8_t iconKind = 0; bool enabled = true; }; @@ -150,7 +148,8 @@ void AppendUIEditorShellCompose( const UIEditorShellComposeModel& model, const UIEditorShellComposeState& state, const UIEditorShellComposePalette& palette = {}, - const UIEditorShellComposeMetrics& metrics = {}); + const UIEditorShellComposeMetrics& metrics = {}, + const App::BuiltInIcons* builtInIcons = nullptr); void AppendUIEditorShellComposeOverlay( ::XCEngine::UI::UIDrawList& drawList, diff --git a/new_editor/include/XCEditor/Widgets/UIEditorTextLayout.h b/new_editor/include/XCEditor/Widgets/UIEditorTextLayout.h index b074d141..573a3fb8 100644 --- a/new_editor/include/XCEditor/Widgets/UIEditorTextLayout.h +++ b/new_editor/include/XCEditor/Widgets/UIEditorTextLayout.h @@ -1,9 +1,12 @@ #pragma once +#include #include #include +#include #include +#include namespace XCEngine::UI::Editor::Widgets { @@ -32,4 +35,79 @@ inline ::XCEngine::UI::UIRect ResolveUIEditorTextClipRect( rect.height + extraPadding + 1.0f); } +inline float MeasureUIEditorTextWidth( + const std::string& text, + float fontSize) { + return fontSize * 0.56f * + static_cast(::XCEngine::UI::Text::CountUtf8Codepoints(text)); +} + +inline float MeasureUIEditorTextWidthToCaret( + const std::string& text, + std::size_t caretOffset, + float fontSize) { + return fontSize * 0.56f * + static_cast(::XCEngine::UI::Text::CountUtf8CodepointsInRange( + text, + 0u, + (std::min)(caretOffset, text.size()))); +} + +inline std::uint64_t GetUIEditorTextCaretClockNanoseconds() { + using Clock = std::chrono::steady_clock; + const auto now = Clock::now().time_since_epoch(); + return static_cast( + std::chrono::duration_cast(now).count()); +} + +inline bool ShouldBlinkUIEditorTextCaret(std::uint64_t blinkStartNanoseconds) { + constexpr std::uint64_t kInitialVisibleDurationNanoseconds = 500000000ull; + constexpr std::uint64_t kBlinkPhaseDurationNanoseconds = 500000000ull; + + if (blinkStartNanoseconds == 0u) { + return true; + } + + const std::uint64_t nowNanoseconds = GetUIEditorTextCaretClockNanoseconds(); + if (nowNanoseconds <= blinkStartNanoseconds) { + return true; + } + + const std::uint64_t elapsedNanoseconds = nowNanoseconds - blinkStartNanoseconds; + if (elapsedNanoseconds < kInitialVisibleDurationNanoseconds) { + return true; + } + + const std::uint64_t blinkElapsedNanoseconds = + elapsedNanoseconds - kInitialVisibleDurationNanoseconds; + return ((blinkElapsedNanoseconds / kBlinkPhaseDurationNanoseconds) % 2u) == 0u; +} + +inline void AppendUIEditorTextCaret( + ::XCEngine::UI::UIDrawList& drawList, + const ::XCEngine::UI::UIRect& rect, + const std::string& text, + std::size_t caretOffset, + std::uint64_t blinkStartNanoseconds, + const ::XCEngine::UI::UIColor& color, + float fontSize, + float insetX, + float insetY = 0.0f, + float thickness = 1.0f) { + if (color.a <= 0.0f || !ShouldBlinkUIEditorTextCaret(blinkStartNanoseconds)) { + return; + } + + const float caretX = (std::min)( + rect.x + rect.width - 1.0f, + rect.x + insetX + MeasureUIEditorTextWidthToCaret(text, caretOffset, fontSize)); + const float top = ResolveUIEditorTextTop(rect, fontSize, insetY); + const float bottom = top + MeasureUIEditorTextLayoutHeight(fontSize); + drawList.AddLine( + ::XCEngine::UI::UIPoint(caretX, top), + ::XCEngine::UI::UIPoint(caretX, bottom), + color, + thickness); +} + } // namespace XCEngine::UI::Editor::Widgets diff --git a/new_editor/include/XCEditor/Workspace/UIEditorWindowWorkspaceController.h b/new_editor/include/XCEditor/Workspace/UIEditorWindowWorkspaceController.h index 70ae3014..7a807db6 100644 --- a/new_editor/include/XCEditor/Workspace/UIEditorWindowWorkspaceController.h +++ b/new_editor/include/XCEditor/Workspace/UIEditorWindowWorkspaceController.h @@ -52,6 +52,10 @@ public: std::string_view panelId, std::string_view preferredNewWindowId = {}); + UIEditorWindowWorkspaceOperationResult OpenPanelInNewWindow( + std::string_view panelId, + std::string_view preferredNewWindowId = {}); + UIEditorWindowWorkspaceOperationResult MovePanelToStack( std::string_view sourceWindowId, std::string_view sourceNodeId, diff --git a/new_editor/include/XCEditor/Workspace/UIEditorWorkspaceModel.h b/new_editor/include/XCEditor/Workspace/UIEditorWorkspaceModel.h index bd7f8573..ac43fc1a 100644 --- a/new_editor/include/XCEditor/Workspace/UIEditorWorkspaceModel.h +++ b/new_editor/include/XCEditor/Workspace/UIEditorWorkspaceModel.h @@ -1,6 +1,5 @@ #pragma once -#include #include #include #include diff --git a/new_editor/resources/Icons/camera_gizmo.png b/new_editor/resources/Icons/camera_gizmo.png new file mode 100644 index 00000000..48a411c1 Binary files /dev/null and b/new_editor/resources/Icons/camera_gizmo.png differ diff --git a/new_editor/resources/Icons/directional_light_gizmo.png b/new_editor/resources/Icons/directional_light_gizmo.png new file mode 100644 index 00000000..575cc1a4 Binary files /dev/null and b/new_editor/resources/Icons/directional_light_gizmo.png differ diff --git a/new_editor/resources/Icons/move_tool.png b/new_editor/resources/Icons/move_tool.png new file mode 100644 index 00000000..31720d73 Binary files /dev/null and b/new_editor/resources/Icons/move_tool.png differ diff --git a/new_editor/resources/Icons/move_tool_on.png b/new_editor/resources/Icons/move_tool_on.png new file mode 100644 index 00000000..9d166ff9 Binary files /dev/null and b/new_editor/resources/Icons/move_tool_on.png differ diff --git a/new_editor/resources/Icons/pause_button.png b/new_editor/resources/Icons/pause_button.png new file mode 100644 index 00000000..1345ae0f Binary files /dev/null and b/new_editor/resources/Icons/pause_button.png differ diff --git a/new_editor/resources/Icons/play_button.png b/new_editor/resources/Icons/play_button.png new file mode 100644 index 00000000..e5d7ef42 Binary files /dev/null and b/new_editor/resources/Icons/play_button.png differ diff --git a/new_editor/resources/Icons/point_light_gizmo.png b/new_editor/resources/Icons/point_light_gizmo.png new file mode 100644 index 00000000..8f33f296 Binary files /dev/null and b/new_editor/resources/Icons/point_light_gizmo.png differ diff --git a/new_editor/resources/Icons/rotate_tool.png b/new_editor/resources/Icons/rotate_tool.png new file mode 100644 index 00000000..8174c118 Binary files /dev/null and b/new_editor/resources/Icons/rotate_tool.png differ diff --git a/new_editor/resources/Icons/rotate_tool_on.png b/new_editor/resources/Icons/rotate_tool_on.png new file mode 100644 index 00000000..38c90858 Binary files /dev/null and b/new_editor/resources/Icons/rotate_tool_on.png differ diff --git a/new_editor/resources/Icons/scale_tool.png b/new_editor/resources/Icons/scale_tool.png new file mode 100644 index 00000000..960fc9ff Binary files /dev/null and b/new_editor/resources/Icons/scale_tool.png differ diff --git a/new_editor/resources/Icons/scale_tool_on.png b/new_editor/resources/Icons/scale_tool_on.png new file mode 100644 index 00000000..e30a43ba Binary files /dev/null and b/new_editor/resources/Icons/scale_tool_on.png differ diff --git a/new_editor/resources/Icons/spot_light_gizmo.png b/new_editor/resources/Icons/spot_light_gizmo.png new file mode 100644 index 00000000..8040a1de Binary files /dev/null and b/new_editor/resources/Icons/spot_light_gizmo.png differ diff --git a/new_editor/resources/Icons/step_button.png b/new_editor/resources/Icons/step_button.png new file mode 100644 index 00000000..59630f85 Binary files /dev/null and b/new_editor/resources/Icons/step_button.png differ diff --git a/new_editor/resources/Icons/transform_tool.png b/new_editor/resources/Icons/transform_tool.png new file mode 100644 index 00000000..b11e562a Binary files /dev/null and b/new_editor/resources/Icons/transform_tool.png differ diff --git a/new_editor/resources/Icons/transform_tool_on.png b/new_editor/resources/Icons/transform_tool_on.png new file mode 100644 index 00000000..37d3dbe9 Binary files /dev/null and b/new_editor/resources/Icons/transform_tool_on.png differ diff --git a/new_editor/resources/Icons/view_move_tool.png b/new_editor/resources/Icons/view_move_tool.png new file mode 100644 index 00000000..8b74e096 Binary files /dev/null and b/new_editor/resources/Icons/view_move_tool.png differ diff --git a/new_editor/resources/Icons/view_move_tool_on.png b/new_editor/resources/Icons/view_move_tool_on.png new file mode 100644 index 00000000..9ee93557 Binary files /dev/null and b/new_editor/resources/Icons/view_move_tool_on.png differ diff --git a/new_editor/resources/shaders/scene-viewport/infinite-grid/infinite-grid.shader b/new_editor/resources/shaders/scene-viewport/infinite-grid/infinite-grid.shader new file mode 100644 index 00000000..03eb1872 --- /dev/null +++ b/new_editor/resources/shaders/scene-viewport/infinite-grid/infinite-grid.shader @@ -0,0 +1,177 @@ +Shader "Scene View Infinite Grid" +{ + HLSLINCLUDE + // XC_EDITOR_SCENE_VIEW_INFINITE_GRID_D3D12_SHARED + cbuffer GridConstants + { + float4x4 gViewProjectionMatrix; + float4 gCameraPositionAndScale; + float4 gCameraRightAndFade; + float4 gCameraUpAndTanHalfFov; + float4 gCameraForwardAndAspect; + float4 gViewportNearFar; + float4 gGridTransition; + }; + + struct VSOutput + { + float4 position : SV_POSITION; + }; + + VSOutput MainVS(uint vertexId : SV_VertexID) + { + // XC_EDITOR_SCENE_VIEW_INFINITE_GRID_D3D12_VS + static const float2 positions[3] = { + float2(-1.0, -1.0), + float2(-1.0, 3.0), + float2( 3.0, -1.0) + }; + + VSOutput output; + output.position = float4(positions[vertexId], 0.0, 1.0); + return output; + } + + float PristineGridLine(float2 uv) + { + float2 deriv = max(fwidth(uv), float2(1e-6, 1e-6)); + float2 uvMod = frac(uv); + float2 uvDist = min(uvMod, 1.0 - uvMod); + float2 distInPixels = uvDist / deriv; + float2 lineAlpha = 1.0 - smoothstep(0.0, 1.0, distInPixels); + float density = max(deriv.x, deriv.y); + float densityFade = 1.0 - smoothstep(0.5, 1.0, density); + return max(lineAlpha.x, lineAlpha.y) * densityFade; + } + + float AxisLineAA(float coord, float deriv) + { + float distInPixels = abs(coord) / max(deriv, 1e-6); + return 1.0 - smoothstep(0.0, 1.5, distInPixels); + } + + struct GridLayer + { + float minor; + float major; + }; + + GridLayer SampleGridLayer(float2 worldPos2D, float baseScale) + { + GridLayer layer; + const float2 gridCoord1 = worldPos2D / baseScale; + const float2 gridCoord10 = worldPos2D / (baseScale * 10.0); + const float grid1 = PristineGridLine(gridCoord1); + const float grid10 = PristineGridLine(gridCoord10); + const float2 deriv1 = fwidth(gridCoord1); + const float lodFactor = smoothstep(0.3, 0.6, max(deriv1.x, deriv1.y)); + + layer.major = max(grid10, grid1 * 0.35); + layer.minor = grid1 * (1.0 - lodFactor); + return layer; + } + + struct PSOutput + { + float4 color : SV_TARGET0; + float depth : SV_Depth; + }; + + PSOutput MainPS(VSOutput input) + { + // XC_EDITOR_SCENE_VIEW_INFINITE_GRID_D3D12_PS + const float2 viewportSize = max(gViewportNearFar.xy, float2(1.0, 1.0)); + const float scale = max(gCameraPositionAndScale.w, 1e-4); + const float fadeDistance = max(gCameraRightAndFade.w, scale * 10.0); + const float tanHalfFov = max(gCameraUpAndTanHalfFov.w, 1e-4); + const float aspect = max(gCameraForwardAndAspect.w, 1e-4); + const float transitionBlend = saturate(gGridTransition.x); + const float nearClip = gViewportNearFar.z; + const float sceneFarClip = gViewportNearFar.w; + + const float2 ndc = float2( + (input.position.x / viewportSize.x) * 2.0 - 1.0, + 1.0 - (input.position.y / viewportSize.y) * 2.0); + + const float3 cameraPosition = gCameraPositionAndScale.xyz; + const float3 rayDirection = normalize( + gCameraForwardAndAspect.xyz + + ndc.x * aspect * tanHalfFov * gCameraRightAndFade.xyz + + ndc.y * tanHalfFov * gCameraUpAndTanHalfFov.xyz); + + if (abs(rayDirection.y) < 1e-5) { + discard; + } + + const float t = -cameraPosition.y / rayDirection.y; + if (t <= nearClip) { + discard; + } + + const float3 worldPosition = cameraPosition + rayDirection * t; + float depth = 0.999999; + if (t < sceneFarClip) { + const float4 clipPosition = mul(gViewProjectionMatrix, float4(worldPosition, 1.0)); + if (clipPosition.w <= 1e-6) { + discard; + } + + depth = clipPosition.z / clipPosition.w; + if (depth <= 0.0 || depth >= 1.0) { + discard; + } + } + + const float radialFade = + 1.0 - smoothstep(fadeDistance * 0.3, fadeDistance, length(worldPosition - cameraPosition)); + const float normalFade = smoothstep(0.0, 0.15, abs(rayDirection.y)); + const float fadeFactor = radialFade * normalFade; + if (fadeFactor < 1e-3) { + discard; + } + + const float2 worldPos2D = worldPosition.xz; + const GridLayer baseLayer = SampleGridLayer(worldPos2D, scale); + const GridLayer nextLayer = SampleGridLayer(worldPos2D, scale * 10.0); + const float minorGridIntensity = lerp(baseLayer.minor, nextLayer.minor, transitionBlend); + const float majorGridIntensity = lerp(baseLayer.major, nextLayer.major, transitionBlend); + float3 finalColor = float3(0.56, 0.56, 0.56); + float finalAlpha = max( + 0.13 * minorGridIntensity * fadeFactor, + 0.28 * majorGridIntensity * fadeFactor); + + const float2 worldDeriv = max(fwidth(worldPos2D), float2(1e-6, 1e-6)); + const float xAxisAlpha = AxisLineAA(worldPos2D.y, worldDeriv.y) * fadeFactor; + const float zAxisAlpha = AxisLineAA(worldPos2D.x, worldDeriv.x) * fadeFactor; + + const float axisAlpha = max(xAxisAlpha, zAxisAlpha); + finalAlpha = max(finalAlpha, 0.34 * saturate(axisAlpha)); + + if (finalAlpha < 1e-3) { + discard; + } + + PSOutput output; + output.color = float4(finalColor, finalAlpha); + output.depth = depth; + return output; + } + ENDHLSL + SubShader + { + Pass + { + Name "InfiniteGrid" + Tags { "LightMode" = "InfiniteGrid" } + Cull Off + ZWrite Off + ZTest LEqual + Blend SrcAlpha OneMinusSrcAlpha, One OneMinusSrcAlpha + HLSLPROGRAM + #pragma target 4.5 + #pragma vertex MainVS + #pragma fragment MainPS + ENDHLSL + } + } +} diff --git a/new_editor/src/Collections/UIEditorInlineRenameSession.cpp b/new_editor/src/Collections/UIEditorInlineRenameSession.cpp index 0a022978..d21c4962 100644 --- a/new_editor/src/Collections/UIEditorInlineRenameSession.cpp +++ b/new_editor/src/Collections/UIEditorInlineRenameSession.cpp @@ -60,12 +60,20 @@ void BeginSession( state.textFieldSpec.label.clear(); state.textFieldSpec.value = request.initialText; state.textFieldSpec.readOnly = false; + state.textFieldInteraction.session.focused = true; + BeginUIEditorEditableFieldEdit( + state.textFieldInteraction.session, + request.itemId, + UIEditorEditableFieldInvalidComponentIndex, + request.initialText, + false); state.textFieldInteraction.textFieldState.focused = true; state.textFieldInteraction.textFieldState.editing = true; state.textFieldInteraction.textFieldState.displayText = request.initialText; - state.textFieldInteraction.textInputState.value = request.initialText; - state.textFieldInteraction.textInputState.caret = request.initialText.size(); - state.textFieldInteraction.editModel.BeginEdit(request.itemId, request.initialText); + state.textFieldInteraction.textFieldState.caretOffset = + request.initialText.size(); + state.textFieldInteraction.textFieldState.caretBlinkStartNanoseconds = + state.textFieldInteraction.session.caretBlinkStartNanoseconds; result.sessionStarted = true; result.active = true; diff --git a/new_editor/app/Platform/Win32/WindowManager/TabDragDropTarget.cpp b/new_editor/src/Docking/UIEditorDockHostTransfer.cpp similarity index 73% rename from new_editor/app/Platform/Win32/WindowManager/TabDragDropTarget.cpp rename to new_editor/src/Docking/UIEditorDockHostTransfer.cpp index 79bf4d1a..25b75ba4 100644 --- a/new_editor/app/Platform/Win32/WindowManager/TabDragDropTarget.cpp +++ b/new_editor/src/Docking/UIEditorDockHostTransfer.cpp @@ -1,8 +1,8 @@ -#include "Platform/Win32/WindowManager/TabDragDropTarget.h" +#include #include -namespace XCEngine::UI::Editor::App::Internal { +namespace XCEngine::UI::Editor { namespace { @@ -68,10 +68,45 @@ UIEditorWorkspaceDockPlacement ResolveDropPlacement( } // namespace -EditorWindowTabDragDropTarget ResolveEditorWindowTabDragDropTarget( +bool TryResolveUIEditorDockHostTabDragHotspot( + const Widgets::UIEditorDockHostLayout& layout, + std::string_view nodeId, + std::string_view panelId, + const UIPoint& point, + UIPoint& outHotspot) { + for (const Widgets::UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) { + if (tabStack.nodeId != nodeId) { + continue; + } + + const std::size_t tabCount = + (std::min)(tabStack.items.size(), tabStack.tabStripLayout.tabHeaderRects.size()); + for (std::size_t index = 0u; index < tabCount; ++index) { + if (tabStack.items[index].panelId != panelId) { + continue; + } + + const UIRect& tabRect = tabStack.tabStripLayout.tabHeaderRects[index]; + outHotspot.x = (std::clamp)( + point.x - tabRect.x, + 0.0f, + (std::max)(tabRect.width, 0.0f)); + outHotspot.y = (std::clamp)( + point.y - tabRect.y, + 0.0f, + (std::max)(tabRect.height, 0.0f)); + return true; + } + } + + outHotspot = {}; + return false; +} + +UIEditorDockHostTabDropTarget ResolveUIEditorDockHostTabDropTarget( const Widgets::UIEditorDockHostLayout& layout, const UIPoint& point) { - EditorWindowTabDragDropTarget result = {}; + UIEditorDockHostTabDropTarget result = {}; if (!IsPointInsideRect(layout.bounds, point)) { return result; } @@ -113,4 +148,4 @@ EditorWindowTabDragDropTarget ResolveEditorWindowTabDragDropTarget( return result; } -} // namespace XCEngine::UI::Editor::App::Internal +} // namespace XCEngine::UI::Editor diff --git a/new_editor/src/Fields/ColorFieldRendering.cpp b/new_editor/src/Fields/ColorFieldRendering.cpp index 9a8987a0..65e9df27 100644 --- a/new_editor/src/Fields/ColorFieldRendering.cpp +++ b/new_editor/src/Fields/ColorFieldRendering.cpp @@ -35,12 +35,6 @@ UIEditorColorFieldPalette ResolvePalette(const UIEditorColorFieldPalette& palett const auto& tokens = GetUIEditorInspectorFieldStyleTokens(); UIEditorColorFieldPalette resolved = palette; - if (AreUIEditorFieldColorsEqual(palette.rowHoverColor, UIColor(0.0f, 0.0f, 0.0f, 0.0f))) { - resolved.rowHoverColor = tokens.rowHoverColor; - } - if (AreUIEditorFieldColorsEqual(palette.rowActiveColor, UIColor(0.0f, 0.0f, 0.0f, 0.0f))) { - resolved.rowActiveColor = tokens.rowActiveColor; - } if (AreUIEditorFieldColorsEqual(palette.swatchBorderColor, UIColor(0.14f, 0.14f, 0.14f, 1.0f))) { resolved.swatchBorderColor = tokens.swatchBorderColor; } @@ -130,6 +124,12 @@ UIEditorHsvColor ResolveDisplayHsv( return ResolveUIEditorDisplayHsv(spec.value, state.hue, state.hueValid); } +bool IsEditorVisible( + const UIEditorColorFieldLayout& layout, + const UIEditorColorFieldState& state) { + return layout.embeddedEditor || state.popupOpen; +} + void AppendCheckerboard( UIDrawList& drawList, const UIRect& rect, @@ -379,23 +379,32 @@ void AppendHueWheel( const UIEditorColorFieldMetrics& metrics) { const float outerRadius = layout.hueWheelOuterRadius; const float innerRadius = layout.hueWheelInnerRadius; - const float ringRadius = (outerRadius + innerRadius) * 0.5f; - const float ringThickness = ClampColorFieldNonNegative(outerRadius - innerRadius); - const int segmentCount = (std::max)(192, static_cast(outerRadius * 2.4f)); + if (outerRadius <= innerRadius || innerRadius <= 0.0f) { + return; + } + + const int segmentCount = (std::max)(360, static_cast(outerRadius * 8.0f)); for (int segment = 0; segment < segmentCount; ++segment) { const float t0 = static_cast(segment) / static_cast(segmentCount); const float t1 = static_cast(segment + 1) / static_cast(segmentCount); const float angle0 = t0 * 2.0f * kPi; const float angle1 = t1 * 2.0f * kPi; - drawList.AddLine( - UIPoint( - layout.hueWheelCenter.x + std::cos(angle0) * ringRadius, - layout.hueWheelCenter.y + std::sin(angle0) * ringRadius), - UIPoint( - layout.hueWheelCenter.x + std::cos(angle1) * ringRadius, - layout.hueWheelCenter.y + std::sin(angle1) * ringRadius), - ConvertUIEditorHsvToColor(UIEditorHsvColor { t0, 1.0f, 1.0f, 1.0f }), - ringThickness + 1.0f); + const UIPoint outer0( + layout.hueWheelCenter.x + std::cos(angle0) * outerRadius, + layout.hueWheelCenter.y + std::sin(angle0) * outerRadius); + const UIPoint outer1( + layout.hueWheelCenter.x + std::cos(angle1) * outerRadius, + layout.hueWheelCenter.y + std::sin(angle1) * outerRadius); + const UIPoint inner0( + layout.hueWheelCenter.x + std::cos(angle0) * innerRadius, + layout.hueWheelCenter.y + std::sin(angle0) * innerRadius); + const UIPoint inner1( + layout.hueWheelCenter.x + std::cos(angle1) * innerRadius, + layout.hueWheelCenter.y + std::sin(angle1) * innerRadius); + const UIColor segmentColor = ConvertUIEditorHsvToColor( + UIEditorHsvColor { (t0 + t1) * 0.5f, 1.0f, 1.0f, 1.0f }); + drawList.AddFilledTriangle(outer0, outer1, inner1, segmentColor); + drawList.AddFilledTriangle(outer0, inner1, inner0, segmentColor); } drawList.AddCircleOutline( @@ -442,73 +451,77 @@ void AppendUIEditorColorFieldBackground( const UIEditorColorFieldMetrics& metrics) { const UIEditorColorFieldPalette resolvedPalette = Internal::ResolvePalette(palette); const UIEditorColorFieldMetrics resolvedMetrics = Internal::ResolveMetrics(metrics); - if (state.activeTarget != UIEditorColorFieldHitTargetKind::None && - resolvedPalette.rowActiveColor.a > 0.0f) { - drawList.AddFilledRect(layout.bounds, resolvedPalette.rowActiveColor); - } else if ( - state.hoveredTarget != UIEditorColorFieldHitTargetKind::None && - resolvedPalette.rowHoverColor.a > 0.0f) { - drawList.AddFilledRect(layout.bounds, resolvedPalette.rowHoverColor); - } else if (resolvedPalette.surfaceColor.a > 0.0f) { - drawList.AddFilledRect(layout.bounds, resolvedPalette.surfaceColor); - } + if (!layout.embeddedEditor) { + if (state.activeTarget != UIEditorColorFieldHitTargetKind::None && + resolvedPalette.rowActiveColor.a > 0.0f) { + drawList.AddFilledRect(layout.bounds, resolvedPalette.rowActiveColor); + } else if ( + state.hoveredTarget != UIEditorColorFieldHitTargetKind::None && + resolvedPalette.rowHoverColor.a > 0.0f) { + drawList.AddFilledRect(layout.bounds, resolvedPalette.rowHoverColor); + } else if (resolvedPalette.surfaceColor.a > 0.0f) { + drawList.AddFilledRect(layout.bounds, resolvedPalette.surfaceColor); + } - const bool swatchHighlighted = - state.popupOpen || - state.focused || - state.hoveredTarget == UIEditorColorFieldHitTargetKind::Swatch; - Internal::AppendColorSample( - drawList, - layout.swatchRect, - spec.value, - spec.showAlpha, - resolvedPalette, - resolvedMetrics, - swatchHighlighted ? resolvedPalette.swatchHoverBorderColor - : resolvedPalette.swatchBorderColor, - swatchHighlighted ? resolvedMetrics.focusedBorderThickness - : resolvedMetrics.borderThickness, - resolvedMetrics.swatchRounding); - - if (spec.readOnly) { - drawList.AddFilledRect( + const bool swatchHighlighted = + state.popupOpen || + state.focused || + state.hoveredTarget == UIEditorColorFieldHitTargetKind::Swatch; + Internal::AppendColorSample( + drawList, layout.swatchRect, - resolvedPalette.swatchReadOnlyOverlayColor, + spec.value, + spec.showAlpha, + resolvedPalette, + resolvedMetrics, + swatchHighlighted ? resolvedPalette.swatchHoverBorderColor + : resolvedPalette.swatchBorderColor, + swatchHighlighted ? resolvedMetrics.focusedBorderThickness + : resolvedMetrics.borderThickness, resolvedMetrics.swatchRounding); + + if (spec.readOnly) { + drawList.AddFilledRect( + layout.swatchRect, + resolvedPalette.swatchReadOnlyOverlayColor, + resolvedMetrics.swatchRounding); + } } - if (!state.popupOpen) { + if (!Internal::IsEditorVisible(layout, state)) { return; } - drawList.AddFilledRect( - layout.popupRect, - resolvedPalette.popupColor, - resolvedMetrics.popupBorderRounding); - drawList.AddRectOutline( - layout.popupRect, - resolvedPalette.popupBorderColor, - resolvedMetrics.borderThickness, - resolvedMetrics.popupBorderRounding); - drawList.AddFilledRect( - layout.popupHeaderRect, - resolvedPalette.popupHeaderColor, - resolvedMetrics.popupBorderRounding); - drawList.AddFilledRect( - UIRect( - layout.popupHeaderRect.x, - layout.popupHeaderRect.y + layout.popupHeaderRect.height - - resolvedMetrics.borderThickness, - layout.popupHeaderRect.width, - resolvedMetrics.borderThickness), - resolvedPalette.popupBorderColor); + if (!layout.embeddedEditor) { + drawList.AddFilledRect( + layout.popupRect, + resolvedPalette.popupColor, + resolvedMetrics.popupBorderRounding); + drawList.AddRectOutline( + layout.popupRect, + resolvedPalette.popupBorderColor, + resolvedMetrics.borderThickness, + resolvedMetrics.popupBorderRounding); + drawList.AddFilledRect( + layout.popupHeaderRect, + resolvedPalette.popupHeaderColor, + resolvedMetrics.popupBorderRounding); + drawList.AddFilledRect( + UIRect( + layout.popupHeaderRect.x, + layout.popupHeaderRect.y + layout.popupHeaderRect.height - + resolvedMetrics.borderThickness, + layout.popupHeaderRect.width, + resolvedMetrics.borderThickness), + resolvedPalette.popupBorderColor); - drawList.AddFilledRect( - layout.popupCloseButtonRect, - state.hoveredTarget == UIEditorColorFieldHitTargetKind::PopupCloseButton - ? resolvedPalette.closeButtonHoverColor - : resolvedPalette.closeButtonColor, - 0.0f); + drawList.AddFilledRect( + layout.popupCloseButtonRect, + state.hoveredTarget == UIEditorColorFieldHitTargetKind::PopupCloseButton + ? resolvedPalette.closeButtonHoverColor + : resolvedPalette.closeButtonColor, + 0.0f); + } Internal::AppendColorSample( drawList, @@ -603,31 +616,35 @@ void AppendUIEditorColorFieldForeground( const UIEditorColorFieldMetrics& metrics) { const UIEditorColorFieldPalette resolvedPalette = Internal::ResolvePalette(palette); const UIEditorColorFieldMetrics resolvedMetrics = Internal::ResolveMetrics(metrics); - Internal::AppendPopupText( - drawList, - layout.labelRect, - spec.label, - resolvedPalette.labelColor, - resolvedMetrics.labelFontSize, - 0.0f, - resolvedMetrics.labelTextInsetY); + if (!layout.embeddedEditor) { + Internal::AppendPopupText( + drawList, + layout.labelRect, + spec.label, + resolvedPalette.labelColor, + resolvedMetrics.labelFontSize, + 0.0f, + resolvedMetrics.labelTextInsetY); + } - if (!state.popupOpen) { + if (!Internal::IsEditorVisible(layout, state)) { return; } - Internal::AppendPopupText( - drawList, - layout.popupTitleRect, - "Color", - resolvedPalette.popupTitleColor, - resolvedMetrics.titleFontSize, - 0.0f, - 0.0f); - Internal::AppendCloseGlyph( - drawList, - layout.popupCloseButtonRect, - resolvedPalette.closeGlyphColor); + if (!layout.embeddedEditor) { + Internal::AppendPopupText( + drawList, + layout.popupTitleRect, + "Color", + resolvedPalette.popupTitleColor, + resolvedMetrics.titleFontSize, + 0.0f, + 0.0f); + Internal::AppendCloseGlyph( + drawList, + layout.popupCloseButtonRect, + resolvedPalette.closeGlyphColor); + } const UIEditorHsvColor hsv = Internal::ResolveDisplayHsv(spec, state); Internal::AppendWheelHandle( diff --git a/new_editor/src/Fields/PropertyGridInteractionAsset.cpp b/new_editor/src/Fields/PropertyGridInteractionAsset.cpp deleted file mode 100644 index e820eb95..00000000 --- a/new_editor/src/Fields/PropertyGridInteractionAsset.cpp +++ /dev/null @@ -1,300 +0,0 @@ -#include "Fields/PropertyGridInteractionInternal.h" - -#include - -#include - -namespace XCEngine::UI::Editor::Internal { - -using Widgets::FindUIEditorPropertyGridVisibleFieldIndex; -using Widgets::UIEditorPropertyGridField; -using Widgets::UIEditorPropertyGridFieldKind; -using Widgets::UIEditorPropertyGridHitTargetKind; -using Widgets::UIEditorPropertyGridInvalidIndex; -using Widgets::UIEditorPropertyGridLayout; -using Widgets::UIEditorPropertyGridSection; - -namespace { - -template -Entry* FindMutableEntry( - std::vector& entries, - std::string_view fieldId) { - for (Entry& entry : entries) { - if (entry.fieldId == fieldId) { - return &entry; - } - } - - return nullptr; -} - -template -const Entry* FindEntry( - const std::vector& entries, - std::string_view fieldId) { - for (const Entry& entry : entries) { - if (entry.fieldId == fieldId) { - return &entry; - } - } - - return nullptr; -} - -UIEditorAssetFieldInteractionState BuildInteractionState( - const UIEditorPropertyGridInteractionState& state, - std::string_view fieldId) { - UIEditorAssetFieldInteractionState interactionState = {}; - if (const auto* entry = FindEntry(state.assetFieldInteractionStates, fieldId); - entry != nullptr) { - interactionState = entry->state; - } - - interactionState.pointerPosition = state.pointerPosition; - interactionState.hasPointerPosition = state.hasPointerPosition; - return interactionState; -} - -void StoreInteractionState( - UIEditorPropertyGridInteractionState& state, - std::string_view fieldId, - const UIEditorAssetFieldInteractionState& interactionState) { - if (auto* entry = FindMutableEntry(state.assetFieldInteractionStates, fieldId); - entry != nullptr) { - entry->state = interactionState; - } else { - UIEditorPropertyGridAssetFieldInteractionEntry newEntry = {}; - newEntry.fieldId = std::string(fieldId); - newEntry.state = interactionState; - state.assetFieldInteractionStates.push_back(std::move(newEntry)); - } - - if (auto* visualState = - FindMutableEntry(state.propertyGridState.assetFieldStates, fieldId); - visualState != nullptr) { - visualState->state = interactionState.fieldState; - } else { - Widgets::UIEditorPropertyGridAssetFieldVisualState newVisualState = {}; - newVisualState.fieldId = std::string(fieldId); - newVisualState.state = interactionState.fieldState; - state.propertyGridState.assetFieldStates.push_back(std::move(newVisualState)); - } -} - -bool IsVisibleAssetFieldId( - const UIEditorPropertyGridLayout& layout, - const std::vector& sections, - std::string_view fieldId) { - const std::size_t visibleFieldIndex = - FindUIEditorPropertyGridVisibleFieldIndex(layout, fieldId, sections); - if (visibleFieldIndex == UIEditorPropertyGridInvalidIndex || - visibleFieldIndex >= layout.visibleFieldSectionIndices.size() || - visibleFieldIndex >= layout.visibleFieldIndices.size()) { - return false; - } - - const std::size_t sectionIndex = layout.visibleFieldSectionIndices[visibleFieldIndex]; - const std::size_t fieldIndex = layout.visibleFieldIndices[visibleFieldIndex]; - return sectionIndex < sections.size() && - fieldIndex < sections[sectionIndex].fields.size() && - sections[sectionIndex].fields[fieldIndex].kind == - UIEditorPropertyGridFieldKind::Asset; -} - -void CopySpecToField( - UIEditorPropertyGridField& field, - const Widgets::UIEditorAssetFieldSpec& spec) { - field.assetValue.assetId = spec.assetId; - field.assetValue.displayName = spec.displayName; - field.assetValue.statusText = spec.statusText; - field.assetValue.emptyText = spec.emptyText; - field.assetValue.tint = spec.tint; - field.assetValue.showPickerButton = spec.showPickerButton; - field.assetValue.allowClear = spec.allowClear; - field.assetValue.showStatusBadge = spec.showStatusBadge; -} - -bool HasMeaningfulResult( - const UIEditorAssetFieldInteractionResult& result, - const UIEditorAssetFieldInteractionState& interactionState, - bool hadFocus) { - return result.consumed || - result.focusChanged || - result.valueChanged || - result.activateRequested || - result.pickerRequested || - result.clearRequested || - result.hitTarget.kind != Widgets::UIEditorAssetFieldHitTargetKind::None || - hadFocus || - interactionState.fieldState.focused; -} - -void AssignPropertyGridHitTarget( - UIEditorPropertyGridInteractionResult& result, - std::size_t sectionIndex, - std::size_t fieldIndex, - std::size_t visibleFieldIndex, - Widgets::UIEditorAssetFieldHitTargetKind hitTargetKind) { - switch (hitTargetKind) { - case Widgets::UIEditorAssetFieldHitTargetKind::Row: - result.hitTarget.kind = UIEditorPropertyGridHitTargetKind::FieldRow; - result.hitTarget.sectionIndex = sectionIndex; - result.hitTarget.fieldIndex = fieldIndex; - result.hitTarget.visibleFieldIndex = visibleFieldIndex; - break; - - case Widgets::UIEditorAssetFieldHitTargetKind::ValueBox: - case Widgets::UIEditorAssetFieldHitTargetKind::PickerButton: - case Widgets::UIEditorAssetFieldHitTargetKind::ClearButton: - result.hitTarget.kind = UIEditorPropertyGridHitTargetKind::ValueBox; - result.hitTarget.sectionIndex = sectionIndex; - result.hitTarget.fieldIndex = fieldIndex; - result.hitTarget.visibleFieldIndex = visibleFieldIndex; - break; - - case Widgets::UIEditorAssetFieldHitTargetKind::None: - default: - break; - } -} - -} // namespace - -void PruneAssetFieldVisualStates( - UIEditorPropertyGridInteractionState& state, - const UIEditorPropertyGridLayout& layout, - const std::vector& sections) { - state.assetFieldInteractionStates.erase( - std::remove_if( - state.assetFieldInteractionStates.begin(), - state.assetFieldInteractionStates.end(), - [&layout, §ions]( - const UIEditorPropertyGridAssetFieldInteractionEntry& entry) { - return !IsVisibleAssetFieldId(layout, sections, entry.fieldId); - }), - state.assetFieldInteractionStates.end()); - - state.propertyGridState.assetFieldStates.erase( - std::remove_if( - state.propertyGridState.assetFieldStates.begin(), - state.propertyGridState.assetFieldStates.end(), - [&layout, §ions]( - const Widgets::UIEditorPropertyGridAssetFieldVisualState& entry) { - return !IsVisibleAssetFieldId(layout, sections, entry.fieldId); - }), - state.propertyGridState.assetFieldStates.end()); -} - -bool ProcessAssetFieldEvent( - UIEditorPropertyGridInteractionState& state, - ::XCEngine::UI::Widgets::UISelectionModel& selectionModel, - ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel, - const UIEditorPropertyGridLayout& layout, - std::vector& sections, - const Widgets::UIEditorPropertyGridMetrics& metrics, - const ::XCEngine::UI::UIInputEvent& event, - UIEditorPropertyGridInteractionResult& result) { - const Widgets::UIEditorAssetFieldMetrics assetMetrics = - BuildUIEditorPropertyGridAssetFieldMetrics(metrics); - bool handled = false; - - for (std::size_t visibleFieldIndex = 0u; - visibleFieldIndex < layout.visibleFieldIndices.size(); - ++visibleFieldIndex) { - const std::size_t sectionIndex = layout.visibleFieldSectionIndices[visibleFieldIndex]; - const std::size_t fieldIndex = layout.visibleFieldIndices[visibleFieldIndex]; - if (sectionIndex >= sections.size() || - fieldIndex >= sections[sectionIndex].fields.size()) { - continue; - } - - UIEditorPropertyGridField& field = sections[sectionIndex].fields[fieldIndex]; - if (field.kind != UIEditorPropertyGridFieldKind::Asset) { - continue; - } - - UIEditorAssetFieldInteractionState assetState = - BuildInteractionState(state, field.fieldId); - const bool hadFocus = assetState.fieldState.focused; - Widgets::UIEditorAssetFieldSpec spec = - Widgets::Internal::BuildAssetFieldSpec(field); - const UIEditorAssetFieldInteractionFrame frame = - UpdateUIEditorAssetFieldInteraction( - assetState, - spec, - layout.fieldRowRects[visibleFieldIndex], - { event }, - assetMetrics); - - if (!HasMeaningfulResult(frame.result, assetState, hadFocus)) { - continue; - } - - handled = true; - CopySpecToField(field, spec); - StoreInteractionState(state, field.fieldId, assetState); - state.propertyGridState.focused = assetState.fieldState.focused; - state.propertyGridState.pressedFieldId.clear(); - - if (frame.result.consumed || - frame.result.focusChanged || - frame.result.valueChanged || - frame.result.activateRequested || - frame.result.pickerRequested || - hadFocus || - assetState.fieldState.focused) { - result.selectionChanged = - selectionModel.SetSelection(field.fieldId) || result.selectionChanged; - result.selectedFieldId = field.fieldId; - result.activeFieldId = field.fieldId; - state.keyboardNavigation.SetCurrentIndex(visibleFieldIndex); - } - - if (propertyEditModel.HasActiveEdit() && - propertyEditModel.GetActiveFieldId() != field.fieldId && - (frame.result.hitTarget.kind != - Widgets::UIEditorAssetFieldHitTargetKind::None || - frame.result.valueChanged || - frame.result.activateRequested || - frame.result.pickerRequested || - hadFocus || - assetState.fieldState.focused)) { - CommitActiveEdit(state, propertyEditModel, sections, result); - } - - if (frame.result.hitTarget.kind != - Widgets::UIEditorAssetFieldHitTargetKind::None || - frame.result.valueChanged || - frame.result.activateRequested || - frame.result.pickerRequested) { - ClosePopup(state, result); - } - - result.consumed = result.consumed || frame.result.consumed; - if (frame.result.valueChanged) { - SetChangedValueResult(field, result); - } - if (frame.result.pickerRequested) { - result.pickerRequested = true; - result.requestedFieldId = field.fieldId; - } - if (frame.result.activateRequested) { - result.activateRequested = true; - if (result.requestedFieldId.empty()) { - result.requestedFieldId = field.fieldId; - } - } - - AssignPropertyGridHitTarget( - result, - sectionIndex, - fieldIndex, - visibleFieldIndex, - frame.result.hitTarget.kind); - } - - return handled; -} - -} // namespace XCEngine::UI::Editor::Internal diff --git a/new_editor/src/Fields/PropertyGridInteractionColor.cpp b/new_editor/src/Fields/PropertyGridInteractionColor.cpp deleted file mode 100644 index 719729ec..00000000 --- a/new_editor/src/Fields/PropertyGridInteractionColor.cpp +++ /dev/null @@ -1,228 +0,0 @@ -#include "Fields/PropertyGridInteractionInternal.h" -#include -#include -#include - -namespace XCEngine::UI::Editor::Internal { - -using Widgets::FindUIEditorPropertyGridVisibleFieldIndex; -using Widgets::UIEditorColorFieldHitTargetKind; -using Widgets::UIEditorColorFieldSpec; -using Widgets::UIEditorPropertyGridColorFieldVisualState; -using Widgets::UIEditorPropertyGridField; -using Widgets::UIEditorPropertyGridFieldKind; -using Widgets::UIEditorPropertyGridHitTargetKind; -using Widgets::UIEditorPropertyGridInvalidIndex; -using Widgets::UIEditorPropertyGridLayout; -using Widgets::UIEditorPropertyGridSection; - -UIEditorPropertyGridColorFieldVisualState* FindMutableColorFieldVisualState( - UIEditorPropertyGridInteractionState& state, - std::string_view fieldId) { - for (UIEditorPropertyGridColorFieldVisualState& entry : - state.propertyGridState.colorFieldStates) { - if (entry.fieldId == fieldId) { - return &entry; - } - } - - return nullptr; -} - -const UIEditorPropertyGridColorFieldVisualState* FindColorFieldVisualState( - const UIEditorPropertyGridInteractionState& state, - std::string_view fieldId) { - for (const UIEditorPropertyGridColorFieldVisualState& entry : - state.propertyGridState.colorFieldStates) { - if (entry.fieldId == fieldId) { - return &entry; - } - } - - return nullptr; -} - -UIEditorColorFieldInteractionState BuildColorFieldInteractionState( - const UIEditorPropertyGridInteractionState& state, - std::string_view fieldId) { - UIEditorColorFieldInteractionState interactionState = {}; - if (const UIEditorPropertyGridColorFieldVisualState* entry = - FindColorFieldVisualState(state, fieldId); - entry != nullptr) { - interactionState.colorFieldState = entry->state; - } - - interactionState.pointerPosition = state.pointerPosition; - interactionState.hasPointerPosition = state.hasPointerPosition; - return interactionState; -} - -void StoreColorFieldVisualState( - UIEditorPropertyGridInteractionState& state, - std::string_view fieldId, - const UIEditorColorFieldInteractionState& interactionState) { - if (UIEditorPropertyGridColorFieldVisualState* entry = - FindMutableColorFieldVisualState(state, fieldId); - entry != nullptr) { - entry->state = interactionState.colorFieldState; - return; - } - - UIEditorPropertyGridColorFieldVisualState entry = {}; - entry.fieldId = std::string(fieldId); - entry.state = interactionState.colorFieldState; - state.propertyGridState.colorFieldStates.push_back(std::move(entry)); -} - -void PruneColorFieldVisualStates( - UIEditorPropertyGridInteractionState& state, - const UIEditorPropertyGridLayout& layout, - const std::vector& sections) { - const auto isVisibleColorFieldId = [&layout, §ions](std::string_view fieldId) { - const std::size_t visibleFieldIndex = - FindUIEditorPropertyGridVisibleFieldIndex(layout, fieldId, sections); - if (visibleFieldIndex == UIEditorPropertyGridInvalidIndex || - visibleFieldIndex >= layout.visibleFieldSectionIndices.size() || - visibleFieldIndex >= layout.visibleFieldIndices.size()) { - return false; - } - - const std::size_t sectionIndex = layout.visibleFieldSectionIndices[visibleFieldIndex]; - const std::size_t fieldIndex = layout.visibleFieldIndices[visibleFieldIndex]; - return sectionIndex < sections.size() && - fieldIndex < sections[sectionIndex].fields.size() && - sections[sectionIndex].fields[fieldIndex].kind == - UIEditorPropertyGridFieldKind::Color; - }; - - state.propertyGridState.colorFieldStates.erase( - std::remove_if( - state.propertyGridState.colorFieldStates.begin(), - state.propertyGridState.colorFieldStates.end(), - [&isVisibleColorFieldId]( - const UIEditorPropertyGridColorFieldVisualState& entry) { - return !isVisibleColorFieldId(entry.fieldId); - }), - state.propertyGridState.colorFieldStates.end()); -} - -void CloseOtherColorFieldPopups( - UIEditorPropertyGridInteractionState& state, - std::string_view keepFieldId) { - for (UIEditorPropertyGridColorFieldVisualState& entry : - state.propertyGridState.colorFieldStates) { - if (entry.fieldId == keepFieldId) { - continue; - } - - entry.state.popupOpen = false; - entry.state.activeTarget = UIEditorColorFieldHitTargetKind::None; - } -} - -bool ProcessColorFieldEvent( - UIEditorPropertyGridInteractionState& state, - ::XCEngine::UI::Widgets::UISelectionModel& selectionModel, - ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel, - const UIEditorPropertyGridLayout& layout, - std::vector& sections, - const Widgets::UIEditorPropertyGridMetrics& metrics, - const ::XCEngine::UI::UIInputEvent& event, - UIEditorPropertyGridInteractionResult& result) { - const Widgets::UIEditorColorFieldMetrics colorMetrics = - BuildUIEditorPropertyGridColorFieldMetrics(metrics); - const ::XCEngine::UI::UIRect popupViewportRect = - ResolvePopupViewportRect(layout.bounds); - bool handled = false; - - for (std::size_t visibleFieldIndex = 0u; - visibleFieldIndex < layout.visibleFieldIndices.size(); - ++visibleFieldIndex) { - const std::size_t sectionIndex = layout.visibleFieldSectionIndices[visibleFieldIndex]; - const std::size_t fieldIndex = layout.visibleFieldIndices[visibleFieldIndex]; - if (sectionIndex >= sections.size() || - fieldIndex >= sections[sectionIndex].fields.size()) { - continue; - } - - UIEditorPropertyGridField& field = sections[sectionIndex].fields[fieldIndex]; - if (field.kind != UIEditorPropertyGridFieldKind::Color) { - continue; - } - - UIEditorColorFieldInteractionState colorState = - BuildColorFieldInteractionState(state, field.fieldId); - const bool popupWasOpen = colorState.colorFieldState.popupOpen; - UIEditorColorFieldSpec spec = Widgets::Internal::BuildColorFieldSpec(field); - const UIEditorColorFieldInteractionFrame frame = - UpdateUIEditorColorFieldInteraction( - colorState, - spec, - layout.fieldRowRects[visibleFieldIndex], - { event }, - colorMetrics, - popupViewportRect); - - if (!frame.result.consumed && - frame.result.hitTarget.kind == UIEditorColorFieldHitTargetKind::None && - !popupWasOpen && - !colorState.colorFieldState.popupOpen) { - continue; - } - - handled = true; - field.colorValue.value = spec.value; - field.colorValue.showAlpha = spec.showAlpha; - StoreColorFieldVisualState(state, field.fieldId, colorState); - - if (frame.result.popupOpened) { - ClosePopup(state, result); - CloseOtherColorFieldPopups(state, field.fieldId); - } - - state.propertyGridState.focused = colorState.colorFieldState.focused; - state.propertyGridState.pressedFieldId.clear(); - - if (frame.result.hitTarget.kind != UIEditorColorFieldHitTargetKind::None || - frame.result.popupOpened || - frame.result.colorChanged || - popupWasOpen || - colorState.colorFieldState.popupOpen) { - result.selectionChanged = - selectionModel.SetSelection(field.fieldId) || result.selectionChanged; - result.selectedFieldId = field.fieldId; - result.activeFieldId = field.fieldId; - state.keyboardNavigation.SetCurrentIndex(visibleFieldIndex); - } - - if (propertyEditModel.HasActiveEdit() && - propertyEditModel.GetActiveFieldId() != field.fieldId && - (frame.result.hitTarget.kind != UIEditorColorFieldHitTargetKind::None || - frame.result.popupOpened)) { - CommitActiveEdit(state, propertyEditModel, sections, result); - } - - result.popupOpened = result.popupOpened || frame.result.popupOpened; - result.popupClosed = result.popupClosed || frame.result.popupClosed; - result.consumed = result.consumed || frame.result.consumed; - if (frame.result.colorChanged) { - SetChangedValueResult(field, result); - } - - if (frame.result.hitTarget.kind == UIEditorColorFieldHitTargetKind::Row) { - result.hitTarget.kind = UIEditorPropertyGridHitTargetKind::FieldRow; - result.hitTarget.sectionIndex = sectionIndex; - result.hitTarget.fieldIndex = fieldIndex; - result.hitTarget.visibleFieldIndex = visibleFieldIndex; - } else if (frame.result.hitTarget.kind != UIEditorColorFieldHitTargetKind::None) { - result.hitTarget.kind = UIEditorPropertyGridHitTargetKind::ValueBox; - result.hitTarget.sectionIndex = sectionIndex; - result.hitTarget.fieldIndex = fieldIndex; - result.hitTarget.visibleFieldIndex = visibleFieldIndex; - } - } - - return handled; -} - -} // namespace XCEngine::UI::Editor::Internal diff --git a/new_editor/src/Fields/PropertyGridInteractionEdit.cpp b/new_editor/src/Fields/PropertyGridInteractionEdit.cpp deleted file mode 100644 index 70e8660d..00000000 --- a/new_editor/src/Fields/PropertyGridInteractionEdit.cpp +++ /dev/null @@ -1,180 +0,0 @@ -#include "Fields/PropertyGridInteractionInternal.h" -namespace XCEngine::UI::Editor::Internal { - -using Widgets::ResolveUIEditorPropertyGridFieldValueText; -using Widgets::UIEditorPropertyGridField; -using Widgets::UIEditorPropertyGridFieldKind; -using Widgets::UIEditorPropertyGridLayout; -using Widgets::UIEditorPropertyGridSection; - -bool IsInlineEditable(const UIEditorPropertyGridField& field) { - return field.kind == UIEditorPropertyGridFieldKind::Text || - field.kind == UIEditorPropertyGridFieldKind::Number; -} - -bool IsNumberEditCharacter( - const UIEditorPropertyGridField& field, - std::uint32_t character) { - if (field.kind != UIEditorPropertyGridFieldKind::Number) { - return true; - } - - if (character >= static_cast('0') && - character <= static_cast('9')) { - return true; - } - - if (character == static_cast('-') || - character == static_cast('+')) { - return true; - } - - return !field.numberValue.integerMode && - character == static_cast('.'); -} - -bool SelectVisibleField( - UIEditorPropertyGridInteractionState& state, - ::XCEngine::UI::Widgets::UISelectionModel& selectionModel, - const UIEditorPropertyGridLayout& layout, - const std::vector& sections, - std::size_t visibleFieldIndex, - UIEditorPropertyGridInteractionResult& result) { - if (visibleFieldIndex >= layout.visibleFieldIndices.size() || - visibleFieldIndex >= layout.visibleFieldSectionIndices.size()) { - return false; - } - - const std::size_t sectionIndex = layout.visibleFieldSectionIndices[visibleFieldIndex]; - const std::size_t fieldIndex = layout.visibleFieldIndices[visibleFieldIndex]; - if (sectionIndex >= sections.size() || - fieldIndex >= sections[sectionIndex].fields.size()) { - return false; - } - - const UIEditorPropertyGridField& field = sections[sectionIndex].fields[fieldIndex]; - result.selectionChanged = selectionModel.SetSelection(field.fieldId); - result.selectedFieldId = field.fieldId; - result.consumed = true; - state.keyboardNavigation.SetCurrentIndex(visibleFieldIndex); - return true; -} - -bool BeginFieldEdit( - UIEditorPropertyGridInteractionState& state, - ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel, - const UIEditorPropertyGridField& field, - UIEditorPropertyGridInteractionResult& result) { - if (field.readOnly || !IsInlineEditable(field)) { - return false; - } - - const std::string initialValue = - field.kind == UIEditorPropertyGridFieldKind::Text - ? field.valueText - : ResolveUIEditorPropertyGridFieldValueText(field); - const bool changed = propertyEditModel.BeginEdit(field.fieldId, initialValue); - if (!changed && - (!propertyEditModel.HasActiveEdit() || - propertyEditModel.GetActiveFieldId() != field.fieldId)) { - return false; - } - - state.textInputState.value = propertyEditModel.GetStagedValue(); - state.textInputState.caret = state.textInputState.value.size(); - result.editStarted = changed; - result.activeFieldId = field.fieldId; - result.consumed = true; - return true; -} - -bool CommitActiveEdit( - UIEditorPropertyGridInteractionState& state, - ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel, - std::vector& sections, - UIEditorPropertyGridInteractionResult& result) { - if (!propertyEditModel.HasActiveEdit()) { - return false; - } - - UIEditorPropertyGridField* field = - FindMutableField(sections, propertyEditModel.GetActiveFieldId()); - if (field == nullptr) { - propertyEditModel.CancelEdit(); - state.textInputState = {}; - return false; - } - - result.activeFieldId = field->fieldId; - if (field->kind == UIEditorPropertyGridFieldKind::Number) { - Widgets::UIEditorNumberFieldSpec spec = {}; - spec.fieldId = field->fieldId; - spec.label = field->label; - spec.value = field->numberValue.value; - spec.step = field->numberValue.step; - spec.minValue = field->numberValue.minValue; - spec.maxValue = field->numberValue.maxValue; - spec.integerMode = field->numberValue.integerMode; - spec.readOnly = field->readOnly; - - double parsedValue = field->numberValue.value; - if (!Widgets::TryParseUIEditorNumberFieldValue( - spec, - propertyEditModel.GetStagedValue(), - parsedValue)) { - result.editCommitRejected = true; - result.consumed = true; - return false; - } - - field->numberValue.value = parsedValue; - result.committedFieldId = field->fieldId; - result.committedValue = ResolveUIEditorPropertyGridFieldValueText(*field); - SetChangedValueResult(*field, result); - } else { - field->valueText = propertyEditModel.GetStagedValue(); - result.committedFieldId = field->fieldId; - result.committedValue = field->valueText; - SetChangedValueResult(*field, result); - } - - propertyEditModel.CommitEdit(); - state.textInputState = {}; - result.editCommitted = true; - result.consumed = true; - return true; -} - -bool CancelActiveEdit( - UIEditorPropertyGridInteractionState& state, - ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel, - UIEditorPropertyGridInteractionResult& result) { - if (!propertyEditModel.HasActiveEdit()) { - return false; - } - - result.activeFieldId = propertyEditModel.GetActiveFieldId(); - if (!propertyEditModel.CancelEdit()) { - return false; - } - - state.textInputState = {}; - result.editCanceled = true; - result.consumed = true; - return true; -} - -bool ToggleBoolField( - UIEditorPropertyGridField& field, - UIEditorPropertyGridInteractionResult& result) { - if (field.kind != UIEditorPropertyGridFieldKind::Bool || field.readOnly) { - return false; - } - - field.boolValue = !field.boolValue; - SetChangedValueResult(field, result); - result.consumed = true; - return true; -} - -} // namespace XCEngine::UI::Editor::Internal diff --git a/new_editor/src/Fields/PropertyGridInteractionHelpers.cpp b/new_editor/src/Fields/PropertyGridInteractionHelpers.cpp deleted file mode 100644 index 1ba3091c..00000000 --- a/new_editor/src/Fields/PropertyGridInteractionHelpers.cpp +++ /dev/null @@ -1,221 +0,0 @@ -#include "Fields/PropertyGridInteractionInternal.h" -#include -#include - -namespace XCEngine::UI::Editor::Internal { - -using ::XCEngine::Input::KeyCode; -using ::XCEngine::UI::UIInputEvent; -using ::XCEngine::UI::UIInputEventType; -using Widgets::FindUIEditorPropertyGridFieldLocation; -using Widgets::FindUIEditorPropertyGridVisibleFieldIndex; -using Widgets::HitTestUIEditorPropertyGrid; -using Widgets::ResolveUIEditorPropertyGridFieldValueText; -using Widgets::UIEditorMenuPopupHitTarget; -using Widgets::UIEditorMenuPopupHitTargetKind; -using Widgets::UIEditorPropertyGridField; -using Widgets::UIEditorPropertyGridHitTarget; -using Widgets::UIEditorPropertyGridHitTargetKind; -using Widgets::UIEditorPropertyGridInvalidIndex; -using Widgets::UIEditorPropertyGridLayout; -using Widgets::UIEditorPropertyGridSection; - -bool ShouldUsePropertyGridPointerPosition(const UIInputEvent& event) { - switch (event.type) { - case UIInputEventType::PointerMove: - case UIInputEventType::PointerEnter: - case UIInputEventType::PointerButtonDown: - case UIInputEventType::PointerButtonUp: - return true; - default: - return false; - } -} - -bool HasSystemModifiers(const ::XCEngine::UI::UIInputModifiers& modifiers) { - return modifiers.control || modifiers.alt || modifiers.super; -} - -bool ApplyKeyboardNavigation( - UIEditorPropertyGridInteractionState& state, - std::int32_t keyCode) { - switch (static_cast(keyCode)) { - case KeyCode::Up: - return state.keyboardNavigation.MovePrevious(); - case KeyCode::Down: - return state.keyboardNavigation.MoveNext(); - case KeyCode::Home: - return state.keyboardNavigation.MoveHome(); - case KeyCode::End: - return state.keyboardNavigation.MoveEnd(); - default: - return false; - } -} - -void ClearHoverState(UIEditorPropertyGridInteractionState& state) { - state.propertyGridState.hoveredSectionId.clear(); - state.propertyGridState.hoveredFieldId.clear(); - state.propertyGridState.hoveredHitTarget = UIEditorPropertyGridHitTargetKind::None; -} - -void ClearPopupState(UIEditorPropertyGridInteractionState& state) { - state.propertyGridState.popupFieldId.clear(); - state.propertyGridState.popupHighlightedIndex = UIEditorPropertyGridInvalidIndex; - state.pressedPopupIndex = UIEditorPropertyGridInvalidIndex; -} - -UIEditorPropertyGridField* FindMutableField( - std::vector& sections, - std::string_view fieldId) { - const auto location = FindUIEditorPropertyGridFieldLocation(sections, fieldId); - if (!location.IsValid() || - location.sectionIndex >= sections.size() || - location.fieldIndex >= sections[location.sectionIndex].fields.size()) { - return nullptr; - } - - return §ions[location.sectionIndex].fields[location.fieldIndex]; -} - -const UIEditorPropertyGridField* FindField( - const std::vector& sections, - std::string_view fieldId) { - const auto location = FindUIEditorPropertyGridFieldLocation(sections, fieldId); - if (!location.IsValid() || - location.sectionIndex >= sections.size() || - location.fieldIndex >= sections[location.sectionIndex].fields.size()) { - return nullptr; - } - - return §ions[location.sectionIndex].fields[location.fieldIndex]; -} - -void SetChangedValueResult( - const UIEditorPropertyGridField& field, - UIEditorPropertyGridInteractionResult& result) { - result.fieldValueChanged = true; - result.changedFieldId = field.fieldId; - result.changedValue = ResolveUIEditorPropertyGridFieldValueText(field); -} - -void MergeInteractionResult( - UIEditorPropertyGridInteractionResult& accumulated, - const UIEditorPropertyGridInteractionResult& current) { - accumulated.consumed = accumulated.consumed || current.consumed; - accumulated.sectionToggled = accumulated.sectionToggled || current.sectionToggled; - accumulated.selectionChanged = accumulated.selectionChanged || current.selectionChanged; - accumulated.keyboardNavigated = accumulated.keyboardNavigated || current.keyboardNavigated; - accumulated.editStarted = accumulated.editStarted || current.editStarted; - accumulated.editValueChanged = accumulated.editValueChanged || current.editValueChanged; - accumulated.editCommitted = accumulated.editCommitted || current.editCommitted; - accumulated.editCommitRejected = - accumulated.editCommitRejected || current.editCommitRejected; - accumulated.editCanceled = accumulated.editCanceled || current.editCanceled; - accumulated.popupOpened = accumulated.popupOpened || current.popupOpened; - accumulated.popupClosed = accumulated.popupClosed || current.popupClosed; - accumulated.fieldValueChanged = - accumulated.fieldValueChanged || current.fieldValueChanged; - accumulated.secondaryClicked = - accumulated.secondaryClicked || current.secondaryClicked; - accumulated.pickerRequested = - accumulated.pickerRequested || current.pickerRequested; - accumulated.activateRequested = - accumulated.activateRequested || current.activateRequested; - - if (current.hitTarget.kind != UIEditorPropertyGridHitTargetKind::None) { - accumulated.hitTarget = current.hitTarget; - } - if (!current.toggledSectionId.empty()) { - accumulated.toggledSectionId = current.toggledSectionId; - } - if (!current.selectedFieldId.empty()) { - accumulated.selectedFieldId = current.selectedFieldId; - } - if (!current.activeFieldId.empty()) { - accumulated.activeFieldId = current.activeFieldId; - } - if (!current.committedFieldId.empty()) { - accumulated.committedFieldId = current.committedFieldId; - } - if (!current.committedValue.empty()) { - accumulated.committedValue = current.committedValue; - } - if (!current.changedFieldId.empty()) { - accumulated.changedFieldId = current.changedFieldId; - } - if (!current.changedValue.empty()) { - accumulated.changedValue = current.changedValue; - } - if (!current.requestedFieldId.empty()) { - accumulated.requestedFieldId = current.requestedFieldId; - } -} - -void SyncHoverTarget( - UIEditorPropertyGridInteractionState& state, - const UIEditorPropertyGridLayout& layout, - const std::vector& sections, - const Widgets::UIEditorMenuPopupMetrics& popupMetrics) { - ClearHoverState(state); - if (!state.hasPointerPosition) { - return; - } - - const UIEditorMenuPopupHitTarget popupHit = - ResolvePopupHit(state, layout, sections, popupMetrics); - if (popupHit.kind == UIEditorMenuPopupHitTargetKind::Item) { - state.propertyGridState.popupHighlightedIndex = popupHit.index; - return; - } - if (popupHit.kind != UIEditorMenuPopupHitTargetKind::None) { - return; - } - - const UIEditorPropertyGridHitTarget hitTarget = - HitTestUIEditorPropertyGrid(layout, state.pointerPosition); - state.propertyGridState.hoveredHitTarget = hitTarget.kind; - if (hitTarget.kind == UIEditorPropertyGridHitTargetKind::SectionHeader && - hitTarget.sectionIndex < sections.size()) { - state.propertyGridState.hoveredSectionId = - sections[hitTarget.sectionIndex].sectionId; - return; - } - - if ((hitTarget.kind == UIEditorPropertyGridHitTargetKind::FieldRow || - hitTarget.kind == UIEditorPropertyGridHitTargetKind::ValueBox) && - hitTarget.sectionIndex < sections.size() && - hitTarget.fieldIndex < sections[hitTarget.sectionIndex].fields.size()) { - state.propertyGridState.hoveredFieldId = - sections[hitTarget.sectionIndex].fields[hitTarget.fieldIndex].fieldId; - } -} - -void SyncKeyboardNavigation( - UIEditorPropertyGridInteractionState& state, - const ::XCEngine::UI::Widgets::UISelectionModel& selectionModel, - const UIEditorPropertyGridLayout& layout, - const std::vector& sections) { - state.keyboardNavigation.SetItemCount(layout.visibleFieldIndices.size()); - state.keyboardNavigation.ClampToItemCount(); - - if (!selectionModel.HasSelection()) { - return; - } - - const std::size_t selectedVisibleIndex = - FindUIEditorPropertyGridVisibleFieldIndex( - layout, - selectionModel.GetSelectedId(), - sections); - if (selectedVisibleIndex == UIEditorPropertyGridInvalidIndex) { - return; - } - - if (!state.keyboardNavigation.HasCurrentIndex() || - state.keyboardNavigation.GetCurrentIndex() != selectedVisibleIndex) { - state.keyboardNavigation.SetCurrentIndex(selectedVisibleIndex); - } -} - -} // namespace XCEngine::UI::Editor::Internal diff --git a/new_editor/src/Fields/PropertyGridInteractionInternal.h b/new_editor/src/Fields/PropertyGridInteractionInternal.h index 42da9474..e2e08982 100644 --- a/new_editor/src/Fields/PropertyGridInteractionInternal.h +++ b/new_editor/src/Fields/PropertyGridInteractionInternal.h @@ -1,7 +1,6 @@ #pragma once #include "Fields/PropertyGridInternal.h" -#include #include namespace XCEngine::UI::Editor::Internal { @@ -29,30 +28,18 @@ void SetChangedValueResult( void MergeInteractionResult( UIEditorPropertyGridInteractionResult& accumulated, const UIEditorPropertyGridInteractionResult& current); -Widgets::UIEditorPropertyGridColorFieldVisualState* FindMutableColorFieldVisualState( +void PruneNumberFieldVisualStates( UIEditorPropertyGridInteractionState& state, - std::string_view fieldId); -const Widgets::UIEditorPropertyGridColorFieldVisualState* FindColorFieldVisualState( - const UIEditorPropertyGridInteractionState& state, - std::string_view fieldId); -UIEditorColorFieldInteractionState BuildColorFieldInteractionState( - const UIEditorPropertyGridInteractionState& state, - std::string_view fieldId); -void StoreColorFieldVisualState( + const Widgets::UIEditorPropertyGridLayout& layout, + const std::vector& sections); +void PruneTextFieldVisualStates( UIEditorPropertyGridInteractionState& state, - std::string_view fieldId, - const UIEditorColorFieldInteractionState& interactionState); + const Widgets::UIEditorPropertyGridLayout& layout, + const std::vector& sections); void PruneAssetFieldVisualStates( UIEditorPropertyGridInteractionState& state, const Widgets::UIEditorPropertyGridLayout& layout, const std::vector& sections); -void PruneColorFieldVisualStates( - UIEditorPropertyGridInteractionState& state, - const Widgets::UIEditorPropertyGridLayout& layout, - const std::vector& sections); -void CloseOtherColorFieldPopups( - UIEditorPropertyGridInteractionState& state, - std::string_view keepFieldId); std::vector BuildPopupItems( const Widgets::UIEditorPropertyGridField& field); ::XCEngine::UI::UIRect ResolvePopupViewportRect( @@ -131,6 +118,24 @@ bool ProcessColorFieldEvent( const Widgets::UIEditorPropertyGridMetrics& metrics, const ::XCEngine::UI::UIInputEvent& event, UIEditorPropertyGridInteractionResult& result); +bool ProcessNumberFieldEvent( + UIEditorPropertyGridInteractionState& state, + ::XCEngine::UI::Widgets::UISelectionModel& selectionModel, + ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel, + const Widgets::UIEditorPropertyGridLayout& layout, + std::vector& sections, + const Widgets::UIEditorPropertyGridMetrics& metrics, + const ::XCEngine::UI::UIInputEvent& event, + UIEditorPropertyGridInteractionResult& result); +bool ProcessTextFieldEvent( + UIEditorPropertyGridInteractionState& state, + ::XCEngine::UI::Widgets::UISelectionModel& selectionModel, + ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel, + const Widgets::UIEditorPropertyGridLayout& layout, + std::vector& sections, + const Widgets::UIEditorPropertyGridMetrics& metrics, + const ::XCEngine::UI::UIInputEvent& event, + UIEditorPropertyGridInteractionResult& result); bool ProcessAssetFieldEvent( UIEditorPropertyGridInteractionState& state, ::XCEngine::UI::Widgets::UISelectionModel& selectionModel, diff --git a/new_editor/src/Fields/PropertyGridInteractionPopup.cpp b/new_editor/src/Fields/PropertyGridInteractionPopup.cpp deleted file mode 100644 index d315af9b..00000000 --- a/new_editor/src/Fields/PropertyGridInteractionPopup.cpp +++ /dev/null @@ -1,229 +0,0 @@ -#include "Fields/PropertyGridInteractionInternal.h" -#include -#include -#include -#include - -namespace XCEngine::UI::Editor::Internal { - -using ::XCEngine::UI::UISize; -using ::XCEngine::UI::Widgets::ResolvePopupPlacementRect; -using ::XCEngine::UI::Widgets::UIPopupPlacement; -using Widgets::FindUIEditorPropertyGridVisibleFieldIndex; -using Widgets::UIEditorMenuPopupHitTarget; -using Widgets::UIEditorMenuPopupItem; -using Widgets::UIEditorMenuPopupLayout; -using Widgets::UIEditorPropertyGridField; -using Widgets::UIEditorPropertyGridFieldKind; -using Widgets::UIEditorPropertyGridInvalidIndex; -using Widgets::UIEditorPropertyGridLayout; -using Widgets::UIEditorPropertyGridSection; - -std::vector BuildPopupItems( - const UIEditorPropertyGridField& field) { - std::vector items = {}; - if (field.kind != UIEditorPropertyGridFieldKind::Enum) { - return items; - } - - items.reserve(field.enumValue.options.size()); - const std::size_t selectedIndex = - field.enumValue.options.empty() - ? 0u - : (std::min)( - field.enumValue.selectedIndex, - field.enumValue.options.size() - 1u); - for (std::size_t index = 0u; index < field.enumValue.options.size(); ++index) { - UIEditorMenuPopupItem item = {}; - item.itemId = field.fieldId + "." + std::to_string(index); - item.kind = UIEditorMenuItemKind::Command; - item.label = field.enumValue.options[index]; - item.enabled = !field.readOnly; - item.checked = index == selectedIndex; - items.push_back(std::move(item)); - } - - return items; -} - -::XCEngine::UI::UIRect ResolvePopupViewportRect( - const ::XCEngine::UI::UIRect& bounds) { - return ::XCEngine::UI::UIRect( - bounds.x - 4096.0f, - bounds.y - 4096.0f, - 8192.0f, - 8192.0f); -} - -bool BuildPopupLayout( - const UIEditorPropertyGridInteractionState& state, - const UIEditorPropertyGridLayout& layout, - const std::vector& sections, - const Widgets::UIEditorMenuPopupMetrics& popupMetrics, - UIEditorMenuPopupLayout& popupLayout, - std::vector& popupItems) { - if (state.propertyGridState.popupFieldId.empty()) { - return false; - } - - const std::size_t visibleFieldIndex = - FindUIEditorPropertyGridVisibleFieldIndex( - layout, - state.propertyGridState.popupFieldId, - sections); - if (visibleFieldIndex == UIEditorPropertyGridInvalidIndex || - visibleFieldIndex >= layout.visibleFieldSectionIndices.size() || - visibleFieldIndex >= layout.visibleFieldIndices.size()) { - return false; - } - - const std::size_t sectionIndex = layout.visibleFieldSectionIndices[visibleFieldIndex]; - const std::size_t fieldIndex = layout.visibleFieldIndices[visibleFieldIndex]; - if (sectionIndex >= sections.size() || - fieldIndex >= sections[sectionIndex].fields.size()) { - return false; - } - - const UIEditorPropertyGridField& field = sections[sectionIndex].fields[fieldIndex]; - popupItems = BuildPopupItems(field); - if (popupItems.empty()) { - return false; - } - - const float popupWidth = (std::max)( - layout.fieldValueRects[visibleFieldIndex].width, - Widgets::ResolveUIEditorMenuPopupDesiredWidth(popupItems, popupMetrics)); - const float popupHeight = - Widgets::MeasureUIEditorMenuPopupHeight(popupItems, popupMetrics); - const auto placement = ResolvePopupPlacementRect( - layout.fieldValueRects[visibleFieldIndex], - UISize(popupWidth, popupHeight), - ResolvePopupViewportRect(layout.bounds), - UIPopupPlacement::BottomStart); - popupLayout = - Widgets::BuildUIEditorMenuPopupLayout(placement.rect, popupItems, popupMetrics); - return true; -} - -UIEditorMenuPopupHitTarget ResolvePopupHit( - const UIEditorPropertyGridInteractionState& state, - const UIEditorPropertyGridLayout& layout, - const std::vector& sections, - const Widgets::UIEditorMenuPopupMetrics& popupMetrics) { - if (!state.hasPointerPosition) { - return {}; - } - - UIEditorMenuPopupLayout popupLayout = {}; - std::vector popupItems = {}; - if (!BuildPopupLayout(state, layout, sections, popupMetrics, popupLayout, popupItems)) { - return {}; - } - - return Widgets::HitTestUIEditorMenuPopup( - popupLayout, - popupItems, - state.pointerPosition); -} - -void ClosePopup( - UIEditorPropertyGridInteractionState& state, - UIEditorPropertyGridInteractionResult& result) { - if (state.propertyGridState.popupFieldId.empty()) { - return; - } - - ClearPopupState(state); - result.popupClosed = true; - result.consumed = true; -} - -void OpenPopup( - UIEditorPropertyGridInteractionState& state, - const UIEditorPropertyGridField& field, - UIEditorPropertyGridInteractionResult& result) { - if (field.kind != UIEditorPropertyGridFieldKind::Enum || - field.readOnly || - field.enumValue.options.empty()) { - return; - } - - if (state.propertyGridState.popupFieldId == field.fieldId) { - return; - } - - state.propertyGridState.popupFieldId = field.fieldId; - state.propertyGridState.popupHighlightedIndex = - (std::min)(field.enumValue.selectedIndex, field.enumValue.options.size() - 1u); - result.popupOpened = true; - result.consumed = true; -} - -void MovePopupHighlight( - UIEditorPropertyGridInteractionState& state, - const UIEditorPropertyGridField& field, - int delta) { - if (field.kind != UIEditorPropertyGridFieldKind::Enum || - field.enumValue.options.empty()) { - state.propertyGridState.popupHighlightedIndex = - UIEditorPropertyGridInvalidIndex; - return; - } - - if (state.propertyGridState.popupHighlightedIndex == UIEditorPropertyGridInvalidIndex || - state.propertyGridState.popupHighlightedIndex >= field.enumValue.options.size()) { - state.propertyGridState.popupHighlightedIndex = - (std::min)(field.enumValue.selectedIndex, field.enumValue.options.size() - 1u); - } - - const std::size_t currentIndex = state.propertyGridState.popupHighlightedIndex; - if (delta < 0) { - state.propertyGridState.popupHighlightedIndex = - currentIndex == 0u ? 0u : currentIndex - 1u; - } else { - state.propertyGridState.popupHighlightedIndex = - currentIndex + 1u >= field.enumValue.options.size() - ? field.enumValue.options.size() - 1u - : currentIndex + 1u; - } -} - -void JumpPopupHighlightToEdge( - UIEditorPropertyGridInteractionState& state, - const UIEditorPropertyGridField& field, - bool toEnd) { - if (field.kind != UIEditorPropertyGridFieldKind::Enum || - field.enumValue.options.empty()) { - state.propertyGridState.popupHighlightedIndex = - UIEditorPropertyGridInvalidIndex; - return; - } - - state.propertyGridState.popupHighlightedIndex = - toEnd ? field.enumValue.options.size() - 1u : 0u; -} - -bool CommitPopupSelection( - UIEditorPropertyGridInteractionState& state, - std::vector& sections, - UIEditorPropertyGridInteractionResult& result) { - UIEditorPropertyGridField* field = - FindMutableField(sections, state.propertyGridState.popupFieldId); - if (field == nullptr || - field->kind != UIEditorPropertyGridFieldKind::Enum || - field->readOnly || - field->enumValue.options.empty() || - state.propertyGridState.popupHighlightedIndex == - UIEditorPropertyGridInvalidIndex || - state.propertyGridState.popupHighlightedIndex >= - field->enumValue.options.size()) { - return false; - } - - field->enumValue.selectedIndex = state.propertyGridState.popupHighlightedIndex; - SetChangedValueResult(*field, result); - ClosePopup(state, result); - return true; -} - -} // namespace XCEngine::UI::Editor::Internal diff --git a/new_editor/src/Fields/PropertyGridInteractionVector.cpp b/new_editor/src/Fields/PropertyGridInteractionVector.cpp deleted file mode 100644 index 61c8ac26..00000000 --- a/new_editor/src/Fields/PropertyGridInteractionVector.cpp +++ /dev/null @@ -1,627 +0,0 @@ -#include "Fields/PropertyGridInteractionInternal.h" - -#include - -#include -#include - -namespace XCEngine::UI::Editor::Widgets::Internal { - -const UIEditorPropertyGridVector2FieldVisualState* FindVector2FieldVisualState( - const UIEditorPropertyGridState& state, - std::string_view fieldId) { - for (const UIEditorPropertyGridVector2FieldVisualState& entry : - state.vector2FieldStates) { - if (entry.fieldId == fieldId) { - return &entry; - } - } - - return nullptr; -} - -const UIEditorPropertyGridVector3FieldVisualState* FindVector3FieldVisualState( - const UIEditorPropertyGridState& state, - std::string_view fieldId) { - for (const UIEditorPropertyGridVector3FieldVisualState& entry : - state.vector3FieldStates) { - if (entry.fieldId == fieldId) { - return &entry; - } - } - - return nullptr; -} - -const UIEditorPropertyGridVector4FieldVisualState* FindVector4FieldVisualState( - const UIEditorPropertyGridState& state, - std::string_view fieldId) { - for (const UIEditorPropertyGridVector4FieldVisualState& entry : - state.vector4FieldStates) { - if (entry.fieldId == fieldId) { - return &entry; - } - } - - return nullptr; -} - -} // namespace XCEngine::UI::Editor::Widgets::Internal - -namespace XCEngine::UI::Editor::Internal { - -using Widgets::FindUIEditorPropertyGridVisibleFieldIndex; -using Widgets::UIEditorPropertyGridField; -using Widgets::UIEditorPropertyGridFieldKind; -using Widgets::UIEditorPropertyGridHitTargetKind; -using Widgets::UIEditorPropertyGridInvalidIndex; -using Widgets::UIEditorPropertyGridLayout; -using Widgets::UIEditorPropertyGridSection; - -namespace { - -template -Entry* FindMutableEntry( - std::vector& entries, - std::string_view fieldId) { - for (Entry& entry : entries) { - if (entry.fieldId == fieldId) { - return &entry; - } - } - - return nullptr; -} - -template -const Entry* FindEntry( - const std::vector& entries, - std::string_view fieldId) { - for (const Entry& entry : entries) { - if (entry.fieldId == fieldId) { - return &entry; - } - } - - return nullptr; -} - -template -typename Traits::InteractionState BuildInteractionState( - const UIEditorPropertyGridInteractionState& state, - std::string_view fieldId) { - typename Traits::InteractionState interactionState = {}; - const auto& interactionEntries = Traits::InteractionEntries(state); - if (const auto* entry = FindEntry(interactionEntries, fieldId); - entry != nullptr) { - interactionState = entry->state; - } - - interactionState.pointerPosition = state.pointerPosition; - interactionState.hasPointerPosition = state.hasPointerPosition; - return interactionState; -} - -template -void StoreInteractionState( - UIEditorPropertyGridInteractionState& state, - std::string_view fieldId, - const typename Traits::InteractionState& interactionState) { - auto& interactionEntries = Traits::InteractionEntries(state); - if (auto* entry = FindMutableEntry(interactionEntries, fieldId); - entry != nullptr) { - entry->state = interactionState; - } else { - typename Traits::InteractionEntry interactionEntry = {}; - interactionEntry.fieldId = std::string(fieldId); - interactionEntry.state = interactionState; - interactionEntries.push_back(std::move(interactionEntry)); - } - - auto& visualStates = Traits::VisualStates(state.propertyGridState); - if (auto* visualState = - FindMutableEntry(visualStates, fieldId); - visualState != nullptr) { - visualState->state = Traits::FieldState(interactionState); - return; - } - - typename Traits::VisualStateEntry visualState = {}; - visualState.fieldId = std::string(fieldId); - visualState.state = Traits::FieldState(interactionState); - visualStates.push_back(std::move(visualState)); -} - -template -void PruneStateEntries( - std::vector& interactionEntries, - std::vector& visualStates, - const UIEditorPropertyGridLayout& layout, - const std::vector& sections) { - const auto isVisibleTypedFieldId = [&layout, §ions](std::string_view fieldId) { - const std::size_t visibleFieldIndex = - FindUIEditorPropertyGridVisibleFieldIndex(layout, fieldId, sections); - if (visibleFieldIndex == UIEditorPropertyGridInvalidIndex || - visibleFieldIndex >= layout.visibleFieldSectionIndices.size() || - visibleFieldIndex >= layout.visibleFieldIndices.size()) { - return false; - } - - const std::size_t sectionIndex = layout.visibleFieldSectionIndices[visibleFieldIndex]; - const std::size_t fieldIndex = layout.visibleFieldIndices[visibleFieldIndex]; - return sectionIndex < sections.size() && - fieldIndex < sections[sectionIndex].fields.size() && - sections[sectionIndex].fields[fieldIndex].kind == Traits::kFieldKind; - }; - - interactionEntries.erase( - std::remove_if( - interactionEntries.begin(), - interactionEntries.end(), - [&isVisibleTypedFieldId](const typename Traits::InteractionEntry& entry) { - return !isVisibleTypedFieldId(entry.fieldId); - }), - interactionEntries.end()); - visualStates.erase( - std::remove_if( - visualStates.begin(), - visualStates.end(), - [&isVisibleTypedFieldId](const typename Traits::VisualStateEntry& entry) { - return !isVisibleTypedFieldId(entry.fieldId); - }), - visualStates.end()); -} - -template -bool HasMeaningfulResult( - const typename Traits::InteractionResult& result, - const typename Traits::InteractionState& interactionState, - bool hadFocus, - bool hadEditing) { - return result.consumed || - result.focusChanged || - result.valueChanged || - result.stepApplied || - result.selectionChanged || - result.editStarted || - result.editCommitted || - result.editCommitRejected || - result.editCanceled || - result.hitTarget.kind != Traits::kNoneHitTargetKind || - hadFocus || - hadEditing || - Traits::FieldState(interactionState).focused || - Traits::FieldState(interactionState).editing; -} - -template -void CopyValuesToField( - UIEditorPropertyGridField& field, - const typename Traits::Spec& spec); - -template -void MergeVectorInteractionResult( - UIEditorPropertyGridInteractionResult& result, - const typename Traits::InteractionResult& fieldResult) { - result.consumed = result.consumed || fieldResult.consumed; - result.selectionChanged = result.selectionChanged || fieldResult.selectionChanged; - result.editStarted = result.editStarted || fieldResult.editStarted; - result.editCommitted = result.editCommitted || fieldResult.editCommitted; - result.editCommitRejected = - result.editCommitRejected || fieldResult.editCommitRejected; - result.editCanceled = result.editCanceled || fieldResult.editCanceled; -} - -template -bool ProcessVectorFieldEventImpl( - UIEditorPropertyGridInteractionState& state, - ::XCEngine::UI::Widgets::UISelectionModel& selectionModel, - ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel, - const UIEditorPropertyGridLayout& layout, - std::vector& sections, - const Widgets::UIEditorPropertyGridMetrics& metrics, - const ::XCEngine::UI::UIInputEvent& event, - UIEditorPropertyGridInteractionResult& result) { - const typename Traits::Metrics vectorMetrics = - Traits::BuildMetrics(metrics); - bool handled = false; - - for (std::size_t visibleFieldIndex = 0u; - visibleFieldIndex < layout.visibleFieldIndices.size(); - ++visibleFieldIndex) { - const std::size_t sectionIndex = layout.visibleFieldSectionIndices[visibleFieldIndex]; - const std::size_t fieldIndex = layout.visibleFieldIndices[visibleFieldIndex]; - if (sectionIndex >= sections.size() || - fieldIndex >= sections[sectionIndex].fields.size()) { - continue; - } - - UIEditorPropertyGridField& field = sections[sectionIndex].fields[fieldIndex]; - if (field.kind != Traits::kFieldKind) { - continue; - } - - typename Traits::InteractionState vectorState = - BuildInteractionState(state, field.fieldId); - const bool hadFocus = Traits::FieldState(vectorState).focused; - const bool hadEditing = Traits::FieldState(vectorState).editing; - typename Traits::Spec spec = Traits::BuildSpec(field); - const typename Traits::InteractionFrame frame = - Traits::Update( - vectorState, - spec, - layout.fieldRowRects[visibleFieldIndex], - { event }, - vectorMetrics); - - if (!HasMeaningfulResult( - frame.result, - vectorState, - hadFocus, - hadEditing)) { - continue; - } - - handled = true; - CopyValuesToField(field, spec); - StoreInteractionState(state, field.fieldId, vectorState); - state.propertyGridState.focused = - Traits::FieldState(vectorState).focused; - state.propertyGridState.pressedFieldId.clear(); - - if (frame.result.consumed || - frame.result.selectionChanged || - frame.result.editStarted || - frame.result.valueChanged || - frame.result.stepApplied || - frame.result.editCommitted || - frame.result.editCanceled || - hadFocus || - Traits::FieldState(vectorState).focused) { - result.selectionChanged = - selectionModel.SetSelection(field.fieldId) || result.selectionChanged; - result.selectedFieldId = field.fieldId; - result.activeFieldId = field.fieldId; - state.keyboardNavigation.SetCurrentIndex(visibleFieldIndex); - } - - if (propertyEditModel.HasActiveEdit() && - propertyEditModel.GetActiveFieldId() != field.fieldId && - (frame.result.hitTarget.kind != Traits::kNoneHitTargetKind || - frame.result.selectionChanged || - frame.result.editStarted)) { - CommitActiveEdit(state, propertyEditModel, sections, result); - } - - if (frame.result.hitTarget.kind != Traits::kNoneHitTargetKind || - frame.result.selectionChanged || - frame.result.editStarted) { - ClosePopup(state, result); - } - - MergeVectorInteractionResult(result, frame.result); - if (frame.result.valueChanged || frame.result.stepApplied) { - SetChangedValueResult(field, result); - } - - switch (frame.result.hitTarget.kind) { - case Traits::kRowHitTargetKind: - result.hitTarget.kind = UIEditorPropertyGridHitTargetKind::FieldRow; - result.hitTarget.sectionIndex = sectionIndex; - result.hitTarget.fieldIndex = fieldIndex; - result.hitTarget.visibleFieldIndex = visibleFieldIndex; - break; - case Traits::kComponentHitTargetKind: - result.hitTarget.kind = UIEditorPropertyGridHitTargetKind::ValueBox; - result.hitTarget.sectionIndex = sectionIndex; - result.hitTarget.fieldIndex = fieldIndex; - result.hitTarget.visibleFieldIndex = visibleFieldIndex; - break; - default: - break; - } - } - - return handled; -} - -struct Vector2Traits { - using VisualStateEntry = Widgets::UIEditorPropertyGridVector2FieldVisualState; - using InteractionEntry = UIEditorPropertyGridVector2FieldInteractionEntry; - using InteractionState = UIEditorVector2FieldInteractionState; - using InteractionResult = UIEditorVector2FieldInteractionResult; - using InteractionFrame = UIEditorVector2FieldInteractionFrame; - using Metrics = Widgets::UIEditorVector2FieldMetrics; - using Spec = Widgets::UIEditorVector2FieldSpec; - - static constexpr UIEditorPropertyGridFieldKind kFieldKind = - UIEditorPropertyGridFieldKind::Vector2; - static constexpr auto kNoneHitTargetKind = - Widgets::UIEditorVector2FieldHitTargetKind::None; - static constexpr auto kRowHitTargetKind = - Widgets::UIEditorVector2FieldHitTargetKind::Row; - static constexpr auto kComponentHitTargetKind = - Widgets::UIEditorVector2FieldHitTargetKind::Component; - - static auto& VisualStates(Widgets::UIEditorPropertyGridState& state) { - return state.vector2FieldStates; - } - - static const auto& VisualStates(const Widgets::UIEditorPropertyGridState& state) { - return state.vector2FieldStates; - } - - static auto& InteractionEntries(UIEditorPropertyGridInteractionState& state) { - return state.vector2FieldInteractionStates; - } - - static const auto& InteractionEntries(const UIEditorPropertyGridInteractionState& state) { - return state.vector2FieldInteractionStates; - } - - static auto& FieldState(InteractionState& state) { - return state.vector2FieldState; - } - - static const auto& FieldState(const InteractionState& state) { - return state.vector2FieldState; - } - - static Spec BuildSpec(const UIEditorPropertyGridField& field) { - return Widgets::Internal::BuildVector2FieldSpec(field); - } - - static Metrics BuildMetrics(const Widgets::UIEditorPropertyGridMetrics& metrics) { - return BuildUIEditorPropertyGridVector2FieldMetrics(metrics); - } - - static InteractionFrame Update( - InteractionState& state, - Spec& spec, - const ::XCEngine::UI::UIRect& bounds, - const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents, - const Metrics& metrics) { - return UpdateUIEditorVector2FieldInteraction( - state, - spec, - bounds, - inputEvents, - metrics); - } -}; - -struct Vector3Traits { - using VisualStateEntry = Widgets::UIEditorPropertyGridVector3FieldVisualState; - using InteractionEntry = UIEditorPropertyGridVector3FieldInteractionEntry; - using InteractionState = UIEditorVector3FieldInteractionState; - using InteractionResult = UIEditorVector3FieldInteractionResult; - using InteractionFrame = UIEditorVector3FieldInteractionFrame; - using Metrics = Widgets::UIEditorVector3FieldMetrics; - using Spec = Widgets::UIEditorVector3FieldSpec; - - static constexpr UIEditorPropertyGridFieldKind kFieldKind = - UIEditorPropertyGridFieldKind::Vector3; - static constexpr auto kNoneHitTargetKind = - Widgets::UIEditorVector3FieldHitTargetKind::None; - static constexpr auto kRowHitTargetKind = - Widgets::UIEditorVector3FieldHitTargetKind::Row; - static constexpr auto kComponentHitTargetKind = - Widgets::UIEditorVector3FieldHitTargetKind::Component; - - static auto& VisualStates(Widgets::UIEditorPropertyGridState& state) { - return state.vector3FieldStates; - } - - static const auto& VisualStates(const Widgets::UIEditorPropertyGridState& state) { - return state.vector3FieldStates; - } - - static auto& InteractionEntries(UIEditorPropertyGridInteractionState& state) { - return state.vector3FieldInteractionStates; - } - - static const auto& InteractionEntries(const UIEditorPropertyGridInteractionState& state) { - return state.vector3FieldInteractionStates; - } - - static auto& FieldState(InteractionState& state) { - return state.vector3FieldState; - } - - static const auto& FieldState(const InteractionState& state) { - return state.vector3FieldState; - } - - static Spec BuildSpec(const UIEditorPropertyGridField& field) { - return Widgets::Internal::BuildVector3FieldSpec(field); - } - - static Metrics BuildMetrics(const Widgets::UIEditorPropertyGridMetrics& metrics) { - return BuildUIEditorPropertyGridVector3FieldMetrics(metrics); - } - - static InteractionFrame Update( - InteractionState& state, - Spec& spec, - const ::XCEngine::UI::UIRect& bounds, - const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents, - const Metrics& metrics) { - return UpdateUIEditorVector3FieldInteraction( - state, - spec, - bounds, - inputEvents, - metrics); - } -}; - -struct Vector4Traits { - using VisualStateEntry = Widgets::UIEditorPropertyGridVector4FieldVisualState; - using InteractionEntry = UIEditorPropertyGridVector4FieldInteractionEntry; - using InteractionState = UIEditorVector4FieldInteractionState; - using InteractionResult = UIEditorVector4FieldInteractionResult; - using InteractionFrame = UIEditorVector4FieldInteractionFrame; - using Metrics = Widgets::UIEditorVector4FieldMetrics; - using Spec = Widgets::UIEditorVector4FieldSpec; - - static constexpr UIEditorPropertyGridFieldKind kFieldKind = - UIEditorPropertyGridFieldKind::Vector4; - static constexpr auto kNoneHitTargetKind = - Widgets::UIEditorVector4FieldHitTargetKind::None; - static constexpr auto kRowHitTargetKind = - Widgets::UIEditorVector4FieldHitTargetKind::Row; - static constexpr auto kComponentHitTargetKind = - Widgets::UIEditorVector4FieldHitTargetKind::Component; - - static auto& VisualStates(Widgets::UIEditorPropertyGridState& state) { - return state.vector4FieldStates; - } - - static const auto& VisualStates(const Widgets::UIEditorPropertyGridState& state) { - return state.vector4FieldStates; - } - - static auto& InteractionEntries(UIEditorPropertyGridInteractionState& state) { - return state.vector4FieldInteractionStates; - } - - static const auto& InteractionEntries(const UIEditorPropertyGridInteractionState& state) { - return state.vector4FieldInteractionStates; - } - - static auto& FieldState(InteractionState& state) { - return state.vector4FieldState; - } - - static const auto& FieldState(const InteractionState& state) { - return state.vector4FieldState; - } - - static Spec BuildSpec(const UIEditorPropertyGridField& field) { - return Widgets::Internal::BuildVector4FieldSpec(field); - } - - static Metrics BuildMetrics(const Widgets::UIEditorPropertyGridMetrics& metrics) { - return BuildUIEditorPropertyGridVector4FieldMetrics(metrics); - } - - static InteractionFrame Update( - InteractionState& state, - Spec& spec, - const ::XCEngine::UI::UIRect& bounds, - const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents, - const Metrics& metrics) { - return UpdateUIEditorVector4FieldInteraction( - state, - spec, - bounds, - inputEvents, - metrics); - } -}; - -template <> -void CopyValuesToField( - UIEditorPropertyGridField& field, - const Vector2Traits::Spec& spec) { - field.vector2Value.values = spec.values; -} - -template <> -void CopyValuesToField( - UIEditorPropertyGridField& field, - const Vector3Traits::Spec& spec) { - field.vector3Value.values = spec.values; -} - -template <> -void CopyValuesToField( - UIEditorPropertyGridField& field, - const Vector4Traits::Spec& spec) { - field.vector4Value.values = spec.values; -} - -} // namespace - -void PruneVectorFieldVisualStates( - UIEditorPropertyGridInteractionState& state, - const UIEditorPropertyGridLayout& layout, - const std::vector& sections) { - PruneStateEntries( - state.vector2FieldInteractionStates, - state.propertyGridState.vector2FieldStates, - layout, - sections); - PruneStateEntries( - state.vector3FieldInteractionStates, - state.propertyGridState.vector3FieldStates, - layout, - sections); - PruneStateEntries( - state.vector4FieldInteractionStates, - state.propertyGridState.vector4FieldStates, - layout, - sections); -} - -bool ProcessVector2FieldEvent( - UIEditorPropertyGridInteractionState& state, - ::XCEngine::UI::Widgets::UISelectionModel& selectionModel, - ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel, - const UIEditorPropertyGridLayout& layout, - std::vector& sections, - const Widgets::UIEditorPropertyGridMetrics& metrics, - const ::XCEngine::UI::UIInputEvent& event, - UIEditorPropertyGridInteractionResult& result) { - return ProcessVectorFieldEventImpl( - state, - selectionModel, - propertyEditModel, - layout, - sections, - metrics, - event, - result); -} - -bool ProcessVector3FieldEvent( - UIEditorPropertyGridInteractionState& state, - ::XCEngine::UI::Widgets::UISelectionModel& selectionModel, - ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel, - const UIEditorPropertyGridLayout& layout, - std::vector& sections, - const Widgets::UIEditorPropertyGridMetrics& metrics, - const ::XCEngine::UI::UIInputEvent& event, - UIEditorPropertyGridInteractionResult& result) { - return ProcessVectorFieldEventImpl( - state, - selectionModel, - propertyEditModel, - layout, - sections, - metrics, - event, - result); -} - -bool ProcessVector4FieldEvent( - UIEditorPropertyGridInteractionState& state, - ::XCEngine::UI::Widgets::UISelectionModel& selectionModel, - ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel, - const UIEditorPropertyGridLayout& layout, - std::vector& sections, - const Widgets::UIEditorPropertyGridMetrics& metrics, - const ::XCEngine::UI::UIInputEvent& event, - UIEditorPropertyGridInteractionResult& result) { - return ProcessVectorFieldEventImpl( - state, - selectionModel, - propertyEditModel, - layout, - sections, - metrics, - event, - result); -} - -} // namespace XCEngine::UI::Editor::Internal diff --git a/new_editor/src/Fields/PropertyGridInternal.h b/new_editor/src/Fields/PropertyGridInternal.h index 8048df63..2ad7ac87 100644 --- a/new_editor/src/Fields/PropertyGridInternal.h +++ b/new_editor/src/Fields/PropertyGridInternal.h @@ -78,10 +78,13 @@ UIEditorVector4FieldHitTargetKind ResolveVector4HoveredTarget( std::vector BuildEnumPopupItems( const UIEditorPropertyGridField& field); -const UIEditorPropertyGridAssetFieldVisualState* FindAssetFieldVisualState( +const UIEditorPropertyGridNumberFieldVisualState* FindNumberFieldVisualState( const UIEditorPropertyGridState& state, std::string_view fieldId); -const UIEditorPropertyGridColorFieldVisualState* FindColorFieldVisualState( +const UIEditorPropertyGridTextFieldVisualState* FindTextFieldVisualState( + const UIEditorPropertyGridState& state, + std::string_view fieldId); +const UIEditorPropertyGridAssetFieldVisualState* FindAssetFieldVisualState( const UIEditorPropertyGridState& state, std::string_view fieldId); const UIEditorPropertyGridVector2FieldVisualState* FindVector2FieldVisualState( diff --git a/new_editor/src/Fields/PropertyGridRendering.cpp b/new_editor/src/Fields/PropertyGridRendering.cpp deleted file mode 100644 index af3b2633..00000000 --- a/new_editor/src/Fields/PropertyGridRendering.cpp +++ /dev/null @@ -1,915 +0,0 @@ -#include "Fields/PropertyGridInternal.h" -#include -#include -#include - -#include - -#include -#include - -namespace XCEngine::UI::Editor::Widgets::Internal { - -using ::XCEngine::UI::UIPoint; -using ::XCEngine::UI::UIRect; -using ::XCEngine::UI::UISize; -using ::XCEngine::UI::Editor::UIEditorMenuItemKind; -using ::XCEngine::UI::Widgets::ResolvePopupPlacementRect; -using ::XCEngine::UI::Widgets::UIPopupPlacement; - -float ClampNonNegative(float value) { - return (std::max)(value, 0.0f); -} - -float ResolveSectionHeaderHeight( - const UIEditorPropertyGridSection& section, - const UIEditorPropertyGridMetrics& metrics) { - return section.desiredHeaderHeight > 0.0f - ? section.desiredHeaderHeight - : metrics.sectionHeaderHeight; -} - -float ResolveFieldRowHeight( - const UIEditorPropertyGridField& field, - const UIEditorPropertyGridMetrics& metrics) { - return field.desiredHeight > 0.0f - ? field.desiredHeight - : metrics.fieldRowHeight; -} - -UIPoint ResolveDisclosureGlyphPosition( - const UIRect& rect, - const UIEditorPropertyGridMetrics& metrics) { - return UIPoint( - rect.x + metrics.disclosureGlyphInsetX, - rect.y + metrics.sectionTextInsetY + metrics.disclosureGlyphInsetY); -} - -UIEditorBoolFieldSpec BuildBoolFieldSpec(const UIEditorPropertyGridField& field) { - UIEditorBoolFieldSpec spec = {}; - spec.fieldId = field.fieldId; - spec.label = field.label; - spec.value = field.boolValue; - spec.readOnly = field.readOnly; - return spec; -} - -UIEditorNumberFieldSpec BuildNumberFieldSpec(const UIEditorPropertyGridField& field) { - UIEditorNumberFieldSpec spec = {}; - spec.fieldId = field.fieldId; - spec.label = field.label; - spec.value = field.numberValue.value; - spec.step = field.numberValue.step; - spec.minValue = field.numberValue.minValue; - spec.maxValue = field.numberValue.maxValue; - spec.integerMode = field.numberValue.integerMode; - spec.readOnly = field.readOnly; - return spec; -} - -UIEditorEnumFieldSpec BuildEnumFieldSpec(const UIEditorPropertyGridField& field) { - UIEditorEnumFieldSpec spec = {}; - spec.fieldId = field.fieldId; - spec.label = field.label; - spec.options = field.enumValue.options; - spec.selectedIndex = field.enumValue.selectedIndex; - spec.readOnly = field.readOnly; - return spec; -} - -UIEditorAssetFieldSpec BuildAssetFieldSpec(const UIEditorPropertyGridField& field) { - UIEditorAssetFieldSpec spec = {}; - spec.fieldId = field.fieldId; - spec.label = field.label; - spec.assetId = field.assetValue.assetId; - spec.displayName = field.assetValue.displayName; - spec.statusText = field.assetValue.statusText; - spec.emptyText = field.assetValue.emptyText; - spec.tint = field.assetValue.tint; - spec.readOnly = field.readOnly; - spec.showPickerButton = field.assetValue.showPickerButton; - spec.allowClear = field.assetValue.allowClear; - spec.showStatusBadge = field.assetValue.showStatusBadge; - return spec; -} - -UIEditorColorFieldSpec BuildColorFieldSpec(const UIEditorPropertyGridField& field) { - UIEditorColorFieldSpec spec = {}; - spec.fieldId = field.fieldId; - spec.label = field.label; - spec.value = field.colorValue.value; - spec.showAlpha = field.colorValue.showAlpha; - spec.readOnly = field.readOnly; - return spec; -} - -UIEditorTextFieldSpec BuildTextFieldSpec(const UIEditorPropertyGridField& field) { - UIEditorTextFieldSpec spec = {}; - spec.fieldId = field.fieldId; - spec.label = field.label; - spec.value = field.valueText; - spec.readOnly = field.readOnly; - return spec; -} - -UIEditorVector2FieldSpec BuildVector2FieldSpec(const UIEditorPropertyGridField& field) { - UIEditorVector2FieldSpec spec = {}; - spec.fieldId = field.fieldId; - spec.label = field.label; - spec.values = field.vector2Value.values; - spec.componentLabels = field.vector2Value.componentLabels; - spec.step = field.vector2Value.step; - spec.minValue = field.vector2Value.minValue; - spec.maxValue = field.vector2Value.maxValue; - spec.integerMode = field.vector2Value.integerMode; - spec.readOnly = field.readOnly; - return spec; -} - -UIEditorVector3FieldSpec BuildVector3FieldSpec(const UIEditorPropertyGridField& field) { - UIEditorVector3FieldSpec spec = {}; - spec.fieldId = field.fieldId; - spec.label = field.label; - spec.values = field.vector3Value.values; - spec.componentLabels = field.vector3Value.componentLabels; - spec.step = field.vector3Value.step; - spec.minValue = field.vector3Value.minValue; - spec.maxValue = field.vector3Value.maxValue; - spec.integerMode = field.vector3Value.integerMode; - spec.readOnly = field.readOnly; - return spec; -} - -UIEditorVector4FieldSpec BuildVector4FieldSpec(const UIEditorPropertyGridField& field) { - UIEditorVector4FieldSpec spec = {}; - spec.fieldId = field.fieldId; - spec.label = field.label; - spec.values = field.vector4Value.values; - spec.componentLabels = field.vector4Value.componentLabels; - spec.step = field.vector4Value.step; - spec.minValue = field.vector4Value.minValue; - spec.maxValue = field.vector4Value.maxValue; - spec.integerMode = field.vector4Value.integerMode; - spec.readOnly = field.readOnly; - return spec; -} - -UIEditorPropertyGridFieldRects ResolveFieldRects( - const UIRect& rowRect, - const UIEditorPropertyGridField& field, - const UIEditorPropertyGridMetrics& metrics) { - switch (field.kind) { - case UIEditorPropertyGridFieldKind::Bool: { - const UIEditorBoolFieldLayout fieldLayout = BuildUIEditorBoolFieldLayout( - rowRect, - BuildBoolFieldSpec(field), - ::XCEngine::UI::Editor::BuildUIEditorPropertyGridBoolFieldMetrics(metrics)); - return { fieldLayout.labelRect, fieldLayout.controlRect }; - } - - case UIEditorPropertyGridFieldKind::Number: { - const UIEditorNumberFieldLayout fieldLayout = BuildUIEditorNumberFieldLayout( - rowRect, - BuildNumberFieldSpec(field), - ::XCEngine::UI::Editor::BuildUIEditorPropertyGridNumberFieldMetrics(metrics)); - return { fieldLayout.labelRect, fieldLayout.valueRect }; - } - - case UIEditorPropertyGridFieldKind::Enum: { - const UIEditorEnumFieldLayout fieldLayout = BuildUIEditorEnumFieldLayout( - rowRect, - BuildEnumFieldSpec(field), - ::XCEngine::UI::Editor::BuildUIEditorPropertyGridEnumFieldMetrics(metrics)); - return { fieldLayout.labelRect, fieldLayout.valueRect }; - } - - case UIEditorPropertyGridFieldKind::Asset: { - const UIEditorAssetFieldLayout fieldLayout = BuildUIEditorAssetFieldLayout( - rowRect, - BuildAssetFieldSpec(field), - ::XCEngine::UI::Editor::BuildUIEditorPropertyGridAssetFieldMetrics(metrics)); - return { fieldLayout.labelRect, fieldLayout.controlRect }; - } - - case UIEditorPropertyGridFieldKind::Color: { - const UIEditorColorFieldLayout fieldLayout = BuildUIEditorColorFieldLayout( - rowRect, - BuildColorFieldSpec(field), - ::XCEngine::UI::Editor::BuildUIEditorPropertyGridColorFieldMetrics(metrics)); - return { fieldLayout.labelRect, fieldLayout.controlRect }; - } - - case UIEditorPropertyGridFieldKind::Vector2: { - const UIEditorVector2FieldLayout fieldLayout = BuildUIEditorVector2FieldLayout( - rowRect, - BuildVector2FieldSpec(field), - ::XCEngine::UI::Editor::BuildUIEditorPropertyGridVector2FieldMetrics(metrics)); - return { fieldLayout.labelRect, fieldLayout.controlRect }; - } - - case UIEditorPropertyGridFieldKind::Vector3: { - const UIEditorVector3FieldLayout fieldLayout = BuildUIEditorVector3FieldLayout( - rowRect, - BuildVector3FieldSpec(field), - ::XCEngine::UI::Editor::BuildUIEditorPropertyGridVector3FieldMetrics(metrics)); - return { fieldLayout.labelRect, fieldLayout.controlRect }; - } - - case UIEditorPropertyGridFieldKind::Vector4: { - const UIEditorVector4FieldLayout fieldLayout = BuildUIEditorVector4FieldLayout( - rowRect, - BuildVector4FieldSpec(field), - ::XCEngine::UI::Editor::BuildUIEditorPropertyGridVector4FieldMetrics(metrics)); - return { fieldLayout.labelRect, fieldLayout.controlRect }; - } - - case UIEditorPropertyGridFieldKind::Text: - default: { - const UIEditorTextFieldLayout fieldLayout = BuildUIEditorTextFieldLayout( - rowRect, - BuildTextFieldSpec(field), - ::XCEngine::UI::Editor::BuildUIEditorPropertyGridTextFieldMetrics(metrics)); - return { fieldLayout.labelRect, fieldLayout.valueRect }; - } - } -} - -const std::string& ResolveDisplayedTextFieldValue( - const UIEditorPropertyGridField& field, - const ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel) { - if (field.kind == UIEditorPropertyGridFieldKind::Text && - propertyEditModel.HasActiveEdit() && - propertyEditModel.GetActiveFieldId() == field.fieldId) { - return propertyEditModel.GetStagedValue(); - } - - return field.valueText; -} - -UIEditorBoolFieldHitTargetKind ResolveBoolHoveredTarget( - const UIEditorPropertyGridState& state, - const UIEditorPropertyGridField& field) { - if (state.hoveredFieldId != field.fieldId) { - return UIEditorBoolFieldHitTargetKind::None; - } - - return state.hoveredHitTarget == UIEditorPropertyGridHitTargetKind::ValueBox - ? UIEditorBoolFieldHitTargetKind::Checkbox - : UIEditorBoolFieldHitTargetKind::Row; -} - -UIEditorNumberFieldHitTargetKind ResolveNumberHoveredTarget( - const UIEditorPropertyGridState& state, - const UIEditorPropertyGridField& field) { - if (state.hoveredFieldId != field.fieldId) { - return UIEditorNumberFieldHitTargetKind::None; - } - - return state.hoveredHitTarget == UIEditorPropertyGridHitTargetKind::ValueBox - ? UIEditorNumberFieldHitTargetKind::ValueBox - : UIEditorNumberFieldHitTargetKind::Row; -} - -UIEditorEnumFieldHitTargetKind ResolveEnumHoveredTarget( - const UIEditorPropertyGridState& state, - const UIEditorPropertyGridField& field) { - if (state.hoveredFieldId != field.fieldId) { - return UIEditorEnumFieldHitTargetKind::None; - } - - return state.hoveredHitTarget == UIEditorPropertyGridHitTargetKind::ValueBox - ? UIEditorEnumFieldHitTargetKind::ValueBox - : UIEditorEnumFieldHitTargetKind::Row; -} - -UIEditorAssetFieldHitTargetKind ResolveAssetHoveredTarget( - const UIEditorPropertyGridState& state, - const UIEditorPropertyGridField& field) { - if (state.hoveredFieldId != field.fieldId) { - return UIEditorAssetFieldHitTargetKind::None; - } - - return state.hoveredHitTarget == UIEditorPropertyGridHitTargetKind::ValueBox - ? UIEditorAssetFieldHitTargetKind::ValueBox - : UIEditorAssetFieldHitTargetKind::Row; -} - -UIEditorColorFieldHitTargetKind ResolveColorHoveredTarget( - const UIEditorPropertyGridState& state, - const UIEditorPropertyGridField& field) { - if (state.hoveredFieldId != field.fieldId) { - return UIEditorColorFieldHitTargetKind::None; - } - - return state.hoveredHitTarget == UIEditorPropertyGridHitTargetKind::ValueBox - ? UIEditorColorFieldHitTargetKind::Swatch - : UIEditorColorFieldHitTargetKind::Row; -} - -UIEditorTextFieldHitTargetKind ResolveTextHoveredTarget( - const UIEditorPropertyGridState& state, - const UIEditorPropertyGridField& field) { - if (state.hoveredFieldId != field.fieldId) { - return UIEditorTextFieldHitTargetKind::None; - } - - return state.hoveredHitTarget == UIEditorPropertyGridHitTargetKind::ValueBox - ? UIEditorTextFieldHitTargetKind::ValueBox - : UIEditorTextFieldHitTargetKind::Row; -} - -UIEditorVector2FieldHitTargetKind ResolveVector2HoveredTarget( - const UIEditorPropertyGridState& state, - const UIEditorPropertyGridField& field) { - if (state.hoveredFieldId != field.fieldId) { - return UIEditorVector2FieldHitTargetKind::None; - } - - return UIEditorVector2FieldHitTargetKind::Row; -} - -UIEditorVector3FieldHitTargetKind ResolveVector3HoveredTarget( - const UIEditorPropertyGridState& state, - const UIEditorPropertyGridField& field) { - if (state.hoveredFieldId != field.fieldId) { - return UIEditorVector3FieldHitTargetKind::None; - } - - return UIEditorVector3FieldHitTargetKind::Row; -} - -UIEditorVector4FieldHitTargetKind ResolveVector4HoveredTarget( - const UIEditorPropertyGridState& state, - const UIEditorPropertyGridField& field) { - if (state.hoveredFieldId != field.fieldId) { - return UIEditorVector4FieldHitTargetKind::None; - } - - return UIEditorVector4FieldHitTargetKind::Row; -} - -std::vector BuildEnumPopupItems( - const UIEditorPropertyGridField& field) { - std::vector items = {}; - if (field.kind != UIEditorPropertyGridFieldKind::Enum) { - return items; - } - - items.reserve(field.enumValue.options.size()); - const std::size_t selectedIndex = - field.enumValue.options.empty() - ? 0u - : (std::min)( - field.enumValue.selectedIndex, - field.enumValue.options.size() - 1u); - for (std::size_t index = 0u; index < field.enumValue.options.size(); ++index) { - UIEditorMenuPopupItem item = {}; - item.itemId = field.fieldId + "." + std::to_string(index); - item.kind = UIEditorMenuItemKind::Command; - item.label = field.enumValue.options[index]; - item.enabled = !field.readOnly; - item.checked = index == selectedIndex; - items.push_back(std::move(item)); - } - - return items; -} - -const UIEditorPropertyGridColorFieldVisualState* FindColorFieldVisualState( - const UIEditorPropertyGridState& state, - std::string_view fieldId) { - for (const UIEditorPropertyGridColorFieldVisualState& entry : - state.colorFieldStates) { - if (entry.fieldId == fieldId) { - return &entry; - } - } - - return nullptr; -} - -const UIEditorPropertyGridAssetFieldVisualState* FindAssetFieldVisualState( - const UIEditorPropertyGridState& state, - std::string_view fieldId) { - for (const UIEditorPropertyGridAssetFieldVisualState& entry : - state.assetFieldStates) { - if (entry.fieldId == fieldId) { - return &entry; - } - } - - return nullptr; -} - -UIRect ResolvePopupViewportRect(const UIRect& bounds) { - return UIRect(bounds.x - 4096.0f, bounds.y - 4096.0f, 8192.0f, 8192.0f); -} - -bool BuildEnumPopupRuntime( - const UIEditorPropertyGridLayout& layout, - const std::vector& sections, - const UIEditorPropertyGridState& state, - const UIEditorMenuPopupMetrics& popupMetrics, - UIEditorMenuPopupLayout& popupLayout, - UIEditorMenuPopupState& popupState, - std::vector& popupItems) { - if (state.popupFieldId.empty()) { - return false; - } - - const std::size_t visibleFieldIndex = - FindUIEditorPropertyGridVisibleFieldIndex(layout, state.popupFieldId, sections); - if (visibleFieldIndex == UIEditorPropertyGridInvalidIndex || - visibleFieldIndex >= layout.visibleFieldSectionIndices.size() || - visibleFieldIndex >= layout.visibleFieldIndices.size()) { - return false; - } - - const std::size_t sectionIndex = layout.visibleFieldSectionIndices[visibleFieldIndex]; - const std::size_t fieldIndex = layout.visibleFieldIndices[visibleFieldIndex]; - if (sectionIndex >= sections.size() || - fieldIndex >= sections[sectionIndex].fields.size()) { - return false; - } - - const UIEditorPropertyGridField& field = sections[sectionIndex].fields[fieldIndex]; - popupItems = BuildEnumPopupItems(field); - if (popupItems.empty()) { - return false; - } - - const float popupWidth = (std::max)( - layout.fieldValueRects[visibleFieldIndex].width, - ResolveUIEditorMenuPopupDesiredWidth(popupItems, popupMetrics)); - const float popupHeight = - MeasureUIEditorMenuPopupHeight(popupItems, popupMetrics); - const auto placement = ResolvePopupPlacementRect( - layout.fieldValueRects[visibleFieldIndex], - UISize(popupWidth, popupHeight), - ResolvePopupViewportRect(layout.bounds), - UIPopupPlacement::BottomStart); - - popupLayout = BuildUIEditorMenuPopupLayout( - placement.rect, - popupItems, - popupMetrics); - popupState.focused = state.focused || !state.popupFieldId.empty(); - popupState.hoveredIndex = - state.popupHighlightedIndex < popupItems.size() - ? state.popupHighlightedIndex - : UIEditorMenuPopupInvalidIndex; - return true; -} - -std::string FormatVector2ValueText(const UIEditorPropertyGridField& field) { - const UIEditorVector2FieldSpec spec = BuildVector2FieldSpec(field); - return FormatUIEditorVector2FieldComponentValue(spec, 0u) + ", " + - FormatUIEditorVector2FieldComponentValue(spec, 1u); -} - -std::string FormatColorValueText(const UIEditorPropertyGridField& field) { - return FormatUIEditorColorFieldHexText(BuildColorFieldSpec(field)); -} - -std::string FormatVector3ValueText(const UIEditorPropertyGridField& field) { - const UIEditorVector3FieldSpec spec = BuildVector3FieldSpec(field); - return FormatUIEditorVector3FieldComponentValue(spec, 0u) + ", " + - FormatUIEditorVector3FieldComponentValue(spec, 1u) + ", " + - FormatUIEditorVector3FieldComponentValue(spec, 2u); -} - -std::string FormatVector4ValueText(const UIEditorPropertyGridField& field) { - const UIEditorVector4FieldSpec spec = BuildVector4FieldSpec(field); - return FormatUIEditorVector4FieldComponentValue(spec, 0u) + ", " + - FormatUIEditorVector4FieldComponentValue(spec, 1u) + ", " + - FormatUIEditorVector4FieldComponentValue(spec, 2u) + ", " + - FormatUIEditorVector4FieldComponentValue(spec, 3u); -} - -} // namespace XCEngine::UI::Editor::Widgets::Internal - -namespace XCEngine::UI::Editor::Widgets { - -void AppendUIEditorPropertyGridBackground( - ::XCEngine::UI::UIDrawList& drawList, - const UIEditorPropertyGridLayout& layout, - const std::vector& sections, - const ::XCEngine::UI::Widgets::UISelectionModel& selectionModel, - const ::XCEngine::UI::Widgets::UIPropertyEditModel&, - const UIEditorPropertyGridState& state, - const UIEditorPropertyGridPalette& palette, - const UIEditorPropertyGridMetrics& metrics) { - drawList.AddFilledRect(layout.bounds, palette.surfaceColor, metrics.cornerRounding); - drawList.AddRectOutline( - layout.bounds, - state.focused ? palette.focusedBorderColor : palette.borderColor, - state.focused ? metrics.focusedBorderThickness : metrics.borderThickness, - metrics.cornerRounding); - - for (std::size_t sectionVisibleIndex = 0u; - sectionVisibleIndex < layout.sectionHeaderRects.size(); - ++sectionVisibleIndex) { - const UIEditorPropertyGridSection& section = - sections[layout.sectionIndices[sectionVisibleIndex]]; - const bool hovered = - state.hoveredFieldId.empty() && - state.hoveredSectionId == section.sectionId; - drawList.AddFilledRect( - layout.sectionHeaderRects[sectionVisibleIndex], - hovered ? palette.sectionHeaderHoverColor : palette.sectionHeaderColor, - metrics.cornerRounding); - } - - for (std::size_t visibleFieldIndex = 0u; - visibleFieldIndex < layout.fieldRowRects.size(); - ++visibleFieldIndex) { - const UIEditorPropertyGridSection& section = - sections[layout.visibleFieldSectionIndices[visibleFieldIndex]]; - const UIEditorPropertyGridField& field = - section.fields[layout.visibleFieldIndices[visibleFieldIndex]]; - const bool selected = selectionModel.IsSelected(field.fieldId); - const bool hovered = state.hoveredFieldId == field.fieldId; - - if (selected || hovered) { - drawList.AddFilledRect( - layout.fieldRowRects[visibleFieldIndex], - selected - ? (state.focused ? palette.fieldSelectedFocusedColor : palette.fieldSelectedColor) - : palette.fieldHoverColor, - metrics.cornerRounding); - } - } -} - -void AppendUIEditorPropertyGridForeground( - ::XCEngine::UI::UIDrawList& drawList, - const UIEditorPropertyGridLayout& layout, - const std::vector& sections, - const UIEditorPropertyGridState& state, - const ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel, - const UIEditorPropertyGridPalette& palette, - const UIEditorPropertyGridMetrics& metrics, - const UIEditorMenuPopupPalette& popupPalette, - const UIEditorMenuPopupMetrics& popupMetrics) { - drawList.PushClipRect(layout.bounds); - - for (std::size_t sectionVisibleIndex = 0u; - sectionVisibleIndex < layout.sectionHeaderRects.size(); - ++sectionVisibleIndex) { - const UIEditorPropertyGridSection& section = - sections[layout.sectionIndices[sectionVisibleIndex]]; - drawList.AddText( - Internal::ResolveDisclosureGlyphPosition( - layout.sectionDisclosureRects[sectionVisibleIndex], - metrics), - layout.sectionExpanded[sectionVisibleIndex] ? "v" : ">", - palette.disclosureColor, - metrics.disclosureGlyphFontSize); - drawList.AddText( - ::XCEngine::UI::UIPoint( - layout.sectionTitleRects[sectionVisibleIndex].x, - layout.sectionTitleRects[sectionVisibleIndex].y + metrics.sectionTextInsetY), - section.title, - palette.sectionTextColor, - metrics.sectionFontSize); - } - - const UIEditorBoolFieldMetrics boolMetrics = - ::XCEngine::UI::Editor::BuildUIEditorPropertyGridBoolFieldMetrics(metrics); - const UIEditorBoolFieldPalette boolPalette = - ::XCEngine::UI::Editor::BuildUIEditorPropertyGridBoolFieldPalette(palette); - const UIEditorNumberFieldMetrics numberMetrics = - ::XCEngine::UI::Editor::BuildUIEditorPropertyGridNumberFieldMetrics(metrics); - const UIEditorNumberFieldPalette numberPalette = - ::XCEngine::UI::Editor::BuildUIEditorPropertyGridNumberFieldPalette(palette); - const UIEditorAssetFieldMetrics assetMetrics = - ::XCEngine::UI::Editor::BuildUIEditorPropertyGridAssetFieldMetrics(metrics); - const UIEditorAssetFieldPalette assetPalette = - ::XCEngine::UI::Editor::BuildUIEditorPropertyGridAssetFieldPalette(palette); - const UIEditorTextFieldMetrics textMetrics = - ::XCEngine::UI::Editor::BuildUIEditorPropertyGridTextFieldMetrics(metrics); - const UIEditorTextFieldPalette textPalette = - ::XCEngine::UI::Editor::BuildUIEditorPropertyGridTextFieldPalette(palette); - const UIEditorVector2FieldMetrics vector2Metrics = - ::XCEngine::UI::Editor::BuildUIEditorPropertyGridVector2FieldMetrics(metrics); - const UIEditorVector2FieldPalette vector2Palette = - ::XCEngine::UI::Editor::BuildUIEditorPropertyGridVector2FieldPalette(palette); - const UIEditorVector3FieldMetrics vector3Metrics = - ::XCEngine::UI::Editor::BuildUIEditorPropertyGridVector3FieldMetrics(metrics); - const UIEditorVector3FieldPalette vector3Palette = - ::XCEngine::UI::Editor::BuildUIEditorPropertyGridVector3FieldPalette(palette); - const UIEditorVector4FieldMetrics vector4Metrics = - ::XCEngine::UI::Editor::BuildUIEditorPropertyGridVector4FieldMetrics(metrics); - const UIEditorVector4FieldPalette vector4Palette = - ::XCEngine::UI::Editor::BuildUIEditorPropertyGridVector4FieldPalette(palette); - const UIEditorEnumFieldMetrics enumMetrics = - ::XCEngine::UI::Editor::BuildUIEditorPropertyGridEnumFieldMetrics(metrics); - const UIEditorEnumFieldPalette enumPalette = - ::XCEngine::UI::Editor::BuildUIEditorPropertyGridEnumFieldPalette(palette); - const UIEditorColorFieldMetrics colorMetrics = - ::XCEngine::UI::Editor::BuildUIEditorPropertyGridColorFieldMetrics(metrics); - const UIEditorColorFieldPalette colorPalette = - ::XCEngine::UI::Editor::BuildUIEditorPropertyGridColorFieldPalette(palette); - const ::XCEngine::UI::UIRect popupViewportRect = - Internal::ResolvePopupViewportRect(layout.bounds); - - for (std::size_t visibleFieldIndex = 0u; - visibleFieldIndex < layout.fieldRowRects.size(); - ++visibleFieldIndex) { - const UIEditorPropertyGridSection& section = - sections[layout.visibleFieldSectionIndices[visibleFieldIndex]]; - const UIEditorPropertyGridField& field = - section.fields[layout.visibleFieldIndices[visibleFieldIndex]]; - const bool editing = propertyEditModel.HasActiveEdit() && - propertyEditModel.GetActiveFieldId() == field.fieldId; - - switch (field.kind) { - case UIEditorPropertyGridFieldKind::Bool: { - UIEditorBoolFieldState fieldState = {}; - fieldState.hoveredTarget = Internal::ResolveBoolHoveredTarget(state, field); - fieldState.focused = state.focused; - fieldState.active = state.pressedFieldId == field.fieldId; - AppendUIEditorBoolField( - drawList, - layout.fieldRowRects[visibleFieldIndex], - Internal::BuildBoolFieldSpec(field), - fieldState, - boolPalette, - boolMetrics); - break; - } - - case UIEditorPropertyGridFieldKind::Number: { - UIEditorNumberFieldState fieldState = {}; - fieldState.hoveredTarget = Internal::ResolveNumberHoveredTarget(state, field); - fieldState.activeTarget = - state.pressedFieldId == field.fieldId - ? UIEditorNumberFieldHitTargetKind::ValueBox - : UIEditorNumberFieldHitTargetKind::None; - fieldState.focused = state.focused; - fieldState.editing = editing; - fieldState.displayText = editing - ? propertyEditModel.GetStagedValue() - : ResolveUIEditorPropertyGridFieldValueText(field); - AppendUIEditorNumberField( - drawList, - layout.fieldRowRects[visibleFieldIndex], - Internal::BuildNumberFieldSpec(field), - fieldState, - numberPalette, - numberMetrics); - break; - } - - case UIEditorPropertyGridFieldKind::Enum: { - UIEditorEnumFieldState fieldState = {}; - fieldState.hoveredTarget = Internal::ResolveEnumHoveredTarget(state, field); - fieldState.focused = state.focused; - fieldState.active = state.pressedFieldId == field.fieldId; - fieldState.popupOpen = state.popupFieldId == field.fieldId; - AppendUIEditorEnumField( - drawList, - layout.fieldRowRects[visibleFieldIndex], - Internal::BuildEnumFieldSpec(field), - fieldState, - enumPalette, - enumMetrics); - break; - } - - case UIEditorPropertyGridFieldKind::Asset: { - UIEditorAssetFieldState fieldState = {}; - if (const auto* visualState = - Internal::FindAssetFieldVisualState(state, field.fieldId); - visualState != nullptr) { - fieldState = visualState->state; - } else { - fieldState.hoveredTarget = Internal::ResolveAssetHoveredTarget(state, field); - fieldState.activeTarget = - state.pressedFieldId == field.fieldId - ? UIEditorAssetFieldHitTargetKind::ValueBox - : UIEditorAssetFieldHitTargetKind::None; - fieldState.focused = state.focused; - } - - AppendUIEditorAssetField( - drawList, - layout.fieldRowRects[visibleFieldIndex], - Internal::BuildAssetFieldSpec(field), - fieldState, - assetPalette, - assetMetrics); - break; - } - - case UIEditorPropertyGridFieldKind::Color: { - UIEditorColorFieldState fieldState = {}; - if (const auto* visualState = - Internal::FindColorFieldVisualState(state, field.fieldId); - visualState != nullptr) { - fieldState = visualState->state; - } else { - fieldState.hoveredTarget = Internal::ResolveColorHoveredTarget(state, field); - fieldState.focused = state.focused; - } - - AppendUIEditorColorField( - drawList, - layout.fieldRowRects[visibleFieldIndex], - Internal::BuildColorFieldSpec(field), - fieldState, - colorPalette, - colorMetrics, - popupViewportRect); - break; - } - - case UIEditorPropertyGridFieldKind::Vector2: { - UIEditorVector2FieldState fieldState = {}; - if (const auto* visualState = - Internal::FindVector2FieldVisualState(state, field.fieldId); - visualState != nullptr) { - fieldState = visualState->state; - } else { - fieldState.hoveredTarget = Internal::ResolveVector2HoveredTarget(state, field); - fieldState.activeTarget = - state.pressedFieldId == field.fieldId - ? UIEditorVector2FieldHitTargetKind::Row - : UIEditorVector2FieldHitTargetKind::None; - fieldState.focused = state.focused; - } - AppendUIEditorVector2Field( - drawList, - layout.fieldRowRects[visibleFieldIndex], - Internal::BuildVector2FieldSpec(field), - fieldState, - vector2Palette, - vector2Metrics); - break; - } - - case UIEditorPropertyGridFieldKind::Vector3: { - UIEditorVector3FieldState fieldState = {}; - if (const auto* visualState = - Internal::FindVector3FieldVisualState(state, field.fieldId); - visualState != nullptr) { - fieldState = visualState->state; - } else { - fieldState.hoveredTarget = Internal::ResolveVector3HoveredTarget(state, field); - fieldState.activeTarget = - state.pressedFieldId == field.fieldId - ? UIEditorVector3FieldHitTargetKind::Row - : UIEditorVector3FieldHitTargetKind::None; - fieldState.focused = state.focused; - } - AppendUIEditorVector3Field( - drawList, - layout.fieldRowRects[visibleFieldIndex], - Internal::BuildVector3FieldSpec(field), - fieldState, - vector3Palette, - vector3Metrics); - break; - } - - case UIEditorPropertyGridFieldKind::Vector4: { - UIEditorVector4FieldState fieldState = {}; - if (const auto* visualState = - Internal::FindVector4FieldVisualState(state, field.fieldId); - visualState != nullptr) { - fieldState = visualState->state; - } else { - fieldState.hoveredTarget = Internal::ResolveVector4HoveredTarget(state, field); - fieldState.activeTarget = - state.pressedFieldId == field.fieldId - ? UIEditorVector4FieldHitTargetKind::Row - : UIEditorVector4FieldHitTargetKind::None; - fieldState.focused = state.focused; - } - AppendUIEditorVector4Field( - drawList, - layout.fieldRowRects[visibleFieldIndex], - Internal::BuildVector4FieldSpec(field), - fieldState, - vector4Palette, - vector4Metrics); - break; - } - - case UIEditorPropertyGridFieldKind::Text: - default: { - const std::string& displayedValue = - Internal::ResolveDisplayedTextFieldValue(field, propertyEditModel); - UIEditorTextFieldState fieldState = {}; - fieldState.hoveredTarget = Internal::ResolveTextHoveredTarget(state, field); - fieldState.activeTarget = - state.pressedFieldId == field.fieldId - ? (state.hoveredHitTarget == UIEditorPropertyGridHitTargetKind::ValueBox - ? UIEditorTextFieldHitTargetKind::ValueBox - : UIEditorTextFieldHitTargetKind::Row) - : UIEditorTextFieldHitTargetKind::None; - fieldState.focused = state.focused; - fieldState.editing = editing; - fieldState.displayText = displayedValue; - - UIEditorTextFieldSpec fieldSpec = Internal::BuildTextFieldSpec(field); - fieldSpec.value = editing ? std::string() : displayedValue; - AppendUIEditorTextField( - drawList, - layout.fieldRowRects[visibleFieldIndex], - fieldSpec, - fieldState, - textPalette, - textMetrics); - - if (editing) { - drawList.PushClipRect( - ResolveUIEditorTextClipRect( - layout.fieldValueRects[visibleFieldIndex], - metrics.tagFontSize)); - drawList.AddText( - ::XCEngine::UI::UIPoint( - layout.fieldValueRects[visibleFieldIndex].x + - (std::max)( - 0.0f, - layout.fieldValueRects[visibleFieldIndex].width - 34.0f), - ResolveUIEditorTextTop( - layout.fieldValueRects[visibleFieldIndex], - metrics.tagFontSize, - metrics.valueTextInsetY)), - "EDIT", - palette.editTagColor, - metrics.tagFontSize); - drawList.PopClipRect(); - } - break; - } - } - } - - drawList.PopClipRect(); - - UIEditorMenuPopupLayout popupLayout = {}; - UIEditorMenuPopupState popupState = {}; - std::vector popupItems = {}; - if (Internal::BuildEnumPopupRuntime( - layout, - sections, - state, - popupMetrics, - popupLayout, - popupState, - popupItems)) { - AppendUIEditorMenuPopupBackground( - drawList, - popupLayout, - popupItems, - popupState, - popupPalette, - popupMetrics); - AppendUIEditorMenuPopupForeground( - drawList, - popupLayout, - popupItems, - popupState, - popupPalette, - popupMetrics); - } -} - -void AppendUIEditorPropertyGrid( - ::XCEngine::UI::UIDrawList& drawList, - const ::XCEngine::UI::UIRect& bounds, - const std::vector& sections, - const ::XCEngine::UI::Widgets::UISelectionModel& selectionModel, - const ::XCEngine::UI::Widgets::UIExpansionModel& expansionModel, - const ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel, - const UIEditorPropertyGridState& state, - const UIEditorPropertyGridPalette& palette, - const UIEditorPropertyGridMetrics& metrics, - const UIEditorMenuPopupPalette& popupPalette, - const UIEditorMenuPopupMetrics& popupMetrics) { - const UIEditorPropertyGridLayout layout = - BuildUIEditorPropertyGridLayout(bounds, sections, expansionModel, metrics); - AppendUIEditorPropertyGridBackground( - drawList, - layout, - sections, - selectionModel, - propertyEditModel, - state, - palette, - metrics); - AppendUIEditorPropertyGridForeground( - drawList, - layout, - sections, - state, - propertyEditModel, - palette, - metrics, - popupPalette, - popupMetrics); -} - -} // namespace XCEngine::UI::Editor::Widgets diff --git a/new_editor/src/Fields/UIEditorBoolField.cpp b/new_editor/src/Fields/UIEditorBoolField.cpp index 2d97fec4..3bb14d0b 100644 --- a/new_editor/src/Fields/UIEditorBoolField.cpp +++ b/new_editor/src/Fields/UIEditorBoolField.cpp @@ -19,6 +19,19 @@ bool ContainsPoint(const ::XCEngine::UI::UIRect& rect, const ::XCEngine::UI::UIP point.y <= rect.y + rect.height; } +void AppendCheckMark( + ::XCEngine::UI::UIDrawList& drawList, + const ::XCEngine::UI::UIRect& rect, + const ::XCEngine::UI::UIColor& color) { + const float size = (std::min)(rect.width, rect.height); + const float pad = (std::max)(size * 0.22f, 2.0f); + const ::XCEngine::UI::UIPoint a(rect.x + pad, rect.y + size * 0.56f); + const ::XCEngine::UI::UIPoint b(rect.x + size * 0.44f, rect.y + size - pad); + const ::XCEngine::UI::UIPoint c(rect.x + size - pad, rect.y + pad); + drawList.AddLine(a, b, color, 2.0f); + drawList.AddLine(b, c, color, 2.0f); +} + } // namespace UIEditorBoolFieldLayout BuildUIEditorBoolFieldLayout( @@ -101,16 +114,7 @@ void AppendUIEditorBoolFieldForeground( drawList.PopClipRect(); if (spec.value) { - drawList.AddText( - ::XCEngine::UI::UIPoint( - layout.checkmarkRect.x + metrics.checkboxGlyphInsetX, - ResolveUIEditorTextTop( - layout.checkmarkRect, - metrics.checkboxGlyphFontSize, - metrics.checkboxGlyphInsetY)), - "V", - palette.checkboxMarkColor, - metrics.checkboxGlyphFontSize); + AppendCheckMark(drawList, layout.checkmarkRect, palette.checkboxMarkColor); } } diff --git a/new_editor/src/Fields/UIEditorColorField.cpp b/new_editor/src/Fields/UIEditorColorField.cpp index 8703099a..e53b26d9 100644 --- a/new_editor/src/Fields/UIEditorColorField.cpp +++ b/new_editor/src/Fields/UIEditorColorField.cpp @@ -94,69 +94,88 @@ UIEditorColorFieldLayout BuildUIEditorColorFieldLayout( const UIEditorColorFieldMetrics& metrics, const UIRect& viewportRect) { const UIEditorColorFieldMetrics resolvedMetrics = Internal::ResolveMetrics(metrics); - const UIEditorFieldRowLayout hostLayout = BuildUIEditorFieldRowLayout( - bounds, - resolvedMetrics.swatchWidth, - UIEditorFieldRowLayoutMetrics { - resolvedMetrics.rowHeight, - resolvedMetrics.horizontalPadding, - resolvedMetrics.labelControlGap, - resolvedMetrics.controlColumnStart, - resolvedMetrics.controlTrailingInset, - resolvedMetrics.swatchInsetY, - }); - UIEditorColorFieldLayout layout = {}; - layout.bounds = hostLayout.bounds; - layout.labelRect = hostLayout.labelRect; - layout.controlRect = hostLayout.controlRect; - layout.swatchRect = UIRect( - hostLayout.controlRect.x, - hostLayout.controlRect.y + resolvedMetrics.swatchInsetY, - (std::min)(resolvedMetrics.swatchWidth, hostLayout.controlRect.width), - Internal::ClampColorFieldNonNegative( - hostLayout.controlRect.height - resolvedMetrics.swatchInsetY * 2.0f)); + layout.embeddedEditor = spec.embeddedEditor; - const auto placement = ResolvePopupPlacementRect( - layout.swatchRect, - UISize( - resolvedMetrics.popupWidth, - ResolvePopupHeight(resolvedMetrics, spec.showAlpha)), - ResolveViewportRect(layout.swatchRect, viewportRect), - UIPopupPlacement::BottomStart); - layout.popupRect = placement.rect; - layout.popupHeaderRect = UIRect( - layout.popupRect.x, - layout.popupRect.y, - layout.popupRect.width, - resolvedMetrics.popupHeaderHeight); - layout.popupTitleRect = UIRect( - layout.popupRect.x + resolvedMetrics.popupPadding, - layout.popupRect.y, - Internal::ClampColorFieldNonNegative( - layout.popupRect.width - - resolvedMetrics.popupPadding * 3.0f - - resolvedMetrics.popupCloseButtonSize), - resolvedMetrics.popupHeaderHeight); - layout.popupCloseButtonRect = UIRect( - layout.popupRect.x + layout.popupRect.width - - resolvedMetrics.popupPadding - + if (spec.embeddedEditor) { + layout.bounds = UIRect( + bounds.x, + bounds.y, + Internal::ClampColorFieldNonNegative(bounds.width), + Internal::ClampColorFieldNonNegative(bounds.height)); + layout.popupRect = layout.bounds; + layout.popupBodyRect = UIRect( + layout.popupRect.x + resolvedMetrics.popupPadding, + layout.popupRect.y + resolvedMetrics.popupPadding, + Internal::ClampColorFieldNonNegative( + layout.popupRect.width - resolvedMetrics.popupPadding * 2.0f), + Internal::ClampColorFieldNonNegative( + layout.popupRect.height - resolvedMetrics.popupPadding * 2.0f)); + } else { + const UIEditorFieldRowLayout hostLayout = BuildUIEditorFieldRowLayout( + bounds, + resolvedMetrics.swatchWidth, + UIEditorFieldRowLayoutMetrics { + resolvedMetrics.rowHeight, + resolvedMetrics.horizontalPadding, + resolvedMetrics.labelControlGap, + resolvedMetrics.controlColumnStart, + resolvedMetrics.controlTrailingInset, + resolvedMetrics.swatchInsetY, + }); + + layout.bounds = hostLayout.bounds; + layout.labelRect = hostLayout.labelRect; + layout.controlRect = hostLayout.controlRect; + layout.swatchRect = UIRect( + hostLayout.controlRect.x, + hostLayout.controlRect.y + resolvedMetrics.swatchInsetY, + (std::min)(resolvedMetrics.swatchWidth, hostLayout.controlRect.width), + Internal::ClampColorFieldNonNegative( + hostLayout.controlRect.height - resolvedMetrics.swatchInsetY * 2.0f)); + + const auto placement = ResolvePopupPlacementRect( + layout.swatchRect, + UISize( + resolvedMetrics.popupWidth, + ResolvePopupHeight(resolvedMetrics, spec.showAlpha)), + ResolveViewportRect(layout.swatchRect, viewportRect), + UIPopupPlacement::BottomStart); + layout.popupRect = placement.rect; + layout.popupHeaderRect = UIRect( + layout.popupRect.x, + layout.popupRect.y, + layout.popupRect.width, + resolvedMetrics.popupHeaderHeight); + layout.popupTitleRect = UIRect( + layout.popupRect.x + resolvedMetrics.popupPadding, + layout.popupRect.y, + Internal::ClampColorFieldNonNegative( + layout.popupRect.width - + resolvedMetrics.popupPadding * 3.0f - + resolvedMetrics.popupCloseButtonSize), + resolvedMetrics.popupHeaderHeight); + layout.popupCloseButtonRect = UIRect( + layout.popupRect.x + layout.popupRect.width - + resolvedMetrics.popupPadding - + resolvedMetrics.popupCloseButtonSize, + layout.popupRect.y + + (resolvedMetrics.popupHeaderHeight - + resolvedMetrics.popupCloseButtonSize) * + 0.5f, resolvedMetrics.popupCloseButtonSize, - layout.popupRect.y + - (resolvedMetrics.popupHeaderHeight - - resolvedMetrics.popupCloseButtonSize) * - 0.5f, - resolvedMetrics.popupCloseButtonSize, - resolvedMetrics.popupCloseButtonSize); - layout.popupBodyRect = UIRect( - layout.popupRect.x + resolvedMetrics.popupPadding, - layout.popupRect.y + resolvedMetrics.popupHeaderHeight + - resolvedMetrics.popupPadding, - Internal::ClampColorFieldNonNegative( - layout.popupRect.width - resolvedMetrics.popupPadding * 2.0f), - Internal::ClampColorFieldNonNegative( - layout.popupRect.height - resolvedMetrics.popupHeaderHeight - - resolvedMetrics.popupPadding * 2.0f)); + resolvedMetrics.popupCloseButtonSize); + layout.popupBodyRect = UIRect( + layout.popupRect.x + resolvedMetrics.popupPadding, + layout.popupRect.y + resolvedMetrics.popupHeaderHeight + + resolvedMetrics.popupPadding, + Internal::ClampColorFieldNonNegative( + layout.popupRect.width - resolvedMetrics.popupPadding * 2.0f), + Internal::ClampColorFieldNonNegative( + layout.popupRect.height - resolvedMetrics.popupHeaderHeight - + resolvedMetrics.popupPadding * 2.0f)); + } + layout.popupTopRowRect = UIRect( layout.popupBodyRect.x, layout.popupBodyRect.y, @@ -256,8 +275,10 @@ UIEditorColorFieldHitTarget HitTestUIEditorColorField( const UIEditorColorFieldLayout& layout, bool popupOpen, const UIPoint& point) { - if (popupOpen && ContainsPoint(layout.popupRect, point)) { - if (ContainsPoint(layout.popupCloseButtonRect, point)) { + const bool editorVisible = layout.embeddedEditor || popupOpen; + if (editorVisible && ContainsPoint(layout.popupRect, point)) { + if (!layout.embeddedEditor && + ContainsPoint(layout.popupCloseButtonRect, point)) { return { UIEditorColorFieldHitTargetKind::PopupCloseButton }; } @@ -288,6 +309,10 @@ UIEditorColorFieldHitTarget HitTestUIEditorColorField( return { UIEditorColorFieldHitTargetKind::PopupSurface }; } + if (layout.embeddedEditor) { + return {}; + } + if (ContainsPoint(layout.swatchRect, point)) { return { UIEditorColorFieldHitTargetKind::Swatch }; } diff --git a/new_editor/src/Fields/UIEditorColorFieldInteraction.cpp b/new_editor/src/Fields/UIEditorColorFieldInteraction.cpp index 4090a016..0d52719f 100644 --- a/new_editor/src/Fields/UIEditorColorFieldInteraction.cpp +++ b/new_editor/src/Fields/UIEditorColorFieldInteraction.cpp @@ -55,6 +55,18 @@ bool ShouldUsePointerPosition(const UIInputEvent& event) { } } +bool UsesEmbeddedEditor(const UIEditorColorFieldSpec& spec) { + return spec.embeddedEditor; +} + +void SyncEditorVisibility( + UIEditorColorFieldInteractionState& state, + const UIEditorColorFieldSpec& spec) { + if (UsesEmbeddedEditor(spec)) { + state.colorFieldState.popupOpen = true; + } +} + void SyncRememberedHue( UIEditorColorFieldInteractionState& state, const UIEditorColorFieldSpec& spec) { @@ -80,7 +92,12 @@ void SyncHoverTarget( void ClosePopup( UIEditorColorFieldInteractionState& state, + const UIEditorColorFieldSpec& spec, UIEditorColorFieldInteractionResult& result) { + if (UsesEmbeddedEditor(spec)) { + return; + } + if (!state.colorFieldState.popupOpen) { return; } @@ -95,7 +112,9 @@ void OpenPopup( UIEditorColorFieldInteractionState& state, const UIEditorColorFieldSpec& spec, UIEditorColorFieldInteractionResult& result) { - if (spec.readOnly || state.colorFieldState.popupOpen) { + if (UsesEmbeddedEditor(spec) || + spec.readOnly || + state.colorFieldState.popupOpen) { return; } @@ -299,6 +318,7 @@ UIEditorColorFieldInteractionFrame UpdateUIEditorColorFieldInteraction( spec.value.b = Clamp01(spec.value.b); spec.value.a = Clamp01(spec.value.a); SyncRememberedHue(state, spec); + SyncEditorVisibility(state, spec); UIEditorColorFieldLayout layout = BuildUIEditorColorFieldLayout(bounds, spec, metrics, viewportRect); @@ -323,7 +343,7 @@ UIEditorColorFieldInteractionFrame UpdateUIEditorColorFieldInteraction( case UIInputEventType::FocusLost: eventResult.focusChanged = state.colorFieldState.focused; state.colorFieldState.focused = false; - ClosePopup(state, eventResult); + ClosePopup(state, spec, eventResult); break; case UIInputEventType::PointerMove: @@ -345,7 +365,7 @@ UIEditorColorFieldInteractionFrame UpdateUIEditorColorFieldInteraction( if (hitTarget.kind == UIEditorColorFieldHitTargetKind::None) { if (state.colorFieldState.popupOpen) { - ClosePopup(state, eventResult); + ClosePopup(state, spec, eventResult); } if (state.colorFieldState.focused) { eventResult.focusChanged = true; @@ -383,14 +403,14 @@ UIEditorColorFieldInteractionFrame UpdateUIEditorColorFieldInteraction( hitTarget.kind == UIEditorColorFieldHitTargetKind::Swatch && !spec.readOnly) { if (state.colorFieldState.popupOpen) { - ClosePopup(state, eventResult); + ClosePopup(state, spec, eventResult); } else { OpenPopup(state, spec, eventResult); } } else if ( activeTarget == UIEditorColorFieldHitTargetKind::PopupCloseButton && hitTarget.kind == UIEditorColorFieldHitTargetKind::PopupCloseButton) { - ClosePopup(state, eventResult); + ClosePopup(state, spec, eventResult); } else if (IsDragTarget(activeTarget)) { ApplyActiveDrag(state, spec, layout, eventResult); } else if ( @@ -407,7 +427,7 @@ UIEditorColorFieldInteractionFrame UpdateUIEditorColorFieldInteraction( } if (event.keyCode == static_cast(KeyCode::Escape)) { - ClosePopup(state, eventResult); + ClosePopup(state, spec, eventResult); break; } @@ -415,7 +435,7 @@ UIEditorColorFieldInteractionFrame UpdateUIEditorColorFieldInteraction( event.keyCode == static_cast(KeyCode::Space)) && !spec.readOnly) { if (state.colorFieldState.popupOpen) { - ClosePopup(state, eventResult); + ClosePopup(state, spec, eventResult); } else { OpenPopup(state, spec, eventResult); } @@ -427,6 +447,7 @@ UIEditorColorFieldInteractionFrame UpdateUIEditorColorFieldInteraction( } SyncRememberedHue(state, spec); + SyncEditorVisibility(state, spec); layout = BuildUIEditorColorFieldLayout(bounds, spec, metrics, viewportRect); SyncHoverTarget(state, layout); if (eventResult.hitTarget.kind == UIEditorColorFieldHitTargetKind::None && @@ -439,6 +460,7 @@ UIEditorColorFieldInteractionFrame UpdateUIEditorColorFieldInteraction( } SyncRememberedHue(state, spec); + SyncEditorVisibility(state, spec); layout = BuildUIEditorColorFieldLayout(bounds, spec, metrics, viewportRect); SyncHoverTarget(state, layout); if (interactionResult.hitTarget.kind == UIEditorColorFieldHitTargetKind::None && diff --git a/new_editor/src/Fields/UIEditorEditableFieldCore.cpp b/new_editor/src/Fields/UIEditorEditableFieldCore.cpp new file mode 100644 index 00000000..aa150923 --- /dev/null +++ b/new_editor/src/Fields/UIEditorEditableFieldCore.cpp @@ -0,0 +1,185 @@ +#include +#include + +namespace XCEngine::UI::Editor { + +bool ShouldUseUIEditorEditableFieldPointerPosition( + const ::XCEngine::UI::UIInputEvent& event) { + switch (event.type) { + case ::XCEngine::UI::UIInputEventType::PointerMove: + case ::XCEngine::UI::UIInputEventType::PointerEnter: + case ::XCEngine::UI::UIInputEventType::PointerButtonDown: + case ::XCEngine::UI::UIInputEventType::PointerButtonUp: + case ::XCEngine::UI::UIInputEventType::PointerWheel: + return true; + default: + return false; + } +} + +void UpdateUIEditorEditableFieldPointerPosition( + UIEditorEditableFieldSession& session, + const ::XCEngine::UI::UIInputEvent& event) { + if (ShouldUseUIEditorEditableFieldPointerPosition(event)) { + session.pointerPosition = event.position; + session.hasPointerPosition = true; + } else if (event.type == ::XCEngine::UI::UIInputEventType::PointerLeave) { + session.hasPointerPosition = false; + } +} + +float ComputeUIEditorEditableFieldDistanceSquared( + const ::XCEngine::UI::UIPoint& a, + const ::XCEngine::UI::UIPoint& b) { + const float dx = a.x - b.x; + const float dy = a.y - b.y; + return dx * dx + dy * dy; +} + +void RestartUIEditorEditableFieldCaretBlink( + UIEditorEditableFieldSession& session) { + session.caretBlinkStartNanoseconds = + Widgets::GetUIEditorTextCaretClockNanoseconds(); +} + +bool BeginUIEditorEditableFieldEdit( + UIEditorEditableFieldSession& session, + std::string_view fieldId, + std::size_t componentIndex, + std::string_view baselineText, + bool clearText) { + if (session.editing && + session.activeFieldId == fieldId && + session.activeComponentIndex == componentIndex) { + return false; + } + + session.activeFieldId = std::string(fieldId); + session.activeComponentIndex = componentIndex; + session.editing = true; + session.textInputState.value = clearText ? std::string() : std::string(baselineText); + session.textInputState.caret = session.textInputState.value.size(); + RestartUIEditorEditableFieldCaretBlink(session); + return true; +} + +void CommitUIEditorEditableFieldEdit(UIEditorEditableFieldSession& session) { + session.editing = false; + session.textInputState = {}; +} + +void CancelUIEditorEditableFieldEdit(UIEditorEditableFieldSession& session) { + session.editing = false; + session.textInputState = {}; +} + +void ArmUIEditorEditableFieldDrag( + UIEditorEditableFieldSession& session, + std::string_view fieldId, + std::size_t componentIndex, + double startValue) { + session.activeFieldId = std::string(fieldId); + session.activeComponentIndex = componentIndex; + session.pointerDownPosition = session.pointerPosition; + session.dragStartValue = startValue; + session.dragArmed = true; + session.dragActive = false; +} + +bool TryActivateUIEditorEditableFieldDrag( + UIEditorEditableFieldSession& session, + const ::XCEngine::UI::UIInputEvent& event, + float activationDistanceSquared) { + if (!session.dragArmed || session.dragActive || !event.modifiers.leftMouse) { + return false; + } + + if (ComputeUIEditorEditableFieldDistanceSquared( + session.pointerPosition, + session.pointerDownPosition) < activationDistanceSquared) { + return false; + } + + session.dragActive = true; + return true; +} + +double ResolveUIEditorEditableFieldDragDelta( + const UIEditorEditableFieldSession& session, + double sensitivity) { + return static_cast( + session.pointerPosition.x - session.pointerDownPosition.x) * + sensitivity; +} + +void EndUIEditorEditableFieldDrag(UIEditorEditableFieldSession& session) { + session.dragArmed = false; + session.dragActive = false; +} + +bool IsUIEditorEditableFieldDoubleClick( + const UIEditorEditableFieldSession& session, + std::string_view fieldId, + const ::XCEngine::UI::UIInputEvent& event, + std::size_t componentIndex) { + if (session.lastClickFieldId != fieldId || + session.lastClickComponentIndex != componentIndex) { + return false; + } + + if (event.doubleClick) { + return true; + } + + if (session.lastClickTimestampNanoseconds == 0u || + event.timestampNanoseconds < session.lastClickTimestampNanoseconds) { + return false; + } + + return event.timestampNanoseconds - session.lastClickTimestampNanoseconds <= + UIEditorEditableFieldDoubleClickIntervalNanoseconds && + ComputeUIEditorEditableFieldDistanceSquared( + session.pointerPosition, + session.lastClickPosition) <= + UIEditorEditableFieldDoubleClickDistanceSquared; +} + +void RecordUIEditorEditableFieldClick( + UIEditorEditableFieldSession& session, + std::string_view fieldId, + const ::XCEngine::UI::UIInputEvent& event, + std::size_t componentIndex) { + session.lastClickFieldId = std::string(fieldId); + session.lastClickTimestampNanoseconds = event.timestampNanoseconds; + session.lastClickPosition = session.pointerPosition; + session.lastClickComponentIndex = componentIndex; +} + +::XCEngine::UI::Text::UITextInputEditResult HandleUIEditorEditableFieldKeyDown( + UIEditorEditableFieldSession& session, + std::int32_t keyCode, + const ::XCEngine::UI::UIInputModifiers& modifiers) { + const auto result = ::XCEngine::UI::Text::HandleKeyDown( + session.textInputState, + keyCode, + modifiers); + if (result.handled && result.valueChanged) { + RestartUIEditorEditableFieldCaretBlink(session); + } + return result; +} + +bool InsertUIEditorEditableFieldCharacter( + UIEditorEditableFieldSession& session, + std::uint32_t character) { + if (!::XCEngine::UI::Text::InsertCharacter( + session.textInputState, + character)) { + return false; + } + + RestartUIEditorEditableFieldCaretBlink(session); + return true; +} + +} // namespace XCEngine::UI::Editor diff --git a/new_editor/src/Fields/UIEditorEnumField.cpp b/new_editor/src/Fields/UIEditorEnumField.cpp index b0c40813..f63317c8 100644 --- a/new_editor/src/Fields/UIEditorEnumField.cpp +++ b/new_editor/src/Fields/UIEditorEnumField.cpp @@ -3,6 +3,7 @@ #include #include +#include namespace XCEngine::UI::Editor::Widgets { @@ -33,12 +34,6 @@ UIEditorEnumFieldPalette ResolvePalette(const UIEditorEnumFieldPalette& palette) const auto& tokens = GetUIEditorInspectorFieldStyleTokens(); UIEditorEnumFieldPalette resolved = palette; - if (AreUIEditorFieldColorsEqual(palette.rowHoverColor, ::XCEngine::UI::UIColor(0.0f, 0.0f, 0.0f, 0.0f))) { - resolved.rowHoverColor = tokens.rowHoverColor; - } - if (AreUIEditorFieldColorsEqual(palette.rowActiveColor, ::XCEngine::UI::UIColor(0.0f, 0.0f, 0.0f, 0.0f))) { - resolved.rowActiveColor = tokens.rowActiveColor; - } if (AreUIEditorFieldColorsEqual(palette.focusedBorderColor, ::XCEngine::UI::UIColor(0.14f, 0.14f, 0.14f, 1.0f))) { resolved.focusedBorderColor = tokens.controlFocusedBorderColor; } @@ -86,20 +81,15 @@ void AppendDropdownChevron( ::XCEngine::UI::UIDrawList& drawList, const ::XCEngine::UI::UIRect& rect, const ::XCEngine::UI::UIColor& color) { - const float centerX = rect.x + rect.width * 0.5f; - const float centerY = rect.y + rect.height * 0.5f; + const float centerX = std::floor(rect.x + rect.width * 0.5f) + 0.5f; + const float centerY = std::floor(rect.y + rect.height * 0.5f) + 0.5f; const float halfWidth = ClampNonNegative(rect.width * 0.18f); - const float halfHeight = ClampNonNegative(rect.height * 0.10f); - drawList.AddLine( + const float halfHeight = ClampNonNegative(rect.height * 0.12f); + drawList.AddFilledTriangle( ::XCEngine::UI::UIPoint(centerX - halfWidth, centerY - halfHeight), - ::XCEngine::UI::UIPoint(centerX, centerY + halfHeight), - color, - 1.0f); - drawList.AddLine( - ::XCEngine::UI::UIPoint(centerX, centerY + halfHeight), ::XCEngine::UI::UIPoint(centerX + halfWidth, centerY - halfHeight), - color, - 1.0f); + ::XCEngine::UI::UIPoint(centerX, centerY + halfHeight), + color); } } // namespace @@ -181,11 +171,29 @@ void AppendUIEditorEnumFieldBackground( const bool controlHovered = state.hoveredTarget == UIEditorEnumFieldHitTargetKind::ValueBox || state.hoveredTarget == UIEditorEnumFieldHitTargetKind::DropdownArrow; - const ::XCEngine::UI::UIColor controlColor = + const ::XCEngine::UI::UIColor previewColor = spec.readOnly ? resolvedPalette.readOnlyColor : (controlHovered || state.popupOpen ? resolvedPalette.valueBoxHoverColor : resolvedPalette.valueBoxColor); - drawList.AddFilledRect(layout.valueRect, controlColor, resolvedMetrics.valueBoxRounding); + const ::XCEngine::UI::UIColor arrowColor = + spec.readOnly + ? resolvedPalette.readOnlyColor + : (state.popupOpen || state.hoveredTarget == UIEditorEnumFieldHitTargetKind::DropdownArrow + ? resolvedPalette.valueBoxHoverColor + : resolvedPalette.valueBoxColor); + const ::XCEngine::UI::UIRect previewRect( + layout.valueRect.x, + layout.valueRect.y, + ClampNonNegative(layout.arrowRect.x - layout.valueRect.x), + layout.valueRect.height); + drawList.AddFilledRect( + previewRect, + previewColor, + resolvedMetrics.valueBoxRounding); + drawList.AddFilledRect( + layout.arrowRect, + arrowColor, + resolvedMetrics.valueBoxRounding); drawList.AddRectOutline( layout.valueRect, state.popupOpen ? resolvedPalette.focusedBorderColor : resolvedPalette.controlBorderColor, diff --git a/new_editor/src/Fields/UIEditorFieldStyle.cpp b/new_editor/src/Fields/UIEditorFieldStyle.cpp index 8eea856b..64fb8f95 100644 --- a/new_editor/src/Fields/UIEditorFieldStyle.cpp +++ b/new_editor/src/Fields/UIEditorFieldStyle.cpp @@ -12,12 +12,66 @@ constexpr UIColor kTransparent(0.0f, 0.0f, 0.0f, 0.0f); } // namespace const Widgets::UIEditorPropertyGridMetrics& GetUIEditorFixedPropertyGridMetrics() { - static const Widgets::UIEditorPropertyGridMetrics kMetrics = {}; + static const Widgets::UIEditorPropertyGridMetrics kMetrics = [] { + const Widgets::UIEditorTreeViewMetrics& treeMetrics = + ResolveUIEditorTreeViewMetrics(); + Widgets::UIEditorPropertyGridMetrics metrics = {}; + metrics.contentInset = 0.0f; + metrics.sectionGap = 4.0f; + metrics.sectionHeaderHeight = 24.0f; + metrics.fieldRowHeight = 22.0f; + metrics.rowGap = 3.0f; + metrics.horizontalPadding = 12.0f; + metrics.sectionHeaderHorizontalPadding = 6.0f; + metrics.controlColumnStart = 236.0f; + metrics.labelControlGap = 20.0f; + metrics.disclosureExtent = treeMetrics.disclosureExtent; + metrics.disclosureLabelGap = treeMetrics.disclosureLabelGap; + metrics.sectionTextInsetY = 6.0f; + metrics.sectionFontSize = 12.0f; + metrics.labelTextInsetY = 0.0f; + metrics.labelFontSize = 11.0f; + metrics.valueTextInsetY = 0.0f; + metrics.valueFontSize = 12.0f; + metrics.valueBoxInsetY = 1.0f; + metrics.valueBoxInsetX = 5.0f; + metrics.tagFontSize = 11.0f; + metrics.cornerRounding = 2.0f; + metrics.valueBoxRounding = 2.0f; + metrics.borderThickness = 1.0f; + metrics.focusedBorderThickness = 1.0f; + metrics.editOutlineThickness = 1.0f; + return metrics; + }(); return kMetrics; } const Widgets::UIEditorPropertyGridPalette& GetUIEditorFixedPropertyGridPalette() { - return ResolveUIEditorPropertyGridPalette(); + static const Widgets::UIEditorPropertyGridPalette kPalette = [] { + Widgets::UIEditorPropertyGridPalette palette = ResolveUIEditorPropertyGridPalette(); + palette.surfaceColor = kTransparent; + palette.borderColor = kTransparent; + palette.focusedBorderColor = kTransparent; + palette.sectionHeaderColor = UIColor(0.11f, 0.11f, 0.11f, 1.0f); + palette.sectionHeaderHoverColor = UIColor(0.14f, 0.14f, 0.14f, 1.0f); + palette.fieldHoverColor = kTransparent; + palette.fieldSelectedColor = kTransparent; + palette.fieldSelectedFocusedColor = kTransparent; + palette.valueBoxColor = UIColor(0.10f, 0.10f, 0.10f, 1.0f); + palette.valueBoxHoverColor = UIColor(0.12f, 0.12f, 0.12f, 1.0f); + palette.valueBoxEditingColor = UIColor(0.14f, 0.14f, 0.14f, 1.0f); + palette.valueBoxReadOnlyColor = UIColor(0.10f, 0.10f, 0.10f, 1.0f); + palette.valueBoxBorderColor = UIColor(0.15f, 0.15f, 0.15f, 1.0f); + palette.valueBoxEditingBorderColor = UIColor(0.19f, 0.19f, 0.19f, 1.0f); + palette.disclosureColor = UIColor(0.44f, 0.44f, 0.44f, 1.0f); + palette.sectionTextColor = UIColor(0.88f, 0.88f, 0.88f, 1.0f); + palette.labelTextColor = UIColor(0.72f, 0.72f, 0.72f, 1.0f); + palette.valueTextColor = UIColor(0.88f, 0.88f, 0.88f, 1.0f); + palette.readOnlyValueTextColor = UIColor(0.62f, 0.62f, 0.62f, 1.0f); + palette.editTagColor = UIColor(0.80f, 0.80f, 0.80f, 1.0f); + return palette; + }(); + return kPalette; } Widgets::UIEditorBoolFieldMetrics BuildUIEditorPropertyGridBoolFieldMetrics( @@ -324,12 +378,16 @@ Widgets::UIEditorColorFieldMetrics BuildUIEditorPropertyGridColorFieldMetrics( const Widgets::UIEditorPropertyGridMetrics& propertyGridMetrics, const Widgets::UIEditorColorFieldMetrics& fallback) { Widgets::UIEditorColorFieldMetrics hosted = fallback; + const Widgets::UIEditorBoolFieldMetrics boolMetrics = + BuildUIEditorPropertyGridBoolFieldMetrics(propertyGridMetrics); hosted.rowHeight = propertyGridMetrics.fieldRowHeight; hosted.horizontalPadding = propertyGridMetrics.horizontalPadding; hosted.labelControlGap = propertyGridMetrics.labelControlGap; hosted.controlColumnStart = propertyGridMetrics.controlColumnStart; hosted.controlTrailingInset = propertyGridMetrics.valueBoxInsetX; - hosted.swatchInsetY = propertyGridMetrics.valueBoxInsetY; + hosted.swatchWidth = boolMetrics.checkboxSize; + hosted.swatchInsetY = + (std::max)(0.0f, (hosted.rowHeight - hosted.swatchWidth) * 0.5f); hosted.labelTextInsetY = propertyGridMetrics.labelTextInsetY; hosted.labelFontSize = propertyGridMetrics.labelFontSize; hosted.swatchRounding = propertyGridMetrics.valueBoxRounding; diff --git a/new_editor/src/Fields/UIEditorNumberField.cpp b/new_editor/src/Fields/UIEditorNumberField.cpp index 4d788f37..7485c7cc 100644 --- a/new_editor/src/Fields/UIEditorNumberField.cpp +++ b/new_editor/src/Fields/UIEditorNumberField.cpp @@ -34,12 +34,6 @@ UIEditorNumberFieldPalette ResolvePalette(const UIEditorNumberFieldPalette& pale const auto& tokens = GetUIEditorInspectorFieldStyleTokens(); UIEditorNumberFieldPalette resolved = palette; - if (AreUIEditorFieldColorsEqual(palette.rowHoverColor, ::XCEngine::UI::UIColor(0.0f, 0.0f, 0.0f, 0.0f))) { - resolved.rowHoverColor = tokens.rowHoverColor; - } - if (AreUIEditorFieldColorsEqual(palette.rowActiveColor, ::XCEngine::UI::UIColor(0.0f, 0.0f, 0.0f, 0.0f))) { - resolved.rowActiveColor = tokens.rowActiveColor; - } if (AreUIEditorFieldColorsEqual(palette.valueBoxColor, ::XCEngine::UI::UIColor(0.18f, 0.18f, 0.18f, 1.0f))) { resolved.valueBoxColor = tokens.controlColor; } @@ -149,6 +143,7 @@ void AppendValueText( const UIEditorNumberFieldLayout& layout, std::string_view text, const UIEditorNumberFieldSpec& spec, + const UIEditorNumberFieldState& state, const UIEditorNumberFieldPalette& palette, const UIEditorNumberFieldMetrics& metrics) { const UIEditorNumberFieldPalette resolvedPalette = ResolvePalette(palette); @@ -161,6 +156,18 @@ void AppendValueText( std::string(text), spec.readOnly ? resolvedPalette.readOnlyValueColor : resolvedPalette.valueColor, resolvedMetrics.valueFontSize); + if (state.editing) { + AppendUIEditorTextCaret( + drawList, + layout.valueRect, + state.displayText, + state.caretOffset, + state.caretBlinkStartNanoseconds, + resolvedPalette.valueColor, + resolvedMetrics.valueFontSize, + resolvedMetrics.valueTextInsetX, + resolvedMetrics.valueTextInsetY); + } drawList.PopClipRect(); } @@ -298,6 +305,7 @@ void AppendUIEditorNumberFieldForeground( layout, state.editing ? std::string_view(state.displayText) : std::string_view(FormatUIEditorNumberFieldValue(spec)), spec, + state, resolvedPalette, resolvedMetrics); } diff --git a/new_editor/src/Fields/UIEditorNumberFieldInteraction.cpp b/new_editor/src/Fields/UIEditorNumberFieldInteraction.cpp index 5d43dccb..a5c1b497 100644 --- a/new_editor/src/Fields/UIEditorNumberFieldInteraction.cpp +++ b/new_editor/src/Fields/UIEditorNumberFieldInteraction.cpp @@ -3,7 +3,6 @@ #include #include -#include #include namespace XCEngine::UI::Editor { @@ -14,10 +13,8 @@ using ::XCEngine::Input::KeyCode; using ::XCEngine::UI::UIInputEvent; using ::XCEngine::UI::UIInputEventType; using ::XCEngine::UI::UIPointerButton; -using ::XCEngine::UI::Text::HandleKeyDown; -using ::XCEngine::UI::Text::InsertCharacter; -using ::XCEngine::UI::Editor::Widgets::FormatUIEditorNumberFieldValue; using ::XCEngine::UI::Editor::Widgets::BuildUIEditorNumberFieldLayout; +using ::XCEngine::UI::Editor::Widgets::FormatUIEditorNumberFieldValue; using ::XCEngine::UI::Editor::Widgets::HitTestUIEditorNumberField; using ::XCEngine::UI::Editor::Widgets::IsUIEditorNumberFieldPointInside; using ::XCEngine::UI::Editor::Widgets::UIEditorNumberFieldHitTarget; @@ -26,20 +23,9 @@ using ::XCEngine::UI::Editor::Widgets::UIEditorNumberFieldLayout; using ::XCEngine::UI::Editor::Widgets::UIEditorNumberFieldMetrics; using ::XCEngine::UI::Editor::Widgets::UIEditorNumberFieldSpec; -bool ShouldUsePointerPosition(const UIInputEvent& event) { - switch (event.type) { - case UIInputEventType::PointerMove: - case UIInputEventType::PointerEnter: - case UIInputEventType::PointerButtonDown: - case UIInputEventType::PointerButtonUp: - case UIInputEventType::PointerWheel: - return true; - default: - return false; - } -} - -bool IsPermittedCharacter(const UIEditorNumberFieldSpec& spec, std::uint32_t character) { +bool IsPermittedCharacter( + const UIEditorNumberFieldSpec& spec, + std::uint32_t character) { if (character >= static_cast('0') && character <= static_cast('9')) { return true; @@ -53,24 +39,36 @@ bool IsPermittedCharacter(const UIEditorNumberFieldSpec& spec, std::uint32_t cha return !spec.integerMode && character == static_cast('.'); } -void SyncDisplayText( - UIEditorNumberFieldInteractionState& state, - const UIEditorNumberFieldSpec& spec) { - if (!state.numberFieldState.editing) { - state.numberFieldState.displayText = FormatUIEditorNumberFieldValue(spec); +double ResolveDragSensitivity(const UIEditorNumberFieldSpec& spec) { + if (spec.step != 0.0) { + return spec.step; } + + return spec.integerMode ? 1.0 : 0.1; } -void SyncHoverTarget( +void SyncDisplayState( UIEditorNumberFieldInteractionState& state, + const UIEditorNumberFieldSpec& spec, const UIEditorNumberFieldLayout& layout) { - if (!state.hasPointerPosition) { - state.numberFieldState.hoveredTarget = UIEditorNumberFieldHitTargetKind::None; + auto& fieldState = state.numberFieldState; + const auto& session = state.session; + fieldState.focused = session.focused; + fieldState.editing = session.editing; + fieldState.caretBlinkStartNanoseconds = session.caretBlinkStartNanoseconds; + fieldState.displayText = session.editing + ? session.textInputState.value + : FormatUIEditorNumberFieldValue(spec); + fieldState.caretOffset = session.editing + ? session.textInputState.caret + : fieldState.displayText.size(); + if (!session.hasPointerPosition) { + fieldState.hoveredTarget = UIEditorNumberFieldHitTargetKind::None; return; } - state.numberFieldState.hoveredTarget = - HitTestUIEditorNumberField(layout, state.pointerPosition).kind; + fieldState.hoveredTarget = + HitTestUIEditorNumberField(layout, session.pointerPosition).kind; } bool BeginEdit( @@ -81,35 +79,27 @@ bool BeginEdit( return false; } - const std::string baseline = FormatUIEditorNumberFieldValue(spec); - const bool changed = state.editModel.BeginEdit(spec.fieldId, baseline); - if (!changed && - state.editModel.HasActiveEdit() && - state.editModel.GetActiveFieldId() != spec.fieldId) { - return false; - } - if (!changed && state.numberFieldState.editing) { - return false; - } - - state.numberFieldState.editing = true; - state.textInputState.value = clearText ? std::string() : baseline; - state.textInputState.caret = state.textInputState.value.size(); - state.editModel.UpdateStagedValue(state.textInputState.value); - state.numberFieldState.displayText = state.textInputState.value; - return true; + return BeginUIEditorEditableFieldEdit( + state.session, + spec.fieldId, + UIEditorEditableFieldInvalidComponentIndex, + FormatUIEditorNumberFieldValue(spec), + clearText); } bool CommitEdit( UIEditorNumberFieldInteractionState& state, UIEditorNumberFieldSpec& spec, UIEditorNumberFieldInteractionResult& result) { - if (!state.numberFieldState.editing || !state.editModel.HasActiveEdit()) { + if (!state.session.editing) { return false; } double parsedValue = spec.value; - if (!TryParseUIEditorNumberFieldValue(spec, state.textInputState.value, parsedValue)) { + if (!TryParseUIEditorNumberFieldValue( + spec, + state.session.textInputState.value, + parsedValue)) { result.consumed = true; result.editCommitRejected = true; return false; @@ -122,11 +112,7 @@ bool CommitEdit( result.editCommitted = true; result.consumed = true; result.committedText = FormatUIEditorNumberFieldValue(spec); - - state.editModel.CommitEdit(); - state.textInputState = {}; - state.numberFieldState.editing = false; - state.numberFieldState.displayText = result.committedText; + CommitUIEditorEditableFieldEdit(state.session); return true; } @@ -134,14 +120,11 @@ bool CancelEdit( UIEditorNumberFieldInteractionState& state, const UIEditorNumberFieldSpec& spec, UIEditorNumberFieldInteractionResult& result) { - if (!state.numberFieldState.editing || !state.editModel.HasActiveEdit()) { + if (!state.session.editing) { return false; } - state.editModel.CancelEdit(); - state.textInputState = {}; - state.numberFieldState.editing = false; - state.numberFieldState.displayText = FormatUIEditorNumberFieldValue(spec); + CancelUIEditorEditableFieldEdit(state.session); result.consumed = true; result.editCanceled = true; result.valueBefore = spec.value; @@ -158,21 +141,21 @@ bool ApplyStep( return false; } - if (state.numberFieldState.editing && !CommitEdit(state, spec, result)) { + if (state.session.editing && !CommitEdit(state, spec, result)) { return result.editCommitRejected; } const double step = spec.step == 0.0 ? 1.0 : spec.step; const double before = spec.value; - spec.value = NormalizeUIEditorNumberFieldValue(spec, spec.value + step * direction); - + spec.value = NormalizeUIEditorNumberFieldValue( + spec, + spec.value + step * direction); result.consumed = true; result.stepApplied = true; result.valueBefore = before; result.valueAfter = spec.value; result.stepDelta = step * direction; result.valueChanged = before != spec.value || result.valueChanged; - state.numberFieldState.displayText = FormatUIEditorNumberFieldValue(spec); return true; } @@ -204,7 +187,6 @@ bool ApplyKeyboardStep( result.valueBefore = before; result.valueAfter = spec.value; result.valueChanged = before != spec.value; - state.numberFieldState.displayText = FormatUIEditorNumberFieldValue(spec); return true; } @@ -222,7 +204,6 @@ bool ApplyKeyboardStep( result.valueBefore = before; result.valueAfter = spec.value; result.valueChanged = before != spec.value; - state.numberFieldState.displayText = FormatUIEditorNumberFieldValue(spec); return true; } @@ -233,6 +214,28 @@ bool ApplyKeyboardStep( return false; } +bool ApplyDragValue( + UIEditorNumberFieldInteractionState& state, + UIEditorNumberFieldSpec& spec, + UIEditorNumberFieldInteractionResult& result) { + if (spec.readOnly || state.session.editing) { + return false; + } + + const double before = spec.value; + spec.value = NormalizeUIEditorNumberFieldValue( + spec, + state.session.dragStartValue + + ResolveUIEditorEditableFieldDragDelta( + state.session, + ResolveDragSensitivity(spec))); + result.consumed = true; + result.valueBefore = before; + result.valueAfter = spec.value; + result.valueChanged = before != spec.value; + return true; +} + } // namespace UIEditorNumberFieldInteractionFrame UpdateUIEditorNumberFieldInteraction( @@ -241,32 +244,31 @@ UIEditorNumberFieldInteractionFrame UpdateUIEditorNumberFieldInteraction( const ::XCEngine::UI::UIRect& bounds, const std::vector& inputEvents, const UIEditorNumberFieldMetrics& metrics) { - UIEditorNumberFieldLayout layout = BuildUIEditorNumberFieldLayout(bounds, spec, metrics); - SyncDisplayText(state, spec); - SyncHoverTarget(state, layout); + UIEditorNumberFieldLayout layout = BuildUIEditorNumberFieldLayout( + bounds, + spec, + metrics); + SyncDisplayState(state, spec, layout); UIEditorNumberFieldInteractionResult interactionResult = {}; for (const UIInputEvent& event : inputEvents) { - if (ShouldUsePointerPosition(event)) { - state.pointerPosition = event.position; - state.hasPointerPosition = true; - } else if (event.type == UIInputEventType::PointerLeave) { - state.hasPointerPosition = false; - } + UpdateUIEditorEditableFieldPointerPosition(state.session, event); UIEditorNumberFieldInteractionResult eventResult = {}; switch (event.type) { case UIInputEventType::FocusGained: - eventResult.focusChanged = !state.numberFieldState.focused; - state.numberFieldState.focused = true; + eventResult.focusChanged = !state.session.focused; + state.session.focused = true; break; case UIInputEventType::FocusLost: - eventResult.focusChanged = state.numberFieldState.focused; - state.numberFieldState.focused = false; - state.numberFieldState.activeTarget = UIEditorNumberFieldHitTargetKind::None; - state.hasPointerPosition = false; - if (state.numberFieldState.editing) { + eventResult.focusChanged = state.session.focused; + state.session.focused = false; + state.numberFieldState.activeTarget = + UIEditorNumberFieldHitTargetKind::None; + state.session.hasPointerPosition = false; + EndUIEditorEditableFieldDrag(state.session); + if (state.session.editing) { CommitEdit(state, spec, eventResult); if (eventResult.editCommitRejected) { CancelEdit(state, spec, eventResult); @@ -275,14 +277,24 @@ UIEditorNumberFieldInteractionFrame UpdateUIEditorNumberFieldInteraction( break; case UIInputEventType::PointerMove: + TryActivateUIEditorEditableFieldDrag(state.session, event); + if (state.session.dragActive && + state.numberFieldState.activeTarget == + UIEditorNumberFieldHitTargetKind::ValueBox) { + ApplyDragValue(state, spec, eventResult); + } + break; + case UIInputEventType::PointerEnter: case UIInputEventType::PointerLeave: break; case UIInputEventType::PointerButtonDown: { const UIEditorNumberFieldHitTarget hitTarget = - state.hasPointerPosition - ? HitTestUIEditorNumberField(layout, state.pointerPosition) + state.session.hasPointerPosition + ? HitTestUIEditorNumberField( + layout, + state.session.pointerPosition) : UIEditorNumberFieldHitTarget {}; eventResult.hitTarget = hitTarget; @@ -291,72 +303,107 @@ UIEditorNumberFieldInteractionFrame UpdateUIEditorNumberFieldInteraction( } const bool insideField = - state.hasPointerPosition && - IsUIEditorNumberFieldPointInside(layout.bounds, state.pointerPosition); + state.session.hasPointerPosition && + IsUIEditorNumberFieldPointInside( + layout.bounds, + state.session.pointerPosition); if (insideField) { - eventResult.focusChanged = !state.numberFieldState.focused; - state.numberFieldState.focused = true; + eventResult.focusChanged = !state.session.focused; + state.session.focused = true; state.numberFieldState.activeTarget = hitTarget.kind == UIEditorNumberFieldHitTargetKind::None ? UIEditorNumberFieldHitTargetKind::Row : hitTarget.kind; + if (hitTarget.kind == + UIEditorNumberFieldHitTargetKind::ValueBox && + !state.session.editing) { + ArmUIEditorEditableFieldDrag( + state.session, + spec.fieldId, + UIEditorEditableFieldInvalidComponentIndex, + spec.value); + } else { + EndUIEditorEditableFieldDrag(state.session); + } eventResult.consumed = true; } else { - if (state.numberFieldState.editing) { + if (state.session.editing) { CommitEdit(state, spec, eventResult); if (!eventResult.editCommitRejected) { - eventResult.focusChanged = state.numberFieldState.focused; - state.numberFieldState.focused = false; + eventResult.focusChanged = state.session.focused; + state.session.focused = false; } - } else if (state.numberFieldState.focused) { + } else if (state.session.focused) { eventResult.focusChanged = true; - state.numberFieldState.focused = false; + state.session.focused = false; } - state.numberFieldState.activeTarget = UIEditorNumberFieldHitTargetKind::None; + state.numberFieldState.activeTarget = + UIEditorNumberFieldHitTargetKind::None; + EndUIEditorEditableFieldDrag(state.session); } break; } case UIInputEventType::PointerButtonUp: { const UIEditorNumberFieldHitTarget hitTarget = - state.hasPointerPosition - ? HitTestUIEditorNumberField(layout, state.pointerPosition) + state.session.hasPointerPosition + ? HitTestUIEditorNumberField( + layout, + state.session.pointerPosition) : UIEditorNumberFieldHitTarget {}; eventResult.hitTarget = hitTarget; if (event.pointerButton == UIPointerButton::Left) { - const UIEditorNumberFieldHitTargetKind activeTarget = state.numberFieldState.activeTarget; - state.numberFieldState.activeTarget = UIEditorNumberFieldHitTargetKind::None; + const UIEditorNumberFieldHitTargetKind activeTarget = + state.numberFieldState.activeTarget; + state.numberFieldState.activeTarget = + UIEditorNumberFieldHitTargetKind::None; if (activeTarget == UIEditorNumberFieldHitTargetKind::ValueBox && hitTarget.kind == UIEditorNumberFieldHitTargetKind::ValueBox) { - if (!state.numberFieldState.editing) { - eventResult.editStarted = BeginEdit(state, spec, false); + if (state.session.dragActive) { + eventResult.consumed = true; + } else if (!state.session.editing && + IsUIEditorEditableFieldDoubleClick( + state.session, + spec.fieldId, + event, + UIEditorEditableFieldInvalidComponentIndex)) { + eventResult.editStarted = BeginEdit(state, spec, true); + eventResult.consumed = true; + } else { + RecordUIEditorEditableFieldClick( + state.session, + spec.fieldId, + event, + UIEditorEditableFieldInvalidComponentIndex); + eventResult.consumed = true; } - eventResult.consumed = true; } else if (hitTarget.kind == UIEditorNumberFieldHitTargetKind::Row) { eventResult.consumed = true; } + + EndUIEditorEditableFieldDrag(state.session); } break; } case UIInputEventType::KeyDown: - if (!state.numberFieldState.focused) { + if (!state.session.focused) { break; } - if (state.numberFieldState.editing) { + if (state.session.editing) { if (event.keyCode == static_cast(KeyCode::Escape)) { CancelEdit(state, spec, eventResult); break; } - const auto textResult = - HandleKeyDown(state.textInputState, event.keyCode, event.modifiers); + const auto textResult = HandleUIEditorEditableFieldKeyDown( + state.session, + event.keyCode, + event.modifiers); if (textResult.handled) { - state.editModel.UpdateStagedValue(state.textInputState.value); - state.numberFieldState.displayText = state.textInputState.value; eventResult.consumed = true; if (textResult.valueChanged) { eventResult.valueBefore = spec.value; @@ -379,7 +426,7 @@ UIEditorNumberFieldInteractionFrame UpdateUIEditorNumberFieldInteraction( break; case UIInputEventType::Character: - if (!state.numberFieldState.focused || + if (!state.session.focused || spec.readOnly || event.modifiers.control || event.modifiers.alt || @@ -388,13 +435,13 @@ UIEditorNumberFieldInteractionFrame UpdateUIEditorNumberFieldInteraction( break; } - if (!state.numberFieldState.editing) { + if (!state.session.editing) { eventResult.editStarted = BeginEdit(state, spec, true); } - if (InsertCharacter(state.textInputState, event.character)) { - state.editModel.UpdateStagedValue(state.textInputState.value); - state.numberFieldState.displayText = state.textInputState.value; + if (InsertUIEditorEditableFieldCharacter( + state.session, + event.character)) { eventResult.consumed = true; } break; @@ -404,11 +451,12 @@ UIEditorNumberFieldInteractionFrame UpdateUIEditorNumberFieldInteraction( } layout = BuildUIEditorNumberFieldLayout(bounds, spec, metrics); - SyncDisplayText(state, spec); - SyncHoverTarget(state, layout); + SyncDisplayState(state, spec, layout); if (eventResult.hitTarget.kind == UIEditorNumberFieldHitTargetKind::None && - state.hasPointerPosition) { - eventResult.hitTarget = HitTestUIEditorNumberField(layout, state.pointerPosition); + state.session.hasPointerPosition) { + eventResult.hitTarget = HitTestUIEditorNumberField( + layout, + state.session.pointerPosition); } if (eventResult.consumed || @@ -419,17 +467,19 @@ UIEditorNumberFieldInteractionFrame UpdateUIEditorNumberFieldInteraction( eventResult.editCommitted || eventResult.editCommitRejected || eventResult.editCanceled || - eventResult.hitTarget.kind != UIEditorNumberFieldHitTargetKind::None) { + eventResult.hitTarget.kind != + UIEditorNumberFieldHitTargetKind::None) { interactionResult = std::move(eventResult); } } layout = BuildUIEditorNumberFieldLayout(bounds, spec, metrics); - SyncDisplayText(state, spec); - SyncHoverTarget(state, layout); + SyncDisplayState(state, spec, layout); if (interactionResult.hitTarget.kind == UIEditorNumberFieldHitTargetKind::None && - state.hasPointerPosition) { - interactionResult.hitTarget = HitTestUIEditorNumberField(layout, state.pointerPosition); + state.session.hasPointerPosition) { + interactionResult.hitTarget = HitTestUIEditorNumberField( + layout, + state.session.pointerPosition); } return { diff --git a/new_editor/src/Fields/UIEditorPropertyGrid.cpp b/new_editor/src/Fields/UIEditorPropertyGrid.cpp index 04103444..ce49881c 100644 --- a/new_editor/src/Fields/UIEditorPropertyGrid.cpp +++ b/new_editor/src/Fields/UIEditorPropertyGrid.cpp @@ -1,6 +1,65 @@ #include "Fields/PropertyGridInternal.h" +#include +#include +#include + +#include + #include +#include +#include + +namespace { + +bool HasVisibleAlpha(const ::XCEngine::UI::UIColor& color) { + return color.a > 0.0f; +} + +void AppendDisclosureArrow( + ::XCEngine::UI::UIDrawList& drawList, + const ::XCEngine::UI::UIRect& rect, + bool expanded, + const ::XCEngine::UI::UIColor& color) { + if (!HasVisibleAlpha(color)) { + return; + } + + constexpr float kOpticalCenterYOffset = -1.0f; + const float centerX = std::floor(rect.x + rect.width * 0.5f) + 0.5f; + const float centerY = + std::floor(rect.y + rect.height * 0.5f + kOpticalCenterYOffset) + 0.5f; + const float halfExtent = + (std::max)(3.0f, std::floor((std::min)(rect.width, rect.height) * 0.24f)); + const float triangleHeight = halfExtent * 1.45f; + + ::XCEngine::UI::UIPoint points[3] = {}; + if (expanded) { + points[0] = ::XCEngine::UI::UIPoint( + centerX - halfExtent, + centerY - triangleHeight * 0.5f); + points[1] = ::XCEngine::UI::UIPoint( + centerX + halfExtent, + centerY - triangleHeight * 0.5f); + points[2] = ::XCEngine::UI::UIPoint( + centerX, + centerY + triangleHeight * 0.5f); + } else { + points[0] = ::XCEngine::UI::UIPoint( + centerX - triangleHeight * 0.5f, + centerY - halfExtent); + points[1] = ::XCEngine::UI::UIPoint( + centerX - triangleHeight * 0.5f, + centerY + halfExtent); + points[2] = ::XCEngine::UI::UIPoint( + centerX + triangleHeight * 0.5f, + centerY); + } + + drawList.AddFilledTriangle(points[0], points[1], points[2], color); +} + +} // namespace namespace XCEngine::UI::Editor::Widgets { @@ -126,7 +185,7 @@ UIEditorPropertyGridLayout BuildUIEditorPropertyGridLayout( contentWidth, headerHeight); const ::XCEngine::UI::UIRect disclosureRect( - headerRect.x + metrics.horizontalPadding, + headerRect.x + metrics.sectionHeaderHorizontalPadding, headerRect.y + (headerRect.height - metrics.disclosureExtent) * 0.5f, metrics.disclosureExtent, metrics.disclosureExtent); @@ -137,7 +196,8 @@ UIEditorPropertyGridLayout BuildUIEditorPropertyGridLayout( headerRect.y, (std::max)( 0.0f, - headerRect.x + headerRect.width - titleX - metrics.horizontalPadding), + headerRect.x + headerRect.width - titleX - + metrics.sectionHeaderHorizontalPadding), headerRect.height); layout.sectionIndices.push_back(sectionIndex); @@ -224,3 +284,946 @@ UIEditorPropertyGridHitTarget HitTestUIEditorPropertyGrid( } } // namespace XCEngine::UI::Editor::Widgets + +namespace XCEngine::UI::Editor::Widgets::Internal { + +using ::XCEngine::UI::UIPoint; +using ::XCEngine::UI::UIRect; +using ::XCEngine::UI::UISize; +using ::XCEngine::UI::Editor::UIEditorMenuItemKind; +using ::XCEngine::UI::Widgets::ResolvePopupPlacementRect; +using ::XCEngine::UI::Widgets::UIPopupPlacement; + +float ClampNonNegative(float value) { + return (std::max)(value, 0.0f); +} + +float ResolveSectionHeaderHeight( + const UIEditorPropertyGridSection& section, + const UIEditorPropertyGridMetrics& metrics) { + return section.desiredHeaderHeight > 0.0f + ? section.desiredHeaderHeight + : metrics.sectionHeaderHeight; +} + +float ResolveFieldRowHeight( + const UIEditorPropertyGridField& field, + const UIEditorPropertyGridMetrics& metrics) { + return field.desiredHeight > 0.0f + ? field.desiredHeight + : metrics.fieldRowHeight; +} + +UIEditorBoolFieldSpec BuildBoolFieldSpec(const UIEditorPropertyGridField& field) { + UIEditorBoolFieldSpec spec = {}; + spec.fieldId = field.fieldId; + spec.label = field.label; + spec.value = field.boolValue; + spec.readOnly = field.readOnly; + return spec; +} + +UIEditorNumberFieldSpec BuildNumberFieldSpec(const UIEditorPropertyGridField& field) { + UIEditorNumberFieldSpec spec = {}; + spec.fieldId = field.fieldId; + spec.label = field.label; + spec.value = field.numberValue.value; + spec.step = field.numberValue.step; + spec.minValue = field.numberValue.minValue; + spec.maxValue = field.numberValue.maxValue; + spec.integerMode = field.numberValue.integerMode; + spec.readOnly = field.readOnly; + return spec; +} + +UIEditorEnumFieldSpec BuildEnumFieldSpec(const UIEditorPropertyGridField& field) { + UIEditorEnumFieldSpec spec = {}; + spec.fieldId = field.fieldId; + spec.label = field.label; + spec.options = field.enumValue.options; + spec.selectedIndex = field.enumValue.selectedIndex; + spec.readOnly = field.readOnly; + return spec; +} + +UIEditorAssetFieldSpec BuildAssetFieldSpec(const UIEditorPropertyGridField& field) { + UIEditorAssetFieldSpec spec = {}; + spec.fieldId = field.fieldId; + spec.label = field.label; + spec.assetId = field.assetValue.assetId; + spec.displayName = field.assetValue.displayName; + spec.statusText = field.assetValue.statusText; + spec.emptyText = field.assetValue.emptyText; + spec.tint = field.assetValue.tint; + spec.readOnly = field.readOnly; + spec.showPickerButton = field.assetValue.showPickerButton; + spec.allowClear = field.assetValue.allowClear; + spec.showStatusBadge = field.assetValue.showStatusBadge; + return spec; +} + +UIEditorColorFieldSpec BuildColorFieldSpec(const UIEditorPropertyGridField& field) { + UIEditorColorFieldSpec spec = {}; + spec.fieldId = field.fieldId; + spec.label = field.label; + spec.value = field.colorValue.value; + spec.showAlpha = field.colorValue.showAlpha; + spec.readOnly = field.readOnly; + return spec; +} + +UIEditorTextFieldSpec BuildTextFieldSpec(const UIEditorPropertyGridField& field) { + UIEditorTextFieldSpec spec = {}; + spec.fieldId = field.fieldId; + spec.label = field.label; + spec.value = field.valueText; + spec.readOnly = field.readOnly; + return spec; +} + +UIEditorVector2FieldSpec BuildVector2FieldSpec(const UIEditorPropertyGridField& field) { + UIEditorVector2FieldSpec spec = {}; + spec.fieldId = field.fieldId; + spec.label = field.label; + spec.values = field.vector2Value.values; + spec.componentLabels = field.vector2Value.componentLabels; + spec.step = field.vector2Value.step; + spec.minValue = field.vector2Value.minValue; + spec.maxValue = field.vector2Value.maxValue; + spec.integerMode = field.vector2Value.integerMode; + spec.readOnly = field.readOnly; + return spec; +} + +UIEditorVector3FieldSpec BuildVector3FieldSpec(const UIEditorPropertyGridField& field) { + UIEditorVector3FieldSpec spec = {}; + spec.fieldId = field.fieldId; + spec.label = field.label; + spec.values = field.vector3Value.values; + spec.componentLabels = field.vector3Value.componentLabels; + spec.step = field.vector3Value.step; + spec.minValue = field.vector3Value.minValue; + spec.maxValue = field.vector3Value.maxValue; + spec.integerMode = field.vector3Value.integerMode; + spec.readOnly = field.readOnly; + return spec; +} + +UIEditorVector4FieldSpec BuildVector4FieldSpec(const UIEditorPropertyGridField& field) { + UIEditorVector4FieldSpec spec = {}; + spec.fieldId = field.fieldId; + spec.label = field.label; + spec.values = field.vector4Value.values; + spec.componentLabels = field.vector4Value.componentLabels; + spec.step = field.vector4Value.step; + spec.minValue = field.vector4Value.minValue; + spec.maxValue = field.vector4Value.maxValue; + spec.integerMode = field.vector4Value.integerMode; + spec.readOnly = field.readOnly; + return spec; +} + +UIEditorPropertyGridFieldRects ResolveFieldRects( + const UIRect& rowRect, + const UIEditorPropertyGridField& field, + const UIEditorPropertyGridMetrics& metrics) { + switch (field.kind) { + case UIEditorPropertyGridFieldKind::Bool: { + const UIEditorBoolFieldLayout fieldLayout = BuildUIEditorBoolFieldLayout( + rowRect, + BuildBoolFieldSpec(field), + ::XCEngine::UI::Editor::BuildUIEditorPropertyGridBoolFieldMetrics(metrics)); + return { fieldLayout.labelRect, fieldLayout.checkboxRect }; + } + + case UIEditorPropertyGridFieldKind::Number: { + const UIEditorNumberFieldLayout fieldLayout = BuildUIEditorNumberFieldLayout( + rowRect, + BuildNumberFieldSpec(field), + ::XCEngine::UI::Editor::BuildUIEditorPropertyGridNumberFieldMetrics(metrics)); + return { fieldLayout.labelRect, fieldLayout.valueRect }; + } + + case UIEditorPropertyGridFieldKind::Enum: { + const UIEditorEnumFieldLayout fieldLayout = BuildUIEditorEnumFieldLayout( + rowRect, + BuildEnumFieldSpec(field), + ::XCEngine::UI::Editor::BuildUIEditorPropertyGridEnumFieldMetrics(metrics)); + return { fieldLayout.labelRect, fieldLayout.valueRect }; + } + + case UIEditorPropertyGridFieldKind::Asset: { + const UIEditorAssetFieldLayout fieldLayout = BuildUIEditorAssetFieldLayout( + rowRect, + BuildAssetFieldSpec(field), + ::XCEngine::UI::Editor::BuildUIEditorPropertyGridAssetFieldMetrics(metrics)); + return { fieldLayout.labelRect, fieldLayout.controlRect }; + } + + case UIEditorPropertyGridFieldKind::Color: { + const UIEditorColorFieldLayout fieldLayout = BuildUIEditorColorFieldLayout( + rowRect, + BuildColorFieldSpec(field), + ::XCEngine::UI::Editor::BuildUIEditorPropertyGridColorFieldMetrics(metrics)); + return { fieldLayout.labelRect, fieldLayout.swatchRect }; + } + + case UIEditorPropertyGridFieldKind::Vector2: { + const UIEditorVector2FieldLayout fieldLayout = BuildUIEditorVector2FieldLayout( + rowRect, + BuildVector2FieldSpec(field), + ::XCEngine::UI::Editor::BuildUIEditorPropertyGridVector2FieldMetrics(metrics)); + return { fieldLayout.labelRect, fieldLayout.controlRect }; + } + + case UIEditorPropertyGridFieldKind::Vector3: { + const UIEditorVector3FieldLayout fieldLayout = BuildUIEditorVector3FieldLayout( + rowRect, + BuildVector3FieldSpec(field), + ::XCEngine::UI::Editor::BuildUIEditorPropertyGridVector3FieldMetrics(metrics)); + return { fieldLayout.labelRect, fieldLayout.controlRect }; + } + + case UIEditorPropertyGridFieldKind::Vector4: { + const UIEditorVector4FieldLayout fieldLayout = BuildUIEditorVector4FieldLayout( + rowRect, + BuildVector4FieldSpec(field), + ::XCEngine::UI::Editor::BuildUIEditorPropertyGridVector4FieldMetrics(metrics)); + return { fieldLayout.labelRect, fieldLayout.controlRect }; + } + + case UIEditorPropertyGridFieldKind::Text: + default: { + const UIEditorTextFieldLayout fieldLayout = BuildUIEditorTextFieldLayout( + rowRect, + BuildTextFieldSpec(field), + ::XCEngine::UI::Editor::BuildUIEditorPropertyGridTextFieldMetrics(metrics)); + return { fieldLayout.labelRect, fieldLayout.valueRect }; + } + } +} + +const std::string& ResolveDisplayedTextFieldValue( + const UIEditorPropertyGridField& field, + const ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel) { + if (field.kind == UIEditorPropertyGridFieldKind::Text && + propertyEditModel.HasActiveEdit() && + propertyEditModel.GetActiveFieldId() == field.fieldId) { + return propertyEditModel.GetStagedValue(); + } + + return field.valueText; +} + +UIEditorBoolFieldHitTargetKind ResolveBoolHoveredTarget( + const UIEditorPropertyGridState& state, + const UIEditorPropertyGridField& field) { + if (state.hoveredFieldId != field.fieldId) { + return UIEditorBoolFieldHitTargetKind::None; + } + + return state.hoveredHitTarget == UIEditorPropertyGridHitTargetKind::ValueBox + ? UIEditorBoolFieldHitTargetKind::Checkbox + : UIEditorBoolFieldHitTargetKind::Row; +} + +UIEditorNumberFieldHitTargetKind ResolveNumberHoveredTarget( + const UIEditorPropertyGridState& state, + const UIEditorPropertyGridField& field) { + if (state.hoveredFieldId != field.fieldId) { + return UIEditorNumberFieldHitTargetKind::None; + } + + return state.hoveredHitTarget == UIEditorPropertyGridHitTargetKind::ValueBox + ? UIEditorNumberFieldHitTargetKind::ValueBox + : UIEditorNumberFieldHitTargetKind::Row; +} + +UIEditorEnumFieldHitTargetKind ResolveEnumHoveredTarget( + const UIEditorPropertyGridState& state, + const UIEditorPropertyGridField& field) { + if (state.hoveredFieldId != field.fieldId) { + return UIEditorEnumFieldHitTargetKind::None; + } + + return state.hoveredHitTarget == UIEditorPropertyGridHitTargetKind::ValueBox + ? UIEditorEnumFieldHitTargetKind::ValueBox + : UIEditorEnumFieldHitTargetKind::Row; +} + +UIEditorAssetFieldHitTargetKind ResolveAssetHoveredTarget( + const UIEditorPropertyGridState& state, + const UIEditorPropertyGridField& field) { + if (state.hoveredFieldId != field.fieldId) { + return UIEditorAssetFieldHitTargetKind::None; + } + + return state.hoveredHitTarget == UIEditorPropertyGridHitTargetKind::ValueBox + ? UIEditorAssetFieldHitTargetKind::ValueBox + : UIEditorAssetFieldHitTargetKind::Row; +} + +UIEditorColorFieldHitTargetKind ResolveColorHoveredTarget( + const UIEditorPropertyGridState& state, + const UIEditorPropertyGridField& field) { + if (state.hoveredFieldId != field.fieldId) { + return UIEditorColorFieldHitTargetKind::None; + } + + return state.hoveredHitTarget == UIEditorPropertyGridHitTargetKind::ValueBox + ? UIEditorColorFieldHitTargetKind::Swatch + : UIEditorColorFieldHitTargetKind::Row; +} + +UIEditorTextFieldHitTargetKind ResolveTextHoveredTarget( + const UIEditorPropertyGridState& state, + const UIEditorPropertyGridField& field) { + if (state.hoveredFieldId != field.fieldId) { + return UIEditorTextFieldHitTargetKind::None; + } + + return state.hoveredHitTarget == UIEditorPropertyGridHitTargetKind::ValueBox + ? UIEditorTextFieldHitTargetKind::ValueBox + : UIEditorTextFieldHitTargetKind::Row; +} + +UIEditorVector2FieldHitTargetKind ResolveVector2HoveredTarget( + const UIEditorPropertyGridState& state, + const UIEditorPropertyGridField& field) { + if (state.hoveredFieldId != field.fieldId) { + return UIEditorVector2FieldHitTargetKind::None; + } + + return UIEditorVector2FieldHitTargetKind::Row; +} + +UIEditorVector3FieldHitTargetKind ResolveVector3HoveredTarget( + const UIEditorPropertyGridState& state, + const UIEditorPropertyGridField& field) { + if (state.hoveredFieldId != field.fieldId) { + return UIEditorVector3FieldHitTargetKind::None; + } + + return UIEditorVector3FieldHitTargetKind::Row; +} + +UIEditorVector4FieldHitTargetKind ResolveVector4HoveredTarget( + const UIEditorPropertyGridState& state, + const UIEditorPropertyGridField& field) { + if (state.hoveredFieldId != field.fieldId) { + return UIEditorVector4FieldHitTargetKind::None; + } + + return UIEditorVector4FieldHitTargetKind::Row; +} + +std::vector BuildEnumPopupItems( + const UIEditorPropertyGridField& field) { + std::vector items = {}; + if (field.kind != UIEditorPropertyGridFieldKind::Enum) { + return items; + } + + items.reserve(field.enumValue.options.size()); + const std::size_t selectedIndex = + field.enumValue.options.empty() + ? 0u + : (std::min)( + field.enumValue.selectedIndex, + field.enumValue.options.size() - 1u); + for (std::size_t index = 0u; index < field.enumValue.options.size(); ++index) { + UIEditorMenuPopupItem item = {}; + item.itemId = field.fieldId + "." + std::to_string(index); + item.kind = UIEditorMenuItemKind::Command; + item.label = field.enumValue.options[index]; + item.enabled = !field.readOnly; + item.checked = index == selectedIndex; + items.push_back(std::move(item)); + } + + return items; +} + +const UIEditorPropertyGridNumberFieldVisualState* FindNumberFieldVisualState( + const UIEditorPropertyGridState& state, + std::string_view fieldId) { + for (const UIEditorPropertyGridNumberFieldVisualState& entry : + state.numberFieldStates) { + if (entry.fieldId == fieldId) { + return &entry; + } + } + + return nullptr; +} + +const UIEditorPropertyGridTextFieldVisualState* FindTextFieldVisualState( + const UIEditorPropertyGridState& state, + std::string_view fieldId) { + for (const UIEditorPropertyGridTextFieldVisualState& entry : + state.textFieldStates) { + if (entry.fieldId == fieldId) { + return &entry; + } + } + + return nullptr; +} + +const UIEditorPropertyGridAssetFieldVisualState* FindAssetFieldVisualState( + const UIEditorPropertyGridState& state, + std::string_view fieldId) { + for (const UIEditorPropertyGridAssetFieldVisualState& entry : + state.assetFieldStates) { + if (entry.fieldId == fieldId) { + return &entry; + } + } + + return nullptr; +} + +UIRect ResolvePopupViewportRect(const UIRect& bounds) { + return UIRect(bounds.x - 4096.0f, bounds.y - 4096.0f, 8192.0f, 8192.0f); +} + +bool BuildEnumPopupRuntime( + const UIEditorPropertyGridLayout& layout, + const std::vector& sections, + const UIEditorPropertyGridState& state, + const UIEditorMenuPopupMetrics& popupMetrics, + UIEditorMenuPopupLayout& popupLayout, + UIEditorMenuPopupState& popupState, + std::vector& popupItems) { + if (state.popupFieldId.empty()) { + return false; + } + + const std::size_t visibleFieldIndex = + FindUIEditorPropertyGridVisibleFieldIndex(layout, state.popupFieldId, sections); + if (visibleFieldIndex == UIEditorPropertyGridInvalidIndex || + visibleFieldIndex >= layout.visibleFieldSectionIndices.size() || + visibleFieldIndex >= layout.visibleFieldIndices.size()) { + return false; + } + + const std::size_t sectionIndex = layout.visibleFieldSectionIndices[visibleFieldIndex]; + const std::size_t fieldIndex = layout.visibleFieldIndices[visibleFieldIndex]; + if (sectionIndex >= sections.size() || + fieldIndex >= sections[sectionIndex].fields.size()) { + return false; + } + + const UIEditorPropertyGridField& field = sections[sectionIndex].fields[fieldIndex]; + popupItems = BuildEnumPopupItems(field); + if (popupItems.empty()) { + return false; + } + + const float popupWidth = (std::max)( + layout.fieldValueRects[visibleFieldIndex].width, + ResolveUIEditorMenuPopupDesiredWidth(popupItems, popupMetrics)); + const float popupHeight = + MeasureUIEditorMenuPopupHeight(popupItems, popupMetrics); + const auto placement = ResolvePopupPlacementRect( + layout.fieldValueRects[visibleFieldIndex], + UISize(popupWidth, popupHeight), + ResolvePopupViewportRect(layout.bounds), + UIPopupPlacement::BottomStart); + + popupLayout = BuildUIEditorMenuPopupLayout( + placement.rect, + popupItems, + popupMetrics); + popupState.focused = state.focused || !state.popupFieldId.empty(); + popupState.hoveredIndex = + state.popupHighlightedIndex < popupItems.size() + ? state.popupHighlightedIndex + : UIEditorMenuPopupInvalidIndex; + return true; +} + +std::string FormatVector2ValueText(const UIEditorPropertyGridField& field) { + const UIEditorVector2FieldSpec spec = BuildVector2FieldSpec(field); + return FormatUIEditorVector2FieldComponentValue(spec, 0u) + ", " + + FormatUIEditorVector2FieldComponentValue(spec, 1u); +} + +std::string FormatColorValueText(const UIEditorPropertyGridField& field) { + return FormatUIEditorColorFieldHexText(BuildColorFieldSpec(field)); +} + +std::string FormatVector3ValueText(const UIEditorPropertyGridField& field) { + const UIEditorVector3FieldSpec spec = BuildVector3FieldSpec(field); + return FormatUIEditorVector3FieldComponentValue(spec, 0u) + ", " + + FormatUIEditorVector3FieldComponentValue(spec, 1u) + ", " + + FormatUIEditorVector3FieldComponentValue(spec, 2u); +} + +std::string FormatVector4ValueText(const UIEditorPropertyGridField& field) { + const UIEditorVector4FieldSpec spec = BuildVector4FieldSpec(field); + return FormatUIEditorVector4FieldComponentValue(spec, 0u) + ", " + + FormatUIEditorVector4FieldComponentValue(spec, 1u) + ", " + + FormatUIEditorVector4FieldComponentValue(spec, 2u) + ", " + + FormatUIEditorVector4FieldComponentValue(spec, 3u); +} + +} // namespace XCEngine::UI::Editor::Widgets::Internal + +namespace XCEngine::UI::Editor::Widgets { + +void AppendUIEditorPropertyGridBackground( + ::XCEngine::UI::UIDrawList& drawList, + const UIEditorPropertyGridLayout& layout, + const std::vector& sections, + const ::XCEngine::UI::Widgets::UISelectionModel& selectionModel, + const ::XCEngine::UI::Widgets::UIPropertyEditModel&, + const UIEditorPropertyGridState& state, + const UIEditorPropertyGridPalette& palette, + const UIEditorPropertyGridMetrics& metrics) { + if (HasVisibleAlpha(palette.surfaceColor)) { + drawList.AddFilledRect(layout.bounds, palette.surfaceColor, metrics.cornerRounding); + } + + const ::XCEngine::UI::UIColor outlineColor = + state.focused ? palette.focusedBorderColor : palette.borderColor; + if (HasVisibleAlpha(outlineColor)) { + drawList.AddRectOutline( + layout.bounds, + outlineColor, + state.focused ? metrics.focusedBorderThickness : metrics.borderThickness, + metrics.cornerRounding); + } + + for (std::size_t sectionVisibleIndex = 0u; + sectionVisibleIndex < layout.sectionHeaderRects.size(); + ++sectionVisibleIndex) { + const UIEditorPropertyGridSection& section = + sections[layout.sectionIndices[sectionVisibleIndex]]; + const bool hovered = + state.hoveredFieldId.empty() && + state.hoveredSectionId == section.sectionId; + const ::XCEngine::UI::UIColor fillColor = + hovered ? palette.sectionHeaderHoverColor : palette.sectionHeaderColor; + if (HasVisibleAlpha(fillColor)) { + drawList.AddFilledRect( + layout.sectionHeaderRects[sectionVisibleIndex], + fillColor, + metrics.cornerRounding); + } + } + + for (std::size_t visibleFieldIndex = 0u; + visibleFieldIndex < layout.fieldRowRects.size(); + ++visibleFieldIndex) { + const UIEditorPropertyGridSection& section = + sections[layout.visibleFieldSectionIndices[visibleFieldIndex]]; + const UIEditorPropertyGridField& field = + section.fields[layout.visibleFieldIndices[visibleFieldIndex]]; + const bool selected = selectionModel.IsSelected(field.fieldId); + const bool hovered = state.hoveredFieldId == field.fieldId; + + if (selected || hovered) { + const ::XCEngine::UI::UIColor fillColor = + selected + ? (state.focused ? palette.fieldSelectedFocusedColor : palette.fieldSelectedColor) + : palette.fieldHoverColor; + if (!HasVisibleAlpha(fillColor)) { + continue; + } + + drawList.AddFilledRect( + layout.fieldRowRects[visibleFieldIndex], + fillColor, + metrics.cornerRounding); + } + } +} + +void AppendUIEditorPropertyGridForeground( + ::XCEngine::UI::UIDrawList& drawList, + const UIEditorPropertyGridLayout& layout, + const std::vector& sections, + const ::XCEngine::UI::Widgets::UISelectionModel& selectionModel, + const UIEditorPropertyGridState& state, + const ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel, + const UIEditorPropertyGridPalette& palette, + const UIEditorPropertyGridMetrics& metrics, + const UIEditorMenuPopupPalette& popupPalette, + const UIEditorMenuPopupMetrics& popupMetrics) { + (void)propertyEditModel; + drawList.PushClipRect(layout.bounds); + + for (std::size_t sectionVisibleIndex = 0u; + sectionVisibleIndex < layout.sectionHeaderRects.size(); + ++sectionVisibleIndex) { + const UIEditorPropertyGridSection& section = + sections[layout.sectionIndices[sectionVisibleIndex]]; + AppendDisclosureArrow( + drawList, + layout.sectionDisclosureRects[sectionVisibleIndex], + layout.sectionExpanded[sectionVisibleIndex], + palette.disclosureColor); + drawList.PushClipRect(layout.sectionTitleRects[sectionVisibleIndex], true); + drawList.AddText( + ::XCEngine::UI::UIPoint( + layout.sectionTitleRects[sectionVisibleIndex].x, + layout.sectionTitleRects[sectionVisibleIndex].y + metrics.sectionTextInsetY), + section.title, + palette.sectionTextColor, + metrics.sectionFontSize); + drawList.PopClipRect(); + } + + const UIEditorBoolFieldMetrics boolMetrics = + ::XCEngine::UI::Editor::BuildUIEditorPropertyGridBoolFieldMetrics(metrics); + const UIEditorBoolFieldPalette boolPalette = + ::XCEngine::UI::Editor::BuildUIEditorPropertyGridBoolFieldPalette(palette); + const UIEditorNumberFieldMetrics numberMetrics = + ::XCEngine::UI::Editor::BuildUIEditorPropertyGridNumberFieldMetrics(metrics); + const UIEditorNumberFieldPalette numberPalette = + ::XCEngine::UI::Editor::BuildUIEditorPropertyGridNumberFieldPalette(palette); + const UIEditorAssetFieldMetrics assetMetrics = + ::XCEngine::UI::Editor::BuildUIEditorPropertyGridAssetFieldMetrics(metrics); + const UIEditorAssetFieldPalette assetPalette = + ::XCEngine::UI::Editor::BuildUIEditorPropertyGridAssetFieldPalette(palette); + const UIEditorTextFieldMetrics textMetrics = + ::XCEngine::UI::Editor::BuildUIEditorPropertyGridTextFieldMetrics(metrics); + const UIEditorTextFieldPalette textPalette = + ::XCEngine::UI::Editor::BuildUIEditorPropertyGridTextFieldPalette(palette); + const UIEditorVector2FieldMetrics vector2Metrics = + ::XCEngine::UI::Editor::BuildUIEditorPropertyGridVector2FieldMetrics(metrics); + const UIEditorVector2FieldPalette vector2Palette = + ::XCEngine::UI::Editor::BuildUIEditorPropertyGridVector2FieldPalette(palette); + const UIEditorVector3FieldMetrics vector3Metrics = + ::XCEngine::UI::Editor::BuildUIEditorPropertyGridVector3FieldMetrics(metrics); + const UIEditorVector3FieldPalette vector3Palette = + ::XCEngine::UI::Editor::BuildUIEditorPropertyGridVector3FieldPalette(palette); + const UIEditorVector4FieldMetrics vector4Metrics = + ::XCEngine::UI::Editor::BuildUIEditorPropertyGridVector4FieldMetrics(metrics); + const UIEditorVector4FieldPalette vector4Palette = + ::XCEngine::UI::Editor::BuildUIEditorPropertyGridVector4FieldPalette(palette); + const UIEditorEnumFieldMetrics enumMetrics = + ::XCEngine::UI::Editor::BuildUIEditorPropertyGridEnumFieldMetrics(metrics); + const UIEditorEnumFieldPalette enumPalette = + ::XCEngine::UI::Editor::BuildUIEditorPropertyGridEnumFieldPalette(palette); + const UIEditorColorFieldMetrics colorMetrics = + ::XCEngine::UI::Editor::BuildUIEditorPropertyGridColorFieldMetrics(metrics); + const UIEditorColorFieldPalette colorPalette = + ::XCEngine::UI::Editor::BuildUIEditorPropertyGridColorFieldPalette(palette); + const ::XCEngine::UI::UIRect popupViewportRect = + Internal::ResolvePopupViewportRect(layout.bounds); + + for (std::size_t visibleFieldIndex = 0u; + visibleFieldIndex < layout.fieldRowRects.size(); + ++visibleFieldIndex) { + const UIEditorPropertyGridSection& section = + sections[layout.visibleFieldSectionIndices[visibleFieldIndex]]; + const UIEditorPropertyGridField& field = + section.fields[layout.visibleFieldIndices[visibleFieldIndex]]; + + switch (field.kind) { + case UIEditorPropertyGridFieldKind::Bool: { + UIEditorBoolFieldState fieldState = {}; + fieldState.hoveredTarget = Internal::ResolveBoolHoveredTarget(state, field); + fieldState.focused = state.focused; + fieldState.active = state.pressedFieldId == field.fieldId; + AppendUIEditorBoolField( + drawList, + layout.fieldRowRects[visibleFieldIndex], + Internal::BuildBoolFieldSpec(field), + fieldState, + boolPalette, + boolMetrics); + break; + } + + case UIEditorPropertyGridFieldKind::Number: { + UIEditorNumberFieldState fieldState = {}; + if (const auto* visualState = + Internal::FindNumberFieldVisualState(state, field.fieldId); + visualState != nullptr) { + fieldState = visualState->state; + } else { + fieldState.hoveredTarget = Internal::ResolveNumberHoveredTarget(state, field); + fieldState.activeTarget = + state.pressedFieldId == field.fieldId + ? UIEditorNumberFieldHitTargetKind::ValueBox + : UIEditorNumberFieldHitTargetKind::None; + fieldState.focused = state.focused; + fieldState.displayText = + ResolveUIEditorPropertyGridFieldValueText(field); + } + AppendUIEditorNumberField( + drawList, + layout.fieldRowRects[visibleFieldIndex], + Internal::BuildNumberFieldSpec(field), + fieldState, + numberPalette, + numberMetrics); + break; + } + + case UIEditorPropertyGridFieldKind::Enum: { + UIEditorEnumFieldState fieldState = {}; + fieldState.hoveredTarget = Internal::ResolveEnumHoveredTarget(state, field); + fieldState.focused = state.focused; + fieldState.active = state.pressedFieldId == field.fieldId; + fieldState.popupOpen = state.popupFieldId == field.fieldId; + AppendUIEditorEnumField( + drawList, + layout.fieldRowRects[visibleFieldIndex], + Internal::BuildEnumFieldSpec(field), + fieldState, + enumPalette, + enumMetrics); + break; + } + + case UIEditorPropertyGridFieldKind::Asset: { + UIEditorAssetFieldState fieldState = {}; + if (const auto* visualState = + Internal::FindAssetFieldVisualState(state, field.fieldId); + visualState != nullptr) { + fieldState = visualState->state; + } else { + fieldState.hoveredTarget = Internal::ResolveAssetHoveredTarget(state, field); + fieldState.activeTarget = + state.pressedFieldId == field.fieldId + ? UIEditorAssetFieldHitTargetKind::ValueBox + : UIEditorAssetFieldHitTargetKind::None; + fieldState.focused = state.focused; + } + + AppendUIEditorAssetField( + drawList, + layout.fieldRowRects[visibleFieldIndex], + Internal::BuildAssetFieldSpec(field), + fieldState, + assetPalette, + assetMetrics); + break; + } + + case UIEditorPropertyGridFieldKind::Color: { + UIEditorColorFieldState fieldState = {}; + fieldState.hoveredTarget = Internal::ResolveColorHoveredTarget(state, field); + fieldState.activeTarget = + state.pressedFieldId == field.fieldId + ? (state.hoveredHitTarget == + UIEditorPropertyGridHitTargetKind::ValueBox + ? UIEditorColorFieldHitTargetKind::Swatch + : UIEditorColorFieldHitTargetKind::Row) + : UIEditorColorFieldHitTargetKind::None; + fieldState.focused = + state.focused && selectionModel.IsSelected(field.fieldId); + fieldState.popupOpen = false; + + const UIEditorColorFieldSpec fieldSpec = + Internal::BuildColorFieldSpec(field); + AppendUIEditorColorField( + drawList, + layout.fieldRowRects[visibleFieldIndex], + fieldSpec, + fieldState, + colorPalette, + colorMetrics, + popupViewportRect); + break; + } + + case UIEditorPropertyGridFieldKind::Vector2: { + UIEditorVector2FieldState fieldState = {}; + if (const auto* visualState = + Internal::FindVector2FieldVisualState(state, field.fieldId); + visualState != nullptr) { + fieldState = visualState->state; + } else { + fieldState.hoveredTarget = Internal::ResolveVector2HoveredTarget(state, field); + fieldState.activeTarget = + state.pressedFieldId == field.fieldId + ? UIEditorVector2FieldHitTargetKind::Row + : UIEditorVector2FieldHitTargetKind::None; + fieldState.focused = state.focused; + } + AppendUIEditorVector2Field( + drawList, + layout.fieldRowRects[visibleFieldIndex], + Internal::BuildVector2FieldSpec(field), + fieldState, + vector2Palette, + vector2Metrics); + break; + } + + case UIEditorPropertyGridFieldKind::Vector3: { + UIEditorVector3FieldState fieldState = {}; + if (const auto* visualState = + Internal::FindVector3FieldVisualState(state, field.fieldId); + visualState != nullptr) { + fieldState = visualState->state; + } else { + fieldState.hoveredTarget = Internal::ResolveVector3HoveredTarget(state, field); + fieldState.activeTarget = + state.pressedFieldId == field.fieldId + ? UIEditorVector3FieldHitTargetKind::Row + : UIEditorVector3FieldHitTargetKind::None; + fieldState.focused = state.focused; + } + AppendUIEditorVector3Field( + drawList, + layout.fieldRowRects[visibleFieldIndex], + Internal::BuildVector3FieldSpec(field), + fieldState, + vector3Palette, + vector3Metrics); + break; + } + + case UIEditorPropertyGridFieldKind::Vector4: { + UIEditorVector4FieldState fieldState = {}; + if (const auto* visualState = + Internal::FindVector4FieldVisualState(state, field.fieldId); + visualState != nullptr) { + fieldState = visualState->state; + } else { + fieldState.hoveredTarget = Internal::ResolveVector4HoveredTarget(state, field); + fieldState.activeTarget = + state.pressedFieldId == field.fieldId + ? UIEditorVector4FieldHitTargetKind::Row + : UIEditorVector4FieldHitTargetKind::None; + fieldState.focused = state.focused; + } + AppendUIEditorVector4Field( + drawList, + layout.fieldRowRects[visibleFieldIndex], + Internal::BuildVector4FieldSpec(field), + fieldState, + vector4Palette, + vector4Metrics); + break; + } + + case UIEditorPropertyGridFieldKind::Text: + default: { + UIEditorTextFieldState fieldState = {}; + if (const auto* visualState = + Internal::FindTextFieldVisualState(state, field.fieldId); + visualState != nullptr) { + fieldState = visualState->state; + } else { + fieldState.hoveredTarget = Internal::ResolveTextHoveredTarget(state, field); + fieldState.activeTarget = + state.pressedFieldId == field.fieldId + ? (state.hoveredHitTarget == + UIEditorPropertyGridHitTargetKind::ValueBox + ? UIEditorTextFieldHitTargetKind::ValueBox + : UIEditorTextFieldHitTargetKind::Row) + : UIEditorTextFieldHitTargetKind::None; + fieldState.focused = state.focused; + fieldState.displayText = field.valueText; + } + + UIEditorTextFieldSpec fieldSpec = Internal::BuildTextFieldSpec(field); + fieldSpec.value = + fieldState.editing ? std::string() : fieldState.displayText; + AppendUIEditorTextField( + drawList, + layout.fieldRowRects[visibleFieldIndex], + fieldSpec, + fieldState, + textPalette, + textMetrics); + + if (fieldState.editing) { + drawList.PushClipRect( + ResolveUIEditorTextClipRect( + layout.fieldValueRects[visibleFieldIndex], + metrics.tagFontSize)); + drawList.AddText( + ::XCEngine::UI::UIPoint( + layout.fieldValueRects[visibleFieldIndex].x + + (std::max)( + 0.0f, + layout.fieldValueRects[visibleFieldIndex].width - 34.0f), + ResolveUIEditorTextTop( + layout.fieldValueRects[visibleFieldIndex], + metrics.tagFontSize, + metrics.valueTextInsetY)), + "EDIT", + palette.editTagColor, + metrics.tagFontSize); + drawList.PopClipRect(); + } + break; + } + } + } + + drawList.PopClipRect(); + + UIEditorMenuPopupLayout popupLayout = {}; + UIEditorMenuPopupState popupState = {}; + std::vector popupItems = {}; + if (Internal::BuildEnumPopupRuntime( + layout, + sections, + state, + popupMetrics, + popupLayout, + popupState, + popupItems)) { + AppendUIEditorMenuPopupBackground( + drawList, + popupLayout, + popupItems, + popupState, + popupPalette, + popupMetrics); + AppendUIEditorMenuPopupForeground( + drawList, + popupLayout, + popupItems, + popupState, + popupPalette, + popupMetrics); + } +} + +void AppendUIEditorPropertyGrid( + ::XCEngine::UI::UIDrawList& drawList, + const ::XCEngine::UI::UIRect& bounds, + const std::vector& sections, + const ::XCEngine::UI::Widgets::UISelectionModel& selectionModel, + const ::XCEngine::UI::Widgets::UIExpansionModel& expansionModel, + const ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel, + const UIEditorPropertyGridState& state, + const UIEditorPropertyGridPalette& palette, + const UIEditorPropertyGridMetrics& metrics, + const UIEditorMenuPopupPalette& popupPalette, + const UIEditorMenuPopupMetrics& popupMetrics) { + const UIEditorPropertyGridLayout layout = + BuildUIEditorPropertyGridLayout(bounds, sections, expansionModel, metrics); + AppendUIEditorPropertyGridBackground( + drawList, + layout, + sections, + selectionModel, + propertyEditModel, + state, + palette, + metrics); + AppendUIEditorPropertyGridForeground( + drawList, + layout, + sections, + selectionModel, + state, + propertyEditModel, + palette, + metrics, + popupPalette, + popupMetrics); +} + +} // namespace XCEngine::UI::Editor::Widgets diff --git a/new_editor/src/Fields/UIEditorPropertyGridInteraction.cpp b/new_editor/src/Fields/UIEditorPropertyGridInteraction.cpp index 27008d05..f5b5a468 100644 --- a/new_editor/src/Fields/UIEditorPropertyGridInteraction.cpp +++ b/new_editor/src/Fields/UIEditorPropertyGridInteraction.cpp @@ -1,7 +1,12 @@ #include "Fields/PropertyGridInteractionInternal.h" -#include +#include +#include +#include +#include + +#include #include namespace XCEngine::UI::Editor { @@ -9,8 +14,6 @@ namespace XCEngine::UI::Editor { namespace { using ::XCEngine::Input::KeyCode; -using ::XCEngine::UI::Text::HandleKeyDown; -using ::XCEngine::UI::Text::InsertCharacter; using ::XCEngine::UI::UIInputEvent; using ::XCEngine::UI::UIInputEventType; using ::XCEngine::UI::UIPointerButton; @@ -26,6 +29,19 @@ using Widgets::UIEditorPropertyGridInvalidIndex; using Widgets::UIEditorPropertyGridLayout; using Widgets::UIEditorPropertyGridSection; +bool IsSharedEditableFieldKind(UIEditorPropertyGridFieldKind kind) { + switch (kind) { + case UIEditorPropertyGridFieldKind::Text: + case UIEditorPropertyGridFieldKind::Number: + case UIEditorPropertyGridFieldKind::Vector2: + case UIEditorPropertyGridFieldKind::Vector3: + case UIEditorPropertyGridFieldKind::Vector4: + return true; + default: + return false; + } +} + } // namespace UIEditorPropertyGridInteractionFrame UpdateUIEditorPropertyGridInteraction( @@ -38,13 +54,17 @@ UIEditorPropertyGridInteractionFrame UpdateUIEditorPropertyGridInteraction( const std::vector& inputEvents, const Widgets::UIEditorPropertyGridMetrics& metrics, const Widgets::UIEditorMenuPopupMetrics& popupMetrics) { - UIEditorPropertyGridLayout layout = - BuildUIEditorPropertyGridLayout(bounds, sections, expansionModel, metrics); - Internal::PruneAssetFieldVisualStates(state, layout, sections); - Internal::PruneColorFieldVisualStates(state, layout, sections); - Internal::PruneVectorFieldVisualStates(state, layout, sections); - Internal::SyncKeyboardNavigation(state, selectionModel, layout, sections); - Internal::SyncHoverTarget(state, layout, sections, popupMetrics); + UIEditorPropertyGridLayout layout = {}; + const auto refreshInteractionLayout = [&]() { + layout = BuildUIEditorPropertyGridLayout(bounds, sections, expansionModel, metrics); + Internal::PruneNumberFieldVisualStates(state, layout, sections); + Internal::PruneTextFieldVisualStates(state, layout, sections); + Internal::PruneAssetFieldVisualStates(state, layout, sections); + Internal::PruneVectorFieldVisualStates(state, layout, sections); + Internal::SyncKeyboardNavigation(state, selectionModel, layout, sections); + Internal::SyncHoverTarget(state, layout, sections, popupMetrics); + }; + refreshInteractionLayout(); UIEditorPropertyGridInteractionResult interactionResult = {}; for (const UIInputEvent& event : inputEvents) { @@ -64,6 +84,48 @@ UIEditorPropertyGridInteractionFrame UpdateUIEditorPropertyGridInteraction( : UIEditorPropertyGridHitTarget {}; eventResult.hitTarget = hitTarget; + UIEditorPropertyGridInteractionResult numberEventResult = {}; + if (Internal::ProcessNumberFieldEvent( + state, + selectionModel, + propertyEditModel, + layout, + sections, + metrics, + event, + numberEventResult)) { + Internal::MergeInteractionResult(eventResult, numberEventResult); + refreshInteractionLayout(); + if (numberEventResult.hitTarget.kind != + UIEditorPropertyGridHitTargetKind::None || + numberEventResult.selectionChanged || + numberEventResult.editStarted) { + Internal::MergeInteractionResult(interactionResult, eventResult); + continue; + } + } + + UIEditorPropertyGridInteractionResult textEventResult = {}; + if (Internal::ProcessTextFieldEvent( + state, + selectionModel, + propertyEditModel, + layout, + sections, + metrics, + event, + textEventResult)) { + Internal::MergeInteractionResult(eventResult, textEventResult); + refreshInteractionLayout(); + if (textEventResult.hitTarget.kind != + UIEditorPropertyGridHitTargetKind::None || + textEventResult.selectionChanged || + textEventResult.editStarted) { + Internal::MergeInteractionResult(interactionResult, eventResult); + continue; + } + } + if (Internal::ProcessColorFieldEvent( state, selectionModel, @@ -74,12 +136,7 @@ UIEditorPropertyGridInteractionFrame UpdateUIEditorPropertyGridInteraction( event, eventResult)) { Internal::MergeInteractionResult(interactionResult, eventResult); - layout = BuildUIEditorPropertyGridLayout(bounds, sections, expansionModel, metrics); - Internal::PruneAssetFieldVisualStates(state, layout, sections); - Internal::PruneColorFieldVisualStates(state, layout, sections); - Internal::PruneVectorFieldVisualStates(state, layout, sections); - Internal::SyncKeyboardNavigation(state, selectionModel, layout, sections); - Internal::SyncHoverTarget(state, layout, sections, popupMetrics); + refreshInteractionLayout(); continue; } @@ -114,12 +171,7 @@ UIEditorPropertyGridInteractionFrame UpdateUIEditorPropertyGridInteraction( vectorEventResult); if (vectorHandled) { Internal::MergeInteractionResult(eventResult, vectorEventResult); - layout = BuildUIEditorPropertyGridLayout(bounds, sections, expansionModel, metrics); - Internal::PruneAssetFieldVisualStates(state, layout, sections); - Internal::PruneColorFieldVisualStates(state, layout, sections); - Internal::PruneVectorFieldVisualStates(state, layout, sections); - Internal::SyncKeyboardNavigation(state, selectionModel, layout, sections); - Internal::SyncHoverTarget(state, layout, sections, popupMetrics); + refreshInteractionLayout(); if (vectorEventResult.consumed || vectorEventResult.hitTarget.kind != UIEditorPropertyGridHitTargetKind::None || @@ -141,12 +193,7 @@ UIEditorPropertyGridInteractionFrame UpdateUIEditorPropertyGridInteraction( event, assetEventResult)) { Internal::MergeInteractionResult(eventResult, assetEventResult); - layout = BuildUIEditorPropertyGridLayout(bounds, sections, expansionModel, metrics); - Internal::PruneAssetFieldVisualStates(state, layout, sections); - Internal::PruneColorFieldVisualStates(state, layout, sections); - Internal::PruneVectorFieldVisualStates(state, layout, sections); - Internal::SyncKeyboardNavigation(state, selectionModel, layout, sections); - Internal::SyncHoverTarget(state, layout, sections, popupMetrics); + refreshInteractionLayout(); if (assetEventResult.consumed || assetEventResult.hitTarget.kind != UIEditorPropertyGridHitTargetKind::None || @@ -398,21 +445,29 @@ UIEditorPropertyGridInteractionFrame UpdateUIEditorPropertyGridInteraction( } if (propertyEditModel.HasActiveEdit()) { + const UIEditorPropertyGridField* activeField = Internal::FindField( + sections, + propertyEditModel.GetActiveFieldId()); + if (activeField != nullptr && + IsSharedEditableFieldKind(activeField->kind)) { + break; + } + if (static_cast(event.keyCode) == KeyCode::Escape) { Internal::CancelActiveEdit(state, propertyEditModel, eventResult); break; } - const auto editResult = HandleKeyDown( - state.textInputState, + const auto editResult = HandleUIEditorEditableFieldKeyDown( + state.editableFieldSession, event.keyCode, - event.modifiers, - {}); + event.modifiers); if (editResult.handled) { eventResult.consumed = true; eventResult.activeFieldId = propertyEditModel.GetActiveFieldId(); if (editResult.valueChanged) { - propertyEditModel.UpdateStagedValue(state.textInputState.value); + propertyEditModel.UpdateStagedValue( + state.editableFieldSession.textInputState.value); eventResult.editValueChanged = true; } if (editResult.submitRequested) { @@ -479,12 +534,16 @@ UIEditorPropertyGridInteractionFrame UpdateUIEditorPropertyGridInteraction( sections, propertyEditModel.GetActiveFieldId()); field == nullptr || + IsSharedEditableFieldKind(field->kind) || !Internal::IsNumberEditCharacter(*field, event.character)) { break; } - if (InsertCharacter(state.textInputState, event.character)) { - propertyEditModel.UpdateStagedValue(state.textInputState.value); + if (InsertUIEditorEditableFieldCharacter( + state.editableFieldSession, + event.character)) { + propertyEditModel.UpdateStagedValue( + state.editableFieldSession.textInputState.value); eventResult.consumed = true; eventResult.editValueChanged = true; eventResult.activeFieldId = propertyEditModel.GetActiveFieldId(); @@ -495,12 +554,7 @@ UIEditorPropertyGridInteractionFrame UpdateUIEditorPropertyGridInteraction( break; } - layout = BuildUIEditorPropertyGridLayout(bounds, sections, expansionModel, metrics); - Internal::PruneAssetFieldVisualStates(state, layout, sections); - Internal::PruneColorFieldVisualStates(state, layout, sections); - Internal::PruneVectorFieldVisualStates(state, layout, sections); - Internal::SyncKeyboardNavigation(state, selectionModel, layout, sections); - Internal::SyncHoverTarget(state, layout, sections, popupMetrics); + refreshInteractionLayout(); if (eventResult.hitTarget.kind == UIEditorPropertyGridHitTargetKind::None && state.hasPointerPosition) { eventResult.hitTarget = @@ -533,12 +587,7 @@ UIEditorPropertyGridInteractionFrame UpdateUIEditorPropertyGridInteraction( } } - layout = BuildUIEditorPropertyGridLayout(bounds, sections, expansionModel, metrics); - Internal::PruneAssetFieldVisualStates(state, layout, sections); - Internal::PruneColorFieldVisualStates(state, layout, sections); - Internal::PruneVectorFieldVisualStates(state, layout, sections); - Internal::SyncKeyboardNavigation(state, selectionModel, layout, sections); - Internal::SyncHoverTarget(state, layout, sections, popupMetrics); + refreshInteractionLayout(); if (interactionResult.hitTarget.kind == UIEditorPropertyGridHitTargetKind::None && state.hasPointerPosition) { interactionResult.hitTarget = @@ -549,3 +598,2328 @@ UIEditorPropertyGridInteractionFrame UpdateUIEditorPropertyGridInteraction( } } // namespace XCEngine::UI::Editor + +namespace XCEngine::UI::Editor::Internal { + +using ::XCEngine::Input::KeyCode; +using ::XCEngine::UI::UIInputEvent; +using ::XCEngine::UI::UIInputEventType; +using Widgets::FindUIEditorPropertyGridFieldLocation; +using Widgets::FindUIEditorPropertyGridVisibleFieldIndex; +using Widgets::HitTestUIEditorPropertyGrid; +using Widgets::ResolveUIEditorPropertyGridFieldValueText; +using Widgets::UIEditorMenuPopupHitTarget; +using Widgets::UIEditorMenuPopupHitTargetKind; +using Widgets::UIEditorPropertyGridField; +using Widgets::UIEditorPropertyGridHitTarget; +using Widgets::UIEditorPropertyGridHitTargetKind; +using Widgets::UIEditorPropertyGridInvalidIndex; +using Widgets::UIEditorPropertyGridLayout; +using Widgets::UIEditorPropertyGridSection; + +void ClearEditableFieldSessionActiveState( + UIEditorEditableFieldSession& session) { + session.activeFieldId.clear(); + session.activeComponentIndex = + UIEditorEditableFieldInvalidComponentIndex; + session.focused = false; + session.editing = false; + session.dragArmed = false; + session.dragActive = false; + session.textInputState = {}; +} + +bool ShouldUsePropertyGridPointerPosition(const UIInputEvent& event) { + switch (event.type) { + case UIInputEventType::PointerMove: + case UIInputEventType::PointerEnter: + case UIInputEventType::PointerButtonDown: + case UIInputEventType::PointerButtonUp: + return true; + default: + return false; + } +} + +bool HasSystemModifiers(const ::XCEngine::UI::UIInputModifiers& modifiers) { + return modifiers.control || modifiers.alt || modifiers.super; +} + +bool ApplyKeyboardNavigation( + UIEditorPropertyGridInteractionState& state, + std::int32_t keyCode) { + switch (static_cast(keyCode)) { + case KeyCode::Up: + return state.keyboardNavigation.MovePrevious(); + case KeyCode::Down: + return state.keyboardNavigation.MoveNext(); + case KeyCode::Home: + return state.keyboardNavigation.MoveHome(); + case KeyCode::End: + return state.keyboardNavigation.MoveEnd(); + default: + return false; + } +} + +void ClearHoverState(UIEditorPropertyGridInteractionState& state) { + state.propertyGridState.hoveredSectionId.clear(); + state.propertyGridState.hoveredFieldId.clear(); + state.propertyGridState.hoveredHitTarget = UIEditorPropertyGridHitTargetKind::None; +} + +void ClearPopupState(UIEditorPropertyGridInteractionState& state) { + state.propertyGridState.popupFieldId.clear(); + state.propertyGridState.popupHighlightedIndex = UIEditorPropertyGridInvalidIndex; + state.pressedPopupIndex = UIEditorPropertyGridInvalidIndex; +} + +UIEditorPropertyGridField* FindMutableField( + std::vector& sections, + std::string_view fieldId) { + const auto location = FindUIEditorPropertyGridFieldLocation(sections, fieldId); + if (!location.IsValid() || + location.sectionIndex >= sections.size() || + location.fieldIndex >= sections[location.sectionIndex].fields.size()) { + return nullptr; + } + + return §ions[location.sectionIndex].fields[location.fieldIndex]; +} + +const UIEditorPropertyGridField* FindField( + const std::vector& sections, + std::string_view fieldId) { + const auto location = FindUIEditorPropertyGridFieldLocation(sections, fieldId); + if (!location.IsValid() || + location.sectionIndex >= sections.size() || + location.fieldIndex >= sections[location.sectionIndex].fields.size()) { + return nullptr; + } + + return §ions[location.sectionIndex].fields[location.fieldIndex]; +} + +void SetChangedValueResult( + const UIEditorPropertyGridField& field, + UIEditorPropertyGridInteractionResult& result) { + result.fieldValueChanged = true; + result.changedFieldId = field.fieldId; + result.changedValue = ResolveUIEditorPropertyGridFieldValueText(field); +} + +void MergeInteractionResult( + UIEditorPropertyGridInteractionResult& accumulated, + const UIEditorPropertyGridInteractionResult& current) { + accumulated.consumed = accumulated.consumed || current.consumed; + accumulated.sectionToggled = accumulated.sectionToggled || current.sectionToggled; + accumulated.selectionChanged = accumulated.selectionChanged || current.selectionChanged; + accumulated.keyboardNavigated = accumulated.keyboardNavigated || current.keyboardNavigated; + accumulated.editStarted = accumulated.editStarted || current.editStarted; + accumulated.editValueChanged = accumulated.editValueChanged || current.editValueChanged; + accumulated.editCommitted = accumulated.editCommitted || current.editCommitted; + accumulated.editCommitRejected = + accumulated.editCommitRejected || current.editCommitRejected; + accumulated.editCanceled = accumulated.editCanceled || current.editCanceled; + accumulated.popupOpened = accumulated.popupOpened || current.popupOpened; + accumulated.popupClosed = accumulated.popupClosed || current.popupClosed; + accumulated.fieldValueChanged = + accumulated.fieldValueChanged || current.fieldValueChanged; + accumulated.secondaryClicked = + accumulated.secondaryClicked || current.secondaryClicked; + accumulated.pickerRequested = + accumulated.pickerRequested || current.pickerRequested; + accumulated.activateRequested = + accumulated.activateRequested || current.activateRequested; + + if (current.hitTarget.kind != UIEditorPropertyGridHitTargetKind::None) { + accumulated.hitTarget = current.hitTarget; + } + if (!current.toggledSectionId.empty()) { + accumulated.toggledSectionId = current.toggledSectionId; + } + if (!current.selectedFieldId.empty()) { + accumulated.selectedFieldId = current.selectedFieldId; + } + if (!current.activeFieldId.empty()) { + accumulated.activeFieldId = current.activeFieldId; + } + if (!current.committedFieldId.empty()) { + accumulated.committedFieldId = current.committedFieldId; + } + if (!current.committedValue.empty()) { + accumulated.committedValue = current.committedValue; + } + if (!current.changedFieldId.empty()) { + accumulated.changedFieldId = current.changedFieldId; + } + if (!current.changedValue.empty()) { + accumulated.changedValue = current.changedValue; + } + if (!current.requestedFieldId.empty()) { + accumulated.requestedFieldId = current.requestedFieldId; + } +} + +void SyncHoverTarget( + UIEditorPropertyGridInteractionState& state, + const UIEditorPropertyGridLayout& layout, + const std::vector& sections, + const Widgets::UIEditorMenuPopupMetrics& popupMetrics) { + ClearHoverState(state); + if (!state.hasPointerPosition) { + return; + } + + const UIEditorMenuPopupHitTarget popupHit = + ResolvePopupHit(state, layout, sections, popupMetrics); + if (popupHit.kind == UIEditorMenuPopupHitTargetKind::Item) { + state.propertyGridState.popupHighlightedIndex = popupHit.index; + return; + } + if (popupHit.kind != UIEditorMenuPopupHitTargetKind::None) { + return; + } + + const UIEditorPropertyGridHitTarget hitTarget = + HitTestUIEditorPropertyGrid(layout, state.pointerPosition); + state.propertyGridState.hoveredHitTarget = hitTarget.kind; + if (hitTarget.kind == UIEditorPropertyGridHitTargetKind::SectionHeader && + hitTarget.sectionIndex < sections.size()) { + state.propertyGridState.hoveredSectionId = + sections[hitTarget.sectionIndex].sectionId; + return; + } + + if ((hitTarget.kind == UIEditorPropertyGridHitTargetKind::FieldRow || + hitTarget.kind == UIEditorPropertyGridHitTargetKind::ValueBox) && + hitTarget.sectionIndex < sections.size() && + hitTarget.fieldIndex < sections[hitTarget.sectionIndex].fields.size()) { + state.propertyGridState.hoveredFieldId = + sections[hitTarget.sectionIndex].fields[hitTarget.fieldIndex].fieldId; + } +} + +void SyncKeyboardNavigation( + UIEditorPropertyGridInteractionState& state, + const ::XCEngine::UI::Widgets::UISelectionModel& selectionModel, + const UIEditorPropertyGridLayout& layout, + const std::vector& sections) { + state.keyboardNavigation.SetItemCount(layout.visibleFieldIndices.size()); + state.keyboardNavigation.ClampToItemCount(); + + if (!selectionModel.HasSelection()) { + return; + } + + const std::size_t selectedVisibleIndex = + FindUIEditorPropertyGridVisibleFieldIndex( + layout, + selectionModel.GetSelectedId(), + sections); + if (selectedVisibleIndex == UIEditorPropertyGridInvalidIndex) { + return; + } + + if (!state.keyboardNavigation.HasCurrentIndex() || + state.keyboardNavigation.GetCurrentIndex() != selectedVisibleIndex) { + state.keyboardNavigation.SetCurrentIndex(selectedVisibleIndex); + } +} + +bool IsInlineEditable(const UIEditorPropertyGridField& field) { + (void)field; + // PropertyGrid no longer owns inline editing for value fields. + // Number/Text are bridged to their dedicated field interactions. + return false; +} + +bool IsNumberEditCharacter( + const UIEditorPropertyGridField& field, + std::uint32_t character) { + if (field.kind != UIEditorPropertyGridFieldKind::Number) { + return true; + } + + if (character >= static_cast('0') && + character <= static_cast('9')) { + return true; + } + + if (character == static_cast('-') || + character == static_cast('+')) { + return true; + } + + return !field.numberValue.integerMode && + character == static_cast('.'); +} + +bool SelectVisibleField( + UIEditorPropertyGridInteractionState& state, + ::XCEngine::UI::Widgets::UISelectionModel& selectionModel, + const UIEditorPropertyGridLayout& layout, + const std::vector& sections, + std::size_t visibleFieldIndex, + UIEditorPropertyGridInteractionResult& result) { + if (visibleFieldIndex >= layout.visibleFieldIndices.size() || + visibleFieldIndex >= layout.visibleFieldSectionIndices.size()) { + return false; + } + + const std::size_t sectionIndex = layout.visibleFieldSectionIndices[visibleFieldIndex]; + const std::size_t fieldIndex = layout.visibleFieldIndices[visibleFieldIndex]; + if (sectionIndex >= sections.size() || + fieldIndex >= sections[sectionIndex].fields.size()) { + return false; + } + + const UIEditorPropertyGridField& field = sections[sectionIndex].fields[fieldIndex]; + result.selectionChanged = selectionModel.SetSelection(field.fieldId); + result.selectedFieldId = field.fieldId; + result.consumed = true; + state.keyboardNavigation.SetCurrentIndex(visibleFieldIndex); + return true; +} + +bool BeginFieldEdit( + UIEditorPropertyGridInteractionState& state, + ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel, + const UIEditorPropertyGridField& field, + UIEditorPropertyGridInteractionResult& result) { + if (field.readOnly || !IsInlineEditable(field)) { + return false; + } + + const std::string initialValue = + field.kind == UIEditorPropertyGridFieldKind::Text + ? field.valueText + : ResolveUIEditorPropertyGridFieldValueText(field); + const bool changed = propertyEditModel.BeginEdit(field.fieldId, initialValue); + if (!changed && + (!propertyEditModel.HasActiveEdit() || + propertyEditModel.GetActiveFieldId() != field.fieldId)) { + return false; + } + + state.editableFieldSession.activeFieldId = field.fieldId; + state.editableFieldSession.activeComponentIndex = + UIEditorEditableFieldInvalidComponentIndex; + state.editableFieldSession.focused = true; + state.editableFieldSession.editing = true; + state.editableFieldSession.dragArmed = false; + state.editableFieldSession.dragActive = false; + state.editableFieldSession.textInputState.value = + propertyEditModel.GetStagedValue(); + state.editableFieldSession.textInputState.caret = + state.editableFieldSession.textInputState.value.size(); + RestartUIEditorEditableFieldCaretBlink(state.editableFieldSession); + result.editStarted = changed; + result.activeFieldId = field.fieldId; + result.consumed = true; + return true; +} + +bool CommitActiveEdit( + UIEditorPropertyGridInteractionState& state, + ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel, + std::vector& sections, + UIEditorPropertyGridInteractionResult& result) { + if (!propertyEditModel.HasActiveEdit()) { + return false; + } + + UIEditorPropertyGridField* field = + FindMutableField(sections, propertyEditModel.GetActiveFieldId()); + if (field == nullptr) { + propertyEditModel.CancelEdit(); + ClearEditableFieldSessionActiveState(state.editableFieldSession); + return false; + } + + result.activeFieldId = field->fieldId; + if (field->kind == UIEditorPropertyGridFieldKind::Number) { + Widgets::UIEditorNumberFieldSpec spec = {}; + spec.fieldId = field->fieldId; + spec.label = field->label; + spec.value = field->numberValue.value; + spec.step = field->numberValue.step; + spec.minValue = field->numberValue.minValue; + spec.maxValue = field->numberValue.maxValue; + spec.integerMode = field->numberValue.integerMode; + spec.readOnly = field->readOnly; + + double parsedValue = field->numberValue.value; + if (!Widgets::TryParseUIEditorNumberFieldValue( + spec, + propertyEditModel.GetStagedValue(), + parsedValue)) { + result.editCommitRejected = true; + result.consumed = true; + return false; + } + + field->numberValue.value = parsedValue; + result.committedFieldId = field->fieldId; + result.committedValue = ResolveUIEditorPropertyGridFieldValueText(*field); + SetChangedValueResult(*field, result); + } else if (field->kind == UIEditorPropertyGridFieldKind::Text) { + field->valueText = propertyEditModel.GetStagedValue(); + result.committedFieldId = field->fieldId; + result.committedValue = field->valueText; + SetChangedValueResult(*field, result); + } else if (field->kind == UIEditorPropertyGridFieldKind::Vector2) { + const std::size_t componentIndex = + state.editableFieldSession.activeComponentIndex; + Widgets::UIEditorVector2FieldSpec spec = + Widgets::Internal::BuildVector2FieldSpec(*field); + if (componentIndex >= spec.values.size()) { + result.editCommitRejected = true; + result.consumed = true; + return false; + } + + double parsedValue = spec.values[componentIndex]; + if (!Widgets::TryParseUIEditorVector2FieldComponentValue( + spec, + propertyEditModel.GetStagedValue(), + parsedValue)) { + result.editCommitRejected = true; + result.consumed = true; + return false; + } + + field->vector2Value.values[componentIndex] = + Widgets::NormalizeUIEditorVector2FieldComponentValue( + spec, + parsedValue); + result.committedFieldId = field->fieldId; + result.committedValue = ResolveUIEditorPropertyGridFieldValueText(*field); + SetChangedValueResult(*field, result); + } else if (field->kind == UIEditorPropertyGridFieldKind::Vector3) { + const std::size_t componentIndex = + state.editableFieldSession.activeComponentIndex; + Widgets::UIEditorVector3FieldSpec spec = + Widgets::Internal::BuildVector3FieldSpec(*field); + if (componentIndex >= spec.values.size()) { + result.editCommitRejected = true; + result.consumed = true; + return false; + } + + double parsedValue = spec.values[componentIndex]; + if (!Widgets::TryParseUIEditorVector3FieldComponentValue( + spec, + propertyEditModel.GetStagedValue(), + parsedValue)) { + result.editCommitRejected = true; + result.consumed = true; + return false; + } + + field->vector3Value.values[componentIndex] = + Widgets::NormalizeUIEditorVector3FieldComponentValue( + spec, + parsedValue); + result.committedFieldId = field->fieldId; + result.committedValue = ResolveUIEditorPropertyGridFieldValueText(*field); + SetChangedValueResult(*field, result); + } else if (field->kind == UIEditorPropertyGridFieldKind::Vector4) { + const std::size_t componentIndex = + state.editableFieldSession.activeComponentIndex; + Widgets::UIEditorVector4FieldSpec spec = + Widgets::Internal::BuildVector4FieldSpec(*field); + if (componentIndex >= spec.values.size()) { + result.editCommitRejected = true; + result.consumed = true; + return false; + } + + double parsedValue = spec.values[componentIndex]; + if (!Widgets::TryParseUIEditorVector4FieldComponentValue( + spec, + propertyEditModel.GetStagedValue(), + parsedValue)) { + result.editCommitRejected = true; + result.consumed = true; + return false; + } + + field->vector4Value.values[componentIndex] = + Widgets::NormalizeUIEditorVector4FieldComponentValue( + spec, + parsedValue); + result.committedFieldId = field->fieldId; + result.committedValue = ResolveUIEditorPropertyGridFieldValueText(*field); + SetChangedValueResult(*field, result); + } + + propertyEditModel.CommitEdit(); + ClearEditableFieldSessionActiveState(state.editableFieldSession); + result.editCommitted = true; + result.consumed = true; + return true; +} + +bool CancelActiveEdit( + UIEditorPropertyGridInteractionState& state, + ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel, + UIEditorPropertyGridInteractionResult& result) { + if (!propertyEditModel.HasActiveEdit()) { + return false; + } + + result.activeFieldId = propertyEditModel.GetActiveFieldId(); + if (!propertyEditModel.CancelEdit()) { + return false; + } + + ClearEditableFieldSessionActiveState(state.editableFieldSession); + result.editCanceled = true; + result.consumed = true; + return true; +} + +namespace SingleValueFieldInteractionLocal { + +using Widgets::FindUIEditorPropertyGridVisibleFieldIndex; +using Widgets::UIEditorPropertyGridField; +using Widgets::UIEditorPropertyGridFieldKind; +using Widgets::UIEditorPropertyGridInvalidIndex; +using Widgets::UIEditorPropertyGridLayout; +using Widgets::UIEditorPropertyGridSection; + +template +Entry* FindMutableEntry( + std::vector& entries, + std::string_view fieldId) { + for (Entry& entry : entries) { + if (entry.fieldId == fieldId) { + return &entry; + } + } + + return nullptr; +} + +template +const Entry* FindEntry( + const std::vector& entries, + std::string_view fieldId) { + for (const Entry& entry : entries) { + if (entry.fieldId == fieldId) { + return &entry; + } + } + + return nullptr; +} + +template +typename Traits::InteractionState BuildInteractionState( + const UIEditorPropertyGridInteractionState& state, + std::string_view fieldId) { + typename Traits::InteractionState interactionState = {}; + const auto& visualStates = Traits::VisualStates(state.propertyGridState); + if (const auto* visualState = FindEntry(visualStates, fieldId); + visualState != nullptr) { + Traits::FieldState(interactionState) = visualState->state; + } + + interactionState.session = state.editableFieldSession; + interactionState.session.pointerPosition = state.pointerPosition; + interactionState.session.hasPointerPosition = state.hasPointerPosition; + if (interactionState.session.activeFieldId != fieldId) { + interactionState.session.focused = false; + interactionState.session.editing = false; + interactionState.session.dragArmed = false; + interactionState.session.dragActive = false; + } + return interactionState; +} + +template +void StoreInteractionState( + UIEditorPropertyGridInteractionState& state, + std::string_view fieldId, + const typename Traits::InteractionState& interactionState, + bool storeSharedSession) { + if (storeSharedSession) { + state.editableFieldSession = interactionState.session; + if (Traits::FieldState(interactionState).focused || + Traits::FieldState(interactionState).editing || + interactionState.session.dragArmed || + interactionState.session.dragActive) { + state.editableFieldSession.activeFieldId = std::string(fieldId); + } + } + + auto& visualStates = Traits::VisualStates(state.propertyGridState); + if (auto* visualState = FindMutableEntry(visualStates, fieldId); + visualState != nullptr) { + visualState->state = Traits::FieldState(interactionState); + return; + } + + typename Traits::VisualStateEntry visualState = {}; + visualState.fieldId = std::string(fieldId); + visualState.state = Traits::FieldState(interactionState); + visualStates.push_back(std::move(visualState)); +} + +template +void PruneStateEntries( + std::vector& visualStates, + const UIEditorPropertyGridLayout& layout, + const std::vector& sections) { + const auto isVisibleTypedFieldId = [&layout, §ions](std::string_view fieldId) { + const std::size_t visibleFieldIndex = + FindUIEditorPropertyGridVisibleFieldIndex(layout, fieldId, sections); + if (visibleFieldIndex == UIEditorPropertyGridInvalidIndex || + visibleFieldIndex >= layout.visibleFieldSectionIndices.size() || + visibleFieldIndex >= layout.visibleFieldIndices.size()) { + return false; + } + + const std::size_t sectionIndex = layout.visibleFieldSectionIndices[visibleFieldIndex]; + const std::size_t fieldIndex = layout.visibleFieldIndices[visibleFieldIndex]; + return sectionIndex < sections.size() && + fieldIndex < sections[sectionIndex].fields.size() && + sections[sectionIndex].fields[fieldIndex].kind == Traits::kFieldKind; + }; + + visualStates.erase( + std::remove_if( + visualStates.begin(), + visualStates.end(), + [&isVisibleTypedFieldId](const typename Traits::VisualStateEntry& entry) { + return !isVisibleTypedFieldId(entry.fieldId); + }), + visualStates.end()); +} + +template +bool HasMeaningfulResult( + const typename Traits::InteractionResult& result, + const typename Traits::InteractionState& interactionState, + bool hadFocus, + bool hadEditing) { + return result.consumed || + result.focusChanged || + Traits::HasValueChange(result) || + result.editStarted || + result.editCommitted || + Traits::HasEditCommitRejected(result) || + result.editCanceled || + result.hitTarget.kind != Traits::kNoneHitTargetKind || + hadFocus || + hadEditing || + Traits::FieldState(interactionState).focused || + Traits::FieldState(interactionState).editing; +} + +template +void CopyValuesToField( + UIEditorPropertyGridField& field, + const typename Traits::Spec& spec); + +template +void MergeFieldInteractionResult( + UIEditorPropertyGridInteractionResult& result, + const typename Traits::InteractionResult& fieldResult) { + result.consumed = result.consumed || fieldResult.consumed; + result.editStarted = result.editStarted || fieldResult.editStarted; + result.editCommitted = result.editCommitted || fieldResult.editCommitted; + result.editCanceled = result.editCanceled || fieldResult.editCanceled; + result.editCommitRejected = + result.editCommitRejected || Traits::HasEditCommitRejected(fieldResult); +} + +template +void AssignPropertyGridHitTarget( + UIEditorPropertyGridInteractionResult& result, + std::size_t sectionIndex, + std::size_t fieldIndex, + std::size_t visibleFieldIndex, + typename Traits::HitTargetKind hitTargetKind) { + switch (hitTargetKind) { + case Traits::kRowHitTargetKind: + result.hitTarget.kind = Widgets::UIEditorPropertyGridHitTargetKind::FieldRow; + result.hitTarget.sectionIndex = sectionIndex; + result.hitTarget.fieldIndex = fieldIndex; + result.hitTarget.visibleFieldIndex = visibleFieldIndex; + break; + + case Traits::kValueHitTargetKind: + result.hitTarget.kind = Widgets::UIEditorPropertyGridHitTargetKind::ValueBox; + result.hitTarget.sectionIndex = sectionIndex; + result.hitTarget.fieldIndex = fieldIndex; + result.hitTarget.visibleFieldIndex = visibleFieldIndex; + break; + + case Traits::kNoneHitTargetKind: + default: + break; + } +} + +template +bool ProcessFieldEventImpl( + UIEditorPropertyGridInteractionState& state, + ::XCEngine::UI::Widgets::UISelectionModel& selectionModel, + ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel, + const UIEditorPropertyGridLayout& layout, + std::vector& sections, + const Widgets::UIEditorPropertyGridMetrics& metrics, + const ::XCEngine::UI::UIInputEvent& event, + UIEditorPropertyGridInteractionResult& result) { + const typename Traits::Metrics fieldMetrics = Traits::BuildMetrics(metrics); + bool handled = false; + + for (std::size_t visibleFieldIndex = 0u; + visibleFieldIndex < layout.visibleFieldIndices.size(); + ++visibleFieldIndex) { + const std::size_t sectionIndex = layout.visibleFieldSectionIndices[visibleFieldIndex]; + const std::size_t fieldIndex = layout.visibleFieldIndices[visibleFieldIndex]; + if (sectionIndex >= sections.size() || + fieldIndex >= sections[sectionIndex].fields.size()) { + continue; + } + + UIEditorPropertyGridField& field = sections[sectionIndex].fields[fieldIndex]; + if (field.kind != Traits::kFieldKind) { + continue; + } + + const UIEditorEditableFieldSession previousSharedSession = + state.editableFieldSession; + typename Traits::InteractionState interactionState = + BuildInteractionState(state, field.fieldId); + const bool hadFocus = Traits::FieldState(interactionState).focused; + const bool hadEditing = Traits::FieldState(interactionState).editing; + typename Traits::Spec spec = Traits::BuildSpec(field); + const typename Traits::InteractionFrame frame = + Traits::Update( + interactionState, + spec, + layout.fieldRowRects[visibleFieldIndex], + { event }, + fieldMetrics); + + if (!HasMeaningfulResult( + frame.result, + interactionState, + hadFocus, + hadEditing)) { + continue; + } + + handled = true; + CopyValuesToField(field, spec); + + if (frame.result.consumed || + frame.result.focusChanged || + frame.result.editStarted || + frame.result.editCommitted || + Traits::HasEditCommitRejected(frame.result) || + frame.result.editCanceled || + Traits::HasValueChange(frame.result) || + hadFocus || + Traits::FieldState(interactionState).focused) { + result.selectionChanged = + selectionModel.SetSelection(field.fieldId) || result.selectionChanged; + result.selectedFieldId = field.fieldId; + state.keyboardNavigation.SetCurrentIndex(visibleFieldIndex); + } + + if (frame.result.editStarted || + frame.result.editCommitted || + Traits::FieldState(interactionState).editing) { + result.activeFieldId = field.fieldId; + } + + if (propertyEditModel.HasActiveEdit() && + propertyEditModel.GetActiveFieldId() != field.fieldId && + (frame.result.hitTarget.kind != Traits::kNoneHitTargetKind || + frame.result.editStarted || + hadFocus || + Traits::FieldState(interactionState).focused)) { + CommitActiveEdit(state, propertyEditModel, sections, result); + } + + const bool storeSharedSession = + previousSharedSession.activeFieldId == field.fieldId || + interactionState.session.activeFieldId == field.fieldId || + Traits::FieldState(interactionState).focused || + Traits::FieldState(interactionState).editing || + interactionState.session.dragArmed || + interactionState.session.dragActive || + frame.result.focusChanged || + frame.result.editStarted || + frame.result.editCommitted || + frame.result.editCanceled; + StoreInteractionState( + state, + field.fieldId, + interactionState, + storeSharedSession); + state.propertyGridState.focused = + Traits::FieldState(interactionState).focused; + state.propertyGridState.pressedFieldId.clear(); + if (Traits::FieldState(interactionState).editing) { + propertyEditModel.BeginEdit( + field.fieldId, + interactionState.session.textInputState.value); + propertyEditModel.UpdateStagedValue( + interactionState.session.textInputState.value); + } else if (propertyEditModel.HasActiveEdit() && + propertyEditModel.GetActiveFieldId() == field.fieldId) { + if (frame.result.editCommitted) { + propertyEditModel.CommitEdit(); + } else if (frame.result.editCanceled) { + propertyEditModel.CancelEdit(); + } + } + + if (frame.result.hitTarget.kind != Traits::kNoneHitTargetKind || + frame.result.editStarted) { + ClosePopup(state, result); + } + + MergeFieldInteractionResult(result, frame.result); + if (frame.result.editCommitted) { + result.committedFieldId = field.fieldId; + result.committedValue = + Widgets::ResolveUIEditorPropertyGridFieldValueText(field); + } + if (Traits::HasValueChange(frame.result)) { + SetChangedValueResult(field, result); + } + + AssignPropertyGridHitTarget( + result, + sectionIndex, + fieldIndex, + visibleFieldIndex, + frame.result.hitTarget.kind); + } + + return handled; +} + +struct NumberTraits { + using VisualStateEntry = Widgets::UIEditorPropertyGridNumberFieldVisualState; + using InteractionState = UIEditorNumberFieldInteractionState; + using InteractionResult = UIEditorNumberFieldInteractionResult; + using InteractionFrame = UIEditorNumberFieldInteractionFrame; + using Metrics = Widgets::UIEditorNumberFieldMetrics; + using Spec = Widgets::UIEditorNumberFieldSpec; + using HitTargetKind = Widgets::UIEditorNumberFieldHitTargetKind; + + static constexpr UIEditorPropertyGridFieldKind kFieldKind = + UIEditorPropertyGridFieldKind::Number; + static constexpr auto kNoneHitTargetKind = + Widgets::UIEditorNumberFieldHitTargetKind::None; + static constexpr auto kRowHitTargetKind = + Widgets::UIEditorNumberFieldHitTargetKind::Row; + static constexpr auto kValueHitTargetKind = + Widgets::UIEditorNumberFieldHitTargetKind::ValueBox; + + static auto& VisualStates(Widgets::UIEditorPropertyGridState& state) { + return state.numberFieldStates; + } + + static const auto& VisualStates(const Widgets::UIEditorPropertyGridState& state) { + return state.numberFieldStates; + } + + static auto& FieldState(InteractionState& state) { + return state.numberFieldState; + } + + static const auto& FieldState(const InteractionState& state) { + return state.numberFieldState; + } + + static Spec BuildSpec(const UIEditorPropertyGridField& field) { + return Widgets::Internal::BuildNumberFieldSpec(field); + } + + static Metrics BuildMetrics(const Widgets::UIEditorPropertyGridMetrics& metrics) { + return BuildUIEditorPropertyGridNumberFieldMetrics(metrics); + } + + static InteractionFrame Update( + InteractionState& state, + Spec& spec, + const ::XCEngine::UI::UIRect& bounds, + const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents, + const Metrics& metrics) { + return UpdateUIEditorNumberFieldInteraction( + state, + spec, + bounds, + inputEvents, + metrics); + } + + static bool HasValueChange(const InteractionResult& result) { + return result.valueChanged || result.stepApplied; + } + + static bool HasEditCommitRejected(const InteractionResult& result) { + return result.editCommitRejected; + } +}; + +struct TextTraits { + using VisualStateEntry = Widgets::UIEditorPropertyGridTextFieldVisualState; + using InteractionState = UIEditorTextFieldInteractionState; + using InteractionResult = UIEditorTextFieldInteractionResult; + using InteractionFrame = UIEditorTextFieldInteractionFrame; + using Metrics = Widgets::UIEditorTextFieldMetrics; + using Spec = Widgets::UIEditorTextFieldSpec; + using HitTargetKind = Widgets::UIEditorTextFieldHitTargetKind; + + static constexpr UIEditorPropertyGridFieldKind kFieldKind = + UIEditorPropertyGridFieldKind::Text; + static constexpr auto kNoneHitTargetKind = + Widgets::UIEditorTextFieldHitTargetKind::None; + static constexpr auto kRowHitTargetKind = + Widgets::UIEditorTextFieldHitTargetKind::Row; + static constexpr auto kValueHitTargetKind = + Widgets::UIEditorTextFieldHitTargetKind::ValueBox; + + static auto& VisualStates(Widgets::UIEditorPropertyGridState& state) { + return state.textFieldStates; + } + + static const auto& VisualStates(const Widgets::UIEditorPropertyGridState& state) { + return state.textFieldStates; + } + + static auto& FieldState(InteractionState& state) { + return state.textFieldState; + } + + static const auto& FieldState(const InteractionState& state) { + return state.textFieldState; + } + + static Spec BuildSpec(const UIEditorPropertyGridField& field) { + return Widgets::Internal::BuildTextFieldSpec(field); + } + + static Metrics BuildMetrics(const Widgets::UIEditorPropertyGridMetrics& metrics) { + return BuildUIEditorPropertyGridTextFieldMetrics(metrics); + } + + static InteractionFrame Update( + InteractionState& state, + Spec& spec, + const ::XCEngine::UI::UIRect& bounds, + const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents, + const Metrics& metrics) { + return UpdateUIEditorTextFieldInteraction( + state, + spec, + bounds, + inputEvents, + metrics); + } + + static bool HasValueChange(const InteractionResult& result) { + return result.valueChanged; + } + + static bool HasEditCommitRejected(const InteractionResult&) { + return false; + } +}; + +template <> +void CopyValuesToField( + UIEditorPropertyGridField& field, + const NumberTraits::Spec& spec) { + field.numberValue.value = spec.value; +} + +template <> +void CopyValuesToField( + UIEditorPropertyGridField& field, + const TextTraits::Spec& spec) { + field.valueText = spec.value; +} + +} // namespace SingleValueFieldInteractionLocal + +void PruneNumberFieldVisualStates( + UIEditorPropertyGridInteractionState& state, + const UIEditorPropertyGridLayout& layout, + const std::vector& sections) { + SingleValueFieldInteractionLocal::PruneStateEntries< + SingleValueFieldInteractionLocal::NumberTraits>( + state.propertyGridState.numberFieldStates, + layout, + sections); +} + +void PruneTextFieldVisualStates( + UIEditorPropertyGridInteractionState& state, + const UIEditorPropertyGridLayout& layout, + const std::vector& sections) { + SingleValueFieldInteractionLocal::PruneStateEntries< + SingleValueFieldInteractionLocal::TextTraits>( + state.propertyGridState.textFieldStates, + layout, + sections); +} + +bool ProcessNumberFieldEvent( + UIEditorPropertyGridInteractionState& state, + ::XCEngine::UI::Widgets::UISelectionModel& selectionModel, + ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel, + const UIEditorPropertyGridLayout& layout, + std::vector& sections, + const Widgets::UIEditorPropertyGridMetrics& metrics, + const ::XCEngine::UI::UIInputEvent& event, + UIEditorPropertyGridInteractionResult& result) { + return SingleValueFieldInteractionLocal::ProcessFieldEventImpl< + SingleValueFieldInteractionLocal::NumberTraits>( + state, + selectionModel, + propertyEditModel, + layout, + sections, + metrics, + event, + result); +} + +bool ProcessTextFieldEvent( + UIEditorPropertyGridInteractionState& state, + ::XCEngine::UI::Widgets::UISelectionModel& selectionModel, + ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel, + const UIEditorPropertyGridLayout& layout, + std::vector& sections, + const Widgets::UIEditorPropertyGridMetrics& metrics, + const ::XCEngine::UI::UIInputEvent& event, + UIEditorPropertyGridInteractionResult& result) { + return SingleValueFieldInteractionLocal::ProcessFieldEventImpl< + SingleValueFieldInteractionLocal::TextTraits>( + state, + selectionModel, + propertyEditModel, + layout, + sections, + metrics, + event, + result); +} + +bool ToggleBoolField( + UIEditorPropertyGridField& field, + UIEditorPropertyGridInteractionResult& result) { + if (field.kind != UIEditorPropertyGridFieldKind::Bool || field.readOnly) { + return false; + } + + field.boolValue = !field.boolValue; + SetChangedValueResult(field, result); + result.consumed = true; + return true; +} + +using ::XCEngine::UI::UISize; +using ::XCEngine::UI::Widgets::ResolvePopupPlacementRect; +using ::XCEngine::UI::Widgets::UIPopupPlacement; +using Widgets::UIEditorMenuPopupItem; +using Widgets::UIEditorMenuPopupLayout; + +std::vector BuildPopupItems( + const UIEditorPropertyGridField& field) { + std::vector items = {}; + if (field.kind != UIEditorPropertyGridFieldKind::Enum) { + return items; + } + + items.reserve(field.enumValue.options.size()); + const std::size_t selectedIndex = + field.enumValue.options.empty() + ? 0u + : (std::min)( + field.enumValue.selectedIndex, + field.enumValue.options.size() - 1u); + for (std::size_t index = 0u; index < field.enumValue.options.size(); ++index) { + UIEditorMenuPopupItem item = {}; + item.itemId = field.fieldId + "." + std::to_string(index); + item.kind = UIEditorMenuItemKind::Command; + item.label = field.enumValue.options[index]; + item.enabled = !field.readOnly; + item.checked = index == selectedIndex; + items.push_back(std::move(item)); + } + + return items; +} + +::XCEngine::UI::UIRect ResolvePopupViewportRect( + const ::XCEngine::UI::UIRect& bounds) { + return ::XCEngine::UI::UIRect( + bounds.x - 4096.0f, + bounds.y - 4096.0f, + 8192.0f, + 8192.0f); +} + +bool BuildPopupLayout( + const UIEditorPropertyGridInteractionState& state, + const UIEditorPropertyGridLayout& layout, + const std::vector& sections, + const Widgets::UIEditorMenuPopupMetrics& popupMetrics, + UIEditorMenuPopupLayout& popupLayout, + std::vector& popupItems) { + if (state.propertyGridState.popupFieldId.empty()) { + return false; + } + + const std::size_t visibleFieldIndex = + FindUIEditorPropertyGridVisibleFieldIndex( + layout, + state.propertyGridState.popupFieldId, + sections); + if (visibleFieldIndex == UIEditorPropertyGridInvalidIndex || + visibleFieldIndex >= layout.visibleFieldSectionIndices.size() || + visibleFieldIndex >= layout.visibleFieldIndices.size()) { + return false; + } + + const std::size_t sectionIndex = layout.visibleFieldSectionIndices[visibleFieldIndex]; + const std::size_t fieldIndex = layout.visibleFieldIndices[visibleFieldIndex]; + if (sectionIndex >= sections.size() || + fieldIndex >= sections[sectionIndex].fields.size()) { + return false; + } + + const UIEditorPropertyGridField& field = sections[sectionIndex].fields[fieldIndex]; + popupItems = BuildPopupItems(field); + if (popupItems.empty()) { + return false; + } + + const float popupWidth = (std::max)( + layout.fieldValueRects[visibleFieldIndex].width, + Widgets::ResolveUIEditorMenuPopupDesiredWidth(popupItems, popupMetrics)); + const float popupHeight = + Widgets::MeasureUIEditorMenuPopupHeight(popupItems, popupMetrics); + const auto placement = ResolvePopupPlacementRect( + layout.fieldValueRects[visibleFieldIndex], + UISize(popupWidth, popupHeight), + ResolvePopupViewportRect(layout.bounds), + UIPopupPlacement::BottomStart); + popupLayout = + Widgets::BuildUIEditorMenuPopupLayout(placement.rect, popupItems, popupMetrics); + return true; +} + +UIEditorMenuPopupHitTarget ResolvePopupHit( + const UIEditorPropertyGridInteractionState& state, + const UIEditorPropertyGridLayout& layout, + const std::vector& sections, + const Widgets::UIEditorMenuPopupMetrics& popupMetrics) { + if (!state.hasPointerPosition) { + return {}; + } + + UIEditorMenuPopupLayout popupLayout = {}; + std::vector popupItems = {}; + if (!BuildPopupLayout(state, layout, sections, popupMetrics, popupLayout, popupItems)) { + return {}; + } + + return Widgets::HitTestUIEditorMenuPopup( + popupLayout, + popupItems, + state.pointerPosition); +} + +void ClosePopup( + UIEditorPropertyGridInteractionState& state, + UIEditorPropertyGridInteractionResult& result) { + if (state.propertyGridState.popupFieldId.empty()) { + return; + } + + ClearPopupState(state); + result.popupClosed = true; + result.consumed = true; +} + +void OpenPopup( + UIEditorPropertyGridInteractionState& state, + const UIEditorPropertyGridField& field, + UIEditorPropertyGridInteractionResult& result) { + if (field.kind != UIEditorPropertyGridFieldKind::Enum || + field.readOnly || + field.enumValue.options.empty()) { + return; + } + + if (state.propertyGridState.popupFieldId == field.fieldId) { + return; + } + + state.propertyGridState.popupFieldId = field.fieldId; + state.propertyGridState.popupHighlightedIndex = + (std::min)(field.enumValue.selectedIndex, field.enumValue.options.size() - 1u); + result.popupOpened = true; + result.consumed = true; +} + +void MovePopupHighlight( + UIEditorPropertyGridInteractionState& state, + const UIEditorPropertyGridField& field, + int delta) { + if (field.kind != UIEditorPropertyGridFieldKind::Enum || + field.enumValue.options.empty()) { + state.propertyGridState.popupHighlightedIndex = + UIEditorPropertyGridInvalidIndex; + return; + } + + if (state.propertyGridState.popupHighlightedIndex == UIEditorPropertyGridInvalidIndex || + state.propertyGridState.popupHighlightedIndex >= field.enumValue.options.size()) { + state.propertyGridState.popupHighlightedIndex = + (std::min)(field.enumValue.selectedIndex, field.enumValue.options.size() - 1u); + } + + const std::size_t currentIndex = state.propertyGridState.popupHighlightedIndex; + if (delta < 0) { + state.propertyGridState.popupHighlightedIndex = + currentIndex == 0u ? 0u : currentIndex - 1u; + } else { + state.propertyGridState.popupHighlightedIndex = + currentIndex + 1u >= field.enumValue.options.size() + ? field.enumValue.options.size() - 1u + : currentIndex + 1u; + } +} + +void JumpPopupHighlightToEdge( + UIEditorPropertyGridInteractionState& state, + const UIEditorPropertyGridField& field, + bool toEnd) { + if (field.kind != UIEditorPropertyGridFieldKind::Enum || + field.enumValue.options.empty()) { + state.propertyGridState.popupHighlightedIndex = + UIEditorPropertyGridInvalidIndex; + return; + } + + state.propertyGridState.popupHighlightedIndex = + toEnd ? field.enumValue.options.size() - 1u : 0u; +} + +bool CommitPopupSelection( + UIEditorPropertyGridInteractionState& state, + std::vector& sections, + UIEditorPropertyGridInteractionResult& result) { + UIEditorPropertyGridField* field = + FindMutableField(sections, state.propertyGridState.popupFieldId); + if (field == nullptr || + field->kind != UIEditorPropertyGridFieldKind::Enum || + field->readOnly || + field->enumValue.options.empty() || + state.propertyGridState.popupHighlightedIndex == + UIEditorPropertyGridInvalidIndex || + state.propertyGridState.popupHighlightedIndex >= + field->enumValue.options.size()) { + return false; + } + + field->enumValue.selectedIndex = state.propertyGridState.popupHighlightedIndex; + SetChangedValueResult(*field, result); + ClosePopup(state, result); + return true; +} + +} // namespace XCEngine::UI::Editor::Internal + +namespace XCEngine::UI::Editor::Internal { + +namespace AssetFieldInteractionLocal { + +template +Entry* FindMutableEntry( + std::vector& entries, + std::string_view fieldId) { + for (Entry& entry : entries) { + if (entry.fieldId == fieldId) { + return &entry; + } + } + + return nullptr; +} + +template +const Entry* FindEntry( + const std::vector& entries, + std::string_view fieldId) { + for (const Entry& entry : entries) { + if (entry.fieldId == fieldId) { + return &entry; + } + } + + return nullptr; +} + +UIEditorAssetFieldInteractionState BuildInteractionState( + const UIEditorPropertyGridInteractionState& state, + std::string_view fieldId) { + UIEditorAssetFieldInteractionState interactionState = {}; + if (const auto* entry = FindEntry(state.assetFieldInteractionStates, fieldId); + entry != nullptr) { + interactionState = entry->state; + } + + interactionState.pointerPosition = state.pointerPosition; + interactionState.hasPointerPosition = state.hasPointerPosition; + return interactionState; +} + +void StoreInteractionState( + UIEditorPropertyGridInteractionState& state, + std::string_view fieldId, + const UIEditorAssetFieldInteractionState& interactionState) { + if (auto* entry = FindMutableEntry(state.assetFieldInteractionStates, fieldId); + entry != nullptr) { + entry->state = interactionState; + } else { + UIEditorPropertyGridAssetFieldInteractionEntry newEntry = {}; + newEntry.fieldId = std::string(fieldId); + newEntry.state = interactionState; + state.assetFieldInteractionStates.push_back(std::move(newEntry)); + } + + if (auto* visualState = + FindMutableEntry(state.propertyGridState.assetFieldStates, fieldId); + visualState != nullptr) { + visualState->state = interactionState.fieldState; + } else { + Widgets::UIEditorPropertyGridAssetFieldVisualState newVisualState = {}; + newVisualState.fieldId = std::string(fieldId); + newVisualState.state = interactionState.fieldState; + state.propertyGridState.assetFieldStates.push_back(std::move(newVisualState)); + } +} + +bool IsVisibleAssetFieldId( + const Widgets::UIEditorPropertyGridLayout& layout, + const std::vector& sections, + std::string_view fieldId) { + const std::size_t visibleFieldIndex = + Widgets::FindUIEditorPropertyGridVisibleFieldIndex(layout, fieldId, sections); + if (visibleFieldIndex == Widgets::UIEditorPropertyGridInvalidIndex || + visibleFieldIndex >= layout.visibleFieldSectionIndices.size() || + visibleFieldIndex >= layout.visibleFieldIndices.size()) { + return false; + } + + const std::size_t sectionIndex = layout.visibleFieldSectionIndices[visibleFieldIndex]; + const std::size_t fieldIndex = layout.visibleFieldIndices[visibleFieldIndex]; + return sectionIndex < sections.size() && + fieldIndex < sections[sectionIndex].fields.size() && + sections[sectionIndex].fields[fieldIndex].kind == + Widgets::UIEditorPropertyGridFieldKind::Asset; +} + +void CopySpecToField( + Widgets::UIEditorPropertyGridField& field, + const Widgets::UIEditorAssetFieldSpec& spec) { + field.assetValue.assetId = spec.assetId; + field.assetValue.displayName = spec.displayName; + field.assetValue.statusText = spec.statusText; + field.assetValue.emptyText = spec.emptyText; + field.assetValue.tint = spec.tint; + field.assetValue.showPickerButton = spec.showPickerButton; + field.assetValue.allowClear = spec.allowClear; + field.assetValue.showStatusBadge = spec.showStatusBadge; +} + +bool HasMeaningfulResult( + const UIEditorAssetFieldInteractionResult& result, + const UIEditorAssetFieldInteractionState& interactionState, + bool hadFocus) { + return result.consumed || + result.focusChanged || + result.valueChanged || + result.activateRequested || + result.pickerRequested || + result.clearRequested || + result.hitTarget.kind != Widgets::UIEditorAssetFieldHitTargetKind::None || + hadFocus || + interactionState.fieldState.focused; +} + +void AssignPropertyGridHitTarget( + UIEditorPropertyGridInteractionResult& result, + std::size_t sectionIndex, + std::size_t fieldIndex, + std::size_t visibleFieldIndex, + Widgets::UIEditorAssetFieldHitTargetKind hitTargetKind) { + switch (hitTargetKind) { + case Widgets::UIEditorAssetFieldHitTargetKind::Row: + result.hitTarget.kind = Widgets::UIEditorPropertyGridHitTargetKind::FieldRow; + result.hitTarget.sectionIndex = sectionIndex; + result.hitTarget.fieldIndex = fieldIndex; + result.hitTarget.visibleFieldIndex = visibleFieldIndex; + break; + + case Widgets::UIEditorAssetFieldHitTargetKind::ValueBox: + case Widgets::UIEditorAssetFieldHitTargetKind::PickerButton: + case Widgets::UIEditorAssetFieldHitTargetKind::ClearButton: + result.hitTarget.kind = Widgets::UIEditorPropertyGridHitTargetKind::ValueBox; + result.hitTarget.sectionIndex = sectionIndex; + result.hitTarget.fieldIndex = fieldIndex; + result.hitTarget.visibleFieldIndex = visibleFieldIndex; + break; + + case Widgets::UIEditorAssetFieldHitTargetKind::None: + default: + break; + } +} + +} // namespace AssetFieldInteractionLocal + +using Widgets::UIEditorPropertyGridField; +using Widgets::UIEditorPropertyGridFieldKind; +using Widgets::UIEditorPropertyGridLayout; +using Widgets::UIEditorPropertyGridSection; + +void PruneAssetFieldVisualStates( + UIEditorPropertyGridInteractionState& state, + const UIEditorPropertyGridLayout& layout, + const std::vector& sections) { + state.assetFieldInteractionStates.erase( + std::remove_if( + state.assetFieldInteractionStates.begin(), + state.assetFieldInteractionStates.end(), + [&layout, §ions]( + const UIEditorPropertyGridAssetFieldInteractionEntry& entry) { + return !AssetFieldInteractionLocal::IsVisibleAssetFieldId( + layout, + sections, + entry.fieldId); + }), + state.assetFieldInteractionStates.end()); + + state.propertyGridState.assetFieldStates.erase( + std::remove_if( + state.propertyGridState.assetFieldStates.begin(), + state.propertyGridState.assetFieldStates.end(), + [&layout, §ions]( + const Widgets::UIEditorPropertyGridAssetFieldVisualState& entry) { + return !AssetFieldInteractionLocal::IsVisibleAssetFieldId( + layout, + sections, + entry.fieldId); + }), + state.propertyGridState.assetFieldStates.end()); +} + +bool ProcessAssetFieldEvent( + UIEditorPropertyGridInteractionState& state, + ::XCEngine::UI::Widgets::UISelectionModel& selectionModel, + ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel, + const UIEditorPropertyGridLayout& layout, + std::vector& sections, + const Widgets::UIEditorPropertyGridMetrics& metrics, + const ::XCEngine::UI::UIInputEvent& event, + UIEditorPropertyGridInteractionResult& result) { + const Widgets::UIEditorAssetFieldMetrics assetMetrics = + BuildUIEditorPropertyGridAssetFieldMetrics(metrics); + bool handled = false; + + for (std::size_t visibleFieldIndex = 0u; + visibleFieldIndex < layout.visibleFieldIndices.size(); + ++visibleFieldIndex) { + const std::size_t sectionIndex = layout.visibleFieldSectionIndices[visibleFieldIndex]; + const std::size_t fieldIndex = layout.visibleFieldIndices[visibleFieldIndex]; + if (sectionIndex >= sections.size() || + fieldIndex >= sections[sectionIndex].fields.size()) { + continue; + } + + UIEditorPropertyGridField& field = sections[sectionIndex].fields[fieldIndex]; + if (field.kind != UIEditorPropertyGridFieldKind::Asset) { + continue; + } + + UIEditorAssetFieldInteractionState assetState = + AssetFieldInteractionLocal::BuildInteractionState(state, field.fieldId); + const bool hadFocus = assetState.fieldState.focused; + Widgets::UIEditorAssetFieldSpec spec = + Widgets::Internal::BuildAssetFieldSpec(field); + const UIEditorAssetFieldInteractionFrame frame = + UpdateUIEditorAssetFieldInteraction( + assetState, + spec, + layout.fieldRowRects[visibleFieldIndex], + { event }, + assetMetrics); + + if (!AssetFieldInteractionLocal::HasMeaningfulResult( + frame.result, + assetState, + hadFocus)) { + continue; + } + + handled = true; + AssetFieldInteractionLocal::CopySpecToField(field, spec); + AssetFieldInteractionLocal::StoreInteractionState(state, field.fieldId, assetState); + state.propertyGridState.focused = assetState.fieldState.focused; + state.propertyGridState.pressedFieldId.clear(); + + if (frame.result.consumed || + frame.result.focusChanged || + frame.result.valueChanged || + frame.result.activateRequested || + frame.result.pickerRequested || + hadFocus || + assetState.fieldState.focused) { + result.selectionChanged = + selectionModel.SetSelection(field.fieldId) || result.selectionChanged; + result.selectedFieldId = field.fieldId; + result.activeFieldId = field.fieldId; + state.keyboardNavigation.SetCurrentIndex(visibleFieldIndex); + } + + if (propertyEditModel.HasActiveEdit() && + propertyEditModel.GetActiveFieldId() != field.fieldId && + (frame.result.hitTarget.kind != + Widgets::UIEditorAssetFieldHitTargetKind::None || + frame.result.valueChanged || + frame.result.activateRequested || + frame.result.pickerRequested || + hadFocus || + assetState.fieldState.focused)) { + CommitActiveEdit(state, propertyEditModel, sections, result); + } + + if (frame.result.hitTarget.kind != + Widgets::UIEditorAssetFieldHitTargetKind::None || + frame.result.valueChanged || + frame.result.activateRequested || + frame.result.pickerRequested) { + ClosePopup(state, result); + } + + result.consumed = result.consumed || frame.result.consumed; + if (frame.result.valueChanged) { + SetChangedValueResult(field, result); + } + if (frame.result.pickerRequested) { + result.pickerRequested = true; + result.requestedFieldId = field.fieldId; + } + if (frame.result.activateRequested) { + result.activateRequested = true; + if (result.requestedFieldId.empty()) { + result.requestedFieldId = field.fieldId; + } + } + + AssetFieldInteractionLocal::AssignPropertyGridHitTarget( + result, + sectionIndex, + fieldIndex, + visibleFieldIndex, + frame.result.hitTarget.kind); + } + + return handled; +} + +using Widgets::UIEditorPropertyGridHitTargetKind; +using Widgets::UIEditorPropertyGridInvalidIndex; + +bool ProcessColorFieldEvent( + UIEditorPropertyGridInteractionState& state, + ::XCEngine::UI::Widgets::UISelectionModel& selectionModel, + ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel, + const UIEditorPropertyGridLayout& layout, + std::vector& sections, + const Widgets::UIEditorPropertyGridMetrics& metrics, + const ::XCEngine::UI::UIInputEvent& event, + UIEditorPropertyGridInteractionResult& result) { + const Widgets::UIEditorColorFieldMetrics colorMetrics = + BuildUIEditorPropertyGridColorFieldMetrics(metrics); + + for (std::size_t visibleFieldIndex = 0u; + visibleFieldIndex < layout.visibleFieldIndices.size(); + ++visibleFieldIndex) { + const std::size_t sectionIndex = layout.visibleFieldSectionIndices[visibleFieldIndex]; + const std::size_t fieldIndex = layout.visibleFieldIndices[visibleFieldIndex]; + if (sectionIndex >= sections.size() || + fieldIndex >= sections[sectionIndex].fields.size()) { + continue; + } + + UIEditorPropertyGridField& field = sections[sectionIndex].fields[fieldIndex]; + if (field.kind != UIEditorPropertyGridFieldKind::Color) { + continue; + } + + Widgets::UIEditorColorFieldSpec spec = Widgets::Internal::BuildColorFieldSpec(field); + const Widgets::UIEditorColorFieldLayout fieldLayout = + Widgets::BuildUIEditorColorFieldLayout( + layout.fieldRowRects[visibleFieldIndex], + spec, + colorMetrics); + const Widgets::UIEditorColorFieldHitTarget fieldHitTarget = + state.hasPointerPosition + ? Widgets::HitTestUIEditorColorField( + fieldLayout, + false, + state.pointerPosition) + : Widgets::UIEditorColorFieldHitTarget{}; + const bool selectedField = selectionModel.IsSelected(field.fieldId); + const bool pressedField = state.propertyGridState.pressedFieldId == field.fieldId; + const bool keyboardActivate = + event.type == ::XCEngine::UI::UIInputEventType::KeyDown && + state.propertyGridState.focused && + selectedField && + (event.keyCode == + static_cast(::XCEngine::Input::KeyCode::Enter) || + event.keyCode == + static_cast(::XCEngine::Input::KeyCode::Space)); + const bool swatchHit = + fieldHitTarget.kind == Widgets::UIEditorColorFieldHitTargetKind::Swatch; + + if (!pressedField && + !swatchHit && + !keyboardActivate) { + continue; + } + + auto assignResultSelection = [&]() { + result.selectionChanged = + selectionModel.SetSelection(field.fieldId) || result.selectionChanged; + result.selectedFieldId = field.fieldId; + result.activeFieldId = field.fieldId; + state.keyboardNavigation.SetCurrentIndex(visibleFieldIndex); + }; + + auto assignHitTarget = [&]() { + if (fieldHitTarget.kind == Widgets::UIEditorColorFieldHitTargetKind::Row) { + result.hitTarget.kind = UIEditorPropertyGridHitTargetKind::FieldRow; + } else if (fieldHitTarget.kind == Widgets::UIEditorColorFieldHitTargetKind::Swatch) { + result.hitTarget.kind = UIEditorPropertyGridHitTargetKind::ValueBox; + } else { + return; + } + + result.hitTarget.sectionIndex = sectionIndex; + result.hitTarget.fieldIndex = fieldIndex; + result.hitTarget.visibleFieldIndex = visibleFieldIndex; + }; + + switch (event.type) { + case ::XCEngine::UI::UIInputEventType::PointerButtonDown: + if (event.pointerButton == ::XCEngine::UI::UIPointerButton::Left && + swatchHit) { + state.propertyGridState.focused = true; + state.propertyGridState.pressedFieldId = field.fieldId; + result.consumed = true; + assignHitTarget(); + return true; + } + break; + + case ::XCEngine::UI::UIInputEventType::PointerButtonUp: + if (event.pointerButton == ::XCEngine::UI::UIPointerButton::Left && + pressedField) { + state.propertyGridState.pressedFieldId.clear(); + state.propertyGridState.focused = true; + if (!swatchHit) { + return true; + } + + if (propertyEditModel.HasActiveEdit() && + propertyEditModel.GetActiveFieldId() != field.fieldId) { + CommitActiveEdit(state, propertyEditModel, sections, result); + } + + ClosePopup(state, result); + assignResultSelection(); + assignHitTarget(); + result.consumed = true; + if (!field.readOnly) { + result.pickerRequested = true; + result.requestedFieldId = field.fieldId; + } + return true; + } + break; + + case ::XCEngine::UI::UIInputEventType::KeyDown: + if (keyboardActivate) { + if (propertyEditModel.HasActiveEdit() && + propertyEditModel.GetActiveFieldId() != field.fieldId) { + CommitActiveEdit(state, propertyEditModel, sections, result); + } + + ClosePopup(state, result); + assignResultSelection(); + result.consumed = true; + if (!field.readOnly) { + result.pickerRequested = true; + result.requestedFieldId = field.fieldId; + } + return true; + } + break; + + default: + break; + } + } + + return false; +} + +} // namespace XCEngine::UI::Editor::Internal + +namespace XCEngine::UI::Editor::Widgets::Internal { + +const UIEditorPropertyGridVector2FieldVisualState* FindVector2FieldVisualState( + const UIEditorPropertyGridState& state, + std::string_view fieldId) { + for (const UIEditorPropertyGridVector2FieldVisualState& entry : + state.vector2FieldStates) { + if (entry.fieldId == fieldId) { + return &entry; + } + } + + return nullptr; +} + +const UIEditorPropertyGridVector3FieldVisualState* FindVector3FieldVisualState( + const UIEditorPropertyGridState& state, + std::string_view fieldId) { + for (const UIEditorPropertyGridVector3FieldVisualState& entry : + state.vector3FieldStates) { + if (entry.fieldId == fieldId) { + return &entry; + } + } + + return nullptr; +} + +const UIEditorPropertyGridVector4FieldVisualState* FindVector4FieldVisualState( + const UIEditorPropertyGridState& state, + std::string_view fieldId) { + for (const UIEditorPropertyGridVector4FieldVisualState& entry : + state.vector4FieldStates) { + if (entry.fieldId == fieldId) { + return &entry; + } + } + + return nullptr; +} + +} // namespace XCEngine::UI::Editor::Widgets::Internal + +namespace XCEngine::UI::Editor::Internal { + +namespace VectorFieldInteractionLocal { + +using Widgets::FindUIEditorPropertyGridVisibleFieldIndex; +using Widgets::UIEditorPropertyGridField; +using Widgets::UIEditorPropertyGridFieldKind; +using Widgets::UIEditorPropertyGridInvalidIndex; +using Widgets::UIEditorPropertyGridLayout; +using Widgets::UIEditorPropertyGridSection; + +template +Entry* FindMutableEntry( + std::vector& entries, + std::string_view fieldId) { + for (Entry& entry : entries) { + if (entry.fieldId == fieldId) { + return &entry; + } + } + + return nullptr; +} + +template +const Entry* FindEntry( + const std::vector& entries, + std::string_view fieldId) { + for (const Entry& entry : entries) { + if (entry.fieldId == fieldId) { + return &entry; + } + } + + return nullptr; +} + +template +typename Traits::InteractionState BuildInteractionState( + const UIEditorPropertyGridInteractionState& state, + std::string_view fieldId) { + typename Traits::InteractionState interactionState = {}; + const auto& visualStates = Traits::VisualStates(state.propertyGridState); + if (const auto* visualState = FindEntry(visualStates, fieldId); + visualState != nullptr) { + Traits::FieldState(interactionState) = visualState->state; + } + + interactionState.session = state.editableFieldSession; + interactionState.session.pointerPosition = state.pointerPosition; + interactionState.session.hasPointerPosition = state.hasPointerPosition; + if (interactionState.session.activeFieldId != fieldId) { + interactionState.session.focused = false; + interactionState.session.editing = false; + interactionState.session.dragArmed = false; + interactionState.session.dragActive = false; + } + return interactionState; +} + +template +void StoreInteractionState( + UIEditorPropertyGridInteractionState& state, + std::string_view fieldId, + const typename Traits::InteractionState& interactionState, + bool storeSharedSession) { + if (storeSharedSession) { + state.editableFieldSession = interactionState.session; + if (Traits::FieldState(interactionState).focused || + Traits::FieldState(interactionState).editing || + interactionState.session.dragArmed || + interactionState.session.dragActive) { + state.editableFieldSession.activeFieldId = std::string(fieldId); + } + } + + auto& visualStates = Traits::VisualStates(state.propertyGridState); + if (auto* visualState = + FindMutableEntry(visualStates, fieldId); + visualState != nullptr) { + visualState->state = Traits::FieldState(interactionState); + return; + } + + typename Traits::VisualStateEntry visualState = {}; + visualState.fieldId = std::string(fieldId); + visualState.state = Traits::FieldState(interactionState); + visualStates.push_back(std::move(visualState)); +} + +template +void PruneStateEntries( + std::vector& visualStates, + const UIEditorPropertyGridLayout& layout, + const std::vector& sections) { + const auto isVisibleTypedFieldId = [&layout, §ions](std::string_view fieldId) { + const std::size_t visibleFieldIndex = + FindUIEditorPropertyGridVisibleFieldIndex(layout, fieldId, sections); + if (visibleFieldIndex == UIEditorPropertyGridInvalidIndex || + visibleFieldIndex >= layout.visibleFieldSectionIndices.size() || + visibleFieldIndex >= layout.visibleFieldIndices.size()) { + return false; + } + + const std::size_t sectionIndex = layout.visibleFieldSectionIndices[visibleFieldIndex]; + const std::size_t fieldIndex = layout.visibleFieldIndices[visibleFieldIndex]; + return sectionIndex < sections.size() && + fieldIndex < sections[sectionIndex].fields.size() && + sections[sectionIndex].fields[fieldIndex].kind == Traits::kFieldKind; + }; + + visualStates.erase( + std::remove_if( + visualStates.begin(), + visualStates.end(), + [&isVisibleTypedFieldId](const typename Traits::VisualStateEntry& entry) { + return !isVisibleTypedFieldId(entry.fieldId); + }), + visualStates.end()); +} + +template +bool HasMeaningfulResult( + const typename Traits::InteractionResult& result, + const typename Traits::InteractionState& interactionState, + bool hadFocus, + bool hadEditing) { + return result.consumed || + result.focusChanged || + result.valueChanged || + result.stepApplied || + result.selectionChanged || + result.editStarted || + result.editCommitted || + result.editCommitRejected || + result.editCanceled || + result.hitTarget.kind != Traits::kNoneHitTargetKind || + hadFocus || + hadEditing || + Traits::FieldState(interactionState).focused || + Traits::FieldState(interactionState).editing; +} + +template +void CopyValuesToField( + UIEditorPropertyGridField& field, + const typename Traits::Spec& spec); + +template +void MergeVectorInteractionResult( + UIEditorPropertyGridInteractionResult& result, + const typename Traits::InteractionResult& fieldResult) { + result.consumed = result.consumed || fieldResult.consumed; + result.selectionChanged = result.selectionChanged || fieldResult.selectionChanged; + result.editStarted = result.editStarted || fieldResult.editStarted; + result.editCommitted = result.editCommitted || fieldResult.editCommitted; + result.editCommitRejected = + result.editCommitRejected || fieldResult.editCommitRejected; + result.editCanceled = result.editCanceled || fieldResult.editCanceled; +} + +template +bool ProcessVectorFieldEventImpl( + UIEditorPropertyGridInteractionState& state, + ::XCEngine::UI::Widgets::UISelectionModel& selectionModel, + ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel, + const UIEditorPropertyGridLayout& layout, + std::vector& sections, + const Widgets::UIEditorPropertyGridMetrics& metrics, + const ::XCEngine::UI::UIInputEvent& event, + UIEditorPropertyGridInteractionResult& result) { + const typename Traits::Metrics vectorMetrics = + Traits::BuildMetrics(metrics); + bool handled = false; + + for (std::size_t visibleFieldIndex = 0u; + visibleFieldIndex < layout.visibleFieldIndices.size(); + ++visibleFieldIndex) { + const std::size_t sectionIndex = layout.visibleFieldSectionIndices[visibleFieldIndex]; + const std::size_t fieldIndex = layout.visibleFieldIndices[visibleFieldIndex]; + if (sectionIndex >= sections.size() || + fieldIndex >= sections[sectionIndex].fields.size()) { + continue; + } + + UIEditorPropertyGridField& field = sections[sectionIndex].fields[fieldIndex]; + if (field.kind != Traits::kFieldKind) { + continue; + } + + const UIEditorEditableFieldSession previousSharedSession = + state.editableFieldSession; + typename Traits::InteractionState vectorState = + BuildInteractionState(state, field.fieldId); + const bool hadFocus = Traits::FieldState(vectorState).focused; + const bool hadEditing = Traits::FieldState(vectorState).editing; + typename Traits::Spec spec = Traits::BuildSpec(field); + const typename Traits::InteractionFrame frame = + Traits::Update( + vectorState, + spec, + layout.fieldRowRects[visibleFieldIndex], + { event }, + vectorMetrics); + + if (!HasMeaningfulResult( + frame.result, + vectorState, + hadFocus, + hadEditing)) { + continue; + } + + handled = true; + CopyValuesToField(field, spec); + + if (frame.result.consumed || + frame.result.selectionChanged || + frame.result.editStarted || + frame.result.valueChanged || + frame.result.stepApplied || + frame.result.editCommitted || + frame.result.editCanceled || + hadFocus || + Traits::FieldState(vectorState).focused) { + result.selectionChanged = + selectionModel.SetSelection(field.fieldId) || result.selectionChanged; + result.selectedFieldId = field.fieldId; + result.activeFieldId = field.fieldId; + state.keyboardNavigation.SetCurrentIndex(visibleFieldIndex); + } + + if (propertyEditModel.HasActiveEdit() && + propertyEditModel.GetActiveFieldId() != field.fieldId && + (frame.result.hitTarget.kind != Traits::kNoneHitTargetKind || + frame.result.selectionChanged || + frame.result.editStarted)) { + CommitActiveEdit(state, propertyEditModel, sections, result); + } + + const bool storeSharedSession = + previousSharedSession.activeFieldId == field.fieldId || + vectorState.session.activeFieldId == field.fieldId || + Traits::FieldState(vectorState).focused || + Traits::FieldState(vectorState).editing || + vectorState.session.dragArmed || + vectorState.session.dragActive || + frame.result.focusChanged || + frame.result.selectionChanged || + frame.result.editStarted || + frame.result.editCommitted || + frame.result.editCanceled; + StoreInteractionState( + state, + field.fieldId, + vectorState, + storeSharedSession); + state.propertyGridState.focused = + Traits::FieldState(vectorState).focused; + state.propertyGridState.pressedFieldId.clear(); + if (Traits::FieldState(vectorState).editing) { + propertyEditModel.BeginEdit( + field.fieldId, + vectorState.session.textInputState.value); + propertyEditModel.UpdateStagedValue( + vectorState.session.textInputState.value); + } else if (propertyEditModel.HasActiveEdit() && + propertyEditModel.GetActiveFieldId() == field.fieldId) { + if (frame.result.editCommitted) { + propertyEditModel.CommitEdit(); + } else if (frame.result.editCanceled) { + propertyEditModel.CancelEdit(); + } + } + + if (frame.result.hitTarget.kind != Traits::kNoneHitTargetKind || + frame.result.selectionChanged || + frame.result.editStarted) { + ClosePopup(state, result); + } + + MergeVectorInteractionResult(result, frame.result); + if (frame.result.valueChanged || frame.result.stepApplied) { + SetChangedValueResult(field, result); + } + + switch (frame.result.hitTarget.kind) { + case Traits::kRowHitTargetKind: + result.hitTarget.kind = Widgets::UIEditorPropertyGridHitTargetKind::FieldRow; + result.hitTarget.sectionIndex = sectionIndex; + result.hitTarget.fieldIndex = fieldIndex; + result.hitTarget.visibleFieldIndex = visibleFieldIndex; + break; + case Traits::kComponentHitTargetKind: + result.hitTarget.kind = Widgets::UIEditorPropertyGridHitTargetKind::ValueBox; + result.hitTarget.sectionIndex = sectionIndex; + result.hitTarget.fieldIndex = fieldIndex; + result.hitTarget.visibleFieldIndex = visibleFieldIndex; + break; + default: + break; + } + } + + return handled; +} + +struct Vector2Traits { + using VisualStateEntry = Widgets::UIEditorPropertyGridVector2FieldVisualState; + using InteractionState = UIEditorVector2FieldInteractionState; + using InteractionResult = UIEditorVector2FieldInteractionResult; + using InteractionFrame = UIEditorVector2FieldInteractionFrame; + using Metrics = Widgets::UIEditorVector2FieldMetrics; + using Spec = Widgets::UIEditorVector2FieldSpec; + + static constexpr UIEditorPropertyGridFieldKind kFieldKind = + UIEditorPropertyGridFieldKind::Vector2; + static constexpr auto kNoneHitTargetKind = + Widgets::UIEditorVector2FieldHitTargetKind::None; + static constexpr auto kRowHitTargetKind = + Widgets::UIEditorVector2FieldHitTargetKind::Row; + static constexpr auto kComponentHitTargetKind = + Widgets::UIEditorVector2FieldHitTargetKind::Component; + + static auto& VisualStates(Widgets::UIEditorPropertyGridState& state) { + return state.vector2FieldStates; + } + + static const auto& VisualStates(const Widgets::UIEditorPropertyGridState& state) { + return state.vector2FieldStates; + } + + static auto& FieldState(InteractionState& state) { + return state.vector2FieldState; + } + + static const auto& FieldState(const InteractionState& state) { + return state.vector2FieldState; + } + + static Spec BuildSpec(const UIEditorPropertyGridField& field) { + return Widgets::Internal::BuildVector2FieldSpec(field); + } + + static Metrics BuildMetrics(const Widgets::UIEditorPropertyGridMetrics& metrics) { + return BuildUIEditorPropertyGridVector2FieldMetrics(metrics); + } + + static InteractionFrame Update( + InteractionState& state, + Spec& spec, + const ::XCEngine::UI::UIRect& bounds, + const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents, + const Metrics& metrics) { + return UpdateUIEditorVector2FieldInteraction( + state, + spec, + bounds, + inputEvents, + metrics); + } +}; + +struct Vector3Traits { + using VisualStateEntry = Widgets::UIEditorPropertyGridVector3FieldVisualState; + using InteractionState = UIEditorVector3FieldInteractionState; + using InteractionResult = UIEditorVector3FieldInteractionResult; + using InteractionFrame = UIEditorVector3FieldInteractionFrame; + using Metrics = Widgets::UIEditorVector3FieldMetrics; + using Spec = Widgets::UIEditorVector3FieldSpec; + + static constexpr UIEditorPropertyGridFieldKind kFieldKind = + UIEditorPropertyGridFieldKind::Vector3; + static constexpr auto kNoneHitTargetKind = + Widgets::UIEditorVector3FieldHitTargetKind::None; + static constexpr auto kRowHitTargetKind = + Widgets::UIEditorVector3FieldHitTargetKind::Row; + static constexpr auto kComponentHitTargetKind = + Widgets::UIEditorVector3FieldHitTargetKind::Component; + + static auto& VisualStates(Widgets::UIEditorPropertyGridState& state) { + return state.vector3FieldStates; + } + + static const auto& VisualStates(const Widgets::UIEditorPropertyGridState& state) { + return state.vector3FieldStates; + } + + static auto& FieldState(InteractionState& state) { + return state.vector3FieldState; + } + + static const auto& FieldState(const InteractionState& state) { + return state.vector3FieldState; + } + + static Spec BuildSpec(const UIEditorPropertyGridField& field) { + return Widgets::Internal::BuildVector3FieldSpec(field); + } + + static Metrics BuildMetrics(const Widgets::UIEditorPropertyGridMetrics& metrics) { + return BuildUIEditorPropertyGridVector3FieldMetrics(metrics); + } + + static InteractionFrame Update( + InteractionState& state, + Spec& spec, + const ::XCEngine::UI::UIRect& bounds, + const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents, + const Metrics& metrics) { + return UpdateUIEditorVector3FieldInteraction( + state, + spec, + bounds, + inputEvents, + metrics); + } +}; + +struct Vector4Traits { + using VisualStateEntry = Widgets::UIEditorPropertyGridVector4FieldVisualState; + using InteractionState = UIEditorVector4FieldInteractionState; + using InteractionResult = UIEditorVector4FieldInteractionResult; + using InteractionFrame = UIEditorVector4FieldInteractionFrame; + using Metrics = Widgets::UIEditorVector4FieldMetrics; + using Spec = Widgets::UIEditorVector4FieldSpec; + + static constexpr UIEditorPropertyGridFieldKind kFieldKind = + UIEditorPropertyGridFieldKind::Vector4; + static constexpr auto kNoneHitTargetKind = + Widgets::UIEditorVector4FieldHitTargetKind::None; + static constexpr auto kRowHitTargetKind = + Widgets::UIEditorVector4FieldHitTargetKind::Row; + static constexpr auto kComponentHitTargetKind = + Widgets::UIEditorVector4FieldHitTargetKind::Component; + + static auto& VisualStates(Widgets::UIEditorPropertyGridState& state) { + return state.vector4FieldStates; + } + + static const auto& VisualStates(const Widgets::UIEditorPropertyGridState& state) { + return state.vector4FieldStates; + } + + static auto& FieldState(InteractionState& state) { + return state.vector4FieldState; + } + + static const auto& FieldState(const InteractionState& state) { + return state.vector4FieldState; + } + + static Spec BuildSpec(const UIEditorPropertyGridField& field) { + return Widgets::Internal::BuildVector4FieldSpec(field); + } + + static Metrics BuildMetrics(const Widgets::UIEditorPropertyGridMetrics& metrics) { + return BuildUIEditorPropertyGridVector4FieldMetrics(metrics); + } + + static InteractionFrame Update( + InteractionState& state, + Spec& spec, + const ::XCEngine::UI::UIRect& bounds, + const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents, + const Metrics& metrics) { + return UpdateUIEditorVector4FieldInteraction( + state, + spec, + bounds, + inputEvents, + metrics); + } +}; + +template <> +void CopyValuesToField( + UIEditorPropertyGridField& field, + const Vector2Traits::Spec& spec) { + field.vector2Value.values = spec.values; +} + +template <> +void CopyValuesToField( + UIEditorPropertyGridField& field, + const Vector3Traits::Spec& spec) { + field.vector3Value.values = spec.values; +} + +template <> +void CopyValuesToField( + UIEditorPropertyGridField& field, + const Vector4Traits::Spec& spec) { + field.vector4Value.values = spec.values; +} + +} // namespace VectorFieldInteractionLocal + +using Widgets::UIEditorPropertyGridLayout; +using Widgets::UIEditorPropertyGridSection; + +void PruneVectorFieldVisualStates( + UIEditorPropertyGridInteractionState& state, + const UIEditorPropertyGridLayout& layout, + const std::vector& sections) { + VectorFieldInteractionLocal::PruneStateEntries< + VectorFieldInteractionLocal::Vector2Traits>( + state.propertyGridState.vector2FieldStates, + layout, + sections); + VectorFieldInteractionLocal::PruneStateEntries< + VectorFieldInteractionLocal::Vector3Traits>( + state.propertyGridState.vector3FieldStates, + layout, + sections); + VectorFieldInteractionLocal::PruneStateEntries< + VectorFieldInteractionLocal::Vector4Traits>( + state.propertyGridState.vector4FieldStates, + layout, + sections); +} + +bool ProcessVector2FieldEvent( + UIEditorPropertyGridInteractionState& state, + ::XCEngine::UI::Widgets::UISelectionModel& selectionModel, + ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel, + const UIEditorPropertyGridLayout& layout, + std::vector& sections, + const Widgets::UIEditorPropertyGridMetrics& metrics, + const ::XCEngine::UI::UIInputEvent& event, + UIEditorPropertyGridInteractionResult& result) { + return VectorFieldInteractionLocal::ProcessVectorFieldEventImpl< + VectorFieldInteractionLocal::Vector2Traits>( + state, + selectionModel, + propertyEditModel, + layout, + sections, + metrics, + event, + result); +} + +bool ProcessVector3FieldEvent( + UIEditorPropertyGridInteractionState& state, + ::XCEngine::UI::Widgets::UISelectionModel& selectionModel, + ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel, + const UIEditorPropertyGridLayout& layout, + std::vector& sections, + const Widgets::UIEditorPropertyGridMetrics& metrics, + const ::XCEngine::UI::UIInputEvent& event, + UIEditorPropertyGridInteractionResult& result) { + return VectorFieldInteractionLocal::ProcessVectorFieldEventImpl< + VectorFieldInteractionLocal::Vector3Traits>( + state, + selectionModel, + propertyEditModel, + layout, + sections, + metrics, + event, + result); +} + +bool ProcessVector4FieldEvent( + UIEditorPropertyGridInteractionState& state, + ::XCEngine::UI::Widgets::UISelectionModel& selectionModel, + ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel, + const UIEditorPropertyGridLayout& layout, + std::vector& sections, + const Widgets::UIEditorPropertyGridMetrics& metrics, + const ::XCEngine::UI::UIInputEvent& event, + UIEditorPropertyGridInteractionResult& result) { + return VectorFieldInteractionLocal::ProcessVectorFieldEventImpl< + VectorFieldInteractionLocal::Vector4Traits>( + state, + selectionModel, + propertyEditModel, + layout, + sections, + metrics, + event, + result); +} + +} // namespace XCEngine::UI::Editor::Internal diff --git a/new_editor/src/Fields/UIEditorTextField.cpp b/new_editor/src/Fields/UIEditorTextField.cpp index c6b4f2a0..253fde4c 100644 --- a/new_editor/src/Fields/UIEditorTextField.cpp +++ b/new_editor/src/Fields/UIEditorTextField.cpp @@ -30,12 +30,6 @@ UIEditorTextFieldPalette ResolvePalette(const UIEditorTextFieldPalette& palette) const auto& tokens = GetUIEditorInspectorFieldStyleTokens(); UIEditorTextFieldPalette resolved = palette; - if (AreUIEditorFieldColorsEqual(palette.rowHoverColor, ::XCEngine::UI::UIColor(0.0f, 0.0f, 0.0f, 0.0f))) { - resolved.rowHoverColor = tokens.rowHoverColor; - } - if (AreUIEditorFieldColorsEqual(palette.rowActiveColor, ::XCEngine::UI::UIColor(0.0f, 0.0f, 0.0f, 0.0f))) { - resolved.rowActiveColor = tokens.rowActiveColor; - } if (AreUIEditorFieldColorsEqual(palette.valueBoxColor, ::XCEngine::UI::UIColor(0.18f, 0.18f, 0.18f, 1.0f))) { resolved.valueBoxColor = tokens.controlColor; } @@ -207,6 +201,18 @@ void AppendUIEditorTextFieldForeground( state.editing ? state.displayText : spec.value, spec.readOnly ? resolvedPalette.readOnlyValueColor : resolvedPalette.valueColor, resolvedMetrics.valueFontSize); + if (state.editing) { + AppendUIEditorTextCaret( + drawList, + layout.valueRect, + state.displayText, + state.caretOffset, + state.caretBlinkStartNanoseconds, + resolvedPalette.valueColor, + resolvedMetrics.valueFontSize, + resolvedMetrics.valueTextInsetX, + resolvedMetrics.valueTextInsetY); + } drawList.PopClipRect(); } diff --git a/new_editor/src/Fields/UIEditorTextFieldInteraction.cpp b/new_editor/src/Fields/UIEditorTextFieldInteraction.cpp index 469c4fe4..89b98f80 100644 --- a/new_editor/src/Fields/UIEditorTextFieldInteraction.cpp +++ b/new_editor/src/Fields/UIEditorTextFieldInteraction.cpp @@ -12,8 +12,6 @@ using ::XCEngine::Input::KeyCode; using ::XCEngine::UI::UIInputEvent; using ::XCEngine::UI::UIInputEventType; using ::XCEngine::UI::UIPointerButton; -using ::XCEngine::UI::Text::HandleKeyDown; -using ::XCEngine::UI::Text::InsertCharacter; using ::XCEngine::UI::Editor::Widgets::BuildUIEditorTextFieldLayout; using ::XCEngine::UI::Editor::Widgets::HitTestUIEditorTextField; using ::XCEngine::UI::Editor::Widgets::IsUIEditorTextFieldPointInside; @@ -23,37 +21,28 @@ using ::XCEngine::UI::Editor::Widgets::UIEditorTextFieldLayout; using ::XCEngine::UI::Editor::Widgets::UIEditorTextFieldMetrics; using ::XCEngine::UI::Editor::Widgets::UIEditorTextFieldSpec; -bool ShouldUsePointerPosition(const UIInputEvent& event) { - switch (event.type) { - case UIInputEventType::PointerMove: - case UIInputEventType::PointerEnter: - case UIInputEventType::PointerButtonDown: - case UIInputEventType::PointerButtonUp: - case UIInputEventType::PointerWheel: - return true; - default: - return false; - } -} - -void SyncDisplayText( - UIEditorTextFieldInteractionState& state, - const UIEditorTextFieldSpec& spec) { - if (!state.textFieldState.editing) { - state.textFieldState.displayText = spec.value; - } -} - -void SyncHoverTarget( +void SyncDisplayState( UIEditorTextFieldInteractionState& state, + const UIEditorTextFieldSpec& spec, const UIEditorTextFieldLayout& layout) { - if (!state.hasPointerPosition) { - state.textFieldState.hoveredTarget = UIEditorTextFieldHitTargetKind::None; + auto& fieldState = state.textFieldState; + const auto& session = state.session; + fieldState.focused = session.focused; + fieldState.editing = session.editing; + fieldState.caretBlinkStartNanoseconds = session.caretBlinkStartNanoseconds; + fieldState.displayText = session.editing + ? session.textInputState.value + : spec.value; + fieldState.caretOffset = session.editing + ? session.textInputState.caret + : spec.value.size(); + if (!session.hasPointerPosition) { + fieldState.hoveredTarget = UIEditorTextFieldHitTargetKind::None; return; } - state.textFieldState.hoveredTarget = - HitTestUIEditorTextField(layout, state.pointerPosition).kind; + fieldState.hoveredTarget = + HitTestUIEditorTextField(layout, session.pointerPosition).kind; } bool BeginEdit( @@ -64,44 +53,30 @@ bool BeginEdit( return false; } - const bool changed = state.editModel.BeginEdit(spec.fieldId, spec.value); - if (!changed && - state.editModel.HasActiveEdit() && - state.editModel.GetActiveFieldId() != spec.fieldId) { - return false; - } - if (!changed && state.textFieldState.editing) { - return false; - } - - state.textFieldState.editing = true; - state.textInputState.value = clearText ? std::string() : spec.value; - state.textInputState.caret = state.textInputState.value.size(); - state.editModel.UpdateStagedValue(state.textInputState.value); - state.textFieldState.displayText = state.textInputState.value; - return true; + return BeginUIEditorEditableFieldEdit( + state.session, + spec.fieldId, + UIEditorEditableFieldInvalidComponentIndex, + spec.value, + clearText); } bool CommitEdit( UIEditorTextFieldInteractionState& state, UIEditorTextFieldSpec& spec, UIEditorTextFieldInteractionResult& result) { - if (!state.textFieldState.editing || !state.editModel.HasActiveEdit()) { + if (!state.session.editing) { return false; } result.valueBefore = spec.value; - spec.value = state.textInputState.value; + spec.value = state.session.textInputState.value; result.valueAfter = spec.value; result.valueChanged = result.valueBefore != result.valueAfter; result.editCommitted = true; result.consumed = true; result.committedText = spec.value; - - state.editModel.CommitEdit(); - state.textInputState = {}; - state.textFieldState.editing = false; - state.textFieldState.displayText = spec.value; + CommitUIEditorEditableFieldEdit(state.session); return true; } @@ -109,14 +84,11 @@ bool CancelEdit( UIEditorTextFieldInteractionState& state, const UIEditorTextFieldSpec& spec, UIEditorTextFieldInteractionResult& result) { - if (!state.textFieldState.editing || !state.editModel.HasActiveEdit()) { + if (!state.session.editing) { return false; } - state.editModel.CancelEdit(); - state.textInputState = {}; - state.textFieldState.editing = false; - state.textFieldState.displayText = spec.value; + CancelUIEditorEditableFieldEdit(state.session); result.consumed = true; result.editCanceled = true; result.valueBefore = spec.value; @@ -132,32 +104,30 @@ UIEditorTextFieldInteractionFrame UpdateUIEditorTextFieldInteraction( const ::XCEngine::UI::UIRect& bounds, const std::vector& inputEvents, const UIEditorTextFieldMetrics& metrics) { - UIEditorTextFieldLayout layout = BuildUIEditorTextFieldLayout(bounds, spec, metrics); - SyncDisplayText(state, spec); - SyncHoverTarget(state, layout); + UIEditorTextFieldLayout layout = BuildUIEditorTextFieldLayout( + bounds, + spec, + metrics); + SyncDisplayState(state, spec, layout); UIEditorTextFieldInteractionResult interactionResult = {}; for (const UIInputEvent& event : inputEvents) { - if (ShouldUsePointerPosition(event)) { - state.pointerPosition = event.position; - state.hasPointerPosition = true; - } else if (event.type == UIInputEventType::PointerLeave) { - state.hasPointerPosition = false; - } + UpdateUIEditorEditableFieldPointerPosition(state.session, event); UIEditorTextFieldInteractionResult eventResult = {}; switch (event.type) { case UIInputEventType::FocusGained: - eventResult.focusChanged = !state.textFieldState.focused; - state.textFieldState.focused = true; + eventResult.focusChanged = !state.session.focused; + state.session.focused = true; break; case UIInputEventType::FocusLost: - eventResult.focusChanged = state.textFieldState.focused; - state.textFieldState.focused = false; + eventResult.focusChanged = state.session.focused; + state.session.focused = false; state.textFieldState.activeTarget = UIEditorTextFieldHitTargetKind::None; - state.hasPointerPosition = false; - if (state.textFieldState.editing) { + state.session.hasPointerPosition = false; + EndUIEditorEditableFieldDrag(state.session); + if (state.session.editing) { CommitEdit(state, spec, eventResult); } break; @@ -169,8 +139,10 @@ UIEditorTextFieldInteractionFrame UpdateUIEditorTextFieldInteraction( case UIInputEventType::PointerButtonDown: { const UIEditorTextFieldHitTarget hitTarget = - state.hasPointerPosition - ? HitTestUIEditorTextField(layout, state.pointerPosition) + state.session.hasPointerPosition + ? HitTestUIEditorTextField( + layout, + state.session.pointerPosition) : UIEditorTextFieldHitTarget {}; eventResult.hitTarget = hitTarget; @@ -179,24 +151,26 @@ UIEditorTextFieldInteractionFrame UpdateUIEditorTextFieldInteraction( } const bool insideField = - state.hasPointerPosition && - IsUIEditorTextFieldPointInside(layout.bounds, state.pointerPosition); + state.session.hasPointerPosition && + IsUIEditorTextFieldPointInside( + layout.bounds, + state.session.pointerPosition); if (insideField) { - eventResult.focusChanged = !state.textFieldState.focused; - state.textFieldState.focused = true; + eventResult.focusChanged = !state.session.focused; + state.session.focused = true; state.textFieldState.activeTarget = hitTarget.kind == UIEditorTextFieldHitTargetKind::None ? UIEditorTextFieldHitTargetKind::Row : hitTarget.kind; eventResult.consumed = true; } else { - if (state.textFieldState.editing) { + if (state.session.editing) { CommitEdit(state, spec, eventResult); - eventResult.focusChanged = state.textFieldState.focused; - state.textFieldState.focused = false; - } else if (state.textFieldState.focused) { + eventResult.focusChanged = state.session.focused; + state.session.focused = false; + } else if (state.session.focused) { eventResult.focusChanged = true; - state.textFieldState.focused = false; + state.session.focused = false; } state.textFieldState.activeTarget = UIEditorTextFieldHitTargetKind::None; } @@ -205,21 +179,37 @@ UIEditorTextFieldInteractionFrame UpdateUIEditorTextFieldInteraction( case UIInputEventType::PointerButtonUp: { const UIEditorTextFieldHitTarget hitTarget = - state.hasPointerPosition - ? HitTestUIEditorTextField(layout, state.pointerPosition) + state.session.hasPointerPosition + ? HitTestUIEditorTextField( + layout, + state.session.pointerPosition) : UIEditorTextFieldHitTarget {}; eventResult.hitTarget = hitTarget; if (event.pointerButton == UIPointerButton::Left) { - const UIEditorTextFieldHitTargetKind activeTarget = state.textFieldState.activeTarget; + const UIEditorTextFieldHitTargetKind activeTarget = + state.textFieldState.activeTarget; state.textFieldState.activeTarget = UIEditorTextFieldHitTargetKind::None; if (activeTarget == UIEditorTextFieldHitTargetKind::ValueBox && hitTarget.kind == UIEditorTextFieldHitTargetKind::ValueBox) { - if (!state.textFieldState.editing) { + if (state.session.editing) { + eventResult.consumed = true; + } else if (IsUIEditorEditableFieldDoubleClick( + state.session, + spec.fieldId, + event, + UIEditorEditableFieldInvalidComponentIndex)) { eventResult.editStarted = BeginEdit(state, spec, false); + eventResult.consumed = true; + } else { + RecordUIEditorEditableFieldClick( + state.session, + spec.fieldId, + event, + UIEditorEditableFieldInvalidComponentIndex); + eventResult.consumed = true; } - eventResult.consumed = true; } else if (hitTarget.kind == UIEditorTextFieldHitTargetKind::Row) { eventResult.consumed = true; } @@ -228,21 +218,21 @@ UIEditorTextFieldInteractionFrame UpdateUIEditorTextFieldInteraction( } case UIInputEventType::KeyDown: - if (!state.textFieldState.focused) { + if (!state.session.focused) { break; } - if (state.textFieldState.editing) { + if (state.session.editing) { if (event.keyCode == static_cast(KeyCode::Escape)) { CancelEdit(state, spec, eventResult); break; } - const auto textResult = - HandleKeyDown(state.textInputState, event.keyCode, event.modifiers); + const auto textResult = HandleUIEditorEditableFieldKeyDown( + state.session, + event.keyCode, + event.modifiers); if (textResult.handled) { - state.editModel.UpdateStagedValue(state.textInputState.value); - state.textFieldState.displayText = state.textInputState.value; eventResult.consumed = true; eventResult.valueBefore = spec.value; eventResult.valueAfter = spec.value; @@ -257,7 +247,7 @@ UIEditorTextFieldInteractionFrame UpdateUIEditorTextFieldInteraction( break; case UIInputEventType::Character: - if (!state.textFieldState.focused || + if (!state.session.focused || spec.readOnly || event.modifiers.control || event.modifiers.alt || @@ -265,13 +255,13 @@ UIEditorTextFieldInteractionFrame UpdateUIEditorTextFieldInteraction( break; } - if (!state.textFieldState.editing) { + if (!state.session.editing) { eventResult.editStarted = BeginEdit(state, spec, true); } - if (InsertCharacter(state.textInputState, event.character)) { - state.editModel.UpdateStagedValue(state.textInputState.value); - state.textFieldState.displayText = state.textInputState.value; + if (InsertUIEditorEditableFieldCharacter( + state.session, + event.character)) { eventResult.consumed = true; } break; @@ -281,11 +271,12 @@ UIEditorTextFieldInteractionFrame UpdateUIEditorTextFieldInteraction( } layout = BuildUIEditorTextFieldLayout(bounds, spec, metrics); - SyncDisplayText(state, spec); - SyncHoverTarget(state, layout); + SyncDisplayState(state, spec, layout); if (eventResult.hitTarget.kind == UIEditorTextFieldHitTargetKind::None && - state.hasPointerPosition) { - eventResult.hitTarget = HitTestUIEditorTextField(layout, state.pointerPosition); + state.session.hasPointerPosition) { + eventResult.hitTarget = HitTestUIEditorTextField( + layout, + state.session.pointerPosition); } if (eventResult.consumed || @@ -300,11 +291,12 @@ UIEditorTextFieldInteractionFrame UpdateUIEditorTextFieldInteraction( } layout = BuildUIEditorTextFieldLayout(bounds, spec, metrics); - SyncDisplayText(state, spec); - SyncHoverTarget(state, layout); + SyncDisplayState(state, spec, layout); if (interactionResult.hitTarget.kind == UIEditorTextFieldHitTargetKind::None && - state.hasPointerPosition) { - interactionResult.hitTarget = HitTestUIEditorTextField(layout, state.pointerPosition); + state.session.hasPointerPosition) { + interactionResult.hitTarget = HitTestUIEditorTextField( + layout, + state.session.pointerPosition); } return { diff --git a/new_editor/src/Fields/UIEditorVector2Field.cpp b/new_editor/src/Fields/UIEditorVector2Field.cpp index e64aab7f..68fa8204 100644 --- a/new_editor/src/Fields/UIEditorVector2Field.cpp +++ b/new_editor/src/Fields/UIEditorVector2Field.cpp @@ -42,12 +42,6 @@ UIEditorVector2FieldPalette ResolvePalette(const UIEditorVector2FieldPalette& pa const auto& tokens = GetUIEditorInspectorFieldStyleTokens(); UIEditorVector2FieldPalette resolved = palette; - if (AreUIEditorFieldColorsEqual(palette.rowHoverColor, ::XCEngine::UI::UIColor(0.0f, 0.0f, 0.0f, 0.0f))) { - resolved.rowHoverColor = tokens.rowHoverColor; - } - if (AreUIEditorFieldColorsEqual(palette.rowActiveColor, ::XCEngine::UI::UIColor(0.0f, 0.0f, 0.0f, 0.0f))) { - resolved.rowActiveColor = tokens.rowActiveColor; - } if (AreUIEditorFieldColorsEqual(palette.componentColor, ::XCEngine::UI::UIColor(0.18f, 0.18f, 0.18f, 1.0f))) { resolved.componentColor = tokens.controlColor; } @@ -161,12 +155,6 @@ UIEditorNumberFieldSpec BuildComponentNumberSpec( return palette.componentBorderColor; } -::XCEngine::UI::UIColor ResolveAxisColor( - const UIEditorVector2FieldPalette& palette, - std::size_t componentIndex) { - return componentIndex == 0u ? palette.axisXColor : palette.axisYColor; -} - } // namespace bool IsUIEditorVector2FieldPointInside( @@ -283,19 +271,6 @@ void AppendUIEditorVector2FieldBackground( } for (std::size_t componentIndex = 0u; componentIndex < layout.componentRects.size(); ++componentIndex) { - if (resolvedPalette.prefixColor.a > 0.0f) { - drawList.AddFilledRect( - layout.componentPrefixRects[componentIndex], - resolvedPalette.prefixColor, - resolvedMetrics.componentRounding); - } - if (resolvedPalette.prefixBorderColor.a > 0.0f) { - drawList.AddRectOutline( - layout.componentPrefixRects[componentIndex], - resolvedPalette.prefixBorderColor, - resolvedMetrics.borderThickness, - resolvedMetrics.componentRounding); - } drawList.AddFilledRect( layout.componentValueRects[componentIndex], ResolveComponentFillColor(spec, state, resolvedPalette, componentIndex), @@ -346,7 +321,7 @@ void AppendUIEditorVector2FieldForeground( resolvedMetrics.prefixFontSize, resolvedMetrics.prefixTextInsetY)), componentLabel, - ResolveAxisColor(resolvedPalette, componentIndex), + resolvedPalette.labelColor, resolvedMetrics.prefixFontSize); drawList.PopClipRect(); @@ -364,6 +339,18 @@ void AppendUIEditorVector2FieldForeground( : FormatUIEditorVector2FieldComponentValue(spec, componentIndex), spec.readOnly ? resolvedPalette.readOnlyValueColor : resolvedPalette.valueColor, resolvedMetrics.valueFontSize); + if (state.editing && state.selectedComponentIndex == componentIndex) { + AppendUIEditorTextCaret( + drawList, + layout.componentValueRects[componentIndex], + state.displayTexts[componentIndex], + state.caretOffset, + state.caretBlinkStartNanoseconds, + resolvedPalette.valueColor, + resolvedMetrics.valueFontSize, + resolvedMetrics.valueTextInsetX, + resolvedMetrics.valueTextInsetY); + } drawList.PopClipRect(); } } diff --git a/new_editor/src/Fields/UIEditorVector2FieldInteraction.cpp b/new_editor/src/Fields/UIEditorVector2FieldInteraction.cpp index 24973381..232cc251 100644 --- a/new_editor/src/Fields/UIEditorVector2FieldInteraction.cpp +++ b/new_editor/src/Fields/UIEditorVector2FieldInteraction.cpp @@ -1,13 +1,18 @@ #include -#include "Fields/VectorFieldInteractionInternal.h" +#include +#include #include namespace XCEngine::UI::Editor { namespace { +using ::XCEngine::Input::KeyCode; +using ::XCEngine::UI::UIInputEvent; +using ::XCEngine::UI::UIInputEventType; +using ::XCEngine::UI::UIPointerButton; using ::XCEngine::UI::Editor::Widgets::BuildUIEditorVector2FieldLayout; using ::XCEngine::UI::Editor::Widgets::FormatUIEditorVector2FieldComponentValue; using ::XCEngine::UI::Editor::Widgets::HitTestUIEditorVector2Field; @@ -21,49 +26,249 @@ using ::XCEngine::UI::Editor::Widgets::UIEditorVector2FieldLayout; using ::XCEngine::UI::Editor::Widgets::UIEditorVector2FieldMetrics; using ::XCEngine::UI::Editor::Widgets::UIEditorVector2FieldSpec; -struct Vector2FieldInteractionTraits { - using InteractionState = UIEditorVector2FieldInteractionState; - using InteractionResult = UIEditorVector2FieldInteractionResult; - using Spec = UIEditorVector2FieldSpec; - using Layout = UIEditorVector2FieldLayout; - using Metrics = UIEditorVector2FieldMetrics; - using HitTarget = UIEditorVector2FieldHitTarget; - using HitTargetKind = UIEditorVector2FieldHitTargetKind; +constexpr std::size_t kComponentCount = 2u; +constexpr std::size_t kLastComponentIndex = 1u; - static constexpr std::size_t kComponentCount = 2u; - static constexpr std::size_t kLastComponentIndex = 1u; - static constexpr std::size_t kInvalidComponentIndex = UIEditorVector2FieldInvalidComponentIndex; - static constexpr HitTargetKind kNoneHitTargetKind = UIEditorVector2FieldHitTargetKind::None; - static constexpr HitTargetKind kRowHitTargetKind = UIEditorVector2FieldHitTargetKind::Row; - static constexpr HitTargetKind kComponentHitTargetKind = UIEditorVector2FieldHitTargetKind::Component; +bool IsPermittedCharacter( + const UIEditorVector2FieldSpec& spec, + std::uint32_t character) { + if (character >= static_cast('0') && + character <= static_cast('9')) { + return true; + } + if (character == static_cast('-') || + character == static_cast('+')) { + return true; + } + return !spec.integerMode && character == static_cast('.'); +} - static auto& FieldState(InteractionState& state) { return state.vector2FieldState; } - static const auto& FieldState(const InteractionState& state) { return state.vector2FieldState; } +std::size_t ResolveSelectedComponentIndex( + const UIEditorVector2FieldInteractionState& state) { + return state.vector2FieldState.selectedComponentIndex == + UIEditorVector2FieldInvalidComponentIndex + ? 0u + : state.vector2FieldState.selectedComponentIndex; +} - static Layout BuildLayout(const ::XCEngine::UI::UIRect& bounds, const Spec& spec, const Metrics& metrics) { - return BuildUIEditorVector2FieldLayout(bounds, spec, metrics); +double ResolveDragSensitivity(const UIEditorVector2FieldSpec& spec) { + if (spec.step != 0.0) { + return spec.step; } - static HitTarget HitTest(const Layout& layout, const ::XCEngine::UI::UIPoint& point) { - return HitTestUIEditorVector2Field(layout, point); + return spec.integerMode ? 1.0 : 0.1; +} + +void SyncDisplayState( + UIEditorVector2FieldInteractionState& state, + const UIEditorVector2FieldSpec& spec, + const UIEditorVector2FieldLayout& layout) { + auto& fieldState = state.vector2FieldState; + const auto& session = state.session; + fieldState.focused = session.focused; + fieldState.editing = session.editing; + fieldState.caretBlinkStartNanoseconds = session.caretBlinkStartNanoseconds; + for (std::size_t componentIndex = 0u; componentIndex < kComponentCount; + ++componentIndex) { + fieldState.displayTexts[componentIndex] = + session.editing && + fieldState.selectedComponentIndex == componentIndex + ? session.textInputState.value + : FormatUIEditorVector2FieldComponentValue(spec, componentIndex); + } + if (session.editing && + fieldState.selectedComponentIndex != + UIEditorVector2FieldInvalidComponentIndex) { + fieldState.caretOffset = session.textInputState.caret; + } else if (fieldState.selectedComponentIndex != + UIEditorVector2FieldInvalidComponentIndex) { + fieldState.caretOffset = + fieldState.displayTexts[fieldState.selectedComponentIndex].size(); + } else { + fieldState.caretOffset = 0u; + } + if (!session.hasPointerPosition) { + fieldState.hoveredTarget = UIEditorVector2FieldHitTargetKind::None; + fieldState.hoveredComponentIndex = + UIEditorVector2FieldInvalidComponentIndex; + return; } - static bool IsPointInside(const ::XCEngine::UI::UIRect& rect, const ::XCEngine::UI::UIPoint& point) { - return IsUIEditorVector2FieldPointInside(rect, point); + const UIEditorVector2FieldHitTarget hitTarget = + HitTestUIEditorVector2Field(layout, session.pointerPosition); + fieldState.hoveredTarget = hitTarget.kind; + fieldState.hoveredComponentIndex = hitTarget.componentIndex; +} + +bool SelectComponent( + UIEditorVector2FieldInteractionState& state, + std::size_t componentIndex, + UIEditorVector2FieldInteractionResult& result) { + if (componentIndex >= kComponentCount) { + return false; } - static std::string FormatComponentValue(const Spec& spec, std::size_t componentIndex) { - return FormatUIEditorVector2FieldComponentValue(spec, componentIndex); + const std::size_t before = ResolveSelectedComponentIndex(state); + state.vector2FieldState.selectedComponentIndex = componentIndex; + state.session.activeComponentIndex = componentIndex; + result.selectionChanged = before != componentIndex; + result.selectedComponentIndex = componentIndex; + return true; +} + +bool MoveSelection( + UIEditorVector2FieldInteractionState& state, + int direction, + UIEditorVector2FieldInteractionResult& result) { + const std::size_t before = ResolveSelectedComponentIndex(state); + const std::size_t after = direction < 0 + ? (before == 0u ? 0u : before - 1u) + : (before >= kLastComponentIndex ? kLastComponentIndex : before + 1u); + state.vector2FieldState.selectedComponentIndex = after; + state.session.activeComponentIndex = after; + result.selectionChanged = before != after; + result.selectedComponentIndex = after; + result.consumed = true; + return true; +} + +bool BeginEdit( + UIEditorVector2FieldInteractionState& state, + const UIEditorVector2FieldSpec& spec, + std::size_t componentIndex, + bool clearText) { + if (spec.readOnly || componentIndex >= spec.values.size()) { + return false; } - static bool TryParseComponentValue(const Spec& spec, std::string_view text, double& outValue) { - return TryParseUIEditorVector2FieldComponentValue(spec, text, outValue); + state.vector2FieldState.selectedComponentIndex = componentIndex; + return BeginUIEditorEditableFieldEdit( + state.session, + spec.fieldId, + componentIndex, + FormatUIEditorVector2FieldComponentValue(spec, componentIndex), + clearText); +} + +bool CommitEdit( + UIEditorVector2FieldInteractionState& state, + UIEditorVector2FieldSpec& spec, + UIEditorVector2FieldInteractionResult& result) { + const std::size_t componentIndex = + state.vector2FieldState.selectedComponentIndex; + if (!state.session.editing || componentIndex >= spec.values.size()) { + return false; } - static double NormalizeComponentValue(const Spec& spec, double value) { - return NormalizeUIEditorVector2FieldComponentValue(spec, value); + double parsedValue = spec.values[componentIndex]; + if (!TryParseUIEditorVector2FieldComponentValue( + spec, + state.session.textInputState.value, + parsedValue)) { + result.consumed = true; + result.editCommitRejected = true; + return false; } -}; + + result.valuesBefore = spec.values; + spec.values[componentIndex] = + NormalizeUIEditorVector2FieldComponentValue(spec, parsedValue); + result.valuesAfter = spec.values; + result.valueChanged = result.valuesBefore != result.valuesAfter; + result.editCommitted = true; + result.consumed = true; + result.changedComponentIndex = componentIndex; + result.selectedComponentIndex = componentIndex; + result.committedText = + FormatUIEditorVector2FieldComponentValue(spec, componentIndex); + CommitUIEditorEditableFieldEdit(state.session); + return true; +} + +bool CancelEdit( + UIEditorVector2FieldInteractionState& state, + const UIEditorVector2FieldSpec& spec, + UIEditorVector2FieldInteractionResult& result) { + const std::size_t componentIndex = + state.vector2FieldState.selectedComponentIndex; + if (!state.session.editing || componentIndex >= spec.values.size()) { + return false; + } + + CancelUIEditorEditableFieldEdit(state.session); + result.consumed = true; + result.editCanceled = true; + result.valuesBefore = spec.values; + result.valuesAfter = spec.values; + result.selectedComponentIndex = componentIndex; + return true; +} + +bool ApplyStep( + UIEditorVector2FieldInteractionState& state, + UIEditorVector2FieldSpec& spec, + double direction, + bool snapToEdge, + UIEditorVector2FieldInteractionResult& result) { + if (spec.readOnly) { + return false; + } + + const std::size_t componentIndex = ResolveSelectedComponentIndex(state); + state.vector2FieldState.selectedComponentIndex = componentIndex; + state.session.activeComponentIndex = componentIndex; + if (state.session.editing && !CommitEdit(state, spec, result)) { + return result.editCommitRejected; + } + + result.valuesBefore = spec.values; + if (snapToEdge) { + spec.values[componentIndex] = direction < 0.0 + ? NormalizeUIEditorVector2FieldComponentValue(spec, spec.minValue) + : NormalizeUIEditorVector2FieldComponentValue(spec, spec.maxValue); + } else { + const double step = spec.step == 0.0 ? 1.0 : spec.step; + spec.values[componentIndex] = + NormalizeUIEditorVector2FieldComponentValue( + spec, + spec.values[componentIndex] + step * direction); + result.stepDelta = step * direction; + } + + result.valuesAfter = spec.values; + result.stepApplied = true; + result.valueChanged = result.valuesBefore != result.valuesAfter; + result.changedComponentIndex = componentIndex; + result.selectedComponentIndex = componentIndex; + result.consumed = true; + return true; +} + +bool ApplyDrag( + UIEditorVector2FieldInteractionState& state, + UIEditorVector2FieldSpec& spec, + std::size_t componentIndex, + UIEditorVector2FieldInteractionResult& result) { + if (spec.readOnly || state.session.editing || + componentIndex >= spec.values.size()) { + return false; + } + + result.valuesBefore = spec.values; + spec.values[componentIndex] = + NormalizeUIEditorVector2FieldComponentValue( + spec, + state.session.dragStartValue + + ResolveUIEditorEditableFieldDragDelta( + state.session, + ResolveDragSensitivity(spec))); + result.valuesAfter = spec.values; + result.valueChanged = result.valuesBefore != result.valuesAfter; + result.changedComponentIndex = componentIndex; + result.selectedComponentIndex = componentIndex; + result.consumed = true; + return true; +} } // namespace @@ -73,18 +278,348 @@ UIEditorVector2FieldInteractionFrame UpdateUIEditorVector2FieldInteraction( const ::XCEngine::UI::UIRect& bounds, const std::vector& inputEvents, const UIEditorVector2FieldMetrics& metrics) { - UIEditorVector2FieldLayout layout = {}; - UIEditorVector2FieldInteractionResult result = - Internal::UpdateVectorFieldInteractionImpl( - state, - spec, - bounds, - inputEvents, - metrics, - layout); + UIEditorVector2FieldLayout layout = BuildUIEditorVector2FieldLayout( + bounds, + spec, + metrics); + SyncDisplayState(state, spec, layout); + + UIEditorVector2FieldInteractionResult interactionResult = {}; + interactionResult.selectedComponentIndex = + state.vector2FieldState.selectedComponentIndex; + for (const UIInputEvent& event : inputEvents) { + UpdateUIEditorEditableFieldPointerPosition(state.session, event); + + UIEditorVector2FieldInteractionResult eventResult = {}; + eventResult.selectedComponentIndex = + state.vector2FieldState.selectedComponentIndex; + switch (event.type) { + case UIInputEventType::FocusGained: + eventResult.focusChanged = !state.session.focused; + state.session.focused = true; + if (state.vector2FieldState.selectedComponentIndex == + UIEditorVector2FieldInvalidComponentIndex) { + state.vector2FieldState.selectedComponentIndex = 0u; + } + state.session.activeComponentIndex = + state.vector2FieldState.selectedComponentIndex; + eventResult.selectedComponentIndex = + state.vector2FieldState.selectedComponentIndex; + break; + + case UIInputEventType::FocusLost: + eventResult.focusChanged = state.session.focused; + state.session.focused = false; + state.vector2FieldState.activeTarget = + UIEditorVector2FieldHitTargetKind::None; + state.vector2FieldState.activeComponentIndex = + UIEditorVector2FieldInvalidComponentIndex; + state.session.hasPointerPosition = false; + EndUIEditorEditableFieldDrag(state.session); + if (state.session.editing) { + CommitEdit(state, spec, eventResult); + if (eventResult.editCommitRejected) { + CancelEdit(state, spec, eventResult); + } + } + break; + + case UIInputEventType::PointerMove: + TryActivateUIEditorEditableFieldDrag(state.session, event); + if (state.session.dragActive && + state.vector2FieldState.activeTarget == + UIEditorVector2FieldHitTargetKind::Component && + state.vector2FieldState.activeComponentIndex != + UIEditorVector2FieldInvalidComponentIndex) { + ApplyDrag( + state, + spec, + state.vector2FieldState.activeComponentIndex, + eventResult); + } + break; + + case UIInputEventType::PointerEnter: + case UIInputEventType::PointerLeave: + break; + + case UIInputEventType::PointerButtonDown: { + const UIEditorVector2FieldHitTarget hitTarget = + state.session.hasPointerPosition + ? HitTestUIEditorVector2Field( + layout, + state.session.pointerPosition) + : UIEditorVector2FieldHitTarget {}; + eventResult.hitTarget = hitTarget; + + if (event.pointerButton != UIPointerButton::Left) { + break; + } + + const bool insideField = + state.session.hasPointerPosition && + IsUIEditorVector2FieldPointInside( + layout.bounds, + state.session.pointerPosition); + if (insideField) { + eventResult.focusChanged = !state.session.focused; + state.session.focused = true; + state.vector2FieldState.activeTarget = + hitTarget.kind == UIEditorVector2FieldHitTargetKind::None + ? UIEditorVector2FieldHitTargetKind::Row + : hitTarget.kind; + state.vector2FieldState.activeComponentIndex = + hitTarget.componentIndex; + if (hitTarget.kind == + UIEditorVector2FieldHitTargetKind::Component && + !state.session.editing) { + ArmUIEditorEditableFieldDrag( + state.session, + spec.fieldId, + hitTarget.componentIndex, + hitTarget.componentIndex < spec.values.size() + ? spec.values[hitTarget.componentIndex] + : 0.0); + SelectComponent(state, hitTarget.componentIndex, eventResult); + } else { + EndUIEditorEditableFieldDrag(state.session); + if (state.vector2FieldState.selectedComponentIndex == + UIEditorVector2FieldInvalidComponentIndex) { + state.vector2FieldState.selectedComponentIndex = 0u; + state.session.activeComponentIndex = 0u; + eventResult.selectedComponentIndex = 0u; + } + } + eventResult.consumed = true; + } else { + if (state.session.editing) { + CommitEdit(state, spec, eventResult); + if (!eventResult.editCommitRejected) { + eventResult.focusChanged = state.session.focused; + state.session.focused = false; + } + } else if (state.session.focused) { + eventResult.focusChanged = true; + state.session.focused = false; + } + state.vector2FieldState.activeTarget = + UIEditorVector2FieldHitTargetKind::None; + state.vector2FieldState.activeComponentIndex = + UIEditorVector2FieldInvalidComponentIndex; + EndUIEditorEditableFieldDrag(state.session); + } + break; + } + + case UIInputEventType::PointerButtonUp: { + const UIEditorVector2FieldHitTarget hitTarget = + state.session.hasPointerPosition + ? HitTestUIEditorVector2Field( + layout, + state.session.pointerPosition) + : UIEditorVector2FieldHitTarget {}; + eventResult.hitTarget = hitTarget; + + if (event.pointerButton == UIPointerButton::Left) { + const UIEditorVector2FieldHitTargetKind activeTarget = + state.vector2FieldState.activeTarget; + const std::size_t activeComponentIndex = + state.vector2FieldState.activeComponentIndex; + state.vector2FieldState.activeTarget = + UIEditorVector2FieldHitTargetKind::None; + state.vector2FieldState.activeComponentIndex = + UIEditorVector2FieldInvalidComponentIndex; + + if (activeTarget == UIEditorVector2FieldHitTargetKind::Component && + hitTarget.kind == UIEditorVector2FieldHitTargetKind::Component && + activeComponentIndex == hitTarget.componentIndex) { + SelectComponent(state, hitTarget.componentIndex, eventResult); + if (state.session.dragActive) { + eventResult.consumed = true; + } else if (!state.session.editing && + IsUIEditorEditableFieldDoubleClick( + state.session, + spec.fieldId, + event, + hitTarget.componentIndex)) { + eventResult.editStarted = + BeginEdit(state, spec, hitTarget.componentIndex, false); + eventResult.consumed = true; + } else { + RecordUIEditorEditableFieldClick( + state.session, + spec.fieldId, + event, + hitTarget.componentIndex); + eventResult.consumed = true; + } + } else if (hitTarget.kind == UIEditorVector2FieldHitTargetKind::Row) { + eventResult.consumed = true; + } + + EndUIEditorEditableFieldDrag(state.session); + } + break; + } + + case UIInputEventType::KeyDown: + if (!state.session.focused) { + break; + } + + if (state.session.editing) { + if (event.keyCode == static_cast(KeyCode::Escape)) { + CancelEdit(state, spec, eventResult); + break; + } + if (event.keyCode == static_cast(KeyCode::Tab)) { + if (CommitEdit(state, spec, eventResult)) { + MoveSelection( + state, + event.modifiers.shift ? -1 : 1, + eventResult); + } + eventResult.consumed = true; + break; + } + + const auto textResult = HandleUIEditorEditableFieldKeyDown( + state.session, + event.keyCode, + event.modifiers); + if (textResult.handled) { + eventResult.consumed = true; + eventResult.selectedComponentIndex = + state.vector2FieldState.selectedComponentIndex; + if (textResult.submitRequested) { + CommitEdit(state, spec, eventResult); + } + } + } else { + switch (static_cast(event.keyCode)) { + case KeyCode::Left: + MoveSelection(state, -1, eventResult); + break; + case KeyCode::Right: + case KeyCode::Tab: + MoveSelection( + state, + static_cast(event.keyCode) == KeyCode::Tab && + event.modifiers.shift + ? -1 + : 1, + eventResult); + break; + case KeyCode::Up: + ApplyStep(state, spec, 1.0, false, eventResult); + break; + case KeyCode::Down: + ApplyStep(state, spec, -1.0, false, eventResult); + break; + case KeyCode::Home: + ApplyStep(state, spec, -1.0, true, eventResult); + break; + case KeyCode::End: + ApplyStep(state, spec, 1.0, true, eventResult); + break; + case KeyCode::Enter: + eventResult.selectedComponentIndex = + ResolveSelectedComponentIndex(state); + eventResult.editStarted = BeginEdit( + state, + spec, + eventResult.selectedComponentIndex, + false); + eventResult.consumed = eventResult.editStarted; + break; + default: + break; + } + } + break; + + case UIInputEventType::Character: + if (!state.session.focused || + spec.readOnly || + event.modifiers.control || + event.modifiers.alt || + event.modifiers.super || + !IsPermittedCharacter(spec, event.character)) { + break; + } + + if (state.vector2FieldState.selectedComponentIndex == + UIEditorVector2FieldInvalidComponentIndex) { + state.vector2FieldState.selectedComponentIndex = 0u; + } + state.session.activeComponentIndex = + state.vector2FieldState.selectedComponentIndex; + eventResult.selectedComponentIndex = + state.vector2FieldState.selectedComponentIndex; + if (!state.session.editing) { + eventResult.editStarted = BeginEdit( + state, + spec, + state.vector2FieldState.selectedComponentIndex, + true); + } + if (InsertUIEditorEditableFieldCharacter( + state.session, + event.character)) { + eventResult.consumed = true; + } + break; + + default: + break; + } + + layout = BuildUIEditorVector2FieldLayout(bounds, spec, metrics); + SyncDisplayState(state, spec, layout); + if (eventResult.hitTarget.kind == UIEditorVector2FieldHitTargetKind::None && + state.session.hasPointerPosition) { + eventResult.hitTarget = HitTestUIEditorVector2Field( + layout, + state.session.pointerPosition); + } + if (eventResult.selectedComponentIndex == + UIEditorVector2FieldInvalidComponentIndex) { + eventResult.selectedComponentIndex = + state.vector2FieldState.selectedComponentIndex; + } + + if (eventResult.consumed || + eventResult.focusChanged || + eventResult.valueChanged || + eventResult.stepApplied || + eventResult.selectionChanged || + eventResult.editStarted || + eventResult.editCommitted || + eventResult.editCommitRejected || + eventResult.editCanceled || + eventResult.hitTarget.kind != + UIEditorVector2FieldHitTargetKind::None) { + interactionResult = std::move(eventResult); + } + } + + layout = BuildUIEditorVector2FieldLayout(bounds, spec, metrics); + SyncDisplayState(state, spec, layout); + if (interactionResult.hitTarget.kind == UIEditorVector2FieldHitTargetKind::None && + state.session.hasPointerPosition) { + interactionResult.hitTarget = HitTestUIEditorVector2Field( + layout, + state.session.pointerPosition); + } + if (interactionResult.selectedComponentIndex == + UIEditorVector2FieldInvalidComponentIndex) { + interactionResult.selectedComponentIndex = + state.vector2FieldState.selectedComponentIndex; + } + return { std::move(layout), - std::move(result) + std::move(interactionResult) }; } diff --git a/new_editor/src/Fields/UIEditorVector3Field.cpp b/new_editor/src/Fields/UIEditorVector3Field.cpp index 0840a9ce..a804cd5c 100644 --- a/new_editor/src/Fields/UIEditorVector3Field.cpp +++ b/new_editor/src/Fields/UIEditorVector3Field.cpp @@ -42,12 +42,6 @@ UIEditorVector3FieldPalette ResolvePalette(const UIEditorVector3FieldPalette& pa const auto& tokens = GetUIEditorInspectorFieldStyleTokens(); UIEditorVector3FieldPalette resolved = palette; - if (AreUIEditorFieldColorsEqual(palette.rowHoverColor, ::XCEngine::UI::UIColor(0.0f, 0.0f, 0.0f, 0.0f))) { - resolved.rowHoverColor = tokens.rowHoverColor; - } - if (AreUIEditorFieldColorsEqual(palette.rowActiveColor, ::XCEngine::UI::UIColor(0.0f, 0.0f, 0.0f, 0.0f))) { - resolved.rowActiveColor = tokens.rowActiveColor; - } if (AreUIEditorFieldColorsEqual(palette.componentColor, ::XCEngine::UI::UIColor(0.18f, 0.18f, 0.18f, 1.0f))) { resolved.componentColor = tokens.controlColor; } @@ -164,20 +158,6 @@ UIEditorNumberFieldSpec BuildComponentNumberSpec( return palette.componentBorderColor; } -::XCEngine::UI::UIColor ResolveAxisColor( - const UIEditorVector3FieldPalette& palette, - std::size_t componentIndex) { - switch (componentIndex) { - case 0u: - return palette.axisXColor; - case 1u: - return palette.axisYColor; - case 2u: - default: - return palette.axisZColor; - } -} - } // namespace bool IsUIEditorVector3FieldPointInside( @@ -294,19 +274,6 @@ void AppendUIEditorVector3FieldBackground( } for (std::size_t componentIndex = 0u; componentIndex < layout.componentRects.size(); ++componentIndex) { - if (resolvedPalette.prefixColor.a > 0.0f) { - drawList.AddFilledRect( - layout.componentPrefixRects[componentIndex], - resolvedPalette.prefixColor, - resolvedMetrics.componentRounding); - } - if (resolvedPalette.prefixBorderColor.a > 0.0f) { - drawList.AddRectOutline( - layout.componentPrefixRects[componentIndex], - resolvedPalette.prefixBorderColor, - resolvedMetrics.borderThickness, - resolvedMetrics.componentRounding); - } drawList.AddFilledRect( layout.componentValueRects[componentIndex], ResolveComponentFillColor(spec, state, resolvedPalette, componentIndex), @@ -357,7 +324,7 @@ void AppendUIEditorVector3FieldForeground( resolvedMetrics.prefixFontSize, resolvedMetrics.prefixTextInsetY)), componentLabel, - ResolveAxisColor(resolvedPalette, componentIndex), + resolvedPalette.labelColor, resolvedMetrics.prefixFontSize); drawList.PopClipRect(); @@ -375,6 +342,18 @@ void AppendUIEditorVector3FieldForeground( : FormatUIEditorVector3FieldComponentValue(spec, componentIndex), spec.readOnly ? resolvedPalette.readOnlyValueColor : resolvedPalette.valueColor, resolvedMetrics.valueFontSize); + if (state.editing && state.selectedComponentIndex == componentIndex) { + AppendUIEditorTextCaret( + drawList, + layout.componentValueRects[componentIndex], + state.displayTexts[componentIndex], + state.caretOffset, + state.caretBlinkStartNanoseconds, + resolvedPalette.valueColor, + resolvedMetrics.valueFontSize, + resolvedMetrics.valueTextInsetX, + resolvedMetrics.valueTextInsetY); + } drawList.PopClipRect(); } } diff --git a/new_editor/src/Fields/UIEditorVector3FieldInteraction.cpp b/new_editor/src/Fields/UIEditorVector3FieldInteraction.cpp index ab961f0a..4fc8b819 100644 --- a/new_editor/src/Fields/UIEditorVector3FieldInteraction.cpp +++ b/new_editor/src/Fields/UIEditorVector3FieldInteraction.cpp @@ -1,13 +1,18 @@ #include -#include "Fields/VectorFieldInteractionInternal.h" +#include +#include #include namespace XCEngine::UI::Editor { namespace { +using ::XCEngine::Input::KeyCode; +using ::XCEngine::UI::UIInputEvent; +using ::XCEngine::UI::UIInputEventType; +using ::XCEngine::UI::UIPointerButton; using ::XCEngine::UI::Editor::Widgets::BuildUIEditorVector3FieldLayout; using ::XCEngine::UI::Editor::Widgets::FormatUIEditorVector3FieldComponentValue; using ::XCEngine::UI::Editor::Widgets::HitTestUIEditorVector3Field; @@ -21,49 +26,249 @@ using ::XCEngine::UI::Editor::Widgets::UIEditorVector3FieldLayout; using ::XCEngine::UI::Editor::Widgets::UIEditorVector3FieldMetrics; using ::XCEngine::UI::Editor::Widgets::UIEditorVector3FieldSpec; -struct Vector3FieldInteractionTraits { - using InteractionState = UIEditorVector3FieldInteractionState; - using InteractionResult = UIEditorVector3FieldInteractionResult; - using Spec = UIEditorVector3FieldSpec; - using Layout = UIEditorVector3FieldLayout; - using Metrics = UIEditorVector3FieldMetrics; - using HitTarget = UIEditorVector3FieldHitTarget; - using HitTargetKind = UIEditorVector3FieldHitTargetKind; +constexpr std::size_t kComponentCount = 3u; +constexpr std::size_t kLastComponentIndex = 2u; - static constexpr std::size_t kComponentCount = 3u; - static constexpr std::size_t kLastComponentIndex = 2u; - static constexpr std::size_t kInvalidComponentIndex = UIEditorVector3FieldInvalidComponentIndex; - static constexpr HitTargetKind kNoneHitTargetKind = UIEditorVector3FieldHitTargetKind::None; - static constexpr HitTargetKind kRowHitTargetKind = UIEditorVector3FieldHitTargetKind::Row; - static constexpr HitTargetKind kComponentHitTargetKind = UIEditorVector3FieldHitTargetKind::Component; +bool IsPermittedCharacter( + const UIEditorVector3FieldSpec& spec, + std::uint32_t character) { + if (character >= static_cast('0') && + character <= static_cast('9')) { + return true; + } + if (character == static_cast('-') || + character == static_cast('+')) { + return true; + } + return !spec.integerMode && character == static_cast('.'); +} - static auto& FieldState(InteractionState& state) { return state.vector3FieldState; } - static const auto& FieldState(const InteractionState& state) { return state.vector3FieldState; } +std::size_t ResolveSelectedComponentIndex( + const UIEditorVector3FieldInteractionState& state) { + return state.vector3FieldState.selectedComponentIndex == + UIEditorVector3FieldInvalidComponentIndex + ? 0u + : state.vector3FieldState.selectedComponentIndex; +} - static Layout BuildLayout(const ::XCEngine::UI::UIRect& bounds, const Spec& spec, const Metrics& metrics) { - return BuildUIEditorVector3FieldLayout(bounds, spec, metrics); +double ResolveDragSensitivity(const UIEditorVector3FieldSpec& spec) { + if (spec.step != 0.0) { + return spec.step; } - static HitTarget HitTest(const Layout& layout, const ::XCEngine::UI::UIPoint& point) { - return HitTestUIEditorVector3Field(layout, point); + return spec.integerMode ? 1.0 : 0.1; +} + +void SyncDisplayState( + UIEditorVector3FieldInteractionState& state, + const UIEditorVector3FieldSpec& spec, + const UIEditorVector3FieldLayout& layout) { + auto& fieldState = state.vector3FieldState; + const auto& session = state.session; + fieldState.focused = session.focused; + fieldState.editing = session.editing; + fieldState.caretBlinkStartNanoseconds = session.caretBlinkStartNanoseconds; + for (std::size_t componentIndex = 0u; componentIndex < kComponentCount; + ++componentIndex) { + fieldState.displayTexts[componentIndex] = + session.editing && + fieldState.selectedComponentIndex == componentIndex + ? session.textInputState.value + : FormatUIEditorVector3FieldComponentValue(spec, componentIndex); + } + if (session.editing && + fieldState.selectedComponentIndex != + UIEditorVector3FieldInvalidComponentIndex) { + fieldState.caretOffset = session.textInputState.caret; + } else if (fieldState.selectedComponentIndex != + UIEditorVector3FieldInvalidComponentIndex) { + fieldState.caretOffset = + fieldState.displayTexts[fieldState.selectedComponentIndex].size(); + } else { + fieldState.caretOffset = 0u; + } + if (!session.hasPointerPosition) { + fieldState.hoveredTarget = UIEditorVector3FieldHitTargetKind::None; + fieldState.hoveredComponentIndex = + UIEditorVector3FieldInvalidComponentIndex; + return; } - static bool IsPointInside(const ::XCEngine::UI::UIRect& rect, const ::XCEngine::UI::UIPoint& point) { - return IsUIEditorVector3FieldPointInside(rect, point); + const UIEditorVector3FieldHitTarget hitTarget = + HitTestUIEditorVector3Field(layout, session.pointerPosition); + fieldState.hoveredTarget = hitTarget.kind; + fieldState.hoveredComponentIndex = hitTarget.componentIndex; +} + +bool SelectComponent( + UIEditorVector3FieldInteractionState& state, + std::size_t componentIndex, + UIEditorVector3FieldInteractionResult& result) { + if (componentIndex >= kComponentCount) { + return false; } - static std::string FormatComponentValue(const Spec& spec, std::size_t componentIndex) { - return FormatUIEditorVector3FieldComponentValue(spec, componentIndex); + const std::size_t before = ResolveSelectedComponentIndex(state); + state.vector3FieldState.selectedComponentIndex = componentIndex; + state.session.activeComponentIndex = componentIndex; + result.selectionChanged = before != componentIndex; + result.selectedComponentIndex = componentIndex; + return true; +} + +bool MoveSelection( + UIEditorVector3FieldInteractionState& state, + int direction, + UIEditorVector3FieldInteractionResult& result) { + const std::size_t before = ResolveSelectedComponentIndex(state); + const std::size_t after = direction < 0 + ? (before == 0u ? 0u : before - 1u) + : (before >= kLastComponentIndex ? kLastComponentIndex : before + 1u); + state.vector3FieldState.selectedComponentIndex = after; + state.session.activeComponentIndex = after; + result.selectionChanged = before != after; + result.selectedComponentIndex = after; + result.consumed = true; + return true; +} + +bool BeginEdit( + UIEditorVector3FieldInteractionState& state, + const UIEditorVector3FieldSpec& spec, + std::size_t componentIndex, + bool clearText) { + if (spec.readOnly || componentIndex >= spec.values.size()) { + return false; } - static bool TryParseComponentValue(const Spec& spec, std::string_view text, double& outValue) { - return TryParseUIEditorVector3FieldComponentValue(spec, text, outValue); + state.vector3FieldState.selectedComponentIndex = componentIndex; + return BeginUIEditorEditableFieldEdit( + state.session, + spec.fieldId, + componentIndex, + FormatUIEditorVector3FieldComponentValue(spec, componentIndex), + clearText); +} + +bool CommitEdit( + UIEditorVector3FieldInteractionState& state, + UIEditorVector3FieldSpec& spec, + UIEditorVector3FieldInteractionResult& result) { + const std::size_t componentIndex = + state.vector3FieldState.selectedComponentIndex; + if (!state.session.editing || componentIndex >= spec.values.size()) { + return false; } - static double NormalizeComponentValue(const Spec& spec, double value) { - return NormalizeUIEditorVector3FieldComponentValue(spec, value); + double parsedValue = spec.values[componentIndex]; + if (!TryParseUIEditorVector3FieldComponentValue( + spec, + state.session.textInputState.value, + parsedValue)) { + result.consumed = true; + result.editCommitRejected = true; + return false; } -}; + + result.valuesBefore = spec.values; + spec.values[componentIndex] = + NormalizeUIEditorVector3FieldComponentValue(spec, parsedValue); + result.valuesAfter = spec.values; + result.valueChanged = result.valuesBefore != result.valuesAfter; + result.editCommitted = true; + result.consumed = true; + result.changedComponentIndex = componentIndex; + result.selectedComponentIndex = componentIndex; + result.committedText = + FormatUIEditorVector3FieldComponentValue(spec, componentIndex); + CommitUIEditorEditableFieldEdit(state.session); + return true; +} + +bool CancelEdit( + UIEditorVector3FieldInteractionState& state, + const UIEditorVector3FieldSpec& spec, + UIEditorVector3FieldInteractionResult& result) { + const std::size_t componentIndex = + state.vector3FieldState.selectedComponentIndex; + if (!state.session.editing || componentIndex >= spec.values.size()) { + return false; + } + + CancelUIEditorEditableFieldEdit(state.session); + result.consumed = true; + result.editCanceled = true; + result.valuesBefore = spec.values; + result.valuesAfter = spec.values; + result.selectedComponentIndex = componentIndex; + return true; +} + +bool ApplyStep( + UIEditorVector3FieldInteractionState& state, + UIEditorVector3FieldSpec& spec, + double direction, + bool snapToEdge, + UIEditorVector3FieldInteractionResult& result) { + if (spec.readOnly) { + return false; + } + + const std::size_t componentIndex = ResolveSelectedComponentIndex(state); + state.vector3FieldState.selectedComponentIndex = componentIndex; + state.session.activeComponentIndex = componentIndex; + if (state.session.editing && !CommitEdit(state, spec, result)) { + return result.editCommitRejected; + } + + result.valuesBefore = spec.values; + if (snapToEdge) { + spec.values[componentIndex] = direction < 0.0 + ? NormalizeUIEditorVector3FieldComponentValue(spec, spec.minValue) + : NormalizeUIEditorVector3FieldComponentValue(spec, spec.maxValue); + } else { + const double step = spec.step == 0.0 ? 1.0 : spec.step; + spec.values[componentIndex] = + NormalizeUIEditorVector3FieldComponentValue( + spec, + spec.values[componentIndex] + step * direction); + result.stepDelta = step * direction; + } + + result.valuesAfter = spec.values; + result.stepApplied = true; + result.valueChanged = result.valuesBefore != result.valuesAfter; + result.changedComponentIndex = componentIndex; + result.selectedComponentIndex = componentIndex; + result.consumed = true; + return true; +} + +bool ApplyDrag( + UIEditorVector3FieldInteractionState& state, + UIEditorVector3FieldSpec& spec, + std::size_t componentIndex, + UIEditorVector3FieldInteractionResult& result) { + if (spec.readOnly || state.session.editing || + componentIndex >= spec.values.size()) { + return false; + } + + result.valuesBefore = spec.values; + spec.values[componentIndex] = + NormalizeUIEditorVector3FieldComponentValue( + spec, + state.session.dragStartValue + + ResolveUIEditorEditableFieldDragDelta( + state.session, + ResolveDragSensitivity(spec))); + result.valuesAfter = spec.values; + result.valueChanged = result.valuesBefore != result.valuesAfter; + result.changedComponentIndex = componentIndex; + result.selectedComponentIndex = componentIndex; + result.consumed = true; + return true; +} } // namespace @@ -73,18 +278,348 @@ UIEditorVector3FieldInteractionFrame UpdateUIEditorVector3FieldInteraction( const ::XCEngine::UI::UIRect& bounds, const std::vector& inputEvents, const UIEditorVector3FieldMetrics& metrics) { - UIEditorVector3FieldLayout layout = {}; - UIEditorVector3FieldInteractionResult result = - Internal::UpdateVectorFieldInteractionImpl( - state, - spec, - bounds, - inputEvents, - metrics, - layout); + UIEditorVector3FieldLayout layout = BuildUIEditorVector3FieldLayout( + bounds, + spec, + metrics); + SyncDisplayState(state, spec, layout); + + UIEditorVector3FieldInteractionResult interactionResult = {}; + interactionResult.selectedComponentIndex = + state.vector3FieldState.selectedComponentIndex; + for (const UIInputEvent& event : inputEvents) { + UpdateUIEditorEditableFieldPointerPosition(state.session, event); + + UIEditorVector3FieldInteractionResult eventResult = {}; + eventResult.selectedComponentIndex = + state.vector3FieldState.selectedComponentIndex; + switch (event.type) { + case UIInputEventType::FocusGained: + eventResult.focusChanged = !state.session.focused; + state.session.focused = true; + if (state.vector3FieldState.selectedComponentIndex == + UIEditorVector3FieldInvalidComponentIndex) { + state.vector3FieldState.selectedComponentIndex = 0u; + } + state.session.activeComponentIndex = + state.vector3FieldState.selectedComponentIndex; + eventResult.selectedComponentIndex = + state.vector3FieldState.selectedComponentIndex; + break; + + case UIInputEventType::FocusLost: + eventResult.focusChanged = state.session.focused; + state.session.focused = false; + state.vector3FieldState.activeTarget = + UIEditorVector3FieldHitTargetKind::None; + state.vector3FieldState.activeComponentIndex = + UIEditorVector3FieldInvalidComponentIndex; + state.session.hasPointerPosition = false; + EndUIEditorEditableFieldDrag(state.session); + if (state.session.editing) { + CommitEdit(state, spec, eventResult); + if (eventResult.editCommitRejected) { + CancelEdit(state, spec, eventResult); + } + } + break; + + case UIInputEventType::PointerMove: + TryActivateUIEditorEditableFieldDrag(state.session, event); + if (state.session.dragActive && + state.vector3FieldState.activeTarget == + UIEditorVector3FieldHitTargetKind::Component && + state.vector3FieldState.activeComponentIndex != + UIEditorVector3FieldInvalidComponentIndex) { + ApplyDrag( + state, + spec, + state.vector3FieldState.activeComponentIndex, + eventResult); + } + break; + + case UIInputEventType::PointerEnter: + case UIInputEventType::PointerLeave: + break; + + case UIInputEventType::PointerButtonDown: { + const UIEditorVector3FieldHitTarget hitTarget = + state.session.hasPointerPosition + ? HitTestUIEditorVector3Field( + layout, + state.session.pointerPosition) + : UIEditorVector3FieldHitTarget {}; + eventResult.hitTarget = hitTarget; + + if (event.pointerButton != UIPointerButton::Left) { + break; + } + + const bool insideField = + state.session.hasPointerPosition && + IsUIEditorVector3FieldPointInside( + layout.bounds, + state.session.pointerPosition); + if (insideField) { + eventResult.focusChanged = !state.session.focused; + state.session.focused = true; + state.vector3FieldState.activeTarget = + hitTarget.kind == UIEditorVector3FieldHitTargetKind::None + ? UIEditorVector3FieldHitTargetKind::Row + : hitTarget.kind; + state.vector3FieldState.activeComponentIndex = + hitTarget.componentIndex; + if (hitTarget.kind == + UIEditorVector3FieldHitTargetKind::Component && + !state.session.editing) { + ArmUIEditorEditableFieldDrag( + state.session, + spec.fieldId, + hitTarget.componentIndex, + hitTarget.componentIndex < spec.values.size() + ? spec.values[hitTarget.componentIndex] + : 0.0); + SelectComponent(state, hitTarget.componentIndex, eventResult); + } else { + EndUIEditorEditableFieldDrag(state.session); + if (state.vector3FieldState.selectedComponentIndex == + UIEditorVector3FieldInvalidComponentIndex) { + state.vector3FieldState.selectedComponentIndex = 0u; + state.session.activeComponentIndex = 0u; + eventResult.selectedComponentIndex = 0u; + } + } + eventResult.consumed = true; + } else { + if (state.session.editing) { + CommitEdit(state, spec, eventResult); + if (!eventResult.editCommitRejected) { + eventResult.focusChanged = state.session.focused; + state.session.focused = false; + } + } else if (state.session.focused) { + eventResult.focusChanged = true; + state.session.focused = false; + } + state.vector3FieldState.activeTarget = + UIEditorVector3FieldHitTargetKind::None; + state.vector3FieldState.activeComponentIndex = + UIEditorVector3FieldInvalidComponentIndex; + EndUIEditorEditableFieldDrag(state.session); + } + break; + } + + case UIInputEventType::PointerButtonUp: { + const UIEditorVector3FieldHitTarget hitTarget = + state.session.hasPointerPosition + ? HitTestUIEditorVector3Field( + layout, + state.session.pointerPosition) + : UIEditorVector3FieldHitTarget {}; + eventResult.hitTarget = hitTarget; + + if (event.pointerButton == UIPointerButton::Left) { + const UIEditorVector3FieldHitTargetKind activeTarget = + state.vector3FieldState.activeTarget; + const std::size_t activeComponentIndex = + state.vector3FieldState.activeComponentIndex; + state.vector3FieldState.activeTarget = + UIEditorVector3FieldHitTargetKind::None; + state.vector3FieldState.activeComponentIndex = + UIEditorVector3FieldInvalidComponentIndex; + + if (activeTarget == UIEditorVector3FieldHitTargetKind::Component && + hitTarget.kind == UIEditorVector3FieldHitTargetKind::Component && + activeComponentIndex == hitTarget.componentIndex) { + SelectComponent(state, hitTarget.componentIndex, eventResult); + if (state.session.dragActive) { + eventResult.consumed = true; + } else if (!state.session.editing && + IsUIEditorEditableFieldDoubleClick( + state.session, + spec.fieldId, + event, + hitTarget.componentIndex)) { + eventResult.editStarted = + BeginEdit(state, spec, hitTarget.componentIndex, false); + eventResult.consumed = true; + } else { + RecordUIEditorEditableFieldClick( + state.session, + spec.fieldId, + event, + hitTarget.componentIndex); + eventResult.consumed = true; + } + } else if (hitTarget.kind == UIEditorVector3FieldHitTargetKind::Row) { + eventResult.consumed = true; + } + + EndUIEditorEditableFieldDrag(state.session); + } + break; + } + + case UIInputEventType::KeyDown: + if (!state.session.focused) { + break; + } + + if (state.session.editing) { + if (event.keyCode == static_cast(KeyCode::Escape)) { + CancelEdit(state, spec, eventResult); + break; + } + if (event.keyCode == static_cast(KeyCode::Tab)) { + if (CommitEdit(state, spec, eventResult)) { + MoveSelection( + state, + event.modifiers.shift ? -1 : 1, + eventResult); + } + eventResult.consumed = true; + break; + } + + const auto textResult = HandleUIEditorEditableFieldKeyDown( + state.session, + event.keyCode, + event.modifiers); + if (textResult.handled) { + eventResult.consumed = true; + eventResult.selectedComponentIndex = + state.vector3FieldState.selectedComponentIndex; + if (textResult.submitRequested) { + CommitEdit(state, spec, eventResult); + } + } + } else { + switch (static_cast(event.keyCode)) { + case KeyCode::Left: + MoveSelection(state, -1, eventResult); + break; + case KeyCode::Right: + case KeyCode::Tab: + MoveSelection( + state, + static_cast(event.keyCode) == KeyCode::Tab && + event.modifiers.shift + ? -1 + : 1, + eventResult); + break; + case KeyCode::Up: + ApplyStep(state, spec, 1.0, false, eventResult); + break; + case KeyCode::Down: + ApplyStep(state, spec, -1.0, false, eventResult); + break; + case KeyCode::Home: + ApplyStep(state, spec, -1.0, true, eventResult); + break; + case KeyCode::End: + ApplyStep(state, spec, 1.0, true, eventResult); + break; + case KeyCode::Enter: + eventResult.selectedComponentIndex = + ResolveSelectedComponentIndex(state); + eventResult.editStarted = BeginEdit( + state, + spec, + eventResult.selectedComponentIndex, + false); + eventResult.consumed = eventResult.editStarted; + break; + default: + break; + } + } + break; + + case UIInputEventType::Character: + if (!state.session.focused || + spec.readOnly || + event.modifiers.control || + event.modifiers.alt || + event.modifiers.super || + !IsPermittedCharacter(spec, event.character)) { + break; + } + + if (state.vector3FieldState.selectedComponentIndex == + UIEditorVector3FieldInvalidComponentIndex) { + state.vector3FieldState.selectedComponentIndex = 0u; + } + state.session.activeComponentIndex = + state.vector3FieldState.selectedComponentIndex; + eventResult.selectedComponentIndex = + state.vector3FieldState.selectedComponentIndex; + if (!state.session.editing) { + eventResult.editStarted = BeginEdit( + state, + spec, + state.vector3FieldState.selectedComponentIndex, + true); + } + if (InsertUIEditorEditableFieldCharacter( + state.session, + event.character)) { + eventResult.consumed = true; + } + break; + + default: + break; + } + + layout = BuildUIEditorVector3FieldLayout(bounds, spec, metrics); + SyncDisplayState(state, spec, layout); + if (eventResult.hitTarget.kind == UIEditorVector3FieldHitTargetKind::None && + state.session.hasPointerPosition) { + eventResult.hitTarget = HitTestUIEditorVector3Field( + layout, + state.session.pointerPosition); + } + if (eventResult.selectedComponentIndex == + UIEditorVector3FieldInvalidComponentIndex) { + eventResult.selectedComponentIndex = + state.vector3FieldState.selectedComponentIndex; + } + + if (eventResult.consumed || + eventResult.focusChanged || + eventResult.valueChanged || + eventResult.stepApplied || + eventResult.selectionChanged || + eventResult.editStarted || + eventResult.editCommitted || + eventResult.editCommitRejected || + eventResult.editCanceled || + eventResult.hitTarget.kind != + UIEditorVector3FieldHitTargetKind::None) { + interactionResult = std::move(eventResult); + } + } + + layout = BuildUIEditorVector3FieldLayout(bounds, spec, metrics); + SyncDisplayState(state, spec, layout); + if (interactionResult.hitTarget.kind == UIEditorVector3FieldHitTargetKind::None && + state.session.hasPointerPosition) { + interactionResult.hitTarget = HitTestUIEditorVector3Field( + layout, + state.session.pointerPosition); + } + if (interactionResult.selectedComponentIndex == + UIEditorVector3FieldInvalidComponentIndex) { + interactionResult.selectedComponentIndex = + state.vector3FieldState.selectedComponentIndex; + } + return { std::move(layout), - std::move(result) + std::move(interactionResult) }; } diff --git a/new_editor/src/Fields/UIEditorVector4Field.cpp b/new_editor/src/Fields/UIEditorVector4Field.cpp index 07a06efe..21e0731d 100644 --- a/new_editor/src/Fields/UIEditorVector4Field.cpp +++ b/new_editor/src/Fields/UIEditorVector4Field.cpp @@ -42,12 +42,6 @@ UIEditorVector4FieldPalette ResolvePalette(const UIEditorVector4FieldPalette& pa const auto& tokens = GetUIEditorInspectorFieldStyleTokens(); UIEditorVector4FieldPalette resolved = palette; - if (AreUIEditorFieldColorsEqual(palette.rowHoverColor, ::XCEngine::UI::UIColor(0.0f, 0.0f, 0.0f, 0.0f))) { - resolved.rowHoverColor = tokens.rowHoverColor; - } - if (AreUIEditorFieldColorsEqual(palette.rowActiveColor, ::XCEngine::UI::UIColor(0.0f, 0.0f, 0.0f, 0.0f))) { - resolved.rowActiveColor = tokens.rowActiveColor; - } if (AreUIEditorFieldColorsEqual(palette.componentColor, ::XCEngine::UI::UIColor(0.18f, 0.18f, 0.18f, 1.0f))) { resolved.componentColor = tokens.controlColor; } @@ -167,22 +161,6 @@ UIEditorNumberFieldSpec BuildComponentNumberSpec( return palette.componentBorderColor; } -::XCEngine::UI::UIColor ResolveAxisColor( - const UIEditorVector4FieldPalette& palette, - std::size_t componentIndex) { - switch (componentIndex) { - case 0u: - return palette.axisXColor; - case 1u: - return palette.axisYColor; - case 2u: - return palette.axisZColor; - case 3u: - default: - return palette.axisWColor; - } -} - } // namespace bool IsUIEditorVector4FieldPointInside( @@ -299,19 +277,6 @@ void AppendUIEditorVector4FieldBackground( } for (std::size_t componentIndex = 0u; componentIndex < layout.componentRects.size(); ++componentIndex) { - if (resolvedPalette.prefixColor.a > 0.0f) { - drawList.AddFilledRect( - layout.componentPrefixRects[componentIndex], - resolvedPalette.prefixColor, - resolvedMetrics.componentRounding); - } - if (resolvedPalette.prefixBorderColor.a > 0.0f) { - drawList.AddRectOutline( - layout.componentPrefixRects[componentIndex], - resolvedPalette.prefixBorderColor, - resolvedMetrics.borderThickness, - resolvedMetrics.componentRounding); - } drawList.AddFilledRect( layout.componentValueRects[componentIndex], ResolveComponentFillColor(spec, state, resolvedPalette, componentIndex), @@ -362,7 +327,7 @@ void AppendUIEditorVector4FieldForeground( resolvedMetrics.prefixFontSize, resolvedMetrics.prefixTextInsetY)), componentLabel, - ResolveAxisColor(resolvedPalette, componentIndex), + resolvedPalette.labelColor, resolvedMetrics.prefixFontSize); drawList.PopClipRect(); @@ -380,6 +345,18 @@ void AppendUIEditorVector4FieldForeground( : FormatUIEditorVector4FieldComponentValue(spec, componentIndex), spec.readOnly ? resolvedPalette.readOnlyValueColor : resolvedPalette.valueColor, resolvedMetrics.valueFontSize); + if (state.editing && state.selectedComponentIndex == componentIndex) { + AppendUIEditorTextCaret( + drawList, + layout.componentValueRects[componentIndex], + state.displayTexts[componentIndex], + state.caretOffset, + state.caretBlinkStartNanoseconds, + resolvedPalette.valueColor, + resolvedMetrics.valueFontSize, + resolvedMetrics.valueTextInsetX, + resolvedMetrics.valueTextInsetY); + } drawList.PopClipRect(); } } diff --git a/new_editor/src/Fields/UIEditorVector4FieldInteraction.cpp b/new_editor/src/Fields/UIEditorVector4FieldInteraction.cpp index fc82db4b..f6910892 100644 --- a/new_editor/src/Fields/UIEditorVector4FieldInteraction.cpp +++ b/new_editor/src/Fields/UIEditorVector4FieldInteraction.cpp @@ -1,13 +1,18 @@ #include -#include "Fields/VectorFieldInteractionInternal.h" +#include +#include #include namespace XCEngine::UI::Editor { namespace { +using ::XCEngine::Input::KeyCode; +using ::XCEngine::UI::UIInputEvent; +using ::XCEngine::UI::UIInputEventType; +using ::XCEngine::UI::UIPointerButton; using ::XCEngine::UI::Editor::Widgets::BuildUIEditorVector4FieldLayout; using ::XCEngine::UI::Editor::Widgets::FormatUIEditorVector4FieldComponentValue; using ::XCEngine::UI::Editor::Widgets::HitTestUIEditorVector4Field; @@ -21,49 +26,249 @@ using ::XCEngine::UI::Editor::Widgets::UIEditorVector4FieldLayout; using ::XCEngine::UI::Editor::Widgets::UIEditorVector4FieldMetrics; using ::XCEngine::UI::Editor::Widgets::UIEditorVector4FieldSpec; -struct Vector4FieldInteractionTraits { - using InteractionState = UIEditorVector4FieldInteractionState; - using InteractionResult = UIEditorVector4FieldInteractionResult; - using Spec = UIEditorVector4FieldSpec; - using Layout = UIEditorVector4FieldLayout; - using Metrics = UIEditorVector4FieldMetrics; - using HitTarget = UIEditorVector4FieldHitTarget; - using HitTargetKind = UIEditorVector4FieldHitTargetKind; +constexpr std::size_t kComponentCount = 4u; +constexpr std::size_t kLastComponentIndex = 3u; - static constexpr std::size_t kComponentCount = 4u; - static constexpr std::size_t kLastComponentIndex = 3u; - static constexpr std::size_t kInvalidComponentIndex = UIEditorVector4FieldInvalidComponentIndex; - static constexpr HitTargetKind kNoneHitTargetKind = UIEditorVector4FieldHitTargetKind::None; - static constexpr HitTargetKind kRowHitTargetKind = UIEditorVector4FieldHitTargetKind::Row; - static constexpr HitTargetKind kComponentHitTargetKind = UIEditorVector4FieldHitTargetKind::Component; +bool IsPermittedCharacter( + const UIEditorVector4FieldSpec& spec, + std::uint32_t character) { + if (character >= static_cast('0') && + character <= static_cast('9')) { + return true; + } + if (character == static_cast('-') || + character == static_cast('+')) { + return true; + } + return !spec.integerMode && character == static_cast('.'); +} - static auto& FieldState(InteractionState& state) { return state.vector4FieldState; } - static const auto& FieldState(const InteractionState& state) { return state.vector4FieldState; } +std::size_t ResolveSelectedComponentIndex( + const UIEditorVector4FieldInteractionState& state) { + return state.vector4FieldState.selectedComponentIndex == + UIEditorVector4FieldInvalidComponentIndex + ? 0u + : state.vector4FieldState.selectedComponentIndex; +} - static Layout BuildLayout(const ::XCEngine::UI::UIRect& bounds, const Spec& spec, const Metrics& metrics) { - return BuildUIEditorVector4FieldLayout(bounds, spec, metrics); +double ResolveDragSensitivity(const UIEditorVector4FieldSpec& spec) { + if (spec.step != 0.0) { + return spec.step; } - static HitTarget HitTest(const Layout& layout, const ::XCEngine::UI::UIPoint& point) { - return HitTestUIEditorVector4Field(layout, point); + return spec.integerMode ? 1.0 : 0.1; +} + +void SyncDisplayState( + UIEditorVector4FieldInteractionState& state, + const UIEditorVector4FieldSpec& spec, + const UIEditorVector4FieldLayout& layout) { + auto& fieldState = state.vector4FieldState; + const auto& session = state.session; + fieldState.focused = session.focused; + fieldState.editing = session.editing; + fieldState.caretBlinkStartNanoseconds = session.caretBlinkStartNanoseconds; + for (std::size_t componentIndex = 0u; componentIndex < kComponentCount; + ++componentIndex) { + fieldState.displayTexts[componentIndex] = + session.editing && + fieldState.selectedComponentIndex == componentIndex + ? session.textInputState.value + : FormatUIEditorVector4FieldComponentValue(spec, componentIndex); + } + if (session.editing && + fieldState.selectedComponentIndex != + UIEditorVector4FieldInvalidComponentIndex) { + fieldState.caretOffset = session.textInputState.caret; + } else if (fieldState.selectedComponentIndex != + UIEditorVector4FieldInvalidComponentIndex) { + fieldState.caretOffset = + fieldState.displayTexts[fieldState.selectedComponentIndex].size(); + } else { + fieldState.caretOffset = 0u; + } + if (!session.hasPointerPosition) { + fieldState.hoveredTarget = UIEditorVector4FieldHitTargetKind::None; + fieldState.hoveredComponentIndex = + UIEditorVector4FieldInvalidComponentIndex; + return; } - static bool IsPointInside(const ::XCEngine::UI::UIRect& rect, const ::XCEngine::UI::UIPoint& point) { - return IsUIEditorVector4FieldPointInside(rect, point); + const UIEditorVector4FieldHitTarget hitTarget = + HitTestUIEditorVector4Field(layout, session.pointerPosition); + fieldState.hoveredTarget = hitTarget.kind; + fieldState.hoveredComponentIndex = hitTarget.componentIndex; +} + +bool SelectComponent( + UIEditorVector4FieldInteractionState& state, + std::size_t componentIndex, + UIEditorVector4FieldInteractionResult& result) { + if (componentIndex >= kComponentCount) { + return false; } - static std::string FormatComponentValue(const Spec& spec, std::size_t componentIndex) { - return FormatUIEditorVector4FieldComponentValue(spec, componentIndex); + const std::size_t before = ResolveSelectedComponentIndex(state); + state.vector4FieldState.selectedComponentIndex = componentIndex; + state.session.activeComponentIndex = componentIndex; + result.selectionChanged = before != componentIndex; + result.selectedComponentIndex = componentIndex; + return true; +} + +bool MoveSelection( + UIEditorVector4FieldInteractionState& state, + int direction, + UIEditorVector4FieldInteractionResult& result) { + const std::size_t before = ResolveSelectedComponentIndex(state); + const std::size_t after = direction < 0 + ? (before == 0u ? 0u : before - 1u) + : (before >= kLastComponentIndex ? kLastComponentIndex : before + 1u); + state.vector4FieldState.selectedComponentIndex = after; + state.session.activeComponentIndex = after; + result.selectionChanged = before != after; + result.selectedComponentIndex = after; + result.consumed = true; + return true; +} + +bool BeginEdit( + UIEditorVector4FieldInteractionState& state, + const UIEditorVector4FieldSpec& spec, + std::size_t componentIndex, + bool clearText) { + if (spec.readOnly || componentIndex >= spec.values.size()) { + return false; } - static bool TryParseComponentValue(const Spec& spec, std::string_view text, double& outValue) { - return TryParseUIEditorVector4FieldComponentValue(spec, text, outValue); + state.vector4FieldState.selectedComponentIndex = componentIndex; + return BeginUIEditorEditableFieldEdit( + state.session, + spec.fieldId, + componentIndex, + FormatUIEditorVector4FieldComponentValue(spec, componentIndex), + clearText); +} + +bool CommitEdit( + UIEditorVector4FieldInteractionState& state, + UIEditorVector4FieldSpec& spec, + UIEditorVector4FieldInteractionResult& result) { + const std::size_t componentIndex = + state.vector4FieldState.selectedComponentIndex; + if (!state.session.editing || componentIndex >= spec.values.size()) { + return false; } - static double NormalizeComponentValue(const Spec& spec, double value) { - return NormalizeUIEditorVector4FieldComponentValue(spec, value); + double parsedValue = spec.values[componentIndex]; + if (!TryParseUIEditorVector4FieldComponentValue( + spec, + state.session.textInputState.value, + parsedValue)) { + result.consumed = true; + result.editCommitRejected = true; + return false; } -}; + + result.valuesBefore = spec.values; + spec.values[componentIndex] = + NormalizeUIEditorVector4FieldComponentValue(spec, parsedValue); + result.valuesAfter = spec.values; + result.valueChanged = result.valuesBefore != result.valuesAfter; + result.editCommitted = true; + result.consumed = true; + result.changedComponentIndex = componentIndex; + result.selectedComponentIndex = componentIndex; + result.committedText = + FormatUIEditorVector4FieldComponentValue(spec, componentIndex); + CommitUIEditorEditableFieldEdit(state.session); + return true; +} + +bool CancelEdit( + UIEditorVector4FieldInteractionState& state, + const UIEditorVector4FieldSpec& spec, + UIEditorVector4FieldInteractionResult& result) { + const std::size_t componentIndex = + state.vector4FieldState.selectedComponentIndex; + if (!state.session.editing || componentIndex >= spec.values.size()) { + return false; + } + + CancelUIEditorEditableFieldEdit(state.session); + result.consumed = true; + result.editCanceled = true; + result.valuesBefore = spec.values; + result.valuesAfter = spec.values; + result.selectedComponentIndex = componentIndex; + return true; +} + +bool ApplyStep( + UIEditorVector4FieldInteractionState& state, + UIEditorVector4FieldSpec& spec, + double direction, + bool snapToEdge, + UIEditorVector4FieldInteractionResult& result) { + if (spec.readOnly) { + return false; + } + + const std::size_t componentIndex = ResolveSelectedComponentIndex(state); + state.vector4FieldState.selectedComponentIndex = componentIndex; + state.session.activeComponentIndex = componentIndex; + if (state.session.editing && !CommitEdit(state, spec, result)) { + return result.editCommitRejected; + } + + result.valuesBefore = spec.values; + if (snapToEdge) { + spec.values[componentIndex] = direction < 0.0 + ? NormalizeUIEditorVector4FieldComponentValue(spec, spec.minValue) + : NormalizeUIEditorVector4FieldComponentValue(spec, spec.maxValue); + } else { + const double step = spec.step == 0.0 ? 1.0 : spec.step; + spec.values[componentIndex] = + NormalizeUIEditorVector4FieldComponentValue( + spec, + spec.values[componentIndex] + step * direction); + result.stepDelta = step * direction; + } + + result.valuesAfter = spec.values; + result.stepApplied = true; + result.valueChanged = result.valuesBefore != result.valuesAfter; + result.changedComponentIndex = componentIndex; + result.selectedComponentIndex = componentIndex; + result.consumed = true; + return true; +} + +bool ApplyDrag( + UIEditorVector4FieldInteractionState& state, + UIEditorVector4FieldSpec& spec, + std::size_t componentIndex, + UIEditorVector4FieldInteractionResult& result) { + if (spec.readOnly || state.session.editing || + componentIndex >= spec.values.size()) { + return false; + } + + result.valuesBefore = spec.values; + spec.values[componentIndex] = + NormalizeUIEditorVector4FieldComponentValue( + spec, + state.session.dragStartValue + + ResolveUIEditorEditableFieldDragDelta( + state.session, + ResolveDragSensitivity(spec))); + result.valuesAfter = spec.values; + result.valueChanged = result.valuesBefore != result.valuesAfter; + result.changedComponentIndex = componentIndex; + result.selectedComponentIndex = componentIndex; + result.consumed = true; + return true; +} } // namespace @@ -73,18 +278,348 @@ UIEditorVector4FieldInteractionFrame UpdateUIEditorVector4FieldInteraction( const ::XCEngine::UI::UIRect& bounds, const std::vector& inputEvents, const UIEditorVector4FieldMetrics& metrics) { - UIEditorVector4FieldLayout layout = {}; - UIEditorVector4FieldInteractionResult result = - Internal::UpdateVectorFieldInteractionImpl( - state, - spec, - bounds, - inputEvents, - metrics, - layout); + UIEditorVector4FieldLayout layout = BuildUIEditorVector4FieldLayout( + bounds, + spec, + metrics); + SyncDisplayState(state, spec, layout); + + UIEditorVector4FieldInteractionResult interactionResult = {}; + interactionResult.selectedComponentIndex = + state.vector4FieldState.selectedComponentIndex; + for (const UIInputEvent& event : inputEvents) { + UpdateUIEditorEditableFieldPointerPosition(state.session, event); + + UIEditorVector4FieldInteractionResult eventResult = {}; + eventResult.selectedComponentIndex = + state.vector4FieldState.selectedComponentIndex; + switch (event.type) { + case UIInputEventType::FocusGained: + eventResult.focusChanged = !state.session.focused; + state.session.focused = true; + if (state.vector4FieldState.selectedComponentIndex == + UIEditorVector4FieldInvalidComponentIndex) { + state.vector4FieldState.selectedComponentIndex = 0u; + } + state.session.activeComponentIndex = + state.vector4FieldState.selectedComponentIndex; + eventResult.selectedComponentIndex = + state.vector4FieldState.selectedComponentIndex; + break; + + case UIInputEventType::FocusLost: + eventResult.focusChanged = state.session.focused; + state.session.focused = false; + state.vector4FieldState.activeTarget = + UIEditorVector4FieldHitTargetKind::None; + state.vector4FieldState.activeComponentIndex = + UIEditorVector4FieldInvalidComponentIndex; + state.session.hasPointerPosition = false; + EndUIEditorEditableFieldDrag(state.session); + if (state.session.editing) { + CommitEdit(state, spec, eventResult); + if (eventResult.editCommitRejected) { + CancelEdit(state, spec, eventResult); + } + } + break; + + case UIInputEventType::PointerMove: + TryActivateUIEditorEditableFieldDrag(state.session, event); + if (state.session.dragActive && + state.vector4FieldState.activeTarget == + UIEditorVector4FieldHitTargetKind::Component && + state.vector4FieldState.activeComponentIndex != + UIEditorVector4FieldInvalidComponentIndex) { + ApplyDrag( + state, + spec, + state.vector4FieldState.activeComponentIndex, + eventResult); + } + break; + + case UIInputEventType::PointerEnter: + case UIInputEventType::PointerLeave: + break; + + case UIInputEventType::PointerButtonDown: { + const UIEditorVector4FieldHitTarget hitTarget = + state.session.hasPointerPosition + ? HitTestUIEditorVector4Field( + layout, + state.session.pointerPosition) + : UIEditorVector4FieldHitTarget {}; + eventResult.hitTarget = hitTarget; + + if (event.pointerButton != UIPointerButton::Left) { + break; + } + + const bool insideField = + state.session.hasPointerPosition && + IsUIEditorVector4FieldPointInside( + layout.bounds, + state.session.pointerPosition); + if (insideField) { + eventResult.focusChanged = !state.session.focused; + state.session.focused = true; + state.vector4FieldState.activeTarget = + hitTarget.kind == UIEditorVector4FieldHitTargetKind::None + ? UIEditorVector4FieldHitTargetKind::Row + : hitTarget.kind; + state.vector4FieldState.activeComponentIndex = + hitTarget.componentIndex; + if (hitTarget.kind == + UIEditorVector4FieldHitTargetKind::Component && + !state.session.editing) { + ArmUIEditorEditableFieldDrag( + state.session, + spec.fieldId, + hitTarget.componentIndex, + hitTarget.componentIndex < spec.values.size() + ? spec.values[hitTarget.componentIndex] + : 0.0); + SelectComponent(state, hitTarget.componentIndex, eventResult); + } else { + EndUIEditorEditableFieldDrag(state.session); + if (state.vector4FieldState.selectedComponentIndex == + UIEditorVector4FieldInvalidComponentIndex) { + state.vector4FieldState.selectedComponentIndex = 0u; + state.session.activeComponentIndex = 0u; + eventResult.selectedComponentIndex = 0u; + } + } + eventResult.consumed = true; + } else { + if (state.session.editing) { + CommitEdit(state, spec, eventResult); + if (!eventResult.editCommitRejected) { + eventResult.focusChanged = state.session.focused; + state.session.focused = false; + } + } else if (state.session.focused) { + eventResult.focusChanged = true; + state.session.focused = false; + } + state.vector4FieldState.activeTarget = + UIEditorVector4FieldHitTargetKind::None; + state.vector4FieldState.activeComponentIndex = + UIEditorVector4FieldInvalidComponentIndex; + EndUIEditorEditableFieldDrag(state.session); + } + break; + } + + case UIInputEventType::PointerButtonUp: { + const UIEditorVector4FieldHitTarget hitTarget = + state.session.hasPointerPosition + ? HitTestUIEditorVector4Field( + layout, + state.session.pointerPosition) + : UIEditorVector4FieldHitTarget {}; + eventResult.hitTarget = hitTarget; + + if (event.pointerButton == UIPointerButton::Left) { + const UIEditorVector4FieldHitTargetKind activeTarget = + state.vector4FieldState.activeTarget; + const std::size_t activeComponentIndex = + state.vector4FieldState.activeComponentIndex; + state.vector4FieldState.activeTarget = + UIEditorVector4FieldHitTargetKind::None; + state.vector4FieldState.activeComponentIndex = + UIEditorVector4FieldInvalidComponentIndex; + + if (activeTarget == UIEditorVector4FieldHitTargetKind::Component && + hitTarget.kind == UIEditorVector4FieldHitTargetKind::Component && + activeComponentIndex == hitTarget.componentIndex) { + SelectComponent(state, hitTarget.componentIndex, eventResult); + if (state.session.dragActive) { + eventResult.consumed = true; + } else if (!state.session.editing && + IsUIEditorEditableFieldDoubleClick( + state.session, + spec.fieldId, + event, + hitTarget.componentIndex)) { + eventResult.editStarted = + BeginEdit(state, spec, hitTarget.componentIndex, false); + eventResult.consumed = true; + } else { + RecordUIEditorEditableFieldClick( + state.session, + spec.fieldId, + event, + hitTarget.componentIndex); + eventResult.consumed = true; + } + } else if (hitTarget.kind == UIEditorVector4FieldHitTargetKind::Row) { + eventResult.consumed = true; + } + + EndUIEditorEditableFieldDrag(state.session); + } + break; + } + + case UIInputEventType::KeyDown: + if (!state.session.focused) { + break; + } + + if (state.session.editing) { + if (event.keyCode == static_cast(KeyCode::Escape)) { + CancelEdit(state, spec, eventResult); + break; + } + if (event.keyCode == static_cast(KeyCode::Tab)) { + if (CommitEdit(state, spec, eventResult)) { + MoveSelection( + state, + event.modifiers.shift ? -1 : 1, + eventResult); + } + eventResult.consumed = true; + break; + } + + const auto textResult = HandleUIEditorEditableFieldKeyDown( + state.session, + event.keyCode, + event.modifiers); + if (textResult.handled) { + eventResult.consumed = true; + eventResult.selectedComponentIndex = + state.vector4FieldState.selectedComponentIndex; + if (textResult.submitRequested) { + CommitEdit(state, spec, eventResult); + } + } + } else { + switch (static_cast(event.keyCode)) { + case KeyCode::Left: + MoveSelection(state, -1, eventResult); + break; + case KeyCode::Right: + case KeyCode::Tab: + MoveSelection( + state, + static_cast(event.keyCode) == KeyCode::Tab && + event.modifiers.shift + ? -1 + : 1, + eventResult); + break; + case KeyCode::Up: + ApplyStep(state, spec, 1.0, false, eventResult); + break; + case KeyCode::Down: + ApplyStep(state, spec, -1.0, false, eventResult); + break; + case KeyCode::Home: + ApplyStep(state, spec, -1.0, true, eventResult); + break; + case KeyCode::End: + ApplyStep(state, spec, 1.0, true, eventResult); + break; + case KeyCode::Enter: + eventResult.selectedComponentIndex = + ResolveSelectedComponentIndex(state); + eventResult.editStarted = BeginEdit( + state, + spec, + eventResult.selectedComponentIndex, + false); + eventResult.consumed = eventResult.editStarted; + break; + default: + break; + } + } + break; + + case UIInputEventType::Character: + if (!state.session.focused || + spec.readOnly || + event.modifiers.control || + event.modifiers.alt || + event.modifiers.super || + !IsPermittedCharacter(spec, event.character)) { + break; + } + + if (state.vector4FieldState.selectedComponentIndex == + UIEditorVector4FieldInvalidComponentIndex) { + state.vector4FieldState.selectedComponentIndex = 0u; + } + state.session.activeComponentIndex = + state.vector4FieldState.selectedComponentIndex; + eventResult.selectedComponentIndex = + state.vector4FieldState.selectedComponentIndex; + if (!state.session.editing) { + eventResult.editStarted = BeginEdit( + state, + spec, + state.vector4FieldState.selectedComponentIndex, + true); + } + if (InsertUIEditorEditableFieldCharacter( + state.session, + event.character)) { + eventResult.consumed = true; + } + break; + + default: + break; + } + + layout = BuildUIEditorVector4FieldLayout(bounds, spec, metrics); + SyncDisplayState(state, spec, layout); + if (eventResult.hitTarget.kind == UIEditorVector4FieldHitTargetKind::None && + state.session.hasPointerPosition) { + eventResult.hitTarget = HitTestUIEditorVector4Field( + layout, + state.session.pointerPosition); + } + if (eventResult.selectedComponentIndex == + UIEditorVector4FieldInvalidComponentIndex) { + eventResult.selectedComponentIndex = + state.vector4FieldState.selectedComponentIndex; + } + + if (eventResult.consumed || + eventResult.focusChanged || + eventResult.valueChanged || + eventResult.stepApplied || + eventResult.selectionChanged || + eventResult.editStarted || + eventResult.editCommitted || + eventResult.editCommitRejected || + eventResult.editCanceled || + eventResult.hitTarget.kind != + UIEditorVector4FieldHitTargetKind::None) { + interactionResult = std::move(eventResult); + } + } + + layout = BuildUIEditorVector4FieldLayout(bounds, spec, metrics); + SyncDisplayState(state, spec, layout); + if (interactionResult.hitTarget.kind == UIEditorVector4FieldHitTargetKind::None && + state.session.hasPointerPosition) { + interactionResult.hitTarget = HitTestUIEditorVector4Field( + layout, + state.session.pointerPosition); + } + if (interactionResult.selectedComponentIndex == + UIEditorVector4FieldInvalidComponentIndex) { + interactionResult.selectedComponentIndex = + state.vector4FieldState.selectedComponentIndex; + } + return { std::move(layout), - std::move(result) + std::move(interactionResult) }; } diff --git a/new_editor/src/Fields/VectorFieldInteractionInternal.h b/new_editor/src/Fields/VectorFieldInteractionInternal.h deleted file mode 100644 index 4f747f6e..00000000 --- a/new_editor/src/Fields/VectorFieldInteractionInternal.h +++ /dev/null @@ -1,463 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -namespace XCEngine::UI::Editor::Internal { - -using ::XCEngine::Input::KeyCode; -using ::XCEngine::UI::UIInputEvent; -using ::XCEngine::UI::UIInputEventType; -using ::XCEngine::UI::UIPointerButton; -using ::XCEngine::UI::Text::HandleKeyDown; -using ::XCEngine::UI::Text::InsertCharacter; - -inline bool ShouldUseVectorFieldPointerPosition(const UIInputEvent& event) { - switch (event.type) { - case UIInputEventType::PointerMove: - case UIInputEventType::PointerEnter: - case UIInputEventType::PointerButtonDown: - case UIInputEventType::PointerButtonUp: - case UIInputEventType::PointerWheel: - return true; - default: - return false; - } -} - -template -std::size_t ResolveFallbackSelectedComponentIndex(const typename Traits::InteractionState& state) { - const auto& fieldState = Traits::FieldState(state); - return fieldState.selectedComponentIndex == Traits::kInvalidComponentIndex - ? 0u - : fieldState.selectedComponentIndex; -} - -template -std::string BuildVectorFieldComponentEditFieldId( - const typename Traits::Spec& spec, - std::size_t componentIndex) { - return spec.fieldId + "." + std::to_string(componentIndex); -} - -template -bool IsPermittedVectorFieldCharacter(const typename Traits::Spec& spec, std::uint32_t character) { - if (character >= static_cast('0') && - character <= static_cast('9')) { - return true; - } - if (character == static_cast('-') || - character == static_cast('+')) { - return true; - } - return !spec.integerMode && character == static_cast('.'); -} - -template -void SyncVectorFieldDisplayTexts(typename Traits::InteractionState& state, const typename Traits::Spec& spec) { - auto& fieldState = Traits::FieldState(state); - for (std::size_t componentIndex = 0u; componentIndex < fieldState.displayTexts.size(); ++componentIndex) { - if (fieldState.editing && fieldState.selectedComponentIndex == componentIndex) { - continue; - } - fieldState.displayTexts[componentIndex] = Traits::FormatComponentValue(spec, componentIndex); - } -} - -template -void SyncVectorFieldHoverTarget(typename Traits::InteractionState& state, const typename Traits::Layout& layout) { - auto& fieldState = Traits::FieldState(state); - if (!state.hasPointerPosition) { - fieldState.hoveredTarget = Traits::kNoneHitTargetKind; - fieldState.hoveredComponentIndex = Traits::kInvalidComponentIndex; - return; - } - const typename Traits::HitTarget hitTarget = Traits::HitTest(layout, state.pointerPosition); - fieldState.hoveredTarget = hitTarget.kind; - fieldState.hoveredComponentIndex = hitTarget.componentIndex; -} - -template -bool MoveVectorFieldSelection( - typename Traits::InteractionState& state, - int direction, - typename Traits::InteractionResult& result) { - auto& fieldState = Traits::FieldState(state); - const std::size_t before = ResolveFallbackSelectedComponentIndex(state); - const std::size_t after = direction < 0 - ? (before == 0u ? 0u : before - 1u) - : (before >= Traits::kLastComponentIndex ? Traits::kLastComponentIndex : before + 1u); - fieldState.selectedComponentIndex = after; - result.selectionChanged = before != after; - result.selectedComponentIndex = after; - result.consumed = true; - return true; -} - -template -bool SelectVectorFieldComponent( - typename Traits::InteractionState& state, - std::size_t componentIndex, - typename Traits::InteractionResult& result) { - if (componentIndex >= Traits::kComponentCount) { - return false; - } - auto& fieldState = Traits::FieldState(state); - const std::size_t before = ResolveFallbackSelectedComponentIndex(state); - fieldState.selectedComponentIndex = componentIndex; - result.selectionChanged = before != componentIndex; - result.selectedComponentIndex = componentIndex; - return true; -} - -template -bool BeginVectorFieldEdit( - typename Traits::InteractionState& state, - const typename Traits::Spec& spec, - std::size_t componentIndex, - bool clearText) { - auto& fieldState = Traits::FieldState(state); - if (spec.readOnly || componentIndex >= spec.values.size()) { - return false; - } - const std::string baseline = Traits::FormatComponentValue(spec, componentIndex); - const std::string editFieldId = BuildVectorFieldComponentEditFieldId(spec, componentIndex); - const bool changed = state.editModel.BeginEdit(editFieldId, baseline); - if (!changed && state.editModel.HasActiveEdit() && state.editModel.GetActiveFieldId() != editFieldId) { - return false; - } - if (!changed && fieldState.editing && fieldState.selectedComponentIndex == componentIndex) { - return false; - } - fieldState.selectedComponentIndex = componentIndex; - fieldState.editing = true; - state.textInputState.value = clearText ? std::string() : baseline; - state.textInputState.caret = state.textInputState.value.size(); - state.editModel.UpdateStagedValue(state.textInputState.value); - fieldState.displayTexts[componentIndex] = state.textInputState.value; - return true; -} - -template -bool CommitVectorFieldEdit( - typename Traits::InteractionState& state, - typename Traits::Spec& spec, - typename Traits::InteractionResult& result) { - auto& fieldState = Traits::FieldState(state); - if (!fieldState.editing || !state.editModel.HasActiveEdit() || fieldState.selectedComponentIndex >= spec.values.size()) { - return false; - } - const std::size_t componentIndex = fieldState.selectedComponentIndex; - double parsedValue = spec.values[componentIndex]; - if (!Traits::TryParseComponentValue(spec, state.textInputState.value, parsedValue)) { - result.consumed = true; - result.editCommitRejected = true; - return false; - } - result.valuesBefore = spec.values; - spec.values[componentIndex] = Traits::NormalizeComponentValue(spec, parsedValue); - result.valuesAfter = spec.values; - result.valueChanged = result.valuesBefore != result.valuesAfter; - result.editCommitted = true; - result.consumed = true; - result.changedComponentIndex = componentIndex; - result.selectedComponentIndex = componentIndex; - result.committedText = Traits::FormatComponentValue(spec, componentIndex); - state.editModel.CommitEdit(); - state.textInputState = {}; - fieldState.editing = false; - fieldState.displayTexts[componentIndex] = result.committedText; - return true; -} - -template -bool CancelVectorFieldEdit( - typename Traits::InteractionState& state, - const typename Traits::Spec& spec, - typename Traits::InteractionResult& result) { - auto& fieldState = Traits::FieldState(state); - if (!fieldState.editing || !state.editModel.HasActiveEdit() || fieldState.selectedComponentIndex >= spec.values.size()) { - return false; - } - const std::size_t componentIndex = fieldState.selectedComponentIndex; - state.editModel.CancelEdit(); - state.textInputState = {}; - fieldState.editing = false; - fieldState.displayTexts[componentIndex] = Traits::FormatComponentValue(spec, componentIndex); - result.consumed = true; - result.editCanceled = true; - result.valuesBefore = spec.values; - result.valuesAfter = spec.values; - result.selectedComponentIndex = componentIndex; - return true; -} - -template -bool ApplyVectorFieldStep( - typename Traits::InteractionState& state, - typename Traits::Spec& spec, - double direction, - bool snapToEdge, - typename Traits::InteractionResult& result) { - auto& fieldState = Traits::FieldState(state); - if (spec.readOnly) { - return false; - } - const std::size_t componentIndex = ResolveFallbackSelectedComponentIndex(state); - fieldState.selectedComponentIndex = componentIndex; - if (fieldState.editing && !CommitVectorFieldEdit(state, spec, result)) { - return result.editCommitRejected; - } - result.valuesBefore = spec.values; - if (snapToEdge) { - spec.values[componentIndex] = direction < 0.0 - ? Traits::NormalizeComponentValue(spec, spec.minValue) - : Traits::NormalizeComponentValue(spec, spec.maxValue); - } else { - const double step = spec.step == 0.0 ? 1.0 : spec.step; - spec.values[componentIndex] = Traits::NormalizeComponentValue(spec, spec.values[componentIndex] + step * direction); - result.stepDelta = step * direction; - } - result.valuesAfter = spec.values; - result.stepApplied = true; - result.valueChanged = result.valuesBefore != result.valuesAfter || result.valueChanged; - result.changedComponentIndex = componentIndex; - result.selectedComponentIndex = componentIndex; - result.consumed = true; - fieldState.displayTexts[componentIndex] = Traits::FormatComponentValue(spec, componentIndex); - return true; -} - -template -typename Traits::InteractionResult UpdateVectorFieldInteractionImpl( - typename Traits::InteractionState& state, - typename Traits::Spec& spec, - const ::XCEngine::UI::UIRect& bounds, - const std::vector& inputEvents, - const typename Traits::Metrics& metrics, - typename Traits::Layout& layout) { - auto refresh = [&]() { - layout = Traits::BuildLayout(bounds, spec, metrics); - SyncVectorFieldDisplayTexts(state, spec); - SyncVectorFieldHoverTarget(state, layout); - }; - auto makeResult = [&]() { - typename Traits::InteractionResult result = {}; - result.selectedComponentIndex = Traits::FieldState(state).selectedComponentIndex; - return result; - }; - auto shouldPublish = [&](const typename Traits::InteractionResult& result) { - return result.consumed || result.focusChanged || result.valueChanged || result.stepApplied || - result.selectionChanged || result.editStarted || result.editCommitted || - result.editCommitRejected || result.editCanceled || - result.hitTarget.kind != Traits::kNoneHitTargetKind; - }; - - refresh(); - typename Traits::InteractionResult interactionResult = makeResult(); - for (const UIInputEvent& event : inputEvents) { - if (ShouldUseVectorFieldPointerPosition(event)) { - state.pointerPosition = event.position; - state.hasPointerPosition = true; - } else if (event.type == UIInputEventType::PointerLeave) { - state.hasPointerPosition = false; - } - - auto& fieldState = Traits::FieldState(state); - typename Traits::InteractionResult eventResult = makeResult(); - switch (event.type) { - case UIInputEventType::FocusGained: - eventResult.focusChanged = !fieldState.focused; - fieldState.focused = true; - if (fieldState.selectedComponentIndex == Traits::kInvalidComponentIndex) { - fieldState.selectedComponentIndex = 0u; - } - eventResult.selectedComponentIndex = fieldState.selectedComponentIndex; - break; - case UIInputEventType::FocusLost: - eventResult.focusChanged = fieldState.focused; - fieldState.focused = false; - fieldState.activeTarget = Traits::kNoneHitTargetKind; - fieldState.activeComponentIndex = Traits::kInvalidComponentIndex; - state.hasPointerPosition = false; - if (fieldState.editing) { - CommitVectorFieldEdit(state, spec, eventResult); - if (eventResult.editCommitRejected) { - CancelVectorFieldEdit(state, spec, eventResult); - } - } - break; - case UIInputEventType::PointerMove: - case UIInputEventType::PointerEnter: - case UIInputEventType::PointerLeave: - break; - case UIInputEventType::PointerButtonDown: { - const typename Traits::HitTarget hitTarget = state.hasPointerPosition - ? Traits::HitTest(layout, state.pointerPosition) - : typename Traits::HitTarget {}; - eventResult.hitTarget = hitTarget; - if (event.pointerButton != UIPointerButton::Left) { - break; - } - const bool insideField = state.hasPointerPosition && Traits::IsPointInside(layout.bounds, state.pointerPosition); - if (insideField) { - eventResult.focusChanged = !fieldState.focused; - fieldState.focused = true; - fieldState.activeTarget = hitTarget.kind == Traits::kNoneHitTargetKind ? Traits::kRowHitTargetKind : hitTarget.kind; - fieldState.activeComponentIndex = hitTarget.componentIndex; - if (hitTarget.kind == Traits::kComponentHitTargetKind) { - SelectVectorFieldComponent(state, hitTarget.componentIndex, eventResult); - } else if (fieldState.selectedComponentIndex == Traits::kInvalidComponentIndex) { - fieldState.selectedComponentIndex = 0u; - eventResult.selectedComponentIndex = 0u; - } - eventResult.consumed = true; - } else { - if (fieldState.editing) { - CommitVectorFieldEdit(state, spec, eventResult); - if (!eventResult.editCommitRejected) { - eventResult.focusChanged = fieldState.focused; - fieldState.focused = false; - } - } else if (fieldState.focused) { - eventResult.focusChanged = true; - fieldState.focused = false; - } - fieldState.activeTarget = Traits::kNoneHitTargetKind; - fieldState.activeComponentIndex = Traits::kInvalidComponentIndex; - } - break; - } - case UIInputEventType::PointerButtonUp: { - const typename Traits::HitTarget hitTarget = state.hasPointerPosition - ? Traits::HitTest(layout, state.pointerPosition) - : typename Traits::HitTarget {}; - eventResult.hitTarget = hitTarget; - if (event.pointerButton == UIPointerButton::Left) { - const typename Traits::HitTargetKind activeTarget = fieldState.activeTarget; - const std::size_t activeComponentIndex = fieldState.activeComponentIndex; - fieldState.activeTarget = Traits::kNoneHitTargetKind; - fieldState.activeComponentIndex = Traits::kInvalidComponentIndex; - if (activeTarget == Traits::kComponentHitTargetKind && - hitTarget.kind == Traits::kComponentHitTargetKind && - activeComponentIndex == hitTarget.componentIndex) { - SelectVectorFieldComponent(state, hitTarget.componentIndex, eventResult); - if (!fieldState.editing) { - eventResult.editStarted = BeginVectorFieldEdit(state, spec, hitTarget.componentIndex, false); - } - eventResult.consumed = true; - } else if (hitTarget.kind == Traits::kRowHitTargetKind) { - eventResult.consumed = true; - } - } - break; - } - case UIInputEventType::KeyDown: - if (!fieldState.focused) { - break; - } - if (fieldState.editing) { - if (event.keyCode == static_cast(KeyCode::Escape)) { - CancelVectorFieldEdit(state, spec, eventResult); - break; - } - if (event.keyCode == static_cast(KeyCode::Tab)) { - if (CommitVectorFieldEdit(state, spec, eventResult)) { - MoveVectorFieldSelection(state, event.modifiers.shift ? -1 : 1, eventResult); - } - eventResult.consumed = true; - break; - } - const auto textResult = HandleKeyDown(state.textInputState, event.keyCode, event.modifiers); - if (textResult.handled) { - state.editModel.UpdateStagedValue(state.textInputState.value); - fieldState.displayTexts[fieldState.selectedComponentIndex] = state.textInputState.value; - eventResult.consumed = true; - eventResult.selectedComponentIndex = fieldState.selectedComponentIndex; - if (textResult.submitRequested) { - CommitVectorFieldEdit(state, spec, eventResult); - } - break; - } - } else { - switch (static_cast(event.keyCode)) { - case KeyCode::Left: - MoveVectorFieldSelection(state, -1, eventResult); - break; - case KeyCode::Right: - case KeyCode::Tab: - MoveVectorFieldSelection( - state, - static_cast(event.keyCode) == KeyCode::Tab && event.modifiers.shift ? -1 : 1, - eventResult); - break; - case KeyCode::Up: - ApplyVectorFieldStep(state, spec, 1.0, false, eventResult); - break; - case KeyCode::Down: - ApplyVectorFieldStep(state, spec, -1.0, false, eventResult); - break; - case KeyCode::Home: - ApplyVectorFieldStep(state, spec, -1.0, true, eventResult); - break; - case KeyCode::End: - ApplyVectorFieldStep(state, spec, 1.0, true, eventResult); - break; - case KeyCode::Enter: - eventResult.selectedComponentIndex = ResolveFallbackSelectedComponentIndex(state); - eventResult.editStarted = BeginVectorFieldEdit(state, spec, eventResult.selectedComponentIndex, false); - eventResult.consumed = eventResult.editStarted; - break; - default: - break; - } - } - break; - case UIInputEventType::Character: - if (!fieldState.focused || spec.readOnly || event.modifiers.control || event.modifiers.alt || - event.modifiers.super || !IsPermittedVectorFieldCharacter(spec, event.character)) { - break; - } - if (fieldState.selectedComponentIndex == Traits::kInvalidComponentIndex) { - fieldState.selectedComponentIndex = 0u; - } - eventResult.selectedComponentIndex = fieldState.selectedComponentIndex; - if (!fieldState.editing) { - eventResult.editStarted = BeginVectorFieldEdit(state, spec, fieldState.selectedComponentIndex, true); - } - if (InsertCharacter(state.textInputState, event.character)) { - state.editModel.UpdateStagedValue(state.textInputState.value); - fieldState.displayTexts[fieldState.selectedComponentIndex] = state.textInputState.value; - eventResult.consumed = true; - } - break; - default: - break; - } - - refresh(); - if (eventResult.hitTarget.kind == Traits::kNoneHitTargetKind && state.hasPointerPosition) { - eventResult.hitTarget = Traits::HitTest(layout, state.pointerPosition); - } - if (eventResult.selectedComponentIndex == Traits::kInvalidComponentIndex) { - eventResult.selectedComponentIndex = fieldState.selectedComponentIndex; - } - if (shouldPublish(eventResult)) { - interactionResult = std::move(eventResult); - } - } - - refresh(); - if (interactionResult.hitTarget.kind == Traits::kNoneHitTargetKind && state.hasPointerPosition) { - interactionResult.hitTarget = Traits::HitTest(layout, state.pointerPosition); - } - if (interactionResult.selectedComponentIndex == Traits::kInvalidComponentIndex) { - interactionResult.selectedComponentIndex = Traits::FieldState(state).selectedComponentIndex; - } - return interactionResult; -} - -} // namespace XCEngine::UI::Editor::Internal diff --git a/new_editor/src/Shell/ShellInteractionRendering.cpp b/new_editor/src/Shell/ShellInteractionRendering.cpp deleted file mode 100644 index da4de909..00000000 --- a/new_editor/src/Shell/ShellInteractionRendering.cpp +++ /dev/null @@ -1,62 +0,0 @@ -#include "Shell/ShellInteractionInternal.h" -#include - -namespace XCEngine::UI::Editor { - -void AppendUIEditorShellInteraction( - ::XCEngine::UI::UIDrawList& drawList, - const UIEditorShellInteractionFrame& frame, - const UIEditorShellInteractionModel& model, - const UIEditorShellInteractionState& state, - const UIEditorShellInteractionPalette& palette, - const UIEditorShellInteractionMetrics& metrics) { - const UIEditorShellComposeModel shellModel = - Internal::BuildShellComposeModel(model, frame.request.menuBarItems); - AppendUIEditorShellCompose( - drawList, - frame.shellFrame, - shellModel, - state.composeState, - palette.shellPalette, - metrics.shellMetrics); - - const std::size_t popupCount = - (std::min)(frame.request.popupRequests.size(), frame.popupFrames.size()); - for (std::size_t index = 0; index < popupCount; ++index) { - const UIEditorShellInteractionPopupRequest& popupRequest = - frame.request.popupRequests[index]; - const UIEditorShellInteractionPopupFrame& popupFrame = - frame.popupFrames[index]; - Widgets::AppendUIEditorMenuPopupBackground( - drawList, - popupRequest.layout, - popupRequest.widgetItems, - popupFrame.popupState, - palette.popupPalette, - metrics.popupMetrics); - Widgets::AppendUIEditorMenuPopupForeground( - drawList, - popupRequest.layout, - popupRequest.widgetItems, - popupFrame.popupState, - palette.popupPalette, - metrics.popupMetrics); - } -} - -void AppendUIEditorShellInteraction( - ::XCEngine::UI::UIDrawList& drawList, - const UIEditorShellInteractionFrame& frame, - const UIEditorShellInteractionState& state, - const UIEditorShellInteractionPalette& palette, - const UIEditorShellInteractionMetrics& metrics) { - AppendUIEditorShellInteraction( - drawList, - frame, - frame.model, - state, - palette, - metrics); -} - -} // namespace XCEngine::UI::Editor diff --git a/new_editor/src/Shell/ShellInteractionRequest.cpp b/new_editor/src/Shell/ShellInteractionRequest.cpp deleted file mode 100644 index 629298af..00000000 --- a/new_editor/src/Shell/ShellInteractionRequest.cpp +++ /dev/null @@ -1,546 +0,0 @@ -#include "Shell/ShellInteractionInternal.h" -#include -#include -#include - -namespace XCEngine::UI::Editor { - -namespace { - -using ::XCEngine::UI::UIElementId; -using ::XCEngine::UI::UIInputEvent; -using ::XCEngine::UI::UIInputEventType; -using ::XCEngine::UI::UIInputPath; -using ::XCEngine::UI::UIPoint; -using ::XCEngine::UI::UIRect; -using ::XCEngine::UI::UISize; -using ::XCEngine::UI::Widgets::ResolvePopupPlacementRect; -using ::XCEngine::UI::Widgets::UIPopupPlacement; -using Widgets::BuildUIEditorMenuPopupLayout; -using Widgets::HitTestUIEditorMenuBar; -using Widgets::HitTestUIEditorMenuPopup; -using Widgets::MeasureUIEditorMenuPopupHeight; -using Widgets::ResolveUIEditorMenuPopupDesiredWidth; -using Widgets::UIEditorMenuBarHitTargetKind; -using Widgets::UIEditorMenuBarInvalidIndex; -using Widgets::UIEditorMenuPopupHitTargetKind; -using Widgets::UIEditorMenuPopupInvalidIndex; - -const UIEditorResolvedMenuDescriptor* FindResolvedMenu( - const UIEditorResolvedMenuModel& model, - std::string_view menuId) { - for (const UIEditorResolvedMenuDescriptor& menu : model.menus) { - if (menu.menuId == menuId) { - return &menu; - } - } - - return nullptr; -} - -const UIEditorResolvedMenuItem* FindResolvedMenuItemRecursive( - const std::vector& items, - std::string_view itemId) { - for (const UIEditorResolvedMenuItem& item : items) { - if (item.itemId == itemId) { - return &item; - } - - if (!item.children.empty()) { - if (const UIEditorResolvedMenuItem* child = - FindResolvedMenuItemRecursive(item.children, itemId); - child != nullptr) { - return child; - } - } - } - - return nullptr; -} - -const std::vector* ResolvePopupItems( - const UIEditorResolvedMenuModel& model, - const UIEditorMenuPopupState& popupState) { - const UIEditorResolvedMenuDescriptor* menu = - FindResolvedMenu(model, popupState.menuId); - if (menu == nullptr) { - return nullptr; - } - - if (popupState.itemId.empty()) { - return &menu->items; - } - - const UIEditorResolvedMenuItem* item = - FindResolvedMenuItemRecursive(menu->items, popupState.itemId); - if (item == nullptr || - item->kind != UIEditorMenuItemKind::Submenu || - !item->enabled || - item->children.empty()) { - return nullptr; - } - - return &item->children; -} - -std::size_t FindMenuBarIndex( - const std::vector& items, - std::string_view menuId) { - for (std::size_t index = 0; index < items.size(); ++index) { - if (items[index].menuId == menuId) { - return index; - } - } - - return UIEditorMenuBarInvalidIndex; -} - -std::size_t FindPopupItemIndex( - const std::vector& items, - std::string_view itemId) { - for (std::size_t index = 0; index < items.size(); ++index) { - if (items[index].itemId == itemId) { - return index; - } - } - - return UIEditorMenuPopupInvalidIndex; -} - -} // namespace - -namespace Internal { - -UIElementId HashText(std::string_view text) { - std::uint64_t hash = 1469598103934665603ull; - for (const unsigned char value : text) { - hash ^= value; - hash *= 1099511628211ull; - } - - hash &= 0x7FFFFFFFFFFFFFFFull; - return hash == 0u ? 1u : hash; -} - -std::string BuildRootPopupId(std::string_view menuId) { - return "editor.menu.root." + std::string(menuId); -} - -std::string BuildSubmenuPopupId(std::string_view popupId, std::string_view itemId) { - return std::string(popupId) + ".child." + std::string(itemId); -} - -UIInputPath BuildMenuButtonPath(std::string_view menuId) { - return UIInputPath { kShellPathRoot, kMenuBarPathRoot, HashText(menuId) }; -} - -UIInputPath BuildPopupSurfacePath(std::string_view popupId) { - return UIInputPath { kShellPathRoot, kPopupPathRoot, HashText(popupId) }; -} - -UIInputPath BuildMenuItemPath( - std::string_view popupId, - std::string_view itemId) { - return UIInputPath { - kShellPathRoot, - kPopupPathRoot, - HashText(popupId), - kMenuItemPathRoot, - HashText(itemId) - }; -} - -std::vector BuildMenuBarItems( - const UIEditorResolvedMenuModel& model, - const UIEditorShellInteractionServices& services, - const Widgets::UIEditorMenuBarMetrics& metrics) { - std::vector items = {}; - items.reserve(model.menus.size()); - - for (const UIEditorResolvedMenuDescriptor& menu : model.menus) { - Widgets::UIEditorMenuBarItem item = {}; - item.menuId = menu.menuId; - item.label = menu.label; - item.enabled = !menu.items.empty(); - if (services.textMeasurer != nullptr && !item.label.empty()) { - item.desiredLabelWidth = services.textMeasurer->MeasureTextWidth( - UIEditorTextMeasureRequest { item.label, metrics.labelFontSize }); - } - items.push_back(std::move(item)); - } - - return items; -} - -UIEditorShellComposeModel BuildShellComposeModel( - const UIEditorShellInteractionModel& model, - const std::vector& menuBarItems) { - UIEditorShellComposeModel shellModel = {}; - shellModel.menuBarItems = menuBarItems; - shellModel.toolbarButtons = model.toolbarButtons; - shellModel.statusSegments = model.statusSegments; - shellModel.workspacePresentations = model.workspacePresentations; - return shellModel; -} - -std::vector BuildPopupWidgetItems( - const std::vector& items, - const UIEditorShellInteractionServices& services, - const Widgets::UIEditorMenuPopupMetrics& metrics) { - std::vector widgetItems = {}; - widgetItems.reserve(items.size()); - - for (const UIEditorResolvedMenuItem& item : items) { - Widgets::UIEditorMenuPopupItem widgetItem = {}; - widgetItem.itemId = item.itemId; - widgetItem.kind = item.kind; - widgetItem.label = item.label; - widgetItem.shortcutText = item.shortcutText; - widgetItem.enabled = item.enabled; - widgetItem.checked = item.checked; - widgetItem.hasSubmenu = - item.kind == UIEditorMenuItemKind::Submenu && !item.children.empty(); - if (services.textMeasurer != nullptr) { - if (!widgetItem.label.empty()) { - widgetItem.desiredLabelWidth = services.textMeasurer->MeasureTextWidth( - UIEditorTextMeasureRequest { - widgetItem.label, - metrics.labelFontSize }); - } - if (!widgetItem.shortcutText.empty()) { - widgetItem.desiredShortcutWidth = - services.textMeasurer->MeasureTextWidth( - UIEditorTextMeasureRequest { - widgetItem.shortcutText, - metrics.labelFontSize }); - } - } - widgetItems.push_back(std::move(widgetItem)); - } - - return widgetItems; -} - -bool HasMeaningfulInteractionResult( - const UIEditorShellInteractionResult& result) { - return result.consumed || - result.requestPointerCapture || - result.releasePointerCapture || - result.commandTriggered || - result.commandDispatched || - result.menuMutation.changed || - result.workspaceResult.consumed || - !result.menuId.empty() || - !result.popupId.empty() || - !result.itemId.empty() || - !result.commandId.empty(); -} - -bool ShouldRefreshResolvedShellModel( - const UIEditorShellInteractionResult& result) { - return result.commandDispatched || - result.workspaceResult.dockHostResult.commandExecuted; -} - -BuildRequestOutput BuildRequest( - const UIRect& bounds, - const UIEditorWorkspaceController& controller, - const UIEditorShellInteractionModel& model, - const UIEditorShellInteractionState& state, - const UIEditorShellInteractionMetrics& metrics, - const UIEditorShellInteractionServices& services) { - BuildRequestOutput output = {}; - UIEditorShellInteractionRequest& request = output.request; - request.menuBarItems = BuildMenuBarItems( - model.resolvedMenuModel, - services, - metrics.shellMetrics.menuBarMetrics); - - const UIEditorShellComposeModel shellModel = - BuildShellComposeModel(model, request.menuBarItems); - request.shellRequest = ResolveUIEditorShellComposeRequest( - bounds, - controller.GetPanelRegistry(), - controller.GetWorkspace(), - controller.GetSession(), - shellModel, - state.workspaceInteractionState.dockHostInteractionState.dockHostState, - state.composeState, - metrics.shellMetrics); - - request.menuButtons.reserve(request.menuBarItems.size()); - for (std::size_t index = 0; index < request.menuBarItems.size(); ++index) { - UIEditorShellInteractionMenuButtonRequest button = {}; - button.menuId = request.menuBarItems[index].menuId; - button.label = request.menuBarItems[index].label; - button.popupId = BuildRootPopupId(button.menuId); - button.enabled = request.menuBarItems[index].enabled; - if (index < request.shellRequest.layout.menuBarLayout.buttonRects.size()) { - button.rect = request.shellRequest.layout.menuBarLayout.buttonRects[index]; - } - button.path = BuildMenuButtonPath(button.menuId); - request.menuButtons.push_back(std::move(button)); - } - - const auto& popupStates = state.menuSession.GetPopupStates(); - request.popupRequests.reserve(popupStates.size()); - for (const UIEditorMenuPopupState& popupState : popupStates) { - const auto* overlayEntry = - state.menuSession.GetPopupOverlayModel().FindPopup(popupState.popupId); - const auto* resolvedItems = - ResolvePopupItems(model.resolvedMenuModel, popupState); - if (overlayEntry == nullptr || resolvedItems == nullptr) { - output.hadInvalidPopupState = true; - continue; - } - - UIEditorShellInteractionPopupRequest popupRequest = {}; - popupRequest.popupId = popupState.popupId; - popupRequest.menuId = popupState.menuId; - popupRequest.sourceItemId = popupState.itemId; - popupRequest.overlayEntry = *overlayEntry; - popupRequest.resolvedItems = *resolvedItems; - popupRequest.widgetItems = BuildPopupWidgetItems( - popupRequest.resolvedItems, - services, - metrics.popupMetrics); - - const float popupWidth = ResolveUIEditorMenuPopupDesiredWidth( - popupRequest.widgetItems, - metrics.popupMetrics); - const float popupHeight = MeasureUIEditorMenuPopupHeight( - popupRequest.widgetItems, - metrics.popupMetrics); - popupRequest.placement = ResolvePopupPlacementRect( - overlayEntry->anchorRect, - UISize(popupWidth, popupHeight), - request.shellRequest.layout.bounds, - overlayEntry->placement); - popupRequest.layout = BuildUIEditorMenuPopupLayout( - popupRequest.placement.rect, - popupRequest.widgetItems, - metrics.popupMetrics); - - popupRequest.itemRequests.reserve(popupRequest.resolvedItems.size()); - for (std::size_t index = 0; index < popupRequest.resolvedItems.size(); ++index) { - const UIEditorResolvedMenuItem& resolvedItem = - popupRequest.resolvedItems[index]; - UIEditorShellInteractionPopupItemRequest itemRequest = {}; - itemRequest.popupId = popupRequest.popupId; - itemRequest.menuId = popupRequest.menuId; - itemRequest.itemId = resolvedItem.itemId; - itemRequest.label = resolvedItem.label; - itemRequest.commandId = resolvedItem.commandId; - itemRequest.kind = resolvedItem.kind; - itemRequest.enabled = resolvedItem.enabled; - itemRequest.checked = resolvedItem.checked; - itemRequest.hasSubmenu = - resolvedItem.kind == UIEditorMenuItemKind::Submenu && - !resolvedItem.children.empty(); - if (itemRequest.hasSubmenu) { - itemRequest.childPopupId = - BuildSubmenuPopupId(popupRequest.popupId, itemRequest.itemId); - } - if (index < popupRequest.layout.itemRects.size()) { - itemRequest.rect = popupRequest.layout.itemRects[index]; - } - itemRequest.path = - BuildMenuItemPath(popupRequest.popupId, itemRequest.itemId); - popupRequest.itemRequests.push_back(std::move(itemRequest)); - } - - request.popupRequests.push_back(std::move(popupRequest)); - } - - return output; -} - -RequestHit HitTestRequest( - const UIEditorShellInteractionRequest& request, - const UIPoint& point, - bool hasPointerPosition) { - RequestHit hit = {}; - if (!hasPointerPosition) { - return hit; - } - - for (std::size_t index = request.popupRequests.size(); index > 0u; --index) { - const UIEditorShellInteractionPopupRequest& popupRequest = - request.popupRequests[index - 1u]; - const auto popupHit = - HitTestUIEditorMenuPopup(popupRequest.layout, popupRequest.widgetItems, point); - if (popupHit.kind == UIEditorMenuPopupHitTargetKind::Item && - popupHit.index < popupRequest.itemRequests.size()) { - hit.popupRequest = &popupRequest; - hit.popupItem = &popupRequest.itemRequests[popupHit.index]; - return hit; - } - if (popupHit.kind == UIEditorMenuPopupHitTargetKind::PopupSurface) { - hit.popupRequest = &popupRequest; - return hit; - } - } - - const auto menuHit = - HitTestUIEditorMenuBar(request.shellRequest.layout.menuBarLayout, point); - if (menuHit.kind == UIEditorMenuBarHitTargetKind::Button && - menuHit.index < request.menuButtons.size()) { - hit.menuButton = &request.menuButtons[menuHit.index]; - } - - return hit; -} - -::XCEngine::UI::Widgets::UIPopupOverlayEntry BuildRootPopupEntry( - const UIEditorShellInteractionMenuButtonRequest& button) { - ::XCEngine::UI::Widgets::UIPopupOverlayEntry entry = {}; - entry.popupId = button.popupId; - entry.anchorRect = button.rect; - entry.anchorPath = button.path; - entry.surfacePath = BuildPopupSurfacePath(button.popupId); - entry.placement = UIPopupPlacement::BottomStart; - return entry; -} - -::XCEngine::UI::Widgets::UIPopupOverlayEntry BuildSubmenuPopupEntry( - const UIEditorShellInteractionPopupItemRequest& item) { - ::XCEngine::UI::Widgets::UIPopupOverlayEntry entry = {}; - entry.popupId = item.childPopupId; - entry.parentPopupId = item.popupId; - entry.anchorRect = item.rect; - entry.anchorPath = item.path; - entry.surfacePath = BuildPopupSurfacePath(item.childPopupId); - entry.placement = UIPopupPlacement::RightStart; - return entry; -} - -void UpdateMenuBarVisualState( - UIEditorShellInteractionState& state, - const UIEditorShellInteractionRequest& request, - const RequestHit& hit) { - state.composeState.menuBarState.openIndex = FindMenuBarIndex( - request.menuBarItems, - state.menuSession.GetOpenRootMenuId()); - state.composeState.menuBarState.hoveredIndex = - hit.menuButton != nullptr - ? FindMenuBarIndex(request.menuBarItems, hit.menuButton->menuId) - : UIEditorMenuBarInvalidIndex; - state.composeState.menuBarState.focused = - state.focused || state.menuSession.HasOpenMenu(); -} - -std::vector BuildPopupFrames( - const UIEditorShellInteractionRequest& request, - const UIEditorShellInteractionState& state, - std::string_view hoveredPopupId, - std::string_view hoveredItemId) { - std::vector popupFrames = {}; - popupFrames.reserve(request.popupRequests.size()); - - for (std::size_t index = 0; index < request.popupRequests.size(); ++index) { - const UIEditorShellInteractionPopupRequest& popupRequest = - request.popupRequests[index]; - UIEditorShellInteractionPopupFrame popupFrame = {}; - popupFrame.popupId = popupRequest.popupId; - popupFrame.popupState.focused = state.focused || state.menuSession.HasOpenMenu(); - popupFrame.popupState.hoveredIndex = - popupRequest.popupId == hoveredPopupId - ? FindPopupItemIndex(popupRequest.itemRequests, hoveredItemId) - : UIEditorMenuPopupInvalidIndex; - if (index + 1u < request.popupRequests.size()) { - popupFrame.popupState.submenuOpenIndex = FindPopupItemIndex( - popupRequest.itemRequests, - request.popupRequests[index + 1u].sourceItemId); - } - popupFrames.push_back(std::move(popupFrame)); - } - - return popupFrames; -} - -bool ShouldUseShellPointerPosition(const UIInputEvent& event) { - switch (event.type) { - case UIInputEventType::PointerMove: - case UIInputEventType::PointerEnter: - case UIInputEventType::PointerButtonDown: - case UIInputEventType::PointerButtonUp: - case UIInputEventType::PointerWheel: - return true; - default: - return false; - } -} - -std::vector FilterWorkspaceInputEvents( - const std::vector& inputEvents, - bool menuModalDuringFrame) { - if (!menuModalDuringFrame) { - return inputEvents; - } - - std::vector filtered = {}; - for (const UIInputEvent& event : inputEvents) { - if (event.type == UIInputEventType::FocusGained || - event.type == UIInputEventType::FocusLost) { - filtered.push_back(event); - } - } - - return filtered; -} - -} // namespace Internal - -UIEditorShellInteractionModel ResolveUIEditorShellInteractionModel( - const UIEditorWorkspaceController& controller, - const UIEditorShellInteractionDefinition& definition, - const UIEditorShellInteractionServices& services) { - UIEditorShellInteractionModel model = {}; - if (services.commandDispatcher != nullptr) { - model.resolvedMenuModel = BuildUIEditorResolvedMenuModel( - definition.menuModel, - *services.commandDispatcher, - controller, - services.shortcutManager); - } - model.toolbarButtons = definition.toolbarButtons; - model.statusSegments = definition.statusSegments; - model.workspacePresentations = definition.workspacePresentations; - return model; -} - -UIEditorShellInteractionRequest ResolveUIEditorShellInteractionRequest( - const UIRect& bounds, - const UIEditorWorkspaceController& controller, - const UIEditorShellInteractionModel& model, - const UIEditorShellInteractionState& state, - const UIEditorShellInteractionMetrics& metrics, - const UIEditorShellInteractionServices& services) { - return Internal::BuildRequest( - bounds, - controller, - model, - state, - metrics, - services).request; -} - -UIEditorShellInteractionRequest ResolveUIEditorShellInteractionRequest( - const UIRect& bounds, - const UIEditorWorkspaceController& controller, - const UIEditorShellInteractionDefinition& definition, - const UIEditorShellInteractionState& state, - const UIEditorShellInteractionMetrics& metrics, - const UIEditorShellInteractionServices& services) { - const UIEditorShellInteractionModel model = - ResolveUIEditorShellInteractionModel(controller, definition, services); - return ResolveUIEditorShellInteractionRequest( - bounds, - controller, - model, - state, - metrics, - services); -} - -} // namespace XCEngine::UI::Editor diff --git a/new_editor/src/Shell/UIEditorShellCompose.cpp b/new_editor/src/Shell/UIEditorShellCompose.cpp index 955d5cb5..d75ed579 100644 --- a/new_editor/src/Shell/UIEditorShellCompose.cpp +++ b/new_editor/src/Shell/UIEditorShellCompose.cpp @@ -1,5 +1,7 @@ #include +#include "Rendering/Assets/BuiltInIcons.h" + #include namespace XCEngine::UI::Editor { @@ -71,70 +73,28 @@ UIEditorShellToolbarLayout BuildUIEditorShellToolbarLayout( 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; + std::uint8_t iconKind, + const App::BuiltInIcons* icons, + const UIColor& tintColor) { + if (icons == nullptr) { + return; } + + const ::XCEngine::UI::UITextureHandle& texture = icons->Resolve(static_cast(iconKind)); + if (!texture.IsValid()) { + return; + } + + const float inset = 2.0f; + const UIRect iconRect( + rect.x + inset, + rect.y + inset, + rect.width - inset * 2.0f, + rect.height - inset * 2.0f); + drawList.AddImage(iconRect, texture, tintColor); } void AppendUIEditorShellToolbar( @@ -142,7 +102,8 @@ void AppendUIEditorShellToolbar( const UIEditorShellToolbarLayout& layout, const std::vector& buttons, const UIEditorShellToolbarPalette& palette, - const UIEditorShellToolbarMetrics& metrics) { + const UIEditorShellToolbarMetrics& metrics, + const App::BuiltInIcons* builtInIcons) { if (layout.bounds.width <= 0.0f || layout.bounds.height <= 0.0f) { return; } @@ -183,9 +144,9 @@ void AppendUIEditorShellToolbar( AppendToolbarGlyph( drawList, buttonRect, - buttons[index].glyph, - palette.iconColor, - metrics.iconThickness); + buttons[index].iconKind, + builtInIcons, + palette.iconColor); } } @@ -378,7 +339,8 @@ void AppendUIEditorShellCompose( const UIEditorShellComposeModel& model, const UIEditorShellComposeState& state, const UIEditorShellComposePalette& palette, - const UIEditorShellComposeMetrics& metrics) { + const UIEditorShellComposeMetrics& metrics, + const App::BuiltInIcons* builtInIcons) { drawList.AddFilledRect( frame.layout.bounds, palette.surfaceColor, @@ -409,7 +371,8 @@ void AppendUIEditorShellCompose( frame.layout.toolbarLayout, model.toolbarButtons, palette.toolbarPalette, - metrics.toolbarMetrics); + metrics.toolbarMetrics, + builtInIcons); AppendUIEditorWorkspaceCompose( drawList, diff --git a/new_editor/src/Shell/UIEditorShellInteraction.cpp b/new_editor/src/Shell/UIEditorShellInteraction.cpp index 46495794..f6a7475b 100644 --- a/new_editor/src/Shell/UIEditorShellInteraction.cpp +++ b/new_editor/src/Shell/UIEditorShellInteraction.cpp @@ -2,6 +2,8 @@ #include +#include +#include #include namespace XCEngine::UI::Editor { @@ -9,13 +11,542 @@ namespace XCEngine::UI::Editor { namespace { using ::XCEngine::Input::KeyCode; +using ::XCEngine::UI::UIElementId; using ::XCEngine::UI::UIInputEvent; using ::XCEngine::UI::UIInputEventType; using ::XCEngine::UI::UIInputPath; +using ::XCEngine::UI::UIPoint; using ::XCEngine::UI::UIRect; +using ::XCEngine::UI::UISize; +using ::XCEngine::UI::Widgets::ResolvePopupPlacementRect; +using ::XCEngine::UI::Widgets::UIPopupPlacement; +using Widgets::BuildUIEditorMenuPopupLayout; +using Widgets::HitTestUIEditorMenuBar; +using Widgets::HitTestUIEditorMenuPopup; +using Widgets::MeasureUIEditorMenuPopupHeight; +using Widgets::ResolveUIEditorMenuPopupDesiredWidth; +using Widgets::UIEditorMenuBarHitTargetKind; +using Widgets::UIEditorMenuBarInvalidIndex; +using Widgets::UIEditorMenuPopupHitTargetKind; +using Widgets::UIEditorMenuPopupInvalidIndex; + +const UIEditorResolvedMenuDescriptor* FindResolvedMenu( + const UIEditorResolvedMenuModel& model, + std::string_view menuId) { + for (const UIEditorResolvedMenuDescriptor& menu : model.menus) { + if (menu.menuId == menuId) { + return &menu; + } + } + + return nullptr; +} + +const UIEditorResolvedMenuItem* FindResolvedMenuItemRecursive( + const std::vector& items, + std::string_view itemId) { + for (const UIEditorResolvedMenuItem& item : items) { + if (item.itemId == itemId) { + return &item; + } + + if (!item.children.empty()) { + if (const UIEditorResolvedMenuItem* child = + FindResolvedMenuItemRecursive(item.children, itemId); + child != nullptr) { + return child; + } + } + } + + return nullptr; +} + +const std::vector* ResolvePopupItems( + const UIEditorResolvedMenuModel& model, + const UIEditorMenuPopupState& popupState) { + const UIEditorResolvedMenuDescriptor* menu = + FindResolvedMenu(model, popupState.menuId); + if (menu == nullptr) { + return nullptr; + } + + if (popupState.itemId.empty()) { + return &menu->items; + } + + const UIEditorResolvedMenuItem* item = + FindResolvedMenuItemRecursive(menu->items, popupState.itemId); + if (item == nullptr || + item->kind != UIEditorMenuItemKind::Submenu || + !item->enabled || + item->children.empty()) { + return nullptr; + } + + return &item->children; +} + +std::size_t FindMenuBarIndex( + const std::vector& items, + std::string_view menuId) { + for (std::size_t index = 0; index < items.size(); ++index) { + if (items[index].menuId == menuId) { + return index; + } + } + + return UIEditorMenuBarInvalidIndex; +} + +std::size_t FindPopupItemIndex( + const std::vector& items, + std::string_view itemId) { + for (std::size_t index = 0; index < items.size(); ++index) { + if (items[index].itemId == itemId) { + return index; + } + } + + return UIEditorMenuPopupInvalidIndex; +} } // namespace +namespace Internal { + +UIElementId HashText(std::string_view text) { + std::uint64_t hash = 1469598103934665603ull; + for (const unsigned char value : text) { + hash ^= value; + hash *= 1099511628211ull; + } + + hash &= 0x7FFFFFFFFFFFFFFFull; + return hash == 0u ? 1u : hash; +} + +std::string BuildRootPopupId(std::string_view menuId) { + return "editor.menu.root." + std::string(menuId); +} + +std::string BuildSubmenuPopupId(std::string_view popupId, std::string_view itemId) { + return std::string(popupId) + ".child." + std::string(itemId); +} + +UIInputPath BuildMenuButtonPath(std::string_view menuId) { + return UIInputPath { kShellPathRoot, kMenuBarPathRoot, HashText(menuId) }; +} + +UIInputPath BuildPopupSurfacePath(std::string_view popupId) { + return UIInputPath { kShellPathRoot, kPopupPathRoot, HashText(popupId) }; +} + +UIInputPath BuildMenuItemPath( + std::string_view popupId, + std::string_view itemId) { + return UIInputPath { + kShellPathRoot, + kPopupPathRoot, + HashText(popupId), + kMenuItemPathRoot, + HashText(itemId) + }; +} + +std::vector BuildMenuBarItems( + const UIEditorResolvedMenuModel& model, + const UIEditorShellInteractionServices& services, + const Widgets::UIEditorMenuBarMetrics& metrics) { + std::vector items = {}; + items.reserve(model.menus.size()); + + for (const UIEditorResolvedMenuDescriptor& menu : model.menus) { + Widgets::UIEditorMenuBarItem item = {}; + item.menuId = menu.menuId; + item.label = menu.label; + item.enabled = !menu.items.empty(); + if (services.textMeasurer != nullptr && !item.label.empty()) { + item.desiredLabelWidth = services.textMeasurer->MeasureTextWidth( + UIEditorTextMeasureRequest { item.label, metrics.labelFontSize }); + } + items.push_back(std::move(item)); + } + + return items; +} + +UIEditorShellComposeModel BuildShellComposeModel( + const UIEditorShellInteractionModel& model, + const std::vector& menuBarItems) { + UIEditorShellComposeModel shellModel = {}; + shellModel.menuBarItems = menuBarItems; + shellModel.toolbarButtons = model.toolbarButtons; + shellModel.statusSegments = model.statusSegments; + shellModel.workspacePresentations = model.workspacePresentations; + return shellModel; +} + +std::vector BuildPopupWidgetItems( + const std::vector& items, + const UIEditorShellInteractionServices& services, + const Widgets::UIEditorMenuPopupMetrics& metrics) { + std::vector widgetItems = {}; + widgetItems.reserve(items.size()); + + for (const UIEditorResolvedMenuItem& item : items) { + Widgets::UIEditorMenuPopupItem widgetItem = {}; + widgetItem.itemId = item.itemId; + widgetItem.kind = item.kind; + widgetItem.label = item.label; + widgetItem.shortcutText = item.shortcutText; + widgetItem.enabled = item.enabled; + widgetItem.checked = item.checked; + widgetItem.hasSubmenu = + item.kind == UIEditorMenuItemKind::Submenu && !item.children.empty(); + if (services.textMeasurer != nullptr) { + if (!widgetItem.label.empty()) { + widgetItem.desiredLabelWidth = services.textMeasurer->MeasureTextWidth( + UIEditorTextMeasureRequest { + widgetItem.label, + metrics.labelFontSize }); + } + if (!widgetItem.shortcutText.empty()) { + widgetItem.desiredShortcutWidth = + services.textMeasurer->MeasureTextWidth( + UIEditorTextMeasureRequest { + widgetItem.shortcutText, + metrics.labelFontSize }); + } + } + widgetItems.push_back(std::move(widgetItem)); + } + + return widgetItems; +} + +bool HasMeaningfulInteractionResult( + const UIEditorShellInteractionResult& result) { + return result.consumed || + result.requestPointerCapture || + result.releasePointerCapture || + result.commandTriggered || + result.commandDispatched || + result.menuMutation.changed || + result.workspaceResult.consumed || + !result.menuId.empty() || + !result.popupId.empty() || + !result.itemId.empty() || + !result.commandId.empty(); +} + +bool ShouldRefreshResolvedShellModel( + const UIEditorShellInteractionResult& result) { + return result.commandDispatched || + result.workspaceResult.dockHostResult.commandExecuted; +} + +BuildRequestOutput BuildRequest( + const UIRect& bounds, + const UIEditorWorkspaceController& controller, + const UIEditorShellInteractionModel& model, + const UIEditorShellInteractionState& state, + const UIEditorShellInteractionMetrics& metrics, + const UIEditorShellInteractionServices& services) { + BuildRequestOutput output = {}; + UIEditorShellInteractionRequest& request = output.request; + request.menuBarItems = BuildMenuBarItems( + model.resolvedMenuModel, + services, + metrics.shellMetrics.menuBarMetrics); + + const UIEditorShellComposeModel shellModel = + BuildShellComposeModel(model, request.menuBarItems); + request.shellRequest = ResolveUIEditorShellComposeRequest( + bounds, + controller.GetPanelRegistry(), + controller.GetWorkspace(), + controller.GetSession(), + shellModel, + state.workspaceInteractionState.dockHostInteractionState.dockHostState, + state.composeState, + metrics.shellMetrics); + + request.menuButtons.reserve(request.menuBarItems.size()); + for (std::size_t index = 0; index < request.menuBarItems.size(); ++index) { + UIEditorShellInteractionMenuButtonRequest button = {}; + button.menuId = request.menuBarItems[index].menuId; + button.label = request.menuBarItems[index].label; + button.popupId = BuildRootPopupId(button.menuId); + button.enabled = request.menuBarItems[index].enabled; + if (index < request.shellRequest.layout.menuBarLayout.buttonRects.size()) { + button.rect = request.shellRequest.layout.menuBarLayout.buttonRects[index]; + } + button.path = BuildMenuButtonPath(button.menuId); + request.menuButtons.push_back(std::move(button)); + } + + const auto& popupStates = state.menuSession.GetPopupStates(); + request.popupRequests.reserve(popupStates.size()); + for (const UIEditorMenuPopupState& popupState : popupStates) { + const auto* overlayEntry = + state.menuSession.GetPopupOverlayModel().FindPopup(popupState.popupId); + const auto* resolvedItems = + ResolvePopupItems(model.resolvedMenuModel, popupState); + if (overlayEntry == nullptr || resolvedItems == nullptr) { + output.hadInvalidPopupState = true; + continue; + } + + UIEditorShellInteractionPopupRequest popupRequest = {}; + popupRequest.popupId = popupState.popupId; + popupRequest.menuId = popupState.menuId; + popupRequest.sourceItemId = popupState.itemId; + popupRequest.overlayEntry = *overlayEntry; + popupRequest.resolvedItems = *resolvedItems; + popupRequest.widgetItems = BuildPopupWidgetItems( + popupRequest.resolvedItems, + services, + metrics.popupMetrics); + + const float popupWidth = ResolveUIEditorMenuPopupDesiredWidth( + popupRequest.widgetItems, + metrics.popupMetrics); + const float popupHeight = MeasureUIEditorMenuPopupHeight( + popupRequest.widgetItems, + metrics.popupMetrics); + popupRequest.placement = ResolvePopupPlacementRect( + overlayEntry->anchorRect, + UISize(popupWidth, popupHeight), + request.shellRequest.layout.bounds, + overlayEntry->placement); + popupRequest.layout = BuildUIEditorMenuPopupLayout( + popupRequest.placement.rect, + popupRequest.widgetItems, + metrics.popupMetrics); + + popupRequest.itemRequests.reserve(popupRequest.resolvedItems.size()); + for (std::size_t index = 0; index < popupRequest.resolvedItems.size(); ++index) { + const UIEditorResolvedMenuItem& resolvedItem = + popupRequest.resolvedItems[index]; + UIEditorShellInteractionPopupItemRequest itemRequest = {}; + itemRequest.popupId = popupRequest.popupId; + itemRequest.menuId = popupRequest.menuId; + itemRequest.itemId = resolvedItem.itemId; + itemRequest.label = resolvedItem.label; + itemRequest.commandId = resolvedItem.commandId; + itemRequest.kind = resolvedItem.kind; + itemRequest.enabled = resolvedItem.enabled; + itemRequest.checked = resolvedItem.checked; + itemRequest.hasSubmenu = + resolvedItem.kind == UIEditorMenuItemKind::Submenu && + !resolvedItem.children.empty(); + if (itemRequest.hasSubmenu) { + itemRequest.childPopupId = + BuildSubmenuPopupId(popupRequest.popupId, itemRequest.itemId); + } + if (index < popupRequest.layout.itemRects.size()) { + itemRequest.rect = popupRequest.layout.itemRects[index]; + } + itemRequest.path = + BuildMenuItemPath(popupRequest.popupId, itemRequest.itemId); + popupRequest.itemRequests.push_back(std::move(itemRequest)); + } + + request.popupRequests.push_back(std::move(popupRequest)); + } + + return output; +} + +RequestHit HitTestRequest( + const UIEditorShellInteractionRequest& request, + const UIPoint& point, + bool hasPointerPosition) { + RequestHit hit = {}; + if (!hasPointerPosition) { + return hit; + } + + for (std::size_t index = request.popupRequests.size(); index > 0u; --index) { + const UIEditorShellInteractionPopupRequest& popupRequest = + request.popupRequests[index - 1u]; + const auto popupHit = + HitTestUIEditorMenuPopup(popupRequest.layout, popupRequest.widgetItems, point); + if (popupHit.kind == UIEditorMenuPopupHitTargetKind::Item && + popupHit.index < popupRequest.itemRequests.size()) { + hit.popupRequest = &popupRequest; + hit.popupItem = &popupRequest.itemRequests[popupHit.index]; + return hit; + } + if (popupHit.kind == UIEditorMenuPopupHitTargetKind::PopupSurface) { + hit.popupRequest = &popupRequest; + return hit; + } + } + + const auto menuHit = + HitTestUIEditorMenuBar(request.shellRequest.layout.menuBarLayout, point); + if (menuHit.kind == UIEditorMenuBarHitTargetKind::Button && + menuHit.index < request.menuButtons.size()) { + hit.menuButton = &request.menuButtons[menuHit.index]; + } + + return hit; +} + +::XCEngine::UI::Widgets::UIPopupOverlayEntry BuildRootPopupEntry( + const UIEditorShellInteractionMenuButtonRequest& button) { + ::XCEngine::UI::Widgets::UIPopupOverlayEntry entry = {}; + entry.popupId = button.popupId; + entry.anchorRect = button.rect; + entry.anchorPath = button.path; + entry.surfacePath = BuildPopupSurfacePath(button.popupId); + entry.placement = UIPopupPlacement::BottomStart; + return entry; +} + +::XCEngine::UI::Widgets::UIPopupOverlayEntry BuildSubmenuPopupEntry( + const UIEditorShellInteractionPopupItemRequest& item) { + ::XCEngine::UI::Widgets::UIPopupOverlayEntry entry = {}; + entry.popupId = item.childPopupId; + entry.parentPopupId = item.popupId; + entry.anchorRect = item.rect; + entry.anchorPath = item.path; + entry.surfacePath = BuildPopupSurfacePath(item.childPopupId); + entry.placement = UIPopupPlacement::RightStart; + return entry; +} + +void UpdateMenuBarVisualState( + UIEditorShellInteractionState& state, + const UIEditorShellInteractionRequest& request, + const RequestHit& hit) { + state.composeState.menuBarState.openIndex = FindMenuBarIndex( + request.menuBarItems, + state.menuSession.GetOpenRootMenuId()); + state.composeState.menuBarState.hoveredIndex = + hit.menuButton != nullptr + ? FindMenuBarIndex(request.menuBarItems, hit.menuButton->menuId) + : UIEditorMenuBarInvalidIndex; + state.composeState.menuBarState.focused = + state.focused || state.menuSession.HasOpenMenu(); +} + +std::vector BuildPopupFrames( + const UIEditorShellInteractionRequest& request, + const UIEditorShellInteractionState& state, + std::string_view hoveredPopupId, + std::string_view hoveredItemId) { + std::vector popupFrames = {}; + popupFrames.reserve(request.popupRequests.size()); + + for (std::size_t index = 0; index < request.popupRequests.size(); ++index) { + const UIEditorShellInteractionPopupRequest& popupRequest = + request.popupRequests[index]; + UIEditorShellInteractionPopupFrame popupFrame = {}; + popupFrame.popupId = popupRequest.popupId; + popupFrame.popupState.focused = state.focused || state.menuSession.HasOpenMenu(); + popupFrame.popupState.hoveredIndex = + popupRequest.popupId == hoveredPopupId + ? FindPopupItemIndex(popupRequest.itemRequests, hoveredItemId) + : UIEditorMenuPopupInvalidIndex; + if (index + 1u < request.popupRequests.size()) { + popupFrame.popupState.submenuOpenIndex = FindPopupItemIndex( + popupRequest.itemRequests, + request.popupRequests[index + 1u].sourceItemId); + } + popupFrames.push_back(std::move(popupFrame)); + } + + return popupFrames; +} + +bool ShouldUseShellPointerPosition(const UIInputEvent& event) { + switch (event.type) { + case UIInputEventType::PointerMove: + case UIInputEventType::PointerEnter: + case UIInputEventType::PointerButtonDown: + case UIInputEventType::PointerButtonUp: + case UIInputEventType::PointerWheel: + return true; + default: + return false; + } +} + +std::vector FilterWorkspaceInputEvents( + const std::vector& inputEvents, + bool menuModalDuringFrame) { + if (!menuModalDuringFrame) { + return inputEvents; + } + + std::vector filtered = {}; + for (const UIInputEvent& event : inputEvents) { + if (event.type == UIInputEventType::FocusGained || + event.type == UIInputEventType::FocusLost) { + filtered.push_back(event); + } + } + + return filtered; +} + +} // namespace Internal + +UIEditorShellInteractionModel ResolveUIEditorShellInteractionModel( + const UIEditorWorkspaceController& controller, + const UIEditorShellInteractionDefinition& definition, + const UIEditorShellInteractionServices& services) { + UIEditorShellInteractionModel model = {}; + if (services.commandDispatcher != nullptr) { + model.resolvedMenuModel = BuildUIEditorResolvedMenuModel( + definition.menuModel, + *services.commandDispatcher, + controller, + services.shortcutManager); + } + model.toolbarButtons = definition.toolbarButtons; + model.statusSegments = definition.statusSegments; + model.workspacePresentations = definition.workspacePresentations; + return model; +} + +UIEditorShellInteractionRequest ResolveUIEditorShellInteractionRequest( + const UIRect& bounds, + const UIEditorWorkspaceController& controller, + const UIEditorShellInteractionModel& model, + const UIEditorShellInteractionState& state, + const UIEditorShellInteractionMetrics& metrics, + const UIEditorShellInteractionServices& services) { + return Internal::BuildRequest( + bounds, + controller, + model, + state, + metrics, + services).request; +} + +UIEditorShellInteractionRequest ResolveUIEditorShellInteractionRequest( + const UIRect& bounds, + const UIEditorWorkspaceController& controller, + const UIEditorShellInteractionDefinition& definition, + const UIEditorShellInteractionState& state, + const UIEditorShellInteractionMetrics& metrics, + const UIEditorShellInteractionServices& services) { + const UIEditorShellInteractionModel model = + ResolveUIEditorShellInteractionModel(controller, definition, services); + return ResolveUIEditorShellInteractionRequest( + bounds, + controller, + model, + state, + metrics, + services); +} + UIEditorShellInteractionFrame UpdateUIEditorShellInteraction( UIEditorShellInteractionState& state, UIEditorWorkspaceController& controller, @@ -312,4 +843,60 @@ UIEditorShellInteractionFrame UpdateUIEditorShellInteraction( return refreshedFrame; } +void AppendUIEditorShellInteraction( + ::XCEngine::UI::UIDrawList& drawList, + const UIEditorShellInteractionFrame& frame, + const UIEditorShellInteractionModel& model, + const UIEditorShellInteractionState& state, + const UIEditorShellInteractionPalette& palette, + const UIEditorShellInteractionMetrics& metrics) { + const UIEditorShellComposeModel shellModel = + Internal::BuildShellComposeModel(model, frame.request.menuBarItems); + AppendUIEditorShellCompose( + drawList, + frame.shellFrame, + shellModel, + state.composeState, + palette.shellPalette, + metrics.shellMetrics); + + const std::size_t popupCount = + (std::min)(frame.request.popupRequests.size(), frame.popupFrames.size()); + for (std::size_t index = 0; index < popupCount; ++index) { + const UIEditorShellInteractionPopupRequest& popupRequest = + frame.request.popupRequests[index]; + const UIEditorShellInteractionPopupFrame& popupFrame = + frame.popupFrames[index]; + Widgets::AppendUIEditorMenuPopupBackground( + drawList, + popupRequest.layout, + popupRequest.widgetItems, + popupFrame.popupState, + palette.popupPalette, + metrics.popupMetrics); + Widgets::AppendUIEditorMenuPopupForeground( + drawList, + popupRequest.layout, + popupRequest.widgetItems, + popupFrame.popupState, + palette.popupPalette, + metrics.popupMetrics); + } +} + +void AppendUIEditorShellInteraction( + ::XCEngine::UI::UIDrawList& drawList, + const UIEditorShellInteractionFrame& frame, + const UIEditorShellInteractionState& state, + const UIEditorShellInteractionPalette& palette, + const UIEditorShellInteractionMetrics& metrics) { + AppendUIEditorShellInteraction( + drawList, + frame, + frame.model, + state, + palette, + metrics); +} + } // namespace XCEngine::UI::Editor diff --git a/new_editor/src/Workspace/UIEditorWindowWorkspaceController.cpp b/new_editor/src/Workspace/UIEditorWindowWorkspaceController.cpp index 5c6b2395..b89470b3 100644 --- a/new_editor/src/Workspace/UIEditorWindowWorkspaceController.cpp +++ b/new_editor/src/Workspace/UIEditorWindowWorkspaceController.cpp @@ -63,6 +63,18 @@ bool TryExtractPanelFromWindow( extractedPanel); } +const UIEditorWindowWorkspaceState* FindWindowOwningPanel( + const UIEditorWindowWorkspaceSet& windowSet, + std::string_view panelId) { + for (const UIEditorWindowWorkspaceState& state : windowSet.windows) { + if (FindUIEditorPanelSessionState(state.session, panelId) != nullptr) { + return &state; + } + } + + return nullptr; +} + } // namespace std::string_view GetUIEditorWindowWorkspaceOperationStatusName( @@ -216,6 +228,87 @@ UIEditorWindowWorkspaceOperationResult UIEditorWindowWorkspaceController::Detach panelId); } +UIEditorWindowWorkspaceOperationResult UIEditorWindowWorkspaceController::OpenPanelInNewWindow( + std::string_view panelId, + std::string_view preferredNewWindowId) { + const UIEditorWindowWorkspaceValidationResult stateValidation = ValidateState(); + if (!stateValidation.IsValid()) { + return BuildOperationResult( + UIEditorWindowWorkspaceOperationStatus::Rejected, + "Window workspace state invalid: " + stateValidation.message, + {}, + {}, + panelId); + } + + const UIEditorPanelDescriptor* descriptor = + FindUIEditorPanelDescriptor(m_panelRegistry, panelId); + if (descriptor == nullptr) { + return BuildOperationResult( + UIEditorWindowWorkspaceOperationStatus::Rejected, + "Panel is missing from the panel registry.", + {}, + {}, + panelId); + } + + if (const UIEditorWindowWorkspaceState* existingWindow = + FindWindowOwningPanel(m_windowSet, panelId); + existingWindow != nullptr) { + return BuildOperationResult( + UIEditorWindowWorkspaceOperationStatus::NoOp, + "Panel is already open in a window.", + existingWindow->windowId, + existingWindow->windowId, + panelId); + } + + UIEditorWorkspaceExtractedPanel extractedPanel = {}; + extractedPanel.panelNode = BuildUIEditorWorkspacePanel( + std::string(panelId) + "-panel", + descriptor->panelId, + descriptor->defaultTitle, + descriptor->placeholder); + extractedPanel.sessionState.panelId = descriptor->panelId; + extractedPanel.sessionState.open = true; + extractedPanel.sessionState.visible = true; + + const std::string newWindowId = MakeUniqueWindowId( + preferredNewWindowId.empty() + ? std::string(panelId) + "-window" + : std::string(preferredNewWindowId)); + const std::string previousActiveWindowId = m_windowSet.activeWindowId; + UIEditorWindowWorkspaceState detachedWindow = {}; + detachedWindow.windowId = newWindowId; + detachedWindow.workspace = + BuildUIEditorDetachedWorkspaceFromExtractedPanel( + newWindowId + "-root", + extractedPanel); + detachedWindow.session = + BuildUIEditorDetachedWorkspaceSessionFromExtractedPanel(extractedPanel); + m_windowSet.windows.push_back(std::move(detachedWindow)); + m_windowSet.activeWindowId = newWindowId; + + const UIEditorWindowWorkspaceValidationResult validation = ValidateState(); + if (!validation.IsValid()) { + m_windowSet.windows.pop_back(); + m_windowSet.activeWindowId = previousActiveWindowId; + return BuildOperationResult( + UIEditorWindowWorkspaceOperationStatus::Rejected, + "OpenPanelInNewWindow produced invalid state: " + validation.message, + {}, + newWindowId, + panelId); + } + + return BuildOperationResult( + UIEditorWindowWorkspaceOperationStatus::Changed, + "Panel opened in a new detached window.", + {}, + newWindowId, + panelId); +} + UIEditorWindowWorkspaceOperationResult UIEditorWindowWorkspaceController::MovePanelToStack( std::string_view sourceWindowId, std::string_view sourceNodeId, diff --git a/new_editor/src/Workspace/UIEditorWorkspaceController.cpp b/new_editor/src/Workspace/UIEditorWorkspaceController.cpp index f8334bf6..76734b1c 100644 --- a/new_editor/src/Workspace/UIEditorWorkspaceController.cpp +++ b/new_editor/src/Workspace/UIEditorWorkspaceController.cpp @@ -1,5 +1,10 @@ #include "Workspace/WorkspaceControllerInternal.h" +#include +#include + +#include +#include #include namespace XCEngine::UI::Editor::Internal { @@ -240,4 +245,560 @@ UIEditorWorkspaceController BuildDefaultUIEditorWorkspaceController( BuildDefaultUIEditorWorkspaceSession(panelRegistry, canonicalWorkspace)); } +UIEditorWorkspaceCommandResult UIEditorWorkspaceController::Dispatch( + const UIEditorWorkspaceCommand& command) { + const UIEditorWorkspaceControllerValidationResult validation = ValidateState(); + if (command.kind != UIEditorWorkspaceCommandKind::ResetWorkspace && + !validation.IsValid()) { + return BuildResult( + command, + UIEditorWorkspaceCommandStatus::Rejected, + "Controller state invalid: " + validation.message); + } + + const UIEditorWorkspaceModel previousWorkspace = m_workspace; + const UIEditorWorkspaceSession previousSession = m_session; + const UIEditorPanelSessionState* panelState = + command.kind == UIEditorWorkspaceCommandKind::ResetWorkspace + ? nullptr + : FindUIEditorPanelSessionState(m_session, command.panelId); + const UIEditorPanelDescriptor* panelDescriptor = + command.kind == UIEditorWorkspaceCommandKind::ResetWorkspace + ? nullptr + : FindPanelDescriptor(command.panelId); + + switch (command.kind) { + case UIEditorWorkspaceCommandKind::OpenPanel: + if (command.panelId.empty()) { + return BuildResult( + command, + UIEditorWorkspaceCommandStatus::Rejected, + "OpenPanel requires a panelId."); + } + if (panelDescriptor == nullptr || panelState == nullptr) { + return BuildResult( + command, + UIEditorWorkspaceCommandStatus::Rejected, + "OpenPanel target panel is missing."); + } + if (panelState->open && panelState->visible) { + return BuildResult( + command, + UIEditorWorkspaceCommandStatus::NoOp, + "Panel is already open and visible."); + } + return FinalizeMutation( + command, + TryOpenUIEditorWorkspacePanel( + m_panelRegistry, + m_workspace, + m_session, + command.panelId), + "Panel opened and activated.", + "OpenPanel failed unexpectedly.", + previousWorkspace, + previousSession); + + case UIEditorWorkspaceCommandKind::ClosePanel: + if (command.panelId.empty()) { + return BuildResult( + command, + UIEditorWorkspaceCommandStatus::Rejected, + "ClosePanel requires a panelId."); + } + if (panelDescriptor == nullptr || panelState == nullptr) { + return BuildResult( + command, + UIEditorWorkspaceCommandStatus::Rejected, + "ClosePanel target panel is missing."); + } + if (!panelDescriptor->canClose) { + return BuildResult( + command, + UIEditorWorkspaceCommandStatus::Rejected, + "Panel cannot be closed."); + } + if (!panelState->open) { + return BuildResult( + command, + UIEditorWorkspaceCommandStatus::NoOp, + "Panel is already closed."); + } + return FinalizeMutation( + command, + TryCloseUIEditorWorkspacePanel( + m_panelRegistry, + m_workspace, + m_session, + command.panelId), + "Panel closed.", + "ClosePanel failed unexpectedly.", + previousWorkspace, + previousSession); + + case UIEditorWorkspaceCommandKind::ShowPanel: + if (command.panelId.empty()) { + return BuildResult( + command, + UIEditorWorkspaceCommandStatus::Rejected, + "ShowPanel requires a panelId."); + } + if (panelDescriptor == nullptr || panelState == nullptr) { + return BuildResult( + command, + UIEditorWorkspaceCommandStatus::Rejected, + "ShowPanel target panel is missing."); + } + if (!panelState->open) { + return BuildResult( + command, + UIEditorWorkspaceCommandStatus::Rejected, + "Closed panel must be opened before it can be shown."); + } + if (panelState->visible) { + return BuildResult( + command, + UIEditorWorkspaceCommandStatus::NoOp, + "Panel is already visible."); + } + return FinalizeMutation( + command, + TryShowUIEditorWorkspacePanel( + m_panelRegistry, + m_workspace, + m_session, + command.panelId), + "Panel shown and activated.", + "ShowPanel failed unexpectedly.", + previousWorkspace, + previousSession); + + case UIEditorWorkspaceCommandKind::HidePanel: + if (command.panelId.empty()) { + return BuildResult( + command, + UIEditorWorkspaceCommandStatus::Rejected, + "HidePanel requires a panelId."); + } + if (panelDescriptor == nullptr || panelState == nullptr) { + return BuildResult( + command, + UIEditorWorkspaceCommandStatus::Rejected, + "HidePanel target panel is missing."); + } + if (!panelDescriptor->canHide) { + return BuildResult( + command, + UIEditorWorkspaceCommandStatus::Rejected, + "Panel cannot be hidden."); + } + if (!panelState->open) { + return BuildResult( + command, + UIEditorWorkspaceCommandStatus::Rejected, + "Closed panel cannot be hidden."); + } + if (!panelState->visible) { + return BuildResult( + command, + UIEditorWorkspaceCommandStatus::NoOp, + "Panel is already hidden."); + } + return FinalizeMutation( + command, + TryHideUIEditorWorkspacePanel( + m_panelRegistry, + m_workspace, + m_session, + command.panelId), + "Panel hidden and active panel re-resolved.", + "HidePanel failed unexpectedly.", + previousWorkspace, + previousSession); + + case UIEditorWorkspaceCommandKind::ActivatePanel: + if (command.panelId.empty()) { + return BuildResult( + command, + UIEditorWorkspaceCommandStatus::Rejected, + "ActivatePanel requires a panelId."); + } + if (panelDescriptor == nullptr || panelState == nullptr) { + return BuildResult( + command, + UIEditorWorkspaceCommandStatus::Rejected, + "ActivatePanel target panel is missing."); + } + if (!panelState->open || !panelState->visible) { + return BuildResult( + command, + UIEditorWorkspaceCommandStatus::Rejected, + "Only open and visible panels can be activated."); + } + if (m_workspace.activePanelId == command.panelId) { + return BuildResult( + command, + UIEditorWorkspaceCommandStatus::NoOp, + "Panel is already active."); + } + return FinalizeMutation( + command, + TryActivateUIEditorWorkspacePanel( + m_panelRegistry, + m_workspace, + m_session, + command.panelId), + "Panel activated.", + "ActivatePanel failed unexpectedly.", + previousWorkspace, + previousSession); + + case UIEditorWorkspaceCommandKind::ResetWorkspace: + if (AreUIEditorWorkspaceModelsEquivalent(m_workspace, m_baselineWorkspace) && + AreUIEditorWorkspaceSessionsEquivalent(m_session, m_baselineSession)) { + return BuildResult( + command, + UIEditorWorkspaceCommandStatus::NoOp, + "Workspace already matches the baseline state."); + } + + m_workspace = m_baselineWorkspace; + m_session = m_baselineSession; + return FinalizeMutation( + command, + true, + "Workspace reset to baseline.", + "ResetWorkspace failed unexpectedly.", + previousWorkspace, + previousSession); + } + + return BuildResult( + command, + UIEditorWorkspaceCommandStatus::Rejected, + "Unknown command kind."); +} + +UIEditorWorkspaceLayoutOperationResult UIEditorWorkspaceController::RestoreLayoutSnapshot( + const UIEditorWorkspaceLayoutSnapshot& snapshot) { + UIEditorWorkspaceLayoutSnapshot canonicalSnapshot = snapshot; + canonicalSnapshot.workspace = + CanonicalizeUIEditorWorkspaceModel(std::move(canonicalSnapshot.workspace)); + + const UIEditorPanelRegistryValidationResult registryValidation = + ValidateUIEditorPanelRegistry(m_panelRegistry); + if (!registryValidation.IsValid()) { + return BuildLayoutOperationResult( + UIEditorWorkspaceLayoutOperationStatus::Rejected, + "Panel registry invalid: " + registryValidation.message); + } + + const UIEditorWorkspaceValidationResult workspaceValidation = + ValidateUIEditorWorkspace(canonicalSnapshot.workspace); + if (!workspaceValidation.IsValid()) { + return BuildLayoutOperationResult( + UIEditorWorkspaceLayoutOperationStatus::Rejected, + "Layout workspace invalid: " + workspaceValidation.message); + } + + const UIEditorWorkspaceSessionValidationResult sessionValidation = + ValidateUIEditorWorkspaceSession( + m_panelRegistry, + canonicalSnapshot.workspace, + canonicalSnapshot.session); + if (!sessionValidation.IsValid()) { + return BuildLayoutOperationResult( + UIEditorWorkspaceLayoutOperationStatus::Rejected, + "Layout session invalid: " + sessionValidation.message); + } + + if (AreUIEditorWorkspaceModelsEquivalent(m_workspace, canonicalSnapshot.workspace) && + AreUIEditorWorkspaceSessionsEquivalent(m_session, canonicalSnapshot.session)) { + return BuildLayoutOperationResult( + UIEditorWorkspaceLayoutOperationStatus::NoOp, + "Current state already matches the requested layout snapshot."); + } + + const UIEditorWorkspaceModel previousWorkspace = m_workspace; + const UIEditorWorkspaceSession previousSession = m_session; + m_workspace = canonicalSnapshot.workspace; + m_session = canonicalSnapshot.session; + + const UIEditorWorkspaceControllerValidationResult validation = ValidateState(); + if (!validation.IsValid()) { + m_workspace = previousWorkspace; + m_session = previousSession; + return BuildLayoutOperationResult( + UIEditorWorkspaceLayoutOperationStatus::Rejected, + "Restored layout produced invalid controller state: " + validation.message); + } + + return BuildLayoutOperationResult( + UIEditorWorkspaceLayoutOperationStatus::Changed, + "Layout restored."); +} + +UIEditorWorkspaceLayoutOperationResult UIEditorWorkspaceController::RestoreSerializedLayout( + std::string_view serializedLayout) { + const UIEditorWorkspaceLayoutLoadResult loadResult = + DeserializeUIEditorWorkspaceLayoutSnapshot(m_panelRegistry, serializedLayout); + if (!loadResult.IsValid()) { + return BuildLayoutOperationResult( + UIEditorWorkspaceLayoutOperationStatus::Rejected, + "Serialized layout rejected: " + loadResult.message); + } + + return RestoreLayoutSnapshot(loadResult.snapshot); +} + +UIEditorWorkspaceLayoutOperationResult UIEditorWorkspaceController::SetSplitRatio( + std::string_view nodeId, + float splitRatio) { + const UIEditorWorkspaceControllerValidationResult validation = ValidateState(); + if (!validation.IsValid()) { + return BuildLayoutOperationResult( + UIEditorWorkspaceLayoutOperationStatus::Rejected, + "Controller state invalid: " + validation.message); + } + + if (nodeId.empty()) { + return BuildLayoutOperationResult( + UIEditorWorkspaceLayoutOperationStatus::Rejected, + "SetSplitRatio requires a split node id."); + } + + const UIEditorWorkspaceNode* splitNode = FindUIEditorWorkspaceNode(m_workspace, nodeId); + if (splitNode == nullptr || splitNode->kind != UIEditorWorkspaceNodeKind::Split) { + return BuildLayoutOperationResult( + UIEditorWorkspaceLayoutOperationStatus::Rejected, + "SetSplitRatio target split node is missing."); + } + + if (std::fabs(splitNode->splitRatio - splitRatio) <= 0.0001f) { + return BuildLayoutOperationResult( + UIEditorWorkspaceLayoutOperationStatus::NoOp, + "Split ratio already matches the requested value."); + } + + const UIEditorWorkspaceModel previousWorkspace = m_workspace; + if (!TrySetUIEditorWorkspaceSplitRatio(m_workspace, nodeId, splitRatio)) { + return BuildLayoutOperationResult( + UIEditorWorkspaceLayoutOperationStatus::Rejected, + "Split ratio update rejected."); + } + + const UIEditorWorkspaceControllerValidationResult postValidation = ValidateState(); + if (!postValidation.IsValid()) { + m_workspace = previousWorkspace; + return BuildLayoutOperationResult( + UIEditorWorkspaceLayoutOperationStatus::Rejected, + "Split ratio update produced invalid controller state: " + postValidation.message); + } + + return BuildLayoutOperationResult( + UIEditorWorkspaceLayoutOperationStatus::Changed, + "Split ratio updated."); +} + +UIEditorWorkspaceLayoutOperationResult UIEditorWorkspaceController::MoveTabToStack( + std::string_view sourceNodeId, + std::string_view panelId, + std::string_view targetNodeId, + std::size_t targetVisibleInsertionIndex) { + { + std::ostringstream trace = {}; + trace << "MoveTabToStack begin sourceNode=" << sourceNodeId + << " panel=" << panelId + << " targetNode=" << targetNodeId + << " insertion=" << targetVisibleInsertionIndex; + AppendUIEditorRuntimeTrace("workspace", trace.str()); + } + + const UIEditorWorkspaceControllerValidationResult validation = ValidateState(); + if (!validation.IsValid()) { + return BuildLayoutOperationResult( + UIEditorWorkspaceLayoutOperationStatus::Rejected, + "Controller state invalid: " + validation.message); + } + + if (sourceNodeId.empty() || targetNodeId.empty()) { + return BuildLayoutOperationResult( + UIEditorWorkspaceLayoutOperationStatus::Rejected, + "MoveTabToStack requires both source and target tab stack ids."); + } + + if (panelId.empty()) { + return BuildLayoutOperationResult( + UIEditorWorkspaceLayoutOperationStatus::Rejected, + "MoveTabToStack requires a panel id."); + } + + if (sourceNodeId == targetNodeId) { + return BuildLayoutOperationResult( + UIEditorWorkspaceLayoutOperationStatus::Rejected, + "MoveTabToStack requires distinct source and target tab stack ids."); + } + + const UIEditorWorkspaceNode* sourceTabStack = + FindUIEditorWorkspaceNode(m_workspace, sourceNodeId); + const UIEditorWorkspaceNode* targetTabStack = + FindUIEditorWorkspaceNode(m_workspace, targetNodeId); + if (sourceTabStack == nullptr || + targetTabStack == nullptr || + sourceTabStack->kind != UIEditorWorkspaceNodeKind::TabStack || + targetTabStack->kind != UIEditorWorkspaceNodeKind::TabStack) { + return BuildLayoutOperationResult( + UIEditorWorkspaceLayoutOperationStatus::Rejected, + "MoveTabToStack source or target tab stack is missing."); + } + + const Internal::VisibleTabStackInfo sourceInfo = + Internal::ResolveVisibleTabStackInfo(*sourceTabStack, m_session, panelId); + if (!sourceInfo.panelExists) { + return BuildLayoutOperationResult( + UIEditorWorkspaceLayoutOperationStatus::Rejected, + "MoveTabToStack target panel is missing from the source tab stack."); + } + + if (!sourceInfo.panelVisible) { + return BuildLayoutOperationResult( + UIEditorWorkspaceLayoutOperationStatus::Rejected, + "MoveTabToStack only supports open and visible tabs."); + } + + const std::size_t visibleTargetCount = + Internal::CountVisibleTabs(*targetTabStack, m_session); + if (targetVisibleInsertionIndex > visibleTargetCount) { + return BuildLayoutOperationResult( + UIEditorWorkspaceLayoutOperationStatus::Rejected, + "MoveTabToStack target visible insertion index is out of range."); + } + + const UIEditorWorkspaceModel previousWorkspace = m_workspace; + if (!TryMoveUIEditorWorkspaceTabToStack( + m_workspace, + m_session, + sourceNodeId, + panelId, + targetNodeId, + targetVisibleInsertionIndex)) { + return BuildLayoutOperationResult( + UIEditorWorkspaceLayoutOperationStatus::Rejected, + "MoveTabToStack rejected."); + } + + if (AreUIEditorWorkspaceModelsEquivalent(previousWorkspace, m_workspace)) { + return BuildLayoutOperationResult( + UIEditorWorkspaceLayoutOperationStatus::NoOp, + "Tab already matches the requested target stack insertion."); + } + + const UIEditorWorkspaceControllerValidationResult postValidation = ValidateState(); + if (!postValidation.IsValid()) { + m_workspace = previousWorkspace; + return BuildLayoutOperationResult( + UIEditorWorkspaceLayoutOperationStatus::Rejected, + "MoveTabToStack produced invalid controller state: " + postValidation.message); + } + + return BuildLayoutOperationResult( + UIEditorWorkspaceLayoutOperationStatus::Changed, + "Tab moved to target stack."); +} + +UIEditorWorkspaceLayoutOperationResult UIEditorWorkspaceController::DockTabRelative( + std::string_view sourceNodeId, + std::string_view panelId, + std::string_view targetNodeId, + UIEditorWorkspaceDockPlacement placement, + float splitRatio) { + { + std::ostringstream trace = {}; + trace << "DockTabRelative begin sourceNode=" << sourceNodeId + << " panel=" << panelId + << " targetNode=" << targetNodeId + << " placement=" << static_cast(placement) + << " splitRatio=" << splitRatio; + AppendUIEditorRuntimeTrace("workspace", trace.str()); + } + + const UIEditorWorkspaceControllerValidationResult validation = ValidateState(); + if (!validation.IsValid()) { + return BuildLayoutOperationResult( + UIEditorWorkspaceLayoutOperationStatus::Rejected, + "Controller state invalid: " + validation.message); + } + + if (sourceNodeId.empty() || targetNodeId.empty()) { + return BuildLayoutOperationResult( + UIEditorWorkspaceLayoutOperationStatus::Rejected, + "DockTabRelative requires both source and target tab stack ids."); + } + + if (panelId.empty()) { + return BuildLayoutOperationResult( + UIEditorWorkspaceLayoutOperationStatus::Rejected, + "DockTabRelative requires a panel id."); + } + + const UIEditorWorkspaceNode* sourceTabStack = + FindUIEditorWorkspaceNode(m_workspace, sourceNodeId); + const UIEditorWorkspaceNode* targetTabStack = + FindUIEditorWorkspaceNode(m_workspace, targetNodeId); + if (sourceTabStack == nullptr || + targetTabStack == nullptr || + sourceTabStack->kind != UIEditorWorkspaceNodeKind::TabStack || + targetTabStack->kind != UIEditorWorkspaceNodeKind::TabStack) { + return BuildLayoutOperationResult( + UIEditorWorkspaceLayoutOperationStatus::Rejected, + "DockTabRelative source or target tab stack is missing."); + } + + const Internal::VisibleTabStackInfo sourceInfo = + Internal::ResolveVisibleTabStackInfo(*sourceTabStack, m_session, panelId); + if (!sourceInfo.panelExists) { + return BuildLayoutOperationResult( + UIEditorWorkspaceLayoutOperationStatus::Rejected, + "DockTabRelative target panel is missing from the source tab stack."); + } + + if (!sourceInfo.panelVisible) { + return BuildLayoutOperationResult( + UIEditorWorkspaceLayoutOperationStatus::Rejected, + "DockTabRelative only supports open and visible tabs."); + } + + const UIEditorWorkspaceModel previousWorkspace = m_workspace; + if (!TryDockUIEditorWorkspaceTabRelative( + m_workspace, + m_session, + sourceNodeId, + panelId, + targetNodeId, + placement, + splitRatio)) { + return BuildLayoutOperationResult( + UIEditorWorkspaceLayoutOperationStatus::Rejected, + "DockTabRelative rejected."); + } + + if (AreUIEditorWorkspaceModelsEquivalent(previousWorkspace, m_workspace)) { + return BuildLayoutOperationResult( + UIEditorWorkspaceLayoutOperationStatus::NoOp, + "Dock layout already matches the requested placement."); + } + + const UIEditorWorkspaceControllerValidationResult postValidation = ValidateState(); + if (!postValidation.IsValid()) { + m_workspace = previousWorkspace; + return BuildLayoutOperationResult( + UIEditorWorkspaceLayoutOperationStatus::Rejected, + "DockTabRelative produced invalid controller state: " + postValidation.message); + } + + return BuildLayoutOperationResult( + UIEditorWorkspaceLayoutOperationStatus::Changed, + "Tab docked relative to target stack."); +} + } // namespace XCEngine::UI::Editor diff --git a/new_editor/src/Workspace/UIEditorWorkspaceModel.cpp b/new_editor/src/Workspace/UIEditorWorkspaceModel.cpp index 994ff0e8..088e675f 100644 --- a/new_editor/src/Workspace/UIEditorWorkspaceModel.cpp +++ b/new_editor/src/Workspace/UIEditorWorkspaceModel.cpp @@ -663,4 +663,353 @@ UIEditorWorkspaceModel CanonicalizeUIEditorWorkspaceModel( return workspace; } +std::vector CollectUIEditorWorkspaceVisiblePanels( + const UIEditorWorkspaceModel& workspace) { + std::vector visiblePanels = {}; + Internal::CollectVisiblePanelsRecursive( + workspace.root, + workspace.activePanelId, + visiblePanels); + return visiblePanels; +} + +bool ContainsUIEditorWorkspacePanel( + const UIEditorWorkspaceModel& workspace, + std::string_view panelId) { + return Internal::FindPanelRecursive(workspace.root, panelId) != nullptr; +} + +const UIEditorWorkspaceNode* FindUIEditorWorkspaceNode( + const UIEditorWorkspaceModel& workspace, + std::string_view nodeId) { + return Internal::FindNodeRecursive(workspace.root, nodeId); +} + +const UIEditorWorkspacePanelState* FindUIEditorWorkspaceActivePanel( + const UIEditorWorkspaceModel& workspace) { + if (workspace.activePanelId.empty()) { + return nullptr; + } + + const std::vector visiblePanels = + CollectUIEditorWorkspaceVisiblePanels(workspace); + for (const UIEditorWorkspaceVisiblePanel& panel : visiblePanels) { + if (panel.panelId == workspace.activePanelId) { + return Internal::FindPanelRecursive(workspace.root, workspace.activePanelId); + } + } + + return nullptr; +} + +UIEditorWorkspaceValidationResult ValidateUIEditorWorkspace( + const UIEditorWorkspaceModel& workspace) { + std::unordered_set nodeIds = {}; + std::unordered_set panelIds = {}; + UIEditorWorkspaceValidationResult result = + Internal::ValidateNodeRecursive(workspace.root, nodeIds, panelIds); + if (!result.IsValid()) { + return result; + } + + if (!workspace.activePanelId.empty()) { + const UIEditorWorkspacePanelState* activePanel = + FindUIEditorWorkspaceActivePanel(workspace); + if (activePanel == nullptr) { + return Internal::MakeValidationError( + UIEditorWorkspaceValidationCode::InvalidActivePanelId, + "Active panel id '" + workspace.activePanelId + + "' is missing or hidden by the current tab selection."); + } + } + + return {}; +} + +bool TryActivateUIEditorWorkspacePanel( + UIEditorWorkspaceModel& workspace, + std::string_view panelId) { + if (!Internal::TryActivateRecursive(workspace.root, panelId)) { + return false; + } + + workspace.activePanelId = std::string(panelId); + return true; +} + +bool TrySetUIEditorWorkspaceSplitRatio( + UIEditorWorkspaceModel& workspace, + std::string_view nodeId, + float splitRatio) { + if (!Internal::IsValidSplitRatio(splitRatio)) { + return false; + } + + UIEditorWorkspaceNode* node = + Internal::FindMutableNodeRecursive(workspace.root, nodeId); + if (node == nullptr || node->kind != UIEditorWorkspaceNodeKind::Split) { + return false; + } + + if (std::fabs(node->splitRatio - splitRatio) <= 0.0001f) { + return false; + } + + node->splitRatio = splitRatio; + return true; +} + +bool TryExtractUIEditorWorkspaceVisiblePanelNode( + UIEditorWorkspaceModel& workspace, + const UIEditorWorkspaceSession& session, + std::string_view sourceNodeId, + std::string_view panelId, + UIEditorWorkspaceNode& extractedPanel) { + return Internal::TryExtractVisiblePanelFromTabStack( + workspace, + session, + sourceNodeId, + panelId, + extractedPanel); +} + +bool TryInsertUIEditorWorkspacePanelNodeToStack( + UIEditorWorkspaceModel& workspace, + const UIEditorWorkspaceSession& session, + UIEditorWorkspaceNode panelNode, + std::string_view targetNodeId, + std::size_t targetVisibleInsertionIndex) { + if (targetNodeId.empty() || + panelNode.kind != UIEditorWorkspaceNodeKind::Panel || + panelNode.panel.panelId.empty()) { + return false; + } + + const UIEditorWorkspaceNode* targetNode = + FindUIEditorWorkspaceNode(workspace, targetNodeId); + if (targetNode == nullptr || + targetNode->kind != UIEditorWorkspaceNodeKind::TabStack) { + return false; + } + + if (targetVisibleInsertionIndex > + Internal::CountVisibleChildren(*targetNode, session)) { + return false; + } + + UIEditorWorkspaceNode* targetStack = + Internal::FindMutableNodeRecursive(workspace.root, targetNodeId); + if (targetStack == nullptr || + targetStack->kind != UIEditorWorkspaceNodeKind::TabStack) { + return false; + } + + const std::string movedPanelId = panelNode.panel.panelId; + const std::size_t actualInsertionIndex = + Internal::ResolveActualInsertionIndexForVisibleInsertion( + *targetStack, + session, + targetVisibleInsertionIndex); + if (actualInsertionIndex > targetStack->children.size()) { + return false; + } + + targetStack->children.insert( + targetStack->children.begin() + + static_cast(actualInsertionIndex), + std::move(panelNode)); + targetStack->selectedTabIndex = actualInsertionIndex; + workspace.activePanelId = movedPanelId; + workspace = CanonicalizeUIEditorWorkspaceModel(std::move(workspace)); + return true; +} + +bool TryDockUIEditorWorkspacePanelNodeRelative( + UIEditorWorkspaceModel& workspace, + const UIEditorWorkspaceSession& session, + UIEditorWorkspaceNode panelNode, + std::string_view targetNodeId, + UIEditorWorkspaceDockPlacement placement, + float splitRatio) { + if (targetNodeId.empty() || + panelNode.kind != UIEditorWorkspaceNodeKind::Panel || + panelNode.panel.panelId.empty()) { + return false; + } + + const UIEditorWorkspaceNode* targetNode = + FindUIEditorWorkspaceNode(workspace, targetNodeId); + if (targetNode == nullptr || + targetNode->kind != UIEditorWorkspaceNodeKind::TabStack) { + return false; + } + + if (placement == UIEditorWorkspaceDockPlacement::Center) { + return TryInsertUIEditorWorkspacePanelNodeToStack( + workspace, + session, + std::move(panelNode), + targetNodeId, + Internal::CountVisibleChildren(*targetNode, session)); + } + + UIEditorWorkspaceNode* targetStack = + Internal::FindMutableNodeRecursive(workspace.root, targetNodeId); + if (targetStack == nullptr || + targetStack->kind != UIEditorWorkspaceNodeKind::TabStack) { + return false; + } + + const std::string movedPanelId = panelNode.panel.panelId; + const std::string movedStackNodeId = Internal::MakeUniqueNodeId( + workspace, + std::string(targetNodeId) + "__dock_" + movedPanelId + "_stack"); + UIEditorWorkspaceNode movedStack = {}; + movedStack.kind = UIEditorWorkspaceNodeKind::TabStack; + movedStack.nodeId = movedStackNodeId; + movedStack.selectedTabIndex = 0u; + movedStack.children.push_back(std::move(panelNode)); + + UIEditorWorkspaceNode existingTarget = std::move(*targetStack); + UIEditorWorkspaceNode primary = {}; + UIEditorWorkspaceNode secondary = {}; + if (Internal::IsLeadingDockPlacement(placement)) { + primary = std::move(movedStack); + secondary = std::move(existingTarget); + } else { + primary = std::move(existingTarget); + secondary = std::move(movedStack); + } + + const float requestedRatio = Internal::ClampDockSplitRatio(splitRatio); + const float resolvedSplitRatio = + Internal::IsLeadingDockPlacement(placement) + ? requestedRatio + : (1.0f - requestedRatio); + *targetStack = BuildUIEditorWorkspaceSplit( + Internal::MakeUniqueNodeId( + workspace, + std::string(targetNodeId) + "__dock_split"), + Internal::ResolveDockSplitAxis(placement), + resolvedSplitRatio, + std::move(primary), + std::move(secondary)); + workspace.activePanelId = movedPanelId; + workspace = CanonicalizeUIEditorWorkspaceModel(std::move(workspace)); + return true; +} + +bool TryMoveUIEditorWorkspaceTabToStack( + UIEditorWorkspaceModel& workspace, + const UIEditorWorkspaceSession& session, + std::string_view sourceNodeId, + std::string_view panelId, + std::string_view targetNodeId, + std::size_t targetVisibleInsertionIndex) { + if (sourceNodeId.empty() || + panelId.empty() || + targetNodeId.empty()) { + return false; + } + + if (sourceNodeId == targetNodeId) { + return false; + } + + const UIEditorWorkspaceNode* targetNode = + FindUIEditorWorkspaceNode(workspace, targetNodeId); + if (targetNode == nullptr || + targetNode->kind != UIEditorWorkspaceNodeKind::TabStack) { + return false; + } + + if (targetVisibleInsertionIndex > + Internal::CountVisibleChildren(*targetNode, session)) { + return false; + } + + UIEditorWorkspaceNode extractedPanel = {}; + if (!Internal::TryExtractVisiblePanelFromTabStack( + workspace, + session, + sourceNodeId, + panelId, + extractedPanel)) { + return false; + } + + return TryInsertUIEditorWorkspacePanelNodeToStack( + workspace, + session, + std::move(extractedPanel), + targetNodeId, + targetVisibleInsertionIndex); +} + +bool TryDockUIEditorWorkspaceTabRelative( + UIEditorWorkspaceModel& workspace, + const UIEditorWorkspaceSession& session, + std::string_view sourceNodeId, + std::string_view panelId, + std::string_view targetNodeId, + UIEditorWorkspaceDockPlacement placement, + float splitRatio) { + if (placement == UIEditorWorkspaceDockPlacement::Center) { + const UIEditorWorkspaceNode* targetNode = + FindUIEditorWorkspaceNode(workspace, targetNodeId); + if (targetNode == nullptr || + targetNode->kind != UIEditorWorkspaceNodeKind::TabStack) { + return false; + } + + return TryMoveUIEditorWorkspaceTabToStack( + workspace, + session, + sourceNodeId, + panelId, + targetNodeId, + Internal::CountVisibleChildren(*targetNode, session)); + } + + if (sourceNodeId.empty() || + panelId.empty() || + targetNodeId.empty()) { + return false; + } + + const UIEditorWorkspaceNode* sourceNode = + FindUIEditorWorkspaceNode(workspace, sourceNodeId); + const UIEditorWorkspaceNode* targetNode = + FindUIEditorWorkspaceNode(workspace, targetNodeId); + if (sourceNode == nullptr || + targetNode == nullptr || + sourceNode->kind != UIEditorWorkspaceNodeKind::TabStack || + targetNode->kind != UIEditorWorkspaceNodeKind::TabStack) { + return false; + } + + if (sourceNodeId == targetNodeId && + sourceNode->children.size() <= 1u) { + return false; + } + + UIEditorWorkspaceNode extractedPanel = {}; + if (!Internal::TryExtractVisiblePanelFromTabStack( + workspace, + session, + sourceNodeId, + panelId, + extractedPanel)) { + return false; + } + + return TryDockUIEditorWorkspacePanelNodeRelative( + workspace, + session, + std::move(extractedPanel), + targetNodeId, + placement, + splitRatio); +} + } // namespace XCEngine::UI::Editor diff --git a/new_editor/src/Workspace/WorkspaceControllerDispatch.cpp b/new_editor/src/Workspace/WorkspaceControllerDispatch.cpp deleted file mode 100644 index 12225142..00000000 --- a/new_editor/src/Workspace/WorkspaceControllerDispatch.cpp +++ /dev/null @@ -1,240 +0,0 @@ -#include -#include - -namespace XCEngine::UI::Editor { - -UIEditorWorkspaceCommandResult UIEditorWorkspaceController::Dispatch( - const UIEditorWorkspaceCommand& command) { - const UIEditorWorkspaceControllerValidationResult validation = ValidateState(); - if (command.kind != UIEditorWorkspaceCommandKind::ResetWorkspace && - !validation.IsValid()) { - return BuildResult( - command, - UIEditorWorkspaceCommandStatus::Rejected, - "Controller state invalid: " + validation.message); - } - - const UIEditorWorkspaceModel previousWorkspace = m_workspace; - const UIEditorWorkspaceSession previousSession = m_session; - const UIEditorPanelSessionState* panelState = - command.kind == UIEditorWorkspaceCommandKind::ResetWorkspace - ? nullptr - : FindUIEditorPanelSessionState(m_session, command.panelId); - const UIEditorPanelDescriptor* panelDescriptor = - command.kind == UIEditorWorkspaceCommandKind::ResetWorkspace - ? nullptr - : FindPanelDescriptor(command.panelId); - - switch (command.kind) { - case UIEditorWorkspaceCommandKind::OpenPanel: - if (command.panelId.empty()) { - return BuildResult( - command, - UIEditorWorkspaceCommandStatus::Rejected, - "OpenPanel requires a panelId."); - } - if (panelDescriptor == nullptr || panelState == nullptr) { - return BuildResult( - command, - UIEditorWorkspaceCommandStatus::Rejected, - "OpenPanel target panel is missing."); - } - if (panelState->open && panelState->visible) { - return BuildResult( - command, - UIEditorWorkspaceCommandStatus::NoOp, - "Panel is already open and visible."); - } - return FinalizeMutation( - command, - TryOpenUIEditorWorkspacePanel( - m_panelRegistry, - m_workspace, - m_session, - command.panelId), - "Panel opened and activated.", - "OpenPanel failed unexpectedly.", - previousWorkspace, - previousSession); - - case UIEditorWorkspaceCommandKind::ClosePanel: - if (command.panelId.empty()) { - return BuildResult( - command, - UIEditorWorkspaceCommandStatus::Rejected, - "ClosePanel requires a panelId."); - } - if (panelDescriptor == nullptr || panelState == nullptr) { - return BuildResult( - command, - UIEditorWorkspaceCommandStatus::Rejected, - "ClosePanel target panel is missing."); - } - if (!panelDescriptor->canClose) { - return BuildResult( - command, - UIEditorWorkspaceCommandStatus::Rejected, - "Panel cannot be closed."); - } - if (!panelState->open) { - return BuildResult( - command, - UIEditorWorkspaceCommandStatus::NoOp, - "Panel is already closed."); - } - return FinalizeMutation( - command, - TryCloseUIEditorWorkspacePanel( - m_panelRegistry, - m_workspace, - m_session, - command.panelId), - "Panel closed.", - "ClosePanel failed unexpectedly.", - previousWorkspace, - previousSession); - - case UIEditorWorkspaceCommandKind::ShowPanel: - if (command.panelId.empty()) { - return BuildResult( - command, - UIEditorWorkspaceCommandStatus::Rejected, - "ShowPanel requires a panelId."); - } - if (panelDescriptor == nullptr || panelState == nullptr) { - return BuildResult( - command, - UIEditorWorkspaceCommandStatus::Rejected, - "ShowPanel target panel is missing."); - } - if (!panelState->open) { - return BuildResult( - command, - UIEditorWorkspaceCommandStatus::Rejected, - "Closed panel must be opened before it can be shown."); - } - if (panelState->visible) { - return BuildResult( - command, - UIEditorWorkspaceCommandStatus::NoOp, - "Panel is already visible."); - } - return FinalizeMutation( - command, - TryShowUIEditorWorkspacePanel( - m_panelRegistry, - m_workspace, - m_session, - command.panelId), - "Panel shown and activated.", - "ShowPanel failed unexpectedly.", - previousWorkspace, - previousSession); - - case UIEditorWorkspaceCommandKind::HidePanel: - if (command.panelId.empty()) { - return BuildResult( - command, - UIEditorWorkspaceCommandStatus::Rejected, - "HidePanel requires a panelId."); - } - if (panelDescriptor == nullptr || panelState == nullptr) { - return BuildResult( - command, - UIEditorWorkspaceCommandStatus::Rejected, - "HidePanel target panel is missing."); - } - if (!panelDescriptor->canHide) { - return BuildResult( - command, - UIEditorWorkspaceCommandStatus::Rejected, - "Panel cannot be hidden."); - } - if (!panelState->open) { - return BuildResult( - command, - UIEditorWorkspaceCommandStatus::Rejected, - "Closed panel cannot be hidden."); - } - if (!panelState->visible) { - return BuildResult( - command, - UIEditorWorkspaceCommandStatus::NoOp, - "Panel is already hidden."); - } - return FinalizeMutation( - command, - TryHideUIEditorWorkspacePanel( - m_panelRegistry, - m_workspace, - m_session, - command.panelId), - "Panel hidden and active panel re-resolved.", - "HidePanel failed unexpectedly.", - previousWorkspace, - previousSession); - - case UIEditorWorkspaceCommandKind::ActivatePanel: - if (command.panelId.empty()) { - return BuildResult( - command, - UIEditorWorkspaceCommandStatus::Rejected, - "ActivatePanel requires a panelId."); - } - if (panelDescriptor == nullptr || panelState == nullptr) { - return BuildResult( - command, - UIEditorWorkspaceCommandStatus::Rejected, - "ActivatePanel target panel is missing."); - } - if (!panelState->open || !panelState->visible) { - return BuildResult( - command, - UIEditorWorkspaceCommandStatus::Rejected, - "Only open and visible panels can be activated."); - } - if (m_workspace.activePanelId == command.panelId) { - return BuildResult( - command, - UIEditorWorkspaceCommandStatus::NoOp, - "Panel is already active."); - } - return FinalizeMutation( - command, - TryActivateUIEditorWorkspacePanel( - m_panelRegistry, - m_workspace, - m_session, - command.panelId), - "Panel activated.", - "ActivatePanel failed unexpectedly.", - previousWorkspace, - previousSession); - - case UIEditorWorkspaceCommandKind::ResetWorkspace: - if (AreUIEditorWorkspaceModelsEquivalent(m_workspace, m_baselineWorkspace) && - AreUIEditorWorkspaceSessionsEquivalent(m_session, m_baselineSession)) { - return BuildResult( - command, - UIEditorWorkspaceCommandStatus::NoOp, - "Workspace already matches the baseline state."); - } - - m_workspace = m_baselineWorkspace; - m_session = m_baselineSession; - return FinalizeMutation( - command, - true, - "Workspace reset to baseline.", - "ResetWorkspace failed unexpectedly.", - previousWorkspace, - previousSession); - } - - return BuildResult( - command, - UIEditorWorkspaceCommandStatus::Rejected, - "Unknown command kind."); -} - -} // namespace XCEngine::UI::Editor diff --git a/new_editor/src/Workspace/WorkspaceControllerLayoutOps.cpp b/new_editor/src/Workspace/WorkspaceControllerLayoutOps.cpp deleted file mode 100644 index 8d2161b7..00000000 --- a/new_editor/src/Workspace/WorkspaceControllerLayoutOps.cpp +++ /dev/null @@ -1,333 +0,0 @@ -#include "Workspace/WorkspaceControllerInternal.h" - -#include - -#include -#include -#include - -namespace XCEngine::UI::Editor { - -UIEditorWorkspaceLayoutOperationResult UIEditorWorkspaceController::RestoreLayoutSnapshot( - const UIEditorWorkspaceLayoutSnapshot& snapshot) { - UIEditorWorkspaceLayoutSnapshot canonicalSnapshot = snapshot; - canonicalSnapshot.workspace = - CanonicalizeUIEditorWorkspaceModel(std::move(canonicalSnapshot.workspace)); - - const UIEditorPanelRegistryValidationResult registryValidation = - ValidateUIEditorPanelRegistry(m_panelRegistry); - if (!registryValidation.IsValid()) { - return BuildLayoutOperationResult( - UIEditorWorkspaceLayoutOperationStatus::Rejected, - "Panel registry invalid: " + registryValidation.message); - } - - const UIEditorWorkspaceValidationResult workspaceValidation = - ValidateUIEditorWorkspace(canonicalSnapshot.workspace); - if (!workspaceValidation.IsValid()) { - return BuildLayoutOperationResult( - UIEditorWorkspaceLayoutOperationStatus::Rejected, - "Layout workspace invalid: " + workspaceValidation.message); - } - - const UIEditorWorkspaceSessionValidationResult sessionValidation = - ValidateUIEditorWorkspaceSession( - m_panelRegistry, - canonicalSnapshot.workspace, - canonicalSnapshot.session); - if (!sessionValidation.IsValid()) { - return BuildLayoutOperationResult( - UIEditorWorkspaceLayoutOperationStatus::Rejected, - "Layout session invalid: " + sessionValidation.message); - } - - if (AreUIEditorWorkspaceModelsEquivalent(m_workspace, canonicalSnapshot.workspace) && - AreUIEditorWorkspaceSessionsEquivalent(m_session, canonicalSnapshot.session)) { - return BuildLayoutOperationResult( - UIEditorWorkspaceLayoutOperationStatus::NoOp, - "Current state already matches the requested layout snapshot."); - } - - const UIEditorWorkspaceModel previousWorkspace = m_workspace; - const UIEditorWorkspaceSession previousSession = m_session; - m_workspace = canonicalSnapshot.workspace; - m_session = canonicalSnapshot.session; - - const UIEditorWorkspaceControllerValidationResult validation = ValidateState(); - if (!validation.IsValid()) { - m_workspace = previousWorkspace; - m_session = previousSession; - return BuildLayoutOperationResult( - UIEditorWorkspaceLayoutOperationStatus::Rejected, - "Restored layout produced invalid controller state: " + validation.message); - } - - return BuildLayoutOperationResult( - UIEditorWorkspaceLayoutOperationStatus::Changed, - "Layout restored."); -} - -UIEditorWorkspaceLayoutOperationResult UIEditorWorkspaceController::RestoreSerializedLayout( - std::string_view serializedLayout) { - const UIEditorWorkspaceLayoutLoadResult loadResult = - DeserializeUIEditorWorkspaceLayoutSnapshot(m_panelRegistry, serializedLayout); - if (!loadResult.IsValid()) { - return BuildLayoutOperationResult( - UIEditorWorkspaceLayoutOperationStatus::Rejected, - "Serialized layout rejected: " + loadResult.message); - } - - return RestoreLayoutSnapshot(loadResult.snapshot); -} - -UIEditorWorkspaceLayoutOperationResult UIEditorWorkspaceController::SetSplitRatio( - std::string_view nodeId, - float splitRatio) { - const UIEditorWorkspaceControllerValidationResult validation = ValidateState(); - if (!validation.IsValid()) { - return BuildLayoutOperationResult( - UIEditorWorkspaceLayoutOperationStatus::Rejected, - "Controller state invalid: " + validation.message); - } - - if (nodeId.empty()) { - return BuildLayoutOperationResult( - UIEditorWorkspaceLayoutOperationStatus::Rejected, - "SetSplitRatio requires a split node id."); - } - - const UIEditorWorkspaceNode* splitNode = FindUIEditorWorkspaceNode(m_workspace, nodeId); - if (splitNode == nullptr || splitNode->kind != UIEditorWorkspaceNodeKind::Split) { - return BuildLayoutOperationResult( - UIEditorWorkspaceLayoutOperationStatus::Rejected, - "SetSplitRatio target split node is missing."); - } - - if (std::fabs(splitNode->splitRatio - splitRatio) <= 0.0001f) { - return BuildLayoutOperationResult( - UIEditorWorkspaceLayoutOperationStatus::NoOp, - "Split ratio already matches the requested value."); - } - - const UIEditorWorkspaceModel previousWorkspace = m_workspace; - if (!TrySetUIEditorWorkspaceSplitRatio(m_workspace, nodeId, splitRatio)) { - return BuildLayoutOperationResult( - UIEditorWorkspaceLayoutOperationStatus::Rejected, - "Split ratio update rejected."); - } - - const UIEditorWorkspaceControllerValidationResult postValidation = ValidateState(); - if (!postValidation.IsValid()) { - m_workspace = previousWorkspace; - return BuildLayoutOperationResult( - UIEditorWorkspaceLayoutOperationStatus::Rejected, - "Split ratio update produced invalid controller state: " + postValidation.message); - } - - return BuildLayoutOperationResult( - UIEditorWorkspaceLayoutOperationStatus::Changed, - "Split ratio updated."); -} - -UIEditorWorkspaceLayoutOperationResult UIEditorWorkspaceController::MoveTabToStack( - std::string_view sourceNodeId, - std::string_view panelId, - std::string_view targetNodeId, - std::size_t targetVisibleInsertionIndex) { - { - std::ostringstream trace = {}; - trace << "MoveTabToStack begin sourceNode=" << sourceNodeId - << " panel=" << panelId - << " targetNode=" << targetNodeId - << " insertion=" << targetVisibleInsertionIndex; - AppendUIEditorRuntimeTrace("workspace", trace.str()); - } - - const UIEditorWorkspaceControllerValidationResult validation = ValidateState(); - if (!validation.IsValid()) { - return BuildLayoutOperationResult( - UIEditorWorkspaceLayoutOperationStatus::Rejected, - "Controller state invalid: " + validation.message); - } - - if (sourceNodeId.empty() || targetNodeId.empty()) { - return BuildLayoutOperationResult( - UIEditorWorkspaceLayoutOperationStatus::Rejected, - "MoveTabToStack requires both source and target tab stack ids."); - } - - if (panelId.empty()) { - return BuildLayoutOperationResult( - UIEditorWorkspaceLayoutOperationStatus::Rejected, - "MoveTabToStack requires a panel id."); - } - - if (sourceNodeId == targetNodeId) { - return BuildLayoutOperationResult( - UIEditorWorkspaceLayoutOperationStatus::Rejected, - "MoveTabToStack requires distinct source and target tab stack ids."); - } - - const UIEditorWorkspaceNode* sourceTabStack = - FindUIEditorWorkspaceNode(m_workspace, sourceNodeId); - const UIEditorWorkspaceNode* targetTabStack = - FindUIEditorWorkspaceNode(m_workspace, targetNodeId); - if (sourceTabStack == nullptr || - targetTabStack == nullptr || - sourceTabStack->kind != UIEditorWorkspaceNodeKind::TabStack || - targetTabStack->kind != UIEditorWorkspaceNodeKind::TabStack) { - return BuildLayoutOperationResult( - UIEditorWorkspaceLayoutOperationStatus::Rejected, - "MoveTabToStack source or target tab stack is missing."); - } - - const Internal::VisibleTabStackInfo sourceInfo = - Internal::ResolveVisibleTabStackInfo(*sourceTabStack, m_session, panelId); - if (!sourceInfo.panelExists) { - return BuildLayoutOperationResult( - UIEditorWorkspaceLayoutOperationStatus::Rejected, - "MoveTabToStack target panel is missing from the source tab stack."); - } - - if (!sourceInfo.panelVisible) { - return BuildLayoutOperationResult( - UIEditorWorkspaceLayoutOperationStatus::Rejected, - "MoveTabToStack only supports open and visible tabs."); - } - - const std::size_t visibleTargetCount = - Internal::CountVisibleTabs(*targetTabStack, m_session); - if (targetVisibleInsertionIndex > visibleTargetCount) { - return BuildLayoutOperationResult( - UIEditorWorkspaceLayoutOperationStatus::Rejected, - "MoveTabToStack target visible insertion index is out of range."); - } - - const UIEditorWorkspaceModel previousWorkspace = m_workspace; - if (!TryMoveUIEditorWorkspaceTabToStack( - m_workspace, - m_session, - sourceNodeId, - panelId, - targetNodeId, - targetVisibleInsertionIndex)) { - return BuildLayoutOperationResult( - UIEditorWorkspaceLayoutOperationStatus::Rejected, - "MoveTabToStack rejected."); - } - - if (AreUIEditorWorkspaceModelsEquivalent(previousWorkspace, m_workspace)) { - return BuildLayoutOperationResult( - UIEditorWorkspaceLayoutOperationStatus::NoOp, - "Tab already matches the requested target stack insertion."); - } - - const UIEditorWorkspaceControllerValidationResult postValidation = ValidateState(); - if (!postValidation.IsValid()) { - m_workspace = previousWorkspace; - return BuildLayoutOperationResult( - UIEditorWorkspaceLayoutOperationStatus::Rejected, - "MoveTabToStack produced invalid controller state: " + postValidation.message); - } - - return BuildLayoutOperationResult( - UIEditorWorkspaceLayoutOperationStatus::Changed, - "Tab moved to target stack."); -} - -UIEditorWorkspaceLayoutOperationResult UIEditorWorkspaceController::DockTabRelative( - std::string_view sourceNodeId, - std::string_view panelId, - std::string_view targetNodeId, - UIEditorWorkspaceDockPlacement placement, - float splitRatio) { - { - std::ostringstream trace = {}; - trace << "DockTabRelative begin sourceNode=" << sourceNodeId - << " panel=" << panelId - << " targetNode=" << targetNodeId - << " placement=" << static_cast(placement) - << " splitRatio=" << splitRatio; - AppendUIEditorRuntimeTrace("workspace", trace.str()); - } - - const UIEditorWorkspaceControllerValidationResult validation = ValidateState(); - if (!validation.IsValid()) { - return BuildLayoutOperationResult( - UIEditorWorkspaceLayoutOperationStatus::Rejected, - "Controller state invalid: " + validation.message); - } - - if (sourceNodeId.empty() || targetNodeId.empty()) { - return BuildLayoutOperationResult( - UIEditorWorkspaceLayoutOperationStatus::Rejected, - "DockTabRelative requires both source and target tab stack ids."); - } - - if (panelId.empty()) { - return BuildLayoutOperationResult( - UIEditorWorkspaceLayoutOperationStatus::Rejected, - "DockTabRelative requires a panel id."); - } - - const UIEditorWorkspaceNode* sourceTabStack = - FindUIEditorWorkspaceNode(m_workspace, sourceNodeId); - const UIEditorWorkspaceNode* targetTabStack = - FindUIEditorWorkspaceNode(m_workspace, targetNodeId); - if (sourceTabStack == nullptr || - targetTabStack == nullptr || - sourceTabStack->kind != UIEditorWorkspaceNodeKind::TabStack || - targetTabStack->kind != UIEditorWorkspaceNodeKind::TabStack) { - return BuildLayoutOperationResult( - UIEditorWorkspaceLayoutOperationStatus::Rejected, - "DockTabRelative source or target tab stack is missing."); - } - - const Internal::VisibleTabStackInfo sourceInfo = - Internal::ResolveVisibleTabStackInfo(*sourceTabStack, m_session, panelId); - if (!sourceInfo.panelExists) { - return BuildLayoutOperationResult( - UIEditorWorkspaceLayoutOperationStatus::Rejected, - "DockTabRelative target panel is missing from the source tab stack."); - } - - if (!sourceInfo.panelVisible) { - return BuildLayoutOperationResult( - UIEditorWorkspaceLayoutOperationStatus::Rejected, - "DockTabRelative only supports open and visible tabs."); - } - - const UIEditorWorkspaceModel previousWorkspace = m_workspace; - if (!TryDockUIEditorWorkspaceTabRelative( - m_workspace, - m_session, - sourceNodeId, - panelId, - targetNodeId, - placement, - splitRatio)) { - return BuildLayoutOperationResult( - UIEditorWorkspaceLayoutOperationStatus::Rejected, - "DockTabRelative rejected."); - } - - if (AreUIEditorWorkspaceModelsEquivalent(previousWorkspace, m_workspace)) { - return BuildLayoutOperationResult( - UIEditorWorkspaceLayoutOperationStatus::NoOp, - "Dock layout already matches the requested placement."); - } - - const UIEditorWorkspaceControllerValidationResult postValidation = ValidateState(); - if (!postValidation.IsValid()) { - m_workspace = previousWorkspace; - return BuildLayoutOperationResult( - UIEditorWorkspaceLayoutOperationStatus::Rejected, - "DockTabRelative produced invalid controller state: " + postValidation.message); - } - - return BuildLayoutOperationResult( - UIEditorWorkspaceLayoutOperationStatus::Changed, - "Tab docked relative to target stack."); -} - -} // namespace XCEngine::UI::Editor diff --git a/new_editor/src/Workspace/WorkspaceModelMutation.cpp b/new_editor/src/Workspace/WorkspaceModelMutation.cpp deleted file mode 100644 index ab5cb676..00000000 --- a/new_editor/src/Workspace/WorkspaceModelMutation.cpp +++ /dev/null @@ -1,294 +0,0 @@ -#include "Workspace/WorkspaceModelInternal.h" -#include -#include -#include - -namespace XCEngine::UI::Editor { - -bool TryActivateUIEditorWorkspacePanel( - UIEditorWorkspaceModel& workspace, - std::string_view panelId) { - if (!Internal::TryActivateRecursive(workspace.root, panelId)) { - return false; - } - - workspace.activePanelId = std::string(panelId); - return true; -} - -bool TrySetUIEditorWorkspaceSplitRatio( - UIEditorWorkspaceModel& workspace, - std::string_view nodeId, - float splitRatio) { - if (!Internal::IsValidSplitRatio(splitRatio)) { - return false; - } - - UIEditorWorkspaceNode* node = - Internal::FindMutableNodeRecursive(workspace.root, nodeId); - if (node == nullptr || node->kind != UIEditorWorkspaceNodeKind::Split) { - return false; - } - - if (std::fabs(node->splitRatio - splitRatio) <= 0.0001f) { - return false; - } - - node->splitRatio = splitRatio; - return true; -} - -bool TryExtractUIEditorWorkspaceVisiblePanelNode( - UIEditorWorkspaceModel& workspace, - const UIEditorWorkspaceSession& session, - std::string_view sourceNodeId, - std::string_view panelId, - UIEditorWorkspaceNode& extractedPanel) { - return Internal::TryExtractVisiblePanelFromTabStack( - workspace, - session, - sourceNodeId, - panelId, - extractedPanel); -} - -bool TryInsertUIEditorWorkspacePanelNodeToStack( - UIEditorWorkspaceModel& workspace, - const UIEditorWorkspaceSession& session, - UIEditorWorkspaceNode panelNode, - std::string_view targetNodeId, - std::size_t targetVisibleInsertionIndex) { - if (targetNodeId.empty() || - panelNode.kind != UIEditorWorkspaceNodeKind::Panel || - panelNode.panel.panelId.empty()) { - return false; - } - - const UIEditorWorkspaceNode* targetNode = - FindUIEditorWorkspaceNode(workspace, targetNodeId); - if (targetNode == nullptr || - targetNode->kind != UIEditorWorkspaceNodeKind::TabStack) { - return false; - } - - if (targetVisibleInsertionIndex > - Internal::CountVisibleChildren(*targetNode, session)) { - return false; - } - - UIEditorWorkspaceNode* targetStack = - Internal::FindMutableNodeRecursive(workspace.root, targetNodeId); - if (targetStack == nullptr || - targetStack->kind != UIEditorWorkspaceNodeKind::TabStack) { - return false; - } - - const std::string movedPanelId = panelNode.panel.panelId; - const std::size_t actualInsertionIndex = - Internal::ResolveActualInsertionIndexForVisibleInsertion( - *targetStack, - session, - targetVisibleInsertionIndex); - if (actualInsertionIndex > targetStack->children.size()) { - return false; - } - - targetStack->children.insert( - targetStack->children.begin() + - static_cast(actualInsertionIndex), - std::move(panelNode)); - targetStack->selectedTabIndex = actualInsertionIndex; - workspace.activePanelId = movedPanelId; - workspace = CanonicalizeUIEditorWorkspaceModel(std::move(workspace)); - return true; -} - -bool TryDockUIEditorWorkspacePanelNodeRelative( - UIEditorWorkspaceModel& workspace, - const UIEditorWorkspaceSession& session, - UIEditorWorkspaceNode panelNode, - std::string_view targetNodeId, - UIEditorWorkspaceDockPlacement placement, - float splitRatio) { - if (targetNodeId.empty() || - panelNode.kind != UIEditorWorkspaceNodeKind::Panel || - panelNode.panel.panelId.empty()) { - return false; - } - - const UIEditorWorkspaceNode* targetNode = - FindUIEditorWorkspaceNode(workspace, targetNodeId); - if (targetNode == nullptr || - targetNode->kind != UIEditorWorkspaceNodeKind::TabStack) { - return false; - } - - if (placement == UIEditorWorkspaceDockPlacement::Center) { - return TryInsertUIEditorWorkspacePanelNodeToStack( - workspace, - session, - std::move(panelNode), - targetNodeId, - Internal::CountVisibleChildren(*targetNode, session)); - } - - UIEditorWorkspaceNode* targetStack = - Internal::FindMutableNodeRecursive(workspace.root, targetNodeId); - if (targetStack == nullptr || - targetStack->kind != UIEditorWorkspaceNodeKind::TabStack) { - return false; - } - - const std::string movedPanelId = panelNode.panel.panelId; - const std::string movedStackNodeId = Internal::MakeUniqueNodeId( - workspace, - std::string(targetNodeId) + "__dock_" + movedPanelId + "_stack"); - UIEditorWorkspaceNode movedStack = {}; - movedStack.kind = UIEditorWorkspaceNodeKind::TabStack; - movedStack.nodeId = movedStackNodeId; - movedStack.selectedTabIndex = 0u; - movedStack.children.push_back(std::move(panelNode)); - - UIEditorWorkspaceNode existingTarget = std::move(*targetStack); - UIEditorWorkspaceNode primary = {}; - UIEditorWorkspaceNode secondary = {}; - if (Internal::IsLeadingDockPlacement(placement)) { - primary = std::move(movedStack); - secondary = std::move(existingTarget); - } else { - primary = std::move(existingTarget); - secondary = std::move(movedStack); - } - - const float requestedRatio = Internal::ClampDockSplitRatio(splitRatio); - const float resolvedSplitRatio = - Internal::IsLeadingDockPlacement(placement) - ? requestedRatio - : (1.0f - requestedRatio); - *targetStack = BuildUIEditorWorkspaceSplit( - Internal::MakeUniqueNodeId( - workspace, - std::string(targetNodeId) + "__dock_split"), - Internal::ResolveDockSplitAxis(placement), - resolvedSplitRatio, - std::move(primary), - std::move(secondary)); - workspace.activePanelId = movedPanelId; - workspace = CanonicalizeUIEditorWorkspaceModel(std::move(workspace)); - return true; -} - -bool TryMoveUIEditorWorkspaceTabToStack( - UIEditorWorkspaceModel& workspace, - const UIEditorWorkspaceSession& session, - std::string_view sourceNodeId, - std::string_view panelId, - std::string_view targetNodeId, - std::size_t targetVisibleInsertionIndex) { - if (sourceNodeId.empty() || - panelId.empty() || - targetNodeId.empty()) { - return false; - } - - if (sourceNodeId == targetNodeId) { - return false; - } - - const UIEditorWorkspaceNode* targetNode = - FindUIEditorWorkspaceNode(workspace, targetNodeId); - if (targetNode == nullptr || - targetNode->kind != UIEditorWorkspaceNodeKind::TabStack) { - return false; - } - - if (targetVisibleInsertionIndex > - Internal::CountVisibleChildren(*targetNode, session)) { - return false; - } - - UIEditorWorkspaceNode extractedPanel = {}; - if (!Internal::TryExtractVisiblePanelFromTabStack( - workspace, - session, - sourceNodeId, - panelId, - extractedPanel)) { - return false; - } - - return TryInsertUIEditorWorkspacePanelNodeToStack( - workspace, - session, - std::move(extractedPanel), - targetNodeId, - targetVisibleInsertionIndex); -} - -bool TryDockUIEditorWorkspaceTabRelative( - UIEditorWorkspaceModel& workspace, - const UIEditorWorkspaceSession& session, - std::string_view sourceNodeId, - std::string_view panelId, - std::string_view targetNodeId, - UIEditorWorkspaceDockPlacement placement, - float splitRatio) { - if (placement == UIEditorWorkspaceDockPlacement::Center) { - const UIEditorWorkspaceNode* targetNode = - FindUIEditorWorkspaceNode(workspace, targetNodeId); - if (targetNode == nullptr || - targetNode->kind != UIEditorWorkspaceNodeKind::TabStack) { - return false; - } - - return TryMoveUIEditorWorkspaceTabToStack( - workspace, - session, - sourceNodeId, - panelId, - targetNodeId, - Internal::CountVisibleChildren(*targetNode, session)); - } - - if (sourceNodeId.empty() || - panelId.empty() || - targetNodeId.empty()) { - return false; - } - - const UIEditorWorkspaceNode* sourceNode = - FindUIEditorWorkspaceNode(workspace, sourceNodeId); - const UIEditorWorkspaceNode* targetNode = - FindUIEditorWorkspaceNode(workspace, targetNodeId); - if (sourceNode == nullptr || - targetNode == nullptr || - sourceNode->kind != UIEditorWorkspaceNodeKind::TabStack || - targetNode->kind != UIEditorWorkspaceNodeKind::TabStack) { - return false; - } - - if (sourceNodeId == targetNodeId && - sourceNode->children.size() <= 1u) { - return false; - } - - UIEditorWorkspaceNode extractedPanel = {}; - if (!Internal::TryExtractVisiblePanelFromTabStack( - workspace, - session, - sourceNodeId, - panelId, - extractedPanel)) { - return false; - } - - return TryDockUIEditorWorkspacePanelNodeRelative( - workspace, - session, - std::move(extractedPanel), - targetNodeId, - placement, - splitRatio); -} - -} // namespace XCEngine::UI::Editor diff --git a/new_editor/src/Workspace/WorkspaceModelQueries.cpp b/new_editor/src/Workspace/WorkspaceModelQueries.cpp deleted file mode 100644 index dae0c00c..00000000 --- a/new_editor/src/Workspace/WorkspaceModelQueries.cpp +++ /dev/null @@ -1,44 +0,0 @@ -#include "Workspace/WorkspaceModelInternal.h" - -namespace XCEngine::UI::Editor { - -std::vector CollectUIEditorWorkspaceVisiblePanels( - const UIEditorWorkspaceModel& workspace) { - std::vector visiblePanels = {}; - Internal::CollectVisiblePanelsRecursive( - workspace.root, - workspace.activePanelId, - visiblePanels); - return visiblePanels; -} - -bool ContainsUIEditorWorkspacePanel( - const UIEditorWorkspaceModel& workspace, - std::string_view panelId) { - return Internal::FindPanelRecursive(workspace.root, panelId) != nullptr; -} - -const UIEditorWorkspaceNode* FindUIEditorWorkspaceNode( - const UIEditorWorkspaceModel& workspace, - std::string_view nodeId) { - return Internal::FindNodeRecursive(workspace.root, nodeId); -} - -const UIEditorWorkspacePanelState* FindUIEditorWorkspaceActivePanel( - const UIEditorWorkspaceModel& workspace) { - if (workspace.activePanelId.empty()) { - return nullptr; - } - - const std::vector visiblePanels = - CollectUIEditorWorkspaceVisiblePanels(workspace); - for (const UIEditorWorkspaceVisiblePanel& panel : visiblePanels) { - if (panel.panelId == workspace.activePanelId) { - return Internal::FindPanelRecursive(workspace.root, workspace.activePanelId); - } - } - - return nullptr; -} - -} // namespace XCEngine::UI::Editor diff --git a/new_editor/src/Workspace/WorkspaceModelValidation.cpp b/new_editor/src/Workspace/WorkspaceModelValidation.cpp deleted file mode 100644 index 62e6d951..00000000 --- a/new_editor/src/Workspace/WorkspaceModelValidation.cpp +++ /dev/null @@ -1,29 +0,0 @@ -#include "Workspace/WorkspaceModelInternal.h" - -namespace XCEngine::UI::Editor { - -UIEditorWorkspaceValidationResult ValidateUIEditorWorkspace( - const UIEditorWorkspaceModel& workspace) { - std::unordered_set nodeIds = {}; - std::unordered_set panelIds = {}; - UIEditorWorkspaceValidationResult result = - Internal::ValidateNodeRecursive(workspace.root, nodeIds, panelIds); - if (!result.IsValid()) { - return result; - } - - if (!workspace.activePanelId.empty()) { - const UIEditorWorkspacePanelState* activePanel = - FindUIEditorWorkspaceActivePanel(workspace); - if (activePanel == nullptr) { - return Internal::MakeValidationError( - UIEditorWorkspaceValidationCode::InvalidActivePanelId, - "Active panel id '" + workspace.activePanelId + - "' is missing or hidden by the current tab selection."); - } - } - - return {}; -} - -} // namespace XCEngine::UI::Editor