chore: checkpoint current workspace changes
This commit is contained in:
@@ -147,6 +147,8 @@ if(XCENGINE_BUILD_XCUI_EDITOR_APP)
|
||||
add_executable(XCUIEditorApp WIN32
|
||||
app/main.cpp
|
||||
app/Application.cpp
|
||||
app/Icons/ProductBuiltInIcons.cpp
|
||||
app/Panels/ProductHierarchyPanel.cpp
|
||||
app/Panels/ProductProjectPanel.cpp
|
||||
app/Shell/ProductShellAsset.cpp
|
||||
)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
|
||||
namespace XCEngine::UI::Editor::Host {
|
||||
|
||||
@@ -85,6 +86,11 @@ bool NativeRenderer::Initialize(HWND hwnd) {
|
||||
}
|
||||
|
||||
void NativeRenderer::Shutdown() {
|
||||
while (!m_liveTextures.empty()) {
|
||||
auto it = m_liveTextures.begin();
|
||||
delete *it;
|
||||
m_liveTextures.erase(it);
|
||||
}
|
||||
m_textFormats.clear();
|
||||
m_solidBrush.Reset();
|
||||
m_renderTarget.Reset();
|
||||
@@ -149,6 +155,46 @@ const std::string& NativeRenderer::GetLastRenderError() const {
|
||||
return m_lastRenderError;
|
||||
}
|
||||
|
||||
bool NativeRenderer::LoadTextureFromFile(
|
||||
const std::filesystem::path& path,
|
||||
::XCEngine::UI::UITextureHandle& outTexture,
|
||||
std::string& outError) {
|
||||
outError.clear();
|
||||
ReleaseTexture(outTexture);
|
||||
|
||||
auto texture = std::make_unique<NativeTextureResource>();
|
||||
if (!DecodeTextureFile(path, *texture, outError)) {
|
||||
outTexture = {};
|
||||
return false;
|
||||
}
|
||||
|
||||
outTexture.nativeHandle = reinterpret_cast<std::uintptr_t>(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<NativeTextureResource*>(texture.nativeHandle);
|
||||
if (resource != nullptr) {
|
||||
const auto found = m_liveTextures.find(resource);
|
||||
if (found != m_liveTextures.end()) {
|
||||
m_liveTextures.erase(found);
|
||||
delete resource;
|
||||
}
|
||||
}
|
||||
|
||||
texture = {};
|
||||
}
|
||||
|
||||
float NativeRenderer::MeasureTextWidth(
|
||||
const ::XCEngine::UI::Editor::UIEditorTextMeasureRequest& request) const {
|
||||
if (!m_dwriteFactory || request.text.empty()) {
|
||||
@@ -380,6 +426,7 @@ bool NativeRenderer::EnsureWicFactory(std::string& outError) {
|
||||
}
|
||||
|
||||
void NativeRenderer::DiscardRenderTarget() {
|
||||
InvalidateCachedTextureBitmaps(m_renderTarget.Get());
|
||||
m_solidBrush.Reset();
|
||||
m_renderTarget.Reset();
|
||||
}
|
||||
@@ -427,6 +474,133 @@ bool NativeRenderer::CreateDeviceResources() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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<IWICBitmapDecoder> 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<IWICBitmapFrameDecode> frame;
|
||||
hr = decoder->GetFrame(0u, frame.ReleaseAndGetAddressOf());
|
||||
if (FAILED(hr) || !frame) {
|
||||
outError = HrToString("IWICBitmapDecoder::GetFrame", hr);
|
||||
return false;
|
||||
}
|
||||
|
||||
Microsoft::WRL::ComPtr<IWICFormatConverter> converter;
|
||||
hr = m_wicFactory->CreateFormatConverter(converter.ReleaseAndGetAddressOf());
|
||||
if (FAILED(hr) || !converter) {
|
||||
outError = HrToString("IWICImagingFactory::CreateFormatConverter", hr);
|
||||
return false;
|
||||
}
|
||||
|
||||
hr = converter->Initialize(
|
||||
frame.Get(),
|
||||
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<std::uint8_t> pixels(
|
||||
static_cast<std::size_t>(width) * static_cast<std::size_t>(height) * 4u);
|
||||
hr = converter->CopyPixels(
|
||||
nullptr,
|
||||
width * 4u,
|
||||
static_cast<UINT>(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;
|
||||
}
|
||||
|
||||
bool NativeRenderer::ResolveTextureBitmap(
|
||||
ID2D1RenderTarget& renderTarget,
|
||||
NativeTextureResource& texture,
|
||||
Microsoft::WRL::ComPtr<ID2D1Bitmap>& 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<ID2D1Bitmap> 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::RenderToTarget(
|
||||
ID2D1RenderTarget& renderTarget,
|
||||
ID2D1SolidColorBrush& solidBrush,
|
||||
@@ -624,8 +798,29 @@ void NativeRenderer::RenderCommand(
|
||||
break;
|
||||
}
|
||||
|
||||
auto* texture = reinterpret_cast<NativeTextureResource*>(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);
|
||||
break;
|
||||
}
|
||||
|
||||
Microsoft::WRL::ComPtr<ID2D1Bitmap> bitmap;
|
||||
if (!ResolveTextureBitmap(renderTarget, *texture, bitmap) || !bitmap) {
|
||||
break;
|
||||
}
|
||||
|
||||
const D2D1_RECT_F rect = ToD2DRect(command.rect, dpiScale);
|
||||
renderTarget.DrawRectangle(rect, &solidBrush, 1.0f);
|
||||
const float sourceLeft = static_cast<float>(texture->width) * std::clamp(command.uvMin.x, 0.0f, 1.0f);
|
||||
const float sourceTop = static_cast<float>(texture->height) * std::clamp(command.uvMin.y, 0.0f, 1.0f);
|
||||
const float sourceRight = static_cast<float>(texture->width) * std::clamp(command.uvMax.x, 0.0f, 1.0f);
|
||||
const float sourceBottom = static_cast<float>(texture->height) * 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));
|
||||
break;
|
||||
}
|
||||
case ::XCEngine::UI::UIDrawCommandType::PushClipRect: {
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_set>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
@@ -31,6 +32,11 @@ public:
|
||||
void Resize(UINT width, UINT height);
|
||||
bool Render(const ::XCEngine::UI::UIDrawData& drawData);
|
||||
const std::string& GetLastRenderError() const;
|
||||
bool LoadTextureFromFile(
|
||||
const std::filesystem::path& path,
|
||||
::XCEngine::UI::UITextureHandle& outTexture,
|
||||
std::string& outError);
|
||||
void ReleaseTexture(::XCEngine::UI::UITextureHandle& texture);
|
||||
float MeasureTextWidth(
|
||||
const ::XCEngine::UI::Editor::UIEditorTextMeasureRequest& request) const override;
|
||||
bool CaptureToPng(
|
||||
@@ -45,10 +51,26 @@ private:
|
||||
bool EnsureWicFactory(std::string& outError);
|
||||
void DiscardRenderTarget();
|
||||
bool CreateDeviceResources();
|
||||
void InvalidateCachedTextureBitmaps(const ID2D1RenderTarget* renderTarget);
|
||||
bool RenderToTarget(
|
||||
ID2D1RenderTarget& renderTarget,
|
||||
ID2D1SolidColorBrush& solidBrush,
|
||||
const ::XCEngine::UI::UIDrawData& drawData);
|
||||
struct NativeTextureResource {
|
||||
std::vector<std::uint8_t> pixels = {};
|
||||
Microsoft::WRL::ComPtr<ID2D1Bitmap> cachedBitmap = {};
|
||||
const ID2D1RenderTarget* cachedTarget = nullptr;
|
||||
UINT width = 0u;
|
||||
UINT height = 0u;
|
||||
};
|
||||
bool DecodeTextureFile(
|
||||
const std::filesystem::path& path,
|
||||
NativeTextureResource& outTexture,
|
||||
std::string& outError);
|
||||
bool ResolveTextureBitmap(
|
||||
ID2D1RenderTarget& renderTarget,
|
||||
NativeTextureResource& texture,
|
||||
Microsoft::WRL::ComPtr<ID2D1Bitmap>& outBitmap);
|
||||
void RenderCommand(
|
||||
ID2D1RenderTarget& renderTarget,
|
||||
ID2D1SolidColorBrush& solidBrush,
|
||||
@@ -66,6 +88,7 @@ private:
|
||||
Microsoft::WRL::ComPtr<ID2D1HwndRenderTarget> m_renderTarget;
|
||||
Microsoft::WRL::ComPtr<ID2D1SolidColorBrush> m_solidBrush;
|
||||
mutable std::unordered_map<int, Microsoft::WRL::ComPtr<IDWriteTextFormat>> m_textFormats;
|
||||
std::unordered_set<NativeTextureResource*> m_liveTextures;
|
||||
std::string m_lastRenderError = {};
|
||||
bool m_wicComInitialized = false;
|
||||
float m_dpiScale = 1.0f;
|
||||
|
||||
@@ -376,12 +376,13 @@ int Application::Run(HINSTANCE hInstance, int nCmdShow) {
|
||||
bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) {
|
||||
m_hInstance = hInstance;
|
||||
EnableDpiAwareness();
|
||||
const std::filesystem::path repoRoot = ResolveRepoRootPath();
|
||||
const std::filesystem::path logRoot =
|
||||
GetExecutableDirectory() / "logs";
|
||||
InitializeUIEditorRuntimeTrace(logRoot);
|
||||
SetUnhandledExceptionFilter(&Application::HandleUnhandledException);
|
||||
LogRuntimeTrace("app", "initialize begin");
|
||||
m_shellAsset = BuildProductShellAsset(ResolveRepoRootPath());
|
||||
m_shellAsset = BuildProductShellAsset(repoRoot);
|
||||
m_shellValidation = ValidateEditorShellAsset(m_shellAsset);
|
||||
m_validationMessage = m_shellValidation.message;
|
||||
if (!m_shellValidation.IsValid()) {
|
||||
@@ -399,7 +400,6 @@ bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) {
|
||||
m_shellServices.commandDispatcher = &m_shortcutManager.GetCommandDispatcher();
|
||||
m_shellServices.shortcutManager = &m_shortcutManager;
|
||||
m_shellServices.textMeasurer = &m_renderer;
|
||||
m_projectPanel.Initialize(ResolveRepoRootPath());
|
||||
m_lastStatus = "Ready";
|
||||
m_lastMessage = "Old editor shell baseline loaded.";
|
||||
LogRuntimeTrace("app", "workspace initialized: " + DescribeWorkspaceState());
|
||||
@@ -446,7 +446,15 @@ bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) {
|
||||
LogRuntimeTrace("app", "renderer initialization failed");
|
||||
return false;
|
||||
}
|
||||
m_builtInIcons.Initialize(m_renderer, repoRoot);
|
||||
if (!m_builtInIcons.GetLastError().empty()) {
|
||||
LogRuntimeTrace("icons", m_builtInIcons.GetLastError());
|
||||
}
|
||||
m_hierarchyPanel.SetBuiltInIcons(&m_builtInIcons);
|
||||
m_projectPanel.SetBuiltInIcons(&m_builtInIcons);
|
||||
m_hierarchyPanel.Initialize();
|
||||
m_projectPanel.SetTextMeasurer(&m_renderer);
|
||||
m_projectPanel.Initialize(repoRoot);
|
||||
|
||||
ShowWindow(m_hwnd, nCmdShow);
|
||||
UpdateWindow(m_hwnd);
|
||||
@@ -468,6 +476,7 @@ void Application::Shutdown() {
|
||||
}
|
||||
|
||||
m_autoScreenshot.Shutdown();
|
||||
m_builtInIcons.Shutdown();
|
||||
m_renderer.Shutdown();
|
||||
|
||||
if (m_hwnd != nullptr && IsWindow(m_hwnd)) {
|
||||
@@ -549,6 +558,11 @@ void Application::RenderFrame() {
|
||||
}
|
||||
ApplyHostCaptureRequests(m_shellFrame.result);
|
||||
UpdateLastStatus(m_shellFrame.result);
|
||||
m_hierarchyPanel.Update(
|
||||
m_shellFrame.workspaceInteractionFrame.composeFrame.contentHostFrame,
|
||||
hostedContentEvents,
|
||||
!m_shellFrame.result.workspaceInputSuppressed,
|
||||
m_workspaceController.GetWorkspace().activePanelId == "hierarchy");
|
||||
m_projectPanel.Update(
|
||||
m_shellFrame.workspaceInteractionFrame.composeFrame.contentHostFrame,
|
||||
hostedContentEvents,
|
||||
@@ -565,6 +579,7 @@ void Application::RenderFrame() {
|
||||
m_shellInteractionState.composeState,
|
||||
palette.shellPalette,
|
||||
metrics.shellMetrics);
|
||||
m_hierarchyPanel.Append(drawList);
|
||||
m_projectPanel.Append(drawList);
|
||||
AppendShellPopups(drawList, m_shellFrame, palette, metrics);
|
||||
} else {
|
||||
@@ -599,6 +614,22 @@ float Application::PixelsToDips(float pixels) const {
|
||||
return dpiScale > 0.0f ? pixels / dpiScale : pixels;
|
||||
}
|
||||
|
||||
bool Application::IsPointerInsideClientArea() const {
|
||||
if (m_hwnd == nullptr || !IsWindow(m_hwnd)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
POINT screenPoint = {};
|
||||
if (!GetCursorPos(&screenPoint)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const LPARAM pointParam = MAKELPARAM(
|
||||
static_cast<SHORT>(screenPoint.x),
|
||||
static_cast<SHORT>(screenPoint.y));
|
||||
return SendMessageW(m_hwnd, WM_NCHITTEST, 0, pointParam) == HTCLIENT;
|
||||
}
|
||||
|
||||
LPCWSTR Application::ResolveCurrentCursorResource() const {
|
||||
switch (m_projectPanel.GetCursorKind()) {
|
||||
case App::ProductProjectPanel::CursorKind::ResizeEW:
|
||||
@@ -621,6 +652,10 @@ LPCWSTR Application::ResolveCurrentCursorResource() const {
|
||||
}
|
||||
|
||||
bool Application::ApplyCurrentCursor() const {
|
||||
if (!HasInteractiveCaptureState() && !IsPointerInsideClientArea()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const HCURSOR cursor = LoadCursorW(nullptr, ResolveCurrentCursorResource());
|
||||
if (cursor == nullptr) {
|
||||
return false;
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
#include <Host/InputModifierTracker.h>
|
||||
#include <Host/NativeRenderer.h>
|
||||
|
||||
#include "Icons/ProductBuiltInIcons.h"
|
||||
#include "Panels/ProductHierarchyPanel.h"
|
||||
#include "Panels/ProductProjectPanel.h"
|
||||
|
||||
#include <XCEditor/Foundation/UIEditorShortcutManager.h>
|
||||
@@ -45,6 +47,7 @@ private:
|
||||
void RenderFrame();
|
||||
void OnResize(UINT width, UINT height);
|
||||
void OnDpiChanged(UINT dpi, const RECT& suggestedRect);
|
||||
bool IsPointerInsideClientArea() const;
|
||||
bool ApplyCurrentCursor() const;
|
||||
LPCWSTR ResolveCurrentCursorResource() const;
|
||||
float GetDpiScale() const;
|
||||
@@ -83,6 +86,8 @@ private:
|
||||
EditorShellAssetValidationResult m_shellValidation = {};
|
||||
UIEditorWorkspaceController m_workspaceController = {};
|
||||
UIEditorShortcutManager m_shortcutManager = {};
|
||||
App::ProductBuiltInIcons m_builtInIcons = {};
|
||||
App::ProductHierarchyPanel m_hierarchyPanel = {};
|
||||
App::ProductProjectPanel m_projectPanel = {};
|
||||
UIEditorShellInteractionServices m_shellServices = {};
|
||||
UIEditorShellInteractionState m_shellInteractionState = {};
|
||||
|
||||
94
new_editor/app/Icons/ProductBuiltInIcons.cpp
Normal file
94
new_editor/app/Icons/ProductBuiltInIcons.cpp
Normal file
@@ -0,0 +1,94 @@
|
||||
#include "ProductBuiltInIcons.h"
|
||||
|
||||
#include <sstream>
|
||||
|
||||
namespace XCEngine::UI::Editor::App {
|
||||
|
||||
namespace {
|
||||
|
||||
void AppendLoadError(
|
||||
std::ostringstream& stream,
|
||||
std::string_view label,
|
||||
const std::string& error) {
|
||||
if (error.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (stream.tellp() > 0) {
|
||||
stream << '\n';
|
||||
}
|
||||
stream << label << ": " << error;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void ProductBuiltInIcons::Initialize(
|
||||
Host::NativeRenderer& renderer,
|
||||
const std::filesystem::path& repoRoot) {
|
||||
Shutdown();
|
||||
|
||||
m_renderer = &renderer;
|
||||
m_repoRoot = repoRoot.lexically_normal();
|
||||
|
||||
std::ostringstream errorStream = {};
|
||||
std::string error = {};
|
||||
if (!m_renderer->LoadTextureFromFile(ResolveFolderIconPath(), m_folderIcon, error)) {
|
||||
AppendLoadError(errorStream, "folder_icon.png", error);
|
||||
}
|
||||
|
||||
error.clear();
|
||||
if (!m_renderer->LoadTextureFromFile(ResolveGameObjectIconPath(), m_gameObjectIcon, error)) {
|
||||
AppendLoadError(errorStream, "gameobject_icon.png", error);
|
||||
}
|
||||
|
||||
error.clear();
|
||||
if (!m_renderer->LoadTextureFromFile(ResolveSceneIconPath(), m_sceneIcon, error)) {
|
||||
AppendLoadError(errorStream, "scene_icon.png", error);
|
||||
}
|
||||
|
||||
m_lastError = errorStream.str();
|
||||
}
|
||||
|
||||
void ProductBuiltInIcons::Shutdown() {
|
||||
if (m_renderer != nullptr) {
|
||||
m_renderer->ReleaseTexture(m_folderIcon);
|
||||
m_renderer->ReleaseTexture(m_gameObjectIcon);
|
||||
m_renderer->ReleaseTexture(m_sceneIcon);
|
||||
}
|
||||
|
||||
m_renderer = nullptr;
|
||||
m_repoRoot.clear();
|
||||
m_lastError.clear();
|
||||
}
|
||||
|
||||
const ::XCEngine::UI::UITextureHandle& ProductBuiltInIcons::Resolve(
|
||||
ProductBuiltInIconKind kind) const {
|
||||
switch (kind) {
|
||||
case ProductBuiltInIconKind::Folder:
|
||||
return m_folderIcon;
|
||||
case ProductBuiltInIconKind::GameObject:
|
||||
return m_gameObjectIcon;
|
||||
case ProductBuiltInIconKind::Scene:
|
||||
return m_sceneIcon;
|
||||
default:
|
||||
return m_folderIcon;
|
||||
}
|
||||
}
|
||||
|
||||
const std::string& ProductBuiltInIcons::GetLastError() const {
|
||||
return m_lastError;
|
||||
}
|
||||
|
||||
std::filesystem::path ProductBuiltInIcons::ResolveFolderIconPath() const {
|
||||
return (m_repoRoot / "editor/resources/Icons/folder_icon.png").lexically_normal();
|
||||
}
|
||||
|
||||
std::filesystem::path ProductBuiltInIcons::ResolveGameObjectIconPath() const {
|
||||
return (m_repoRoot / "editor/resources/Icons/gameobject_icon.png").lexically_normal();
|
||||
}
|
||||
|
||||
std::filesystem::path ProductBuiltInIcons::ResolveSceneIconPath() const {
|
||||
return (m_repoRoot / "editor/resources/Icons/scene_icon.png").lexically_normal();
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor::App
|
||||
42
new_editor/app/Icons/ProductBuiltInIcons.h
Normal file
42
new_editor/app/Icons/ProductBuiltInIcons.h
Normal file
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#include <Host/NativeRenderer.h>
|
||||
|
||||
#include <XCEngine/UI/Types.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
namespace XCEngine::UI::Editor::App {
|
||||
|
||||
enum class ProductBuiltInIconKind : std::uint8_t {
|
||||
Folder = 0,
|
||||
GameObject,
|
||||
Scene
|
||||
};
|
||||
|
||||
class ProductBuiltInIcons {
|
||||
public:
|
||||
void Initialize(
|
||||
Host::NativeRenderer& renderer,
|
||||
const std::filesystem::path& repoRoot);
|
||||
void Shutdown();
|
||||
|
||||
const ::XCEngine::UI::UITextureHandle& Resolve(ProductBuiltInIconKind kind) const;
|
||||
const std::string& GetLastError() const;
|
||||
|
||||
private:
|
||||
std::filesystem::path ResolveFolderIconPath() const;
|
||||
std::filesystem::path ResolveGameObjectIconPath() const;
|
||||
std::filesystem::path ResolveSceneIconPath() const;
|
||||
|
||||
Host::NativeRenderer* m_renderer = nullptr;
|
||||
std::filesystem::path m_repoRoot = {};
|
||||
::XCEngine::UI::UITextureHandle m_folderIcon = {};
|
||||
::XCEngine::UI::UITextureHandle m_gameObjectIcon = {};
|
||||
::XCEngine::UI::UITextureHandle m_sceneIcon = {};
|
||||
std::string m_lastError = {};
|
||||
};
|
||||
|
||||
} // namespace XCEngine::UI::Editor::App
|
||||
189
new_editor/app/Panels/ProductHierarchyPanel.cpp
Normal file
189
new_editor/app/Panels/ProductHierarchyPanel.cpp
Normal file
@@ -0,0 +1,189 @@
|
||||
#include "ProductHierarchyPanel.h"
|
||||
|
||||
#include "Icons/ProductBuiltInIcons.h"
|
||||
#include "Panels/ProductTreeViewStyle.h"
|
||||
|
||||
#include <XCEditor/Collections/UIEditorTreeView.h>
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace XCEngine::UI::Editor::App {
|
||||
|
||||
namespace {
|
||||
|
||||
using ::XCEngine::UI::UIDrawList;
|
||||
using ::XCEngine::UI::UIInputEvent;
|
||||
using ::XCEngine::UI::UIInputEventType;
|
||||
using ::XCEngine::UI::UIPoint;
|
||||
using ::XCEngine::UI::UIRect;
|
||||
using Widgets::AppendUIEditorTreeViewBackground;
|
||||
using Widgets::AppendUIEditorTreeViewForeground;
|
||||
|
||||
constexpr std::string_view kHierarchyPanelId = "hierarchy";
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
std::vector<UIInputEvent> FilterHierarchyInputEvents(
|
||||
const UIRect& bounds,
|
||||
const std::vector<UIInputEvent>& inputEvents,
|
||||
bool allowInteraction,
|
||||
bool panelActive) {
|
||||
if (!allowInteraction) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<UIInputEvent> filteredEvents = {};
|
||||
filteredEvents.reserve(inputEvents.size());
|
||||
for (const UIInputEvent& event : inputEvents) {
|
||||
switch (event.type) {
|
||||
case UIInputEventType::PointerMove:
|
||||
case UIInputEventType::PointerButtonDown:
|
||||
case UIInputEventType::PointerButtonUp:
|
||||
case UIInputEventType::PointerWheel:
|
||||
if (ContainsPoint(bounds, event.position)) {
|
||||
filteredEvents.push_back(event);
|
||||
}
|
||||
break;
|
||||
case UIInputEventType::PointerLeave:
|
||||
filteredEvents.push_back(event);
|
||||
break;
|
||||
case UIInputEventType::FocusGained:
|
||||
case UIInputEventType::FocusLost:
|
||||
case UIInputEventType::KeyDown:
|
||||
case UIInputEventType::KeyUp:
|
||||
case UIInputEventType::Character:
|
||||
if (panelActive) {
|
||||
filteredEvents.push_back(event);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return filteredEvents;
|
||||
}
|
||||
|
||||
::XCEngine::UI::UITextureHandle ResolveGameObjectIcon(
|
||||
const ProductBuiltInIcons* icons) {
|
||||
return icons != nullptr
|
||||
? icons->Resolve(ProductBuiltInIconKind::GameObject)
|
||||
: ::XCEngine::UI::UITextureHandle {};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void ProductHierarchyPanel::Initialize() {
|
||||
RebuildItems();
|
||||
}
|
||||
|
||||
void ProductHierarchyPanel::SetBuiltInIcons(const ProductBuiltInIcons* icons) {
|
||||
m_icons = icons;
|
||||
RebuildItems();
|
||||
}
|
||||
|
||||
const UIEditorPanelContentHostPanelState* ProductHierarchyPanel::FindMountedHierarchyPanel(
|
||||
const UIEditorPanelContentHostFrame& contentHostFrame) const {
|
||||
for (const UIEditorPanelContentHostPanelState& panelState : contentHostFrame.panelStates) {
|
||||
if (panelState.panelId == kHierarchyPanelId && panelState.mounted) {
|
||||
return &panelState;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void ProductHierarchyPanel::RebuildItems() {
|
||||
const auto icon = ResolveGameObjectIcon(m_icons);
|
||||
const std::string previousSelection =
|
||||
m_selection.HasSelection() ? m_selection.GetSelectedId() : std::string();
|
||||
|
||||
m_treeItems = {
|
||||
{ "main-camera", "Main Camera", 0u, true, 0.0f, icon },
|
||||
{ "directional-light", "Directional Light", 0u, true, 0.0f, icon },
|
||||
{ "player", "Player", 0u, false, 0.0f, icon },
|
||||
{ "player/camera-pivot", "Camera Pivot", 1u, true, 0.0f, icon },
|
||||
{ "player/mesh", "Mesh", 1u, true, 0.0f, icon },
|
||||
{ "environment", "Environment", 0u, false, 0.0f, icon },
|
||||
{ "environment/ground", "Ground", 1u, true, 0.0f, icon },
|
||||
{ "environment/props", "Props", 1u, false, 0.0f, icon },
|
||||
{ "environment/props/crate-01", "Crate_01", 2u, true, 0.0f, icon },
|
||||
{ "environment/props/barrel-01", "Barrel_01", 2u, true, 0.0f, icon }
|
||||
};
|
||||
|
||||
m_expansion.Expand("player");
|
||||
m_expansion.Expand("environment");
|
||||
m_expansion.Expand("environment/props");
|
||||
|
||||
if (!previousSelection.empty()) {
|
||||
for (const Widgets::UIEditorTreeViewItem& item : m_treeItems) {
|
||||
if (item.itemId == previousSelection) {
|
||||
m_selection.SetSelection(previousSelection);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!m_treeItems.empty()) {
|
||||
m_selection.SetSelection(m_treeItems.front().itemId);
|
||||
}
|
||||
}
|
||||
|
||||
void ProductHierarchyPanel::Update(
|
||||
const UIEditorPanelContentHostFrame& contentHostFrame,
|
||||
const std::vector<UIInputEvent>& inputEvents,
|
||||
bool allowInteraction,
|
||||
bool panelActive) {
|
||||
const UIEditorPanelContentHostPanelState* panelState =
|
||||
FindMountedHierarchyPanel(contentHostFrame);
|
||||
if (panelState == nullptr) {
|
||||
m_visible = false;
|
||||
m_treeFrame = {};
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_treeItems.empty()) {
|
||||
RebuildItems();
|
||||
}
|
||||
|
||||
m_visible = true;
|
||||
m_treeFrame = UpdateUIEditorTreeViewInteraction(
|
||||
m_treeInteractionState,
|
||||
m_selection,
|
||||
m_expansion,
|
||||
panelState->bounds,
|
||||
m_treeItems,
|
||||
FilterHierarchyInputEvents(panelState->bounds, inputEvents, allowInteraction, panelActive),
|
||||
BuildProductTreeViewMetrics());
|
||||
}
|
||||
|
||||
void ProductHierarchyPanel::Append(UIDrawList& drawList) const {
|
||||
if (!m_visible || m_treeFrame.layout.bounds.width <= 0.0f || m_treeFrame.layout.bounds.height <= 0.0f) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Widgets::UIEditorTreeViewPalette palette = BuildProductTreeViewPalette();
|
||||
const Widgets::UIEditorTreeViewMetrics metrics = BuildProductTreeViewMetrics();
|
||||
AppendUIEditorTreeViewBackground(
|
||||
drawList,
|
||||
m_treeFrame.layout,
|
||||
m_treeItems,
|
||||
m_selection,
|
||||
m_treeInteractionState.treeViewState,
|
||||
palette,
|
||||
metrics);
|
||||
AppendUIEditorTreeViewForeground(
|
||||
drawList,
|
||||
m_treeFrame.layout,
|
||||
m_treeItems,
|
||||
palette,
|
||||
metrics);
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor::App
|
||||
41
new_editor/app/Panels/ProductHierarchyPanel.h
Normal file
41
new_editor/app/Panels/ProductHierarchyPanel.h
Normal file
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEditor/Collections/UIEditorTreeViewInteraction.h>
|
||||
#include <XCEditor/Shell/UIEditorPanelContentHost.h>
|
||||
|
||||
#include <XCEngine/UI/DrawData.h>
|
||||
#include <XCEngine/UI/Widgets/UIExpansionModel.h>
|
||||
#include <XCEngine/UI/Widgets/UISelectionModel.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine::UI::Editor::App {
|
||||
|
||||
class ProductBuiltInIcons;
|
||||
|
||||
class ProductHierarchyPanel {
|
||||
public:
|
||||
void Initialize();
|
||||
void SetBuiltInIcons(const ProductBuiltInIcons* icons);
|
||||
void Update(
|
||||
const UIEditorPanelContentHostFrame& contentHostFrame,
|
||||
const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents,
|
||||
bool allowInteraction,
|
||||
bool panelActive);
|
||||
void Append(::XCEngine::UI::UIDrawList& drawList) const;
|
||||
|
||||
private:
|
||||
const UIEditorPanelContentHostPanelState* FindMountedHierarchyPanel(
|
||||
const UIEditorPanelContentHostFrame& contentHostFrame) const;
|
||||
void RebuildItems();
|
||||
|
||||
const ProductBuiltInIcons* m_icons = nullptr;
|
||||
std::vector<Widgets::UIEditorTreeViewItem> m_treeItems = {};
|
||||
::XCEngine::UI::Widgets::UISelectionModel m_selection = {};
|
||||
::XCEngine::UI::Widgets::UIExpansionModel m_expansion = {};
|
||||
UIEditorTreeViewInteractionState m_treeInteractionState = {};
|
||||
UIEditorTreeViewInteractionFrame m_treeFrame = {};
|
||||
bool m_visible = false;
|
||||
};
|
||||
|
||||
} // namespace XCEngine::UI::Editor::App
|
||||
@@ -1,5 +1,8 @@
|
||||
#include "ProductProjectPanel.h"
|
||||
|
||||
#include "Icons/ProductBuiltInIcons.h"
|
||||
#include "Panels/ProductTreeViewStyle.h"
|
||||
|
||||
#include <XCEditor/Collections/UIEditorTreeView.h>
|
||||
#include <XCEditor/Foundation/UIEditorTheme.h>
|
||||
|
||||
@@ -27,14 +30,12 @@ using ::XCEngine::UI::UIPoint;
|
||||
using ::XCEngine::UI::UIRect;
|
||||
using Widgets::AppendUIEditorTreeViewBackground;
|
||||
using Widgets::AppendUIEditorTreeViewForeground;
|
||||
using Widgets::UIEditorTreeViewMetrics;
|
||||
using Widgets::UIEditorTreeViewPalette;
|
||||
|
||||
constexpr std::string_view kProjectPanelId = "project";
|
||||
constexpr std::string_view kAssetsRootId = "Assets";
|
||||
constexpr std::size_t kInvalidLayoutIndex = static_cast<std::size_t>(-1);
|
||||
|
||||
constexpr float kBrowserHeaderHeight = 28.0f;
|
||||
constexpr float kBrowserHeaderHeight = 24.0f;
|
||||
constexpr float kNavigationMinWidth = 180.0f;
|
||||
constexpr float kBrowserMinWidth = 260.0f;
|
||||
constexpr float kHeaderHorizontalPadding = 10.0f;
|
||||
@@ -288,32 +289,10 @@ std::vector<UIInputEvent> FilterTreeInputEvents(
|
||||
return filteredEvents;
|
||||
}
|
||||
|
||||
UIEditorTreeViewMetrics BuildTreeMetrics() {
|
||||
UIEditorTreeViewMetrics metrics = {};
|
||||
metrics.rowHeight = 20.0f;
|
||||
metrics.rowGap = 0.0f;
|
||||
metrics.horizontalPadding = 6.0f;
|
||||
metrics.indentWidth = 14.0f;
|
||||
metrics.disclosureExtent = 10.0f;
|
||||
metrics.disclosureLabelGap = 4.0f;
|
||||
metrics.labelInsetY = 0.0f;
|
||||
metrics.cornerRounding = 0.0f;
|
||||
metrics.borderThickness = 0.0f;
|
||||
metrics.focusedBorderThickness = 0.0f;
|
||||
return metrics;
|
||||
}
|
||||
|
||||
UIEditorTreeViewPalette BuildTreePalette() {
|
||||
UIEditorTreeViewPalette palette = {};
|
||||
palette.surfaceColor = kPaneColor;
|
||||
palette.borderColor = kPaneColor;
|
||||
palette.focusedBorderColor = kPaneColor;
|
||||
palette.rowHoverColor = kTileHoverColor;
|
||||
palette.rowSelectedColor = kTileSelectedColor;
|
||||
palette.rowSelectedFocusedColor = kTileSelectedColor;
|
||||
palette.disclosureColor = kTextMuted;
|
||||
palette.textColor = kTextPrimary;
|
||||
return palette;
|
||||
::XCEngine::UI::UITextureHandle ResolveFolderIcon(const ProductBuiltInIcons* icons) {
|
||||
return icons != nullptr
|
||||
? icons->Resolve(ProductBuiltInIconKind::Folder)
|
||||
: ::XCEngine::UI::UITextureHandle {};
|
||||
}
|
||||
|
||||
float ClampNavigationWidth(float value, float totalWidth) {
|
||||
@@ -398,6 +377,14 @@ void ProductProjectPanel::Initialize(const std::filesystem::path& repoRoot) {
|
||||
RefreshAssetList();
|
||||
}
|
||||
|
||||
void ProductProjectPanel::SetBuiltInIcons(const ProductBuiltInIcons* icons) {
|
||||
m_icons = icons;
|
||||
if (!m_assetsRootPath.empty()) {
|
||||
RefreshFolderTree();
|
||||
SyncCurrentFolderSelection();
|
||||
}
|
||||
}
|
||||
|
||||
void ProductProjectPanel::SetTextMeasurer(const UIEditorTextMeasurer* textMeasurer) {
|
||||
m_textMeasurer = textMeasurer;
|
||||
}
|
||||
@@ -624,6 +611,7 @@ void ProductProjectPanel::RefreshFolderTree() {
|
||||
item.label = PathToUtf8String(folderPath.filename());
|
||||
item.depth = depth;
|
||||
item.forceLeaf = !HasChildDirectories(folderPath);
|
||||
item.leadingIcon = ResolveFolderIcon(m_icons);
|
||||
m_treeItems.push_back(std::move(item));
|
||||
|
||||
const std::vector<std::filesystem::path> childFolders =
|
||||
@@ -776,7 +764,7 @@ void ProductProjectPanel::Update(
|
||||
|
||||
m_navigationWidth = ClampNavigationWidth(m_navigationWidth, panelState->bounds.width);
|
||||
m_layout = BuildLayout(panelState->bounds);
|
||||
const UIEditorTreeViewMetrics treeMetrics = BuildTreeMetrics();
|
||||
const Widgets::UIEditorTreeViewMetrics treeMetrics = BuildProductTreeViewMetrics();
|
||||
const std::vector<UIInputEvent> treeEvents =
|
||||
FilterTreeInputEvents(filteredEvents, m_splitterDragging);
|
||||
m_treeFrame = UpdateUIEditorTreeViewInteraction(
|
||||
@@ -947,8 +935,8 @@ void ProductProjectPanel::Append(UIDrawList& drawList) const {
|
||||
kHeaderBottomBorderThickness),
|
||||
ResolveUIEditorDockHostPalette().splitterColor);
|
||||
|
||||
const UIEditorTreeViewPalette treePalette = BuildTreePalette();
|
||||
const UIEditorTreeViewMetrics treeMetrics = BuildTreeMetrics();
|
||||
const Widgets::UIEditorTreeViewPalette treePalette = BuildProductTreeViewPalette();
|
||||
const Widgets::UIEditorTreeViewMetrics treeMetrics = BuildProductTreeViewMetrics();
|
||||
AppendUIEditorTreeViewBackground(
|
||||
drawList,
|
||||
m_treeFrame.layout,
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
namespace XCEngine::UI::Editor::App {
|
||||
|
||||
class ProductBuiltInIcons;
|
||||
|
||||
class ProductProjectPanel {
|
||||
public:
|
||||
enum class CursorKind : std::uint8_t {
|
||||
@@ -24,6 +26,7 @@ public:
|
||||
};
|
||||
|
||||
void Initialize(const std::filesystem::path& repoRoot);
|
||||
void SetBuiltInIcons(const ProductBuiltInIcons* icons);
|
||||
void SetTextMeasurer(const ::XCEngine::UI::Editor::UIEditorTextMeasurer* textMeasurer);
|
||||
void Update(
|
||||
const UIEditorPanelContentHostFrame& contentHostFrame,
|
||||
@@ -97,6 +100,7 @@ private:
|
||||
std::vector<FolderEntry> m_folderEntries = {};
|
||||
std::vector<Widgets::UIEditorTreeViewItem> m_treeItems = {};
|
||||
std::vector<AssetEntry> m_assetEntries = {};
|
||||
const ProductBuiltInIcons* m_icons = nullptr;
|
||||
const ::XCEngine::UI::Editor::UIEditorTextMeasurer* m_textMeasurer = nullptr;
|
||||
::XCEngine::UI::Widgets::UISelectionModel m_folderSelection = {};
|
||||
::XCEngine::UI::Widgets::UIExpansionModel m_folderExpansion = {};
|
||||
|
||||
43
new_editor/app/Panels/ProductTreeViewStyle.h
Normal file
43
new_editor/app/Panels/ProductTreeViewStyle.h
Normal file
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEditor/Collections/UIEditorTreeView.h>
|
||||
|
||||
namespace XCEngine::UI::Editor::App {
|
||||
|
||||
inline constexpr ::XCEngine::UI::UIColor kProductTreeSurfaceColor(0.205f, 0.205f, 0.205f, 1.0f);
|
||||
inline constexpr ::XCEngine::UI::UIColor kProductTreeHoverColor(0.245f, 0.245f, 0.245f, 1.0f);
|
||||
inline constexpr ::XCEngine::UI::UIColor kProductTreeSelectedColor(0.300f, 0.300f, 0.300f, 1.0f);
|
||||
inline constexpr ::XCEngine::UI::UIColor kProductTreeDisclosureColor(0.560f, 0.560f, 0.560f, 1.0f);
|
||||
inline constexpr ::XCEngine::UI::UIColor kProductTreeTextColor(0.830f, 0.830f, 0.830f, 1.0f);
|
||||
|
||||
inline Widgets::UIEditorTreeViewMetrics BuildProductTreeViewMetrics() {
|
||||
Widgets::UIEditorTreeViewMetrics metrics = {};
|
||||
metrics.rowHeight = 20.0f;
|
||||
metrics.rowGap = 0.0f;
|
||||
metrics.horizontalPadding = 6.0f;
|
||||
metrics.indentWidth = 14.0f;
|
||||
metrics.disclosureExtent = 18.0f;
|
||||
metrics.disclosureLabelGap = 2.0f;
|
||||
metrics.iconExtent = 18.0f;
|
||||
metrics.iconLabelGap = 2.0f;
|
||||
metrics.labelInsetY = 0.0f;
|
||||
metrics.cornerRounding = 0.0f;
|
||||
metrics.borderThickness = 0.0f;
|
||||
metrics.focusedBorderThickness = 0.0f;
|
||||
return metrics;
|
||||
}
|
||||
|
||||
inline Widgets::UIEditorTreeViewPalette BuildProductTreeViewPalette() {
|
||||
Widgets::UIEditorTreeViewPalette palette = {};
|
||||
palette.surfaceColor = kProductTreeSurfaceColor;
|
||||
palette.borderColor = kProductTreeSurfaceColor;
|
||||
palette.focusedBorderColor = kProductTreeSurfaceColor;
|
||||
palette.rowHoverColor = kProductTreeHoverColor;
|
||||
palette.rowSelectedColor = kProductTreeSelectedColor;
|
||||
palette.rowSelectedFocusedColor = kProductTreeSelectedColor;
|
||||
palette.disclosureColor = kProductTreeDisclosureColor;
|
||||
palette.textColor = kProductTreeTextColor;
|
||||
return palette;
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor::App
|
||||
@@ -20,9 +20,9 @@ using Widgets::UIEditorStatusBarTextTone;
|
||||
UIEditorPanelRegistry BuildPanelRegistry() {
|
||||
UIEditorPanelRegistry registry = {};
|
||||
registry.panels = {
|
||||
{ "hierarchy", "Hierarchy", UIEditorPanelPresentationKind::Placeholder, true, false, false },
|
||||
{ "scene", "Scene", UIEditorPanelPresentationKind::ViewportShell, false, false, false },
|
||||
{ "game", "Game", UIEditorPanelPresentationKind::ViewportShell, false, false, false },
|
||||
{ "hierarchy", "Hierarchy", UIEditorPanelPresentationKind::HostedContent, true, false, false },
|
||||
{ "scene", "Scene", UIEditorPanelPresentationKind::HostedContent, false, false, false },
|
||||
{ "game", "Game", UIEditorPanelPresentationKind::HostedContent, false, false, false },
|
||||
{ "inspector", "Inspector", UIEditorPanelPresentationKind::Placeholder, true, false, false },
|
||||
{ "console", "Console", UIEditorPanelPresentationKind::Placeholder, true, false, false },
|
||||
{ "project", "Project", UIEditorPanelPresentationKind::HostedContent, false, false, false }
|
||||
@@ -413,27 +413,6 @@ UIEditorWorkspacePanelPresentationModel BuildPlaceholderPresentation(
|
||||
return presentation;
|
||||
}
|
||||
|
||||
UIEditorWorkspacePanelPresentationModel BuildViewportPresentation(
|
||||
std::string panelId,
|
||||
std::string_view title,
|
||||
std::string_view subtitle,
|
||||
std::string statusText) {
|
||||
UIEditorWorkspacePanelPresentationModel presentation = {};
|
||||
presentation.panelId = std::move(panelId);
|
||||
presentation.kind = UIEditorPanelPresentationKind::ViewportShell;
|
||||
presentation.viewportShellModel.spec.chrome.title = title;
|
||||
presentation.viewportShellModel.spec.chrome.subtitle = subtitle;
|
||||
presentation.viewportShellModel.spec.chrome.showTopBar = true;
|
||||
presentation.viewportShellModel.spec.chrome.showBottomBar = true;
|
||||
presentation.viewportShellModel.spec.statusSegments = {
|
||||
BuildStatusSegment("viewport-mode", std::string(subtitle), UIEditorStatusBarSlot::Leading, UIEditorStatusBarTextTone::Primary, 112.0f, true),
|
||||
BuildStatusSegment("viewport-status", std::move(statusText), UIEditorStatusBarSlot::Trailing, UIEditorStatusBarTextTone::Muted, 168.0f, false)
|
||||
};
|
||||
presentation.viewportShellModel.frame.statusText =
|
||||
presentation.viewportShellModel.spec.statusSegments.back().label;
|
||||
return presentation;
|
||||
}
|
||||
|
||||
UIEditorWorkspacePanelPresentationModel BuildHostedContentPresentation(
|
||||
std::string panelId) {
|
||||
UIEditorWorkspacePanelPresentationModel presentation = {};
|
||||
@@ -452,9 +431,9 @@ UIEditorShellInteractionDefinition BuildBaseShellDefinition() {
|
||||
};
|
||||
definition.statusSegments = {};
|
||||
definition.workspacePresentations = {
|
||||
BuildPlaceholderPresentation("hierarchy"),
|
||||
BuildViewportPresentation("scene", "Scene", "Perspective", "Viewport shell ready"),
|
||||
BuildViewportPresentation("game", "Game", "Display 1", "Game preview host ready"),
|
||||
BuildHostedContentPresentation("hierarchy"),
|
||||
BuildHostedContentPresentation("scene"),
|
||||
BuildHostedContentPresentation("game"),
|
||||
BuildPlaceholderPresentation("inspector"),
|
||||
BuildPlaceholderPresentation("console"),
|
||||
BuildHostedContentPresentation("project")
|
||||
|
||||
@@ -26,6 +26,7 @@ struct UIEditorTreeViewItem {
|
||||
std::uint32_t depth = 0u;
|
||||
bool forceLeaf = false;
|
||||
float desiredHeight = 0.0f;
|
||||
::XCEngine::UI::UITextureHandle leadingIcon = {};
|
||||
};
|
||||
|
||||
struct UIEditorTreeViewState {
|
||||
@@ -40,6 +41,8 @@ struct UIEditorTreeViewMetrics {
|
||||
float indentWidth = 18.0f;
|
||||
float disclosureExtent = 12.0f;
|
||||
float disclosureLabelGap = 6.0f;
|
||||
float iconExtent = 18.0f;
|
||||
float iconLabelGap = 2.0f;
|
||||
float labelInsetY = 6.0f;
|
||||
float cornerRounding = 6.0f;
|
||||
float borderThickness = 1.0f;
|
||||
@@ -70,6 +73,7 @@ struct UIEditorTreeViewLayout {
|
||||
std::vector<std::size_t> visibleItemIndices = {};
|
||||
std::vector<::XCEngine::UI::UIRect> rowRects = {};
|
||||
std::vector<::XCEngine::UI::UIRect> disclosureRects = {};
|
||||
std::vector<::XCEngine::UI::UIRect> iconRects = {};
|
||||
std::vector<::XCEngine::UI::UIRect> labelRects = {};
|
||||
std::vector<bool> itemHasChildren = {};
|
||||
std::vector<bool> itemExpanded = {};
|
||||
|
||||
@@ -35,12 +35,12 @@ struct UIEditorMenuPopupState {
|
||||
|
||||
struct UIEditorMenuPopupMetrics {
|
||||
float contentPaddingX = 2.0f;
|
||||
float contentPaddingY = 6.0f;
|
||||
float itemHeight = 28.0f;
|
||||
float separatorHeight = 9.0f;
|
||||
float contentPaddingY = 3.0f;
|
||||
float itemHeight = 24.0f;
|
||||
float separatorHeight = 7.0f;
|
||||
float checkColumnWidth = 12.0f;
|
||||
float shortcutGap = 14.0f;
|
||||
float submenuIndicatorWidth = 10.0f;
|
||||
float submenuIndicatorWidth = 12.0f;
|
||||
float rowCornerRounding = 2.5f;
|
||||
float popupCornerRounding = 4.0f;
|
||||
float labelInsetX = 4.0f;
|
||||
@@ -55,23 +55,23 @@ struct UIEditorMenuPopupMetrics {
|
||||
|
||||
struct UIEditorMenuPopupPalette {
|
||||
::XCEngine::UI::UIColor popupColor =
|
||||
::XCEngine::UI::UIColor(0.17f, 0.17f, 0.17f, 1.0f);
|
||||
::XCEngine::UI::UIColor(0.97f, 0.97f, 0.97f, 1.0f);
|
||||
::XCEngine::UI::UIColor borderColor =
|
||||
::XCEngine::UI::UIColor(0.31f, 0.31f, 0.31f, 1.0f);
|
||||
::XCEngine::UI::UIColor(0.62f, 0.62f, 0.62f, 1.0f);
|
||||
::XCEngine::UI::UIColor itemHoverColor =
|
||||
::XCEngine::UI::UIColor(0.28f, 0.28f, 0.28f, 1.0f);
|
||||
::XCEngine::UI::UIColor(0.86f, 0.86f, 0.86f, 1.0f);
|
||||
::XCEngine::UI::UIColor itemOpenColor =
|
||||
::XCEngine::UI::UIColor(0.34f, 0.34f, 0.34f, 1.0f);
|
||||
::XCEngine::UI::UIColor(0.82f, 0.82f, 0.82f, 1.0f);
|
||||
::XCEngine::UI::UIColor separatorColor =
|
||||
::XCEngine::UI::UIColor(0.32f, 0.32f, 0.32f, 1.0f);
|
||||
::XCEngine::UI::UIColor(0.78f, 0.78f, 0.78f, 1.0f);
|
||||
::XCEngine::UI::UIColor textPrimary =
|
||||
::XCEngine::UI::UIColor(0.94f, 0.94f, 0.94f, 1.0f);
|
||||
::XCEngine::UI::UIColor(0.10f, 0.10f, 0.10f, 1.0f);
|
||||
::XCEngine::UI::UIColor textMuted =
|
||||
::XCEngine::UI::UIColor(0.76f, 0.76f, 0.76f, 1.0f);
|
||||
::XCEngine::UI::UIColor(0.34f, 0.34f, 0.34f, 1.0f);
|
||||
::XCEngine::UI::UIColor textDisabled =
|
||||
::XCEngine::UI::UIColor(0.50f, 0.50f, 0.50f, 1.0f);
|
||||
::XCEngine::UI::UIColor(0.58f, 0.58f, 0.58f, 1.0f);
|
||||
::XCEngine::UI::UIColor glyphColor =
|
||||
::XCEngine::UI::UIColor(0.90f, 0.90f, 0.90f, 1.0f);
|
||||
::XCEngine::UI::UIColor(0.12f, 0.12f, 0.12f, 1.0f);
|
||||
};
|
||||
|
||||
struct UIEditorMenuPopupLayout {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include <XCEditor/Collections/UIEditorTabStrip.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
@@ -77,15 +78,8 @@ float ResolveTabTextLeft(
|
||||
const UIRect& rect,
|
||||
const UIEditorTabStripItem& item,
|
||||
const UIEditorTabStripMetrics& metrics) {
|
||||
const float padding =
|
||||
(std::max)(
|
||||
ClampNonNegative(metrics.layoutMetrics.tabHorizontalPadding),
|
||||
ClampNonNegative(metrics.labelInsetX));
|
||||
const float availableLeft = rect.x + padding;
|
||||
const float availableRight = rect.x + rect.width - padding;
|
||||
const float availableWidth = (std::max)(availableRight - availableLeft, 0.0f);
|
||||
const float labelWidth = ResolveEstimatedLabelWidth(item, metrics);
|
||||
return availableLeft + (std::max)(0.0f, (availableWidth - labelWidth) * 0.5f);
|
||||
return rect.x + std::floor((std::max)(0.0f, rect.width - labelWidth) * 0.5f);
|
||||
}
|
||||
|
||||
UIColor ResolveTabFillColor(
|
||||
|
||||
@@ -37,12 +37,12 @@ void AppendDisclosureArrow(
|
||||
const ::XCEngine::UI::UIRect& rect,
|
||||
bool expanded,
|
||||
const ::XCEngine::UI::UIColor& color) {
|
||||
constexpr float kOpticalCenterYOffset = -0.5f;
|
||||
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)(2.5f, std::floor((std::min)(rect.width, rect.height) * 0.24f));
|
||||
const float triangleHeight = halfExtent * 1.55f;
|
||||
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) {
|
||||
@@ -185,6 +185,7 @@ UIEditorTreeViewLayout BuildUIEditorTreeViewLayout(
|
||||
layout.visibleItemIndices = CollectUIEditorTreeViewVisibleItemIndices(items, expansionModel);
|
||||
layout.rowRects.reserve(layout.visibleItemIndices.size());
|
||||
layout.disclosureRects.reserve(layout.visibleItemIndices.size());
|
||||
layout.iconRects.reserve(layout.visibleItemIndices.size());
|
||||
layout.labelRects.reserve(layout.visibleItemIndices.size());
|
||||
layout.itemHasChildren.reserve(layout.visibleItemIndices.size());
|
||||
layout.itemExpanded.reserve(layout.visibleItemIndices.size());
|
||||
@@ -211,16 +212,29 @@ UIEditorTreeViewLayout BuildUIEditorTreeViewLayout(
|
||||
rowRect.y + (rowRect.height - metrics.disclosureExtent) * 0.5f,
|
||||
metrics.disclosureExtent,
|
||||
metrics.disclosureExtent);
|
||||
const bool hasLeadingIcon = item.leadingIcon.IsValid();
|
||||
const float iconExtent = ClampNonNegative(metrics.iconExtent);
|
||||
const float contentStartX = disclosureRect.x + metrics.disclosureExtent + metrics.disclosureLabelGap;
|
||||
const ::XCEngine::UI::UIRect iconRect(
|
||||
hasLeadingIcon ? contentStartX : 0.0f,
|
||||
rowRect.y + (rowRect.height - iconExtent) * 0.5f,
|
||||
hasLeadingIcon ? iconExtent : 0.0f,
|
||||
hasLeadingIcon ? iconExtent : 0.0f);
|
||||
const float labelStartX =
|
||||
hasLeadingIcon
|
||||
? iconRect.x + iconRect.width + metrics.iconLabelGap
|
||||
: contentStartX;
|
||||
const ::XCEngine::UI::UIRect labelRect(
|
||||
disclosureRect.x + metrics.disclosureExtent + metrics.disclosureLabelGap,
|
||||
labelStartX,
|
||||
rowRect.y,
|
||||
(rowRect.x + rowRect.width) -
|
||||
(disclosureRect.x + metrics.disclosureExtent + metrics.disclosureLabelGap) -
|
||||
labelStartX -
|
||||
metrics.horizontalPadding,
|
||||
rowRect.height);
|
||||
|
||||
layout.rowRects.push_back(rowRect);
|
||||
layout.disclosureRects.push_back(disclosureRect);
|
||||
layout.iconRects.push_back(iconRect);
|
||||
layout.labelRects.push_back(labelRect);
|
||||
layout.itemHasChildren.push_back(hasChildren);
|
||||
layout.itemExpanded.push_back(expanded);
|
||||
@@ -300,6 +314,9 @@ void AppendUIEditorTreeViewForeground(
|
||||
layout.itemExpanded[visibleOffset],
|
||||
palette.disclosureColor);
|
||||
}
|
||||
if (item.leadingIcon.IsValid()) {
|
||||
drawList.AddImage(layout.iconRects[visibleOffset], item.leadingIcon);
|
||||
}
|
||||
|
||||
drawList.PushClipRect(layout.labelRects[visibleOffset]);
|
||||
drawList.AddText(
|
||||
|
||||
@@ -265,9 +265,13 @@ void AppendUIEditorMenuPopupForeground(
|
||||
}
|
||||
|
||||
if (item.hasSubmenu) {
|
||||
const float submenuLeft =
|
||||
rect.x + rect.width -
|
||||
ClampNonNegative(metrics.shortcutInsetRight) -
|
||||
ClampNonNegative(metrics.submenuIndicatorWidth);
|
||||
drawList.AddText(
|
||||
UIPoint(
|
||||
rect.x + rect.width - ClampNonNegative(metrics.shortcutInsetRight),
|
||||
submenuLeft,
|
||||
ResolveGlyphTop(rect, metrics)),
|
||||
">",
|
||||
palette.glyphColor,
|
||||
|
||||
Reference in New Issue
Block a user