diff options
Diffstat (limited to 'app/source')
104 files changed, 12979 insertions, 0 deletions
diff --git a/app/source/Cplt/Entrypoint/Backend.hpp b/app/source/Cplt/Entrypoint/Backend.hpp new file mode 100644 index 0000000..ca391e6 --- /dev/null +++ b/app/source/Cplt/Entrypoint/Backend.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include <memory> + +class RenderingBackend +{ +public: + // Implemented in Backend_OpenGL2.cpp + static std::unique_ptr<RenderingBackend> CreateOpenGL2Backend(); + // Implemented in Backend_OpenGL3.cpp + static std::unique_ptr<RenderingBackend> CreateOpenGL3Backend(); + // Implemented in Backend_Vulkan.cpp + static std::unique_ptr<RenderingBackend> CreateVulkanBackend(); + // Implemented in Backend_DirectX11.cpp + static std::unique_ptr<RenderingBackend> CreateDx11Backend(); + // Implemented in Backend_DirectX12.cpp + static std::unique_ptr<RenderingBackend> CreateDx12Backend(); + // Implemented in Backend_Metal.cpp + static std::unique_ptr<RenderingBackend> CreateMetalBackend(); + + virtual ~RenderingBackend() = default; + virtual void RunUntilWindowClose(void (*windowContent)()) = 0; +}; diff --git a/app/source/Cplt/Entrypoint/Backend_DirectX11.cpp b/app/source/Cplt/Entrypoint/Backend_DirectX11.cpp new file mode 100644 index 0000000..4dc33f7 --- /dev/null +++ b/app/source/Cplt/Entrypoint/Backend_DirectX11.cpp @@ -0,0 +1,250 @@ +#include "Backend.hpp" + +#if BUILD_CORE_WITH_DX11_BACKEND +# include <backend/imgui_impl_dx11.h> +# include <backend/imgui_impl_dx11.cpp> +# include <stdexcept> +# include <d3d11.h> +# include <tchar.h> +# include <backend/imgui_impl_win32.h> + +// Forward declare message handler from imgui_impl_win32.cpp +extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); + +class DirectX11Backend : public RenderingBackend +{ +private: + HWND hWnd; + WNDCLASSEX wc; + + ID3D11Device* mD3dDevice = nullptr; + ID3D11DeviceContext* mD3dDeviceContext = nullptr; + IDXGISwapChain* mSwapChain = nullptr; + ID3D11RenderTargetView* mMainRenderTargetView = nullptr; + +public: + DirectX11Backend() + { + ImGui_ImplWin32_EnableDpiAwareness(); + + wc.cbSize = sizeof(WNDCLASSEX); + wc.style = CS_CLASSDC; + wc.lpfnWndProc = &StaticWndProc; + wc.cbClsExtra = 0L; + wc.cbWndExtra = 0L; + wc.hInstance = GetModuleHandle(nullptr); + wc.hIcon = nullptr; + wc.hCursor = nullptr; + wc.hbrBackground = nullptr; + wc.lpszMenuName = nullptr; + wc.lpszClassName = _T("Cplt"); + wc.hIconSm = nullptr; + ::RegisterClassEx(&wc); + + hWnd = ::CreateWindow( + wc.lpszClassName, + _T("Cplt main window"), + WS_OVERLAPPEDWINDOW, + /* x */ 100, + /* y */ 100, + /* window width */ 1280, + /* window height */ 800, + nullptr, + nullptr, + wc.hInstance, + this); + + if (!CreateDeviceD3D()) { + CleanupDeviceD3D(); + ::UnregisterClass(wc.lpszClassName, wc.hInstance); + throw std::runtime_error("Failed to create d3d device."); + } + + ::ShowWindow(hWnd, SW_SHOWDEFAULT); + ::UpdateWindow(hWnd); + + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + + ImGui_ImplWin32_Init(hWnd); + ImGui_ImplDX11_Init(mD3dDevice, mD3dDeviceContext); + } + + virtual ~DirectX11Backend() + { + ImGui_ImplDX11_Shutdown(); + ImGui_ImplWin32_Shutdown(); + ImGui::DestroyContext(); + + CleanupDeviceD3D(); + ::DestroyWindow(hWnd); + ::UnregisterClass(wc.lpszClassName, wc.hInstance); + } + + virtual void RunUntilWindowClose(void (*windowContent)()) + { + while (true) { + MSG msg; + bool done = false; + while (::PeekMessage(&msg, nullptr, 0U, 0U, PM_REMOVE)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + if (msg.message == WM_QUIT) { + done = true; + } + } + if (done) break; + + ImGui_ImplDX11_NewFrame(); + ImGui_ImplWin32_NewFrame(); + ImGui::NewFrame(); + + windowContent(); + + ImGui::Render(); + const ImVec4 kClearColor = ImVec4(0.45f, 0.55f, 0.60f, 1.00f); + const float kClearColorWithAlpha[4] = { kClearColor.x * kClearColor.w, kClearColor.y * kClearColor.w, kClearColor.z * kClearColor.w, kClearColor.w }; + mD3dDeviceContext->OMSetRenderTargets(1, &mMainRenderTargetView, nullptr); + mD3dDeviceContext->ClearRenderTargetView(mMainRenderTargetView, kClearColorWithAlpha); + ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData()); + + mSwapChain->Present(1, 0); // Present with vsync + } + } + +private: + bool CreateDeviceD3D() + { + // Setup swap chain + DXGI_SWAP_CHAIN_DESC sd; + ZeroMemory(&sd, sizeof(sd)); + sd.BufferCount = 2; + sd.BufferDesc.Width = 0; + sd.BufferDesc.Height = 0; + sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + sd.BufferDesc.RefreshRate.Numerator = 60; + sd.BufferDesc.RefreshRate.Denominator = 1; + sd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH; + sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; + sd.OutputWindow = hWnd; + sd.SampleDesc.Count = 1; + sd.SampleDesc.Quality = 0; + sd.Windowed = TRUE; + sd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD; + + UINT createDeviceFlags = 0; + //createDeviceFlags |= D3D11_CREATE_DEVICE_DEBUG; + D3D_FEATURE_LEVEL featureLevel; + const D3D_FEATURE_LEVEL featureLevelArray[2] = { + D3D_FEATURE_LEVEL_11_0, + D3D_FEATURE_LEVEL_10_0, + }; + if (D3D11CreateDeviceAndSwapChain(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, createDeviceFlags, featureLevelArray, 2, D3D11_SDK_VERSION, &sd, &mSwapChain, &mD3dDevice, &featureLevel, &mD3dDeviceContext) != S_OK) { + return false; + } + + CreateRenderTarget(); + return true; + } + + void CleanupDeviceD3D() + { + CleanupRenderTarget(); + if (mSwapChain) { + mSwapChain->Release(); + mSwapChain = nullptr; + } + if (mD3dDeviceContext) { + mD3dDeviceContext->Release(); + mD3dDeviceContext = nullptr; + } + if (mD3dDevice) { + mD3dDevice->Release(); + mD3dDevice = nullptr; + } + } + + void CreateRenderTarget() + { + ID3D11Texture2D* pBackBuffer; + mSwapChain->GetBuffer(0, IID_PPV_ARGS(&pBackBuffer)); + mD3dDevice->CreateRenderTargetView(pBackBuffer, nullptr, &mMainRenderTargetView); + pBackBuffer->Release(); + } + + void CleanupRenderTarget() + { + if (mMainRenderTargetView) { + mMainRenderTargetView->Release(); + mMainRenderTargetView = nullptr; + } + } + + static LRESULT CALLBACK StaticWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) + { + DirectX11Backend* self; + if (uMsg == WM_NCCREATE) { + auto lpcs = reinterpret_cast<LPCREATESTRUCT>(lParam); + self = static_cast<DirectX11Backend*>(lpcs->lpCreateParams); + self->hWnd = hWnd; + SetWindowLongPtr(hWnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(self)); + } else { + self = reinterpret_cast<DirectX11Backend*>(GetWindowLongPtr(hWnd, GWLP_USERDATA)); + } + + if (self) { + return self->WndProc(uMsg, wParam, lParam); + } else { + return DefWindowProc(hWnd, uMsg, wParam, lParam); + } + } + + LRESULT WndProc(UINT msg, WPARAM wParam, LPARAM lParam) + { + if (ImGui_ImplWin32_WndProcHandler(hWnd, msg, wParam, lParam)) { + return true; + } + + switch (msg) { + case WM_SIZE: { + if (mD3dDevice != nullptr && wParam != SIZE_MINIMIZED) { + CleanupRenderTarget(); + mSwapChain->ResizeBuffers(0, (UINT)LOWORD(lParam), (UINT)HIWORD(lParam), DXGI_FORMAT_UNKNOWN, 0); + CreateRenderTarget(); + } + return 0; + } + + case WM_SYSCOMMAND: { + // Disable ALT application menu + if ((wParam & 0xfff0) == SC_KEYMENU) { + return 0; + } + } break; + + case WM_DESTROY: { + ::PostQuitMessage(0); + return 0; + } + } + return ::DefWindowProc(hWnd, msg, wParam, lParam); + } +}; + +std::unique_ptr<RenderingBackend> RenderingBackend::CreateDx11Backend() +{ + try { + return std::make_unique<DirectX11Backend>(); + } catch (std::exception& e) { + return nullptr; + } +} + +#else // ^^ BUILD_CORE_WITH_DX11_BACKEND | BUILD_CORE_WITH_DX11_BACKEND vv + +std::unique_ptr<RenderingBackend> RenderingBackend::CreateDx11Backend() +{ + return nullptr; +} + +#endif diff --git a/app/source/Cplt/Entrypoint/Backend_DirectX12.cpp b/app/source/Cplt/Entrypoint/Backend_DirectX12.cpp new file mode 100644 index 0000000..fd4a531 --- /dev/null +++ b/app/source/Cplt/Entrypoint/Backend_DirectX12.cpp @@ -0,0 +1,470 @@ +#include "Backend.hpp" + +#if BUILD_CORE_WITH_DX12_BACKEND +# include <backend/imgui_impl_dx12.h> +# include <backend/imgui_impl_win32.h> +# include <d3d12.h> +# include <dxgi1_4.h> +# include <tchar.h> +# include <backend/imgui_impl_dx12.cpp> +# include <stdexcept> + +constexpr int kNumFramesInFlight = 3; +constexpr int kNumBackBuffers = 3; + +// Forward declare message handler from imgui_impl_win32.cpp +extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); + +class DirectX12Backend : public RenderingBackend +{ +private: + struct FrameContext + { + ID3D12CommandAllocator* CommandAllocator; + UINT64 FenceValue; + }; + + HWND hWnd; + WNDCLASSEX wc; + + FrameContext mFrameContext[kNumFramesInFlight] = {}; + UINT mFrameIndex = 0; + + ID3D12Device* mD3dDevice = nullptr; + ID3D12DescriptorHeap* mD3dRtvDescHeap = nullptr; + ID3D12DescriptorHeap* mD3dSrvDescHeap = nullptr; + ID3D12CommandQueue* mD3dCommandQueue = nullptr; + ID3D12GraphicsCommandList* mD3dCommandList = nullptr; + ID3D12Fence* mFence = nullptr; + HANDLE mFenceEvent = nullptr; + UINT64 mFenceLastSignaledValue = 0; + IDXGISwapChain3* mSwapChain = nullptr; + HANDLE mSwapChainWaitableObject = nullptr; + ID3D12Resource* mMainRenderTargetResource[kNumBackBuffers] = {}; + D3D12_CPU_DESCRIPTOR_HANDLE mMainRenderTargetDescriptor[kNumBackBuffers] = {}; + +public: + DirectX12Backend() + { + ImGui_ImplWin32_EnableDpiAwareness(); + + wc.cbSize = sizeof(WNDCLASSEX); + wc.style = CS_CLASSDC; + wc.lpfnWndProc = &StaticWndProc; + wc.cbClsExtra = 0L; + wc.cbWndExtra = 0L; + wc.hInstance = GetModuleHandle(nullptr); + wc.hIcon = nullptr; + wc.hCursor = nullptr; + wc.hbrBackground = nullptr; + wc.lpszMenuName = nullptr; + wc.lpszClassName = _T("Cplt"); + wc.hIconSm = nullptr; + ::RegisterClassEx(&wc); + + hWnd = ::CreateWindow( + wc.lpszClassName, + _T("Cplt main window"), + WS_OVERLAPPEDWINDOW, + /* x */ 100, + /* y */ 100, + /* window width */ 1280, + /* window height */ 800, + nullptr, + nullptr, + wc.hInstance, + this); + + if (!CreateDeviceD3D()) { + CleanupDeviceD3D(); + ::UnregisterClass(wc.lpszClassName, wc.hInstance); + throw std::runtime_error("Failed to create d3d device."); + } + + ::ShowWindow(hWnd, SW_SHOWDEFAULT); + ::UpdateWindow(hWnd); + + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + + ImGui_ImplWin32_Init(hWnd); + ImGui_ImplDX12_Init(mD3dDevice, kNumFramesInFlight, DXGI_FORMAT_R8G8B8A8_UNORM, mD3dSrvDescHeap, mD3dSrvDescHeap->GetCPUDescriptorHandleForHeapStart(), mD3dSrvDescHeap->GetGPUDescriptorHandleForHeapStart()); + } + + virtual ~DirectX12Backend() + { + WaitForLastSubmittedFrame(); + + // Cleanup + ImGui_ImplDX12_Shutdown(); + ImGui_ImplWin32_Shutdown(); + ImGui::DestroyContext(); + + CleanupDeviceD3D(); + ::DestroyWindow(hWnd); + ::UnregisterClass(wc.lpszClassName, wc.hInstance); + } + + virtual void RunUntilWindowClose(void (*windowContent)()) + { + while (true) { + MSG msg; + bool done = false; + while (::PeekMessage(&msg, nullptr, 0U, 0U, PM_REMOVE)) + { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + if (msg.message == WM_QUIT) { + done = true; + } + } + if (done) break; + + // Start the Dear ImGui frame + ImGui_ImplDX12_NewFrame(); + ImGui_ImplWin32_NewFrame(); + ImGui::NewFrame(); + + windowContent(); + + ImGui::Render(); + + FrameContext* frameCtx = WaitForNextFrameResources(); + UINT backBufferIdx = mSwapChain->GetCurrentBackBufferIndex(); + frameCtx->CommandAllocator->Reset(); + + D3D12_RESOURCE_BARRIER barrier = {}; + barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; + barrier.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE; + barrier.Transition.pResource = mMainRenderTargetResource[backBufferIdx]; + barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; + barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_PRESENT; + barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_RENDER_TARGET; + mD3dCommandList->Reset(frameCtx->CommandAllocator, nullptr); + mD3dCommandList->ResourceBarrier(1, &barrier); + + const ImVec4 kClearColor = ImVec4(0.45f, 0.55f, 0.60f, 1.00f); + const float kClearColorWithAlpha[4] = { kClearColor.x * kClearColor.w, kClearColor.y * kClearColor.w, kClearColor.z * kClearColor.w, kClearColor.w }; + mD3dCommandList->ClearRenderTargetView(mMainRenderTargetDescriptor[backBufferIdx], kClearColorWithAlpha, 0, nullptr); + mD3dCommandList->OMSetRenderTargets(1, &mMainRenderTargetDescriptor[backBufferIdx], FALSE, nullptr); + mD3dCommandList->SetDescriptorHeaps(1, &mD3dSrvDescHeap); + ImGui_ImplDX12_RenderDrawData(ImGui::GetDrawData(), mD3dCommandList); + barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_RENDER_TARGET; + barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_PRESENT; + mD3dCommandList->ResourceBarrier(1, &barrier); + mD3dCommandList->Close(); + + mD3dCommandQueue->ExecuteCommandLists(1, (ID3D12CommandList* const*)&mD3dCommandList); + + mSwapChain->Present(1, 0); // Present with vsync + + UINT64 fenceValue = mFenceLastSignaledValue + 1; + mD3dCommandQueue->Signal(mFence, fenceValue); + mFenceLastSignaledValue = fenceValue; + frameCtx->FenceValue = fenceValue; + } + } + +private: + bool CreateDeviceD3D() + { + // Setup swap chain + DXGI_SWAP_CHAIN_DESC1 sd; + { + ZeroMemory(&sd, sizeof(sd)); + sd.BufferCount = kNumBackBuffers; + sd.Width = 0; + sd.Height = 0; + sd.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + sd.Flags = DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT; + sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; + sd.SampleDesc.Count = 1; + sd.SampleDesc.Quality = 0; + sd.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD; + sd.AlphaMode = DXGI_ALPHA_MODE_UNSPECIFIED; + sd.Scaling = DXGI_SCALING_STRETCH; + sd.Stereo = FALSE; + } + + // Create device + D3D_FEATURE_LEVEL featureLevel = D3D_FEATURE_LEVEL_11_0; + if (D3D12CreateDevice(nullptr, featureLevel, IID_PPV_ARGS(&mD3dDevice)) != S_OK) { + return false; + } + + { + D3D12_DESCRIPTOR_HEAP_DESC desc = {}; + desc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV; + desc.NumDescriptors = kNumBackBuffers; + desc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE; + desc.NodeMask = 1; + if (mD3dDevice->CreateDescriptorHeap(&desc, IID_PPV_ARGS(&mD3dRtvDescHeap)) != S_OK) { + return false; + } + + SIZE_T rtvDescriptorSize = mD3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV); + D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle = mD3dRtvDescHeap->GetCPUDescriptorHandleForHeapStart(); + for (UINT i = 0; i < kNumBackBuffers; i++) { + mMainRenderTargetDescriptor[i] = rtvHandle; + rtvHandle.ptr += rtvDescriptorSize; + } + } + + { + D3D12_DESCRIPTOR_HEAP_DESC desc = {}; + desc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV; + desc.NumDescriptors = 1; + desc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE; + if (mD3dDevice->CreateDescriptorHeap(&desc, IID_PPV_ARGS(&mD3dSrvDescHeap)) != S_OK) { + return false; + } + } + + { + D3D12_COMMAND_QUEUE_DESC desc = {}; + desc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT; + desc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE; + desc.NodeMask = 1; + if (mD3dDevice->CreateCommandQueue(&desc, IID_PPV_ARGS(&mD3dCommandQueue)) != S_OK) { + return false; + } + } + + for (UINT i = 0; i < kNumFramesInFlight; i++) { + if (mD3dDevice->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&mFrameContext[i].CommandAllocator)) != S_OK) { + return false; + } + } + + if (mD3dDevice->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, mFrameContext[0].CommandAllocator, nullptr, IID_PPV_ARGS(&mD3dCommandList)) != S_OK || + mD3dCommandList->Close() != S_OK) + { + return false; + } + + if (mD3dDevice->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&mFence)) != S_OK) return false; + + mFenceEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr); + if (mFenceEvent == nullptr) return false; + + { + IDXGIFactory4* dxgiFactory = nullptr; + IDXGISwapChain1* swapChain1 = nullptr; + if (CreateDXGIFactory1(IID_PPV_ARGS(&dxgiFactory)) != S_OK) + return false; + if (dxgiFactory->CreateSwapChainForHwnd(mD3dCommandQueue, hWnd, &sd, nullptr, nullptr, &swapChain1) != S_OK) + return false; + if (swapChain1->QueryInterface(IID_PPV_ARGS(&mSwapChain)) != S_OK) + return false; + swapChain1->Release(); + dxgiFactory->Release(); + mSwapChain->SetMaximumFrameLatency(kNumBackBuffers); + mSwapChainWaitableObject = mSwapChain->GetFrameLatencyWaitableObject(); + } + + CreateRenderTarget(); + return true; + } + + void CleanupDeviceD3D() + { + CleanupRenderTarget(); + if (mSwapChain) { + mSwapChain->Release(); + mSwapChain = nullptr; + } + if (mSwapChainWaitableObject != nullptr) { + CloseHandle(mSwapChainWaitableObject); + } + for (UINT i = 0; i < kNumFramesInFlight; i++) + if (mFrameContext[i].CommandAllocator) { + mFrameContext[i].CommandAllocator->Release(); + mFrameContext[i].CommandAllocator = nullptr; + } + if (mD3dCommandQueue) { + mD3dCommandQueue->Release(); + mD3dCommandQueue = nullptr; + } + if (mD3dCommandList) { + mD3dCommandList->Release(); + mD3dCommandList = nullptr; + } + if (mD3dRtvDescHeap) { + mD3dRtvDescHeap->Release(); + mD3dRtvDescHeap = nullptr; + } + if (mD3dSrvDescHeap) { + mD3dSrvDescHeap->Release(); + mD3dSrvDescHeap = nullptr; + } + if (mFence) { + mFence->Release(); + mFence = nullptr; + } + if (mFenceEvent) { + CloseHandle(mFenceEvent); + mFenceEvent = nullptr; + } + if (mD3dDevice) { + mD3dDevice->Release(); + mD3dDevice = nullptr; + } + } + + void CreateRenderTarget() + { + for (UINT i = 0; i < kNumBackBuffers; i++) + { + ID3D12Resource* pBackBuffer = nullptr; + mSwapChain->GetBuffer(i, IID_PPV_ARGS(&pBackBuffer)); + mD3dDevice->CreateRenderTargetView(pBackBuffer, nullptr, mMainRenderTargetDescriptor[i]); + mMainRenderTargetResource[i] = pBackBuffer; + } + } + + void CleanupRenderTarget() + { + WaitForLastSubmittedFrame(); + + for (UINT i = 0; i < kNumBackBuffers; i++) + if (mMainRenderTargetResource[i]) { + mMainRenderTargetResource[i]->Release(); + mMainRenderTargetResource[i] = nullptr; + } + } + + void WaitForLastSubmittedFrame() + { + FrameContext* frameCtx = &mFrameContext[mFrameIndex % kNumFramesInFlight]; + + UINT64 fenceValue = frameCtx->FenceValue; + if (fenceValue == 0) + return; // No fence was signaled + + frameCtx->FenceValue = 0; + if (mFence->GetCompletedValue() >= fenceValue) + return; + + mFence->SetEventOnCompletion(fenceValue, mFenceEvent); + WaitForSingleObject(mFenceEvent, INFINITE); + } + + FrameContext* WaitForNextFrameResources() + { + UINT nextFrameIndex = mFrameIndex + 1; + mFrameIndex = nextFrameIndex; + + HANDLE waitableObjects[] = { mSwapChainWaitableObject, nullptr }; + DWORD numWaitableObjects = 1; + + FrameContext* frameCtx = &mFrameContext[nextFrameIndex % kNumFramesInFlight]; + UINT64 fenceValue = frameCtx->FenceValue; + if (fenceValue != 0) // means no fence was signaled + { + frameCtx->FenceValue = 0; + mFence->SetEventOnCompletion(fenceValue, mFenceEvent); + waitableObjects[1] = mFenceEvent; + numWaitableObjects = 2; + } + + WaitForMultipleObjects(numWaitableObjects, waitableObjects, TRUE, INFINITE); + + return frameCtx; + } + + void ResizeSwapChain(int width, int height) + { + DXGI_SWAP_CHAIN_DESC1 sd; + mSwapChain->GetDesc1(&sd); + sd.Width = width; + sd.Height = height; + + IDXGIFactory4* dxgiFactory = nullptr; + mSwapChain->GetParent(IID_PPV_ARGS(&dxgiFactory)); + + mSwapChain->Release(); + CloseHandle(mSwapChainWaitableObject); + + IDXGISwapChain1* swapChain1 = nullptr; + dxgiFactory->CreateSwapChainForHwnd(mD3dCommandQueue, hWnd, &sd, nullptr, nullptr, &swapChain1); + swapChain1->QueryInterface(IID_PPV_ARGS(&mSwapChain)); + swapChain1->Release(); + dxgiFactory->Release(); + + mSwapChain->SetMaximumFrameLatency(kNumBackBuffers); + + mSwapChainWaitableObject = mSwapChain->GetFrameLatencyWaitableObject(); + assert(mSwapChainWaitableObject != nullptr); + } + + static LRESULT CALLBACK StaticWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) + { + DirectX12Backend* self; + if (uMsg == WM_NCCREATE) { + auto lpcs = reinterpret_cast<LPCREATESTRUCT>(lParam); + self = static_cast<DirectX12Backend*>(lpcs->lpCreateParams); + self->hWnd = hWnd; + SetWindowLongPtr(hWnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(self)); + } else { + self = reinterpret_cast<DirectX12Backend*>(GetWindowLongPtr(hWnd, GWLP_USERDATA)); + } + + if (self) { + return self->WndProc(uMsg, wParam, lParam); + } else { + return DefWindowProc(hWnd, uMsg, wParam, lParam); + } + } + + LRESULT WndProc(UINT msg, WPARAM wParam, LPARAM lParam) + { + if (ImGui_ImplWin32_WndProcHandler(hWnd, msg, wParam, lParam)) { + return true; + } + + switch (msg) { + case WM_SIZE: { + if (mD3dDevice != nullptr && wParam != SIZE_MINIMIZED) { + WaitForLastSubmittedFrame(); + ImGui_ImplDX12_InvalidateDeviceObjects(); + CleanupRenderTarget(); + ResizeSwapChain((UINT)LOWORD(lParam), (UINT)HIWORD(lParam)); + CreateRenderTarget(); + ImGui_ImplDX12_CreateDeviceObjects(); + } + return 0; + } + + case WM_SYSCOMMAND: { + // Disable ALT application menu + if ((wParam & 0xfff0) == SC_KEYMENU) { + return 0; + } + } break; + + case WM_DESTROY: { + ::PostQuitMessage(0); + return 0; + } + } + return ::DefWindowProc(hWnd, msg, wParam, lParam); + } +}; + +std::unique_ptr<RenderingBackend> RenderingBackend::CreateDx12Backend() +{ + try { + return std::make_unique<DirectX12Backend>(); + } catch (std::exception& e) { + return nullptr; + } +} + +#else // ^^ BUILD_CORE_WITH_DX12_BACKEND | BUILD_CORE_WITH_DX12_BACKEND vv + +std::unique_ptr<RenderingBackend> RenderingBackend::CreateDx12Backend() +{ + return nullptr; +} + +#endif diff --git a/app/source/Cplt/Entrypoint/Backend_Metal.mm b/app/source/Cplt/Entrypoint/Backend_Metal.mm new file mode 100644 index 0000000..276bef2 --- /dev/null +++ b/app/source/Cplt/Entrypoint/Backend_Metal.mm @@ -0,0 +1,40 @@ +#include "Backend.hpp" + +#if BUILD_CORE_WITH_METAL_BACKEND + +class MetalBackend : public RenderingBackend +{ +public: + MetalBackend() + { + // TODO + } + + virtual ~MetalBackend() + { + // TODO + } + + virtual void RunUntilWindowClose(void (*windowContent)()) + { + // TODO + } +}; + +std::unique_ptr<RenderingBackend> RenderingBackend::CreateMetalBackend() +{ + try { + return std::make_unique<MetalBackend>(); + } catch (std::exception& e) { + return nullptr; + } +} + +#else // ^^ BUILD_CORE_WITH_METAL_BACKEND | BUILD_CORE_WITH_METAL_BACKEND vv + +std::unique_ptr<RenderingBackend> RenderingBackend::CreateMetalBackend() +{ + return nullptr; +} + +#endif diff --git a/app/source/Cplt/Entrypoint/Backend_OpenGL2.cpp b/app/source/Cplt/Entrypoint/Backend_OpenGL2.cpp new file mode 100644 index 0000000..0f20997 --- /dev/null +++ b/app/source/Cplt/Entrypoint/Backend_OpenGL2.cpp @@ -0,0 +1,106 @@ +#include <Cplt/Entrypoint/Backend.hpp> + +#if BUILD_CORE_WITH_OPENGL2_BACKEND +# include <glad/glad.h> + +# include <GLFW/glfw3.h> +# include <backend/imgui_impl_glfw.h> +# include <backend/imgui_impl_opengl2.h> +# include <imgui.h> +# include <stdexcept> +# include <iostream> + +# define IMGUI_IMPL_OPENGL_LOADER_CUSTOM +# include <backend/imgui_impl_opengl2.cpp> + +class OpenGL2Backend : public RenderingBackend +{ +private: + GLFWwindow* mWindow; + +public: + OpenGL2Backend() + { + glfwSetErrorCallback(&GlfwErrorCallback); + if (!glfwInit()) { + throw std::runtime_error("Failed to initialize GLFW."); + } + + mWindow = glfwCreateWindow(1280, 720, "Cplt", nullptr, nullptr); + if (mWindow == nullptr) { + throw std::runtime_error("Failed to create GLFW window."); + } + glfwMakeContextCurrent(mWindow); + glfwSwapInterval(1); // Enable vsync + + if (gladLoadGLLoader((GLADloadproc)glfwGetProcAddress) == 0) { + throw std::runtime_error("Failed to initialize OpenGL."); + } + + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + + ImGui_ImplGlfw_InitForOpenGL(mWindow, true); + ImGui_ImplOpenGL2_Init(); + } + + virtual ~OpenGL2Backend() + { + ImGui_ImplOpenGL2_Shutdown(); + ImGui_ImplGlfw_Shutdown(); + ImGui::DestroyContext(); + + glfwDestroyWindow(mWindow); + glfwTerminate(); + } + + virtual void RunUntilWindowClose(void (*windowContent)()) + { + while (!glfwWindowShouldClose(mWindow)) { + glfwPollEvents(); + + ImGui_ImplOpenGL2_NewFrame(); + ImGui_ImplGlfw_NewFrame(); + ImGui::NewFrame(); + + windowContent(); + + int displayWidth, displayHeight; + glfwGetFramebufferSize(mWindow, &displayWidth, &displayHeight); + glViewport(0, 0, displayWidth, displayHeight); + + const ImVec4 kClearColor = ImVec4(0.45f, 0.55f, 0.60f, 1.00f); + glClearColor(kClearColor.x * kClearColor.w, kClearColor.y * kClearColor.w, kClearColor.z * kClearColor.w, kClearColor.w); + glClear(GL_COLOR_BUFFER_BIT); + + ImGui::Render(); + ImGui_ImplOpenGL2_RenderDrawData(ImGui::GetDrawData()); + + glfwMakeContextCurrent(mWindow); + glfwSwapBuffers(mWindow); + } + } + + static void GlfwErrorCallback(int errorCode, const char* message) + { + std::cerr << "GLFW Error " << errorCode << ": " << message << "\n"; + } +}; + +std::unique_ptr<RenderingBackend> RenderingBackend::CreateOpenGL2Backend() +{ + try { + return std::make_unique<OpenGL2Backend>(); + } catch (std::exception& e) { + return nullptr; + } +} + +#else // ^^ BUILD_CORE_WITH_OPENGL2_BACKEND | !BUILD_CORE_WITH_OPENGL2_BACKEND vv + +std::unique_ptr<RenderingBackend> RenderingBackend::CreateOpenGL2Backend() +{ + return nullptr; +} + +#endif diff --git a/app/source/Cplt/Entrypoint/Backend_OpenGL3.cpp b/app/source/Cplt/Entrypoint/Backend_OpenGL3.cpp new file mode 100644 index 0000000..28a34ca --- /dev/null +++ b/app/source/Cplt/Entrypoint/Backend_OpenGL3.cpp @@ -0,0 +1,121 @@ +#include <Cplt/Entrypoint/Backend.hpp> + +#if BUILD_CORE_WITH_OPENGL3_BACKEND +# include <glad/glad.h> + +# include <GLFW/glfw3.h> +# include <iostream> +# include <backend/imgui_impl_glfw.h> +# include <backend/imgui_impl_opengl3.h> +# include <imgui.h> +# include <stdexcept> + +# define IMGUI_IMPL_OPENGL_LOADER_CUSTOM +# include <backend/imgui_impl_opengl3.cpp> + +class OpenGL3Backend : public RenderingBackend +{ +private: + GLFWwindow* mWindow; + +public: + OpenGL3Backend() + { + glfwSetErrorCallback(&GlfwErrorCallback); + if (!glfwInit()) { + throw std::runtime_error("Failed to initialize GLFW."); + } + +# if defined(__APPLE__) + // GL 3.2 + GLSL 150 + const char* glslVersion = "#version 150"; + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2); + glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 3.2+ only + glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // Required on Mac +# else + // GL 3.0 + GLSL 130 + const char* glslVersion = "#version 130"; + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0); + // glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 3.2+ only + // glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // 3.0+ only +# endif + + mWindow = glfwCreateWindow(1280, 720, "Cplt", nullptr, nullptr); + if (mWindow == nullptr) { + throw std::runtime_error("Failed to create GLFW window."); + } + glfwMakeContextCurrent(mWindow); + glfwSwapInterval(1); // Enable vsync + + if (gladLoadGLLoader((GLADloadproc)glfwGetProcAddress) == 0) { + throw std::runtime_error("Failed to initialize OpenGL."); + } + + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + + ImGui_ImplGlfw_InitForOpenGL(mWindow, true); + ImGui_ImplOpenGL3_Init(glslVersion); + } + + virtual ~OpenGL3Backend() + { + ImGui_ImplOpenGL3_Shutdown(); + ImGui_ImplGlfw_Shutdown(); + ImGui::DestroyContext(); + + glfwDestroyWindow(mWindow); + glfwTerminate(); + } + + virtual void RunUntilWindowClose(void (*windowContent)()) + { + while (!glfwWindowShouldClose(mWindow)) { + glfwPollEvents(); + + ImGui_ImplOpenGL3_NewFrame(); + ImGui_ImplGlfw_NewFrame(); + ImGui::NewFrame(); + + windowContent(); + + int displayWidth, displayHeight; + glfwGetFramebufferSize(mWindow, &displayWidth, &displayHeight); + glViewport(0, 0, displayWidth, displayHeight); + + const ImVec4 kClearColor = ImVec4(0.45f, 0.55f, 0.60f, 1.00f); + glClearColor(kClearColor.x * kClearColor.w, kClearColor.y * kClearColor.w, kClearColor.z * kClearColor.w, kClearColor.w); + glClear(GL_COLOR_BUFFER_BIT); + + ImGui::Render(); + ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); + + glfwSwapBuffers(mWindow); + } + } + + static void GlfwErrorCallback(int errorCode, const char* message) + { + std::cerr << "GLFW Error " << errorCode << ": " << message << "\n"; + } +}; + +std::unique_ptr<RenderingBackend> RenderingBackend::CreateOpenGL3Backend() +{ + try { + return std::make_unique<OpenGL3Backend>(); + } catch (std::exception& e) { + return nullptr; + } +} + +#else // ^^ BUILD_CORE_WITH_OPENGL3_BACKEND | !BUILD_CORE_WITH_OPENGL3_BACKEND vv + +std::unique_ptr<RenderingBackend> RenderingBackend::CreateOpenGL3Backend() +{ + return nullptr; +} + +#endif diff --git a/app/source/Cplt/Entrypoint/Backend_Vulkan.cpp b/app/source/Cplt/Entrypoint/Backend_Vulkan.cpp new file mode 100644 index 0000000..280a82b --- /dev/null +++ b/app/source/Cplt/Entrypoint/Backend_Vulkan.cpp @@ -0,0 +1,438 @@ +#include <Cplt/Entrypoint/Backend.hpp> + +#if BUILD_CORE_WITH_VULKAN_BACKEND +# include <iostream> +# include <stdexcept> + +# define GLFW_INCLUDE_NONE +# define GLFW_INCLUDE_VULKAN +# include <GLFW/glfw3.h> + +# include <backend/imgui_impl_glfw.h> +# include <backend/imgui_impl_vulkan.h> +# include <backend/imgui_impl_vulkan.cpp> + +class VulkanBackend : public RenderingBackend +{ +private: + GLFWwindow* mWindow; + + VkAllocationCallbacks* mAllocator = NULL; + VkInstance mInstance = VK_NULL_HANDLE; + VkPhysicalDevice mPhysicalDevice = VK_NULL_HANDLE; + VkDevice mDevice = VK_NULL_HANDLE; + uint32_t mQueueFamily = (uint32_t)-1; + VkQueue mQueue = VK_NULL_HANDLE; + VkDebugReportCallbackEXT mDebugReport = VK_NULL_HANDLE; + VkPipelineCache mPipelineCache = VK_NULL_HANDLE; + VkDescriptorPool mDescriptorPool = VK_NULL_HANDLE; + + ImGui_ImplVulkanH_Window mMainWindowData; + int mMinImageCount = 2; + bool mSwapChainRebuild = false; + +public: + VulkanBackend() + { + glfwSetErrorCallback(&GlfwErrorCallback); + if (!glfwInit()) { + throw std::runtime_error("Failed to initialize GLFW."); + } + + glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); + mWindow = glfwCreateWindow(1280, 720, "Cplt", nullptr, nullptr); + if (mWindow == nullptr) { + throw std::runtime_error("Failed to create GLFW window."); + } + + if (!glfwVulkanSupported()) { + throw std::runtime_error("GLFW reports vulkan not supported."); + } + + uint32_t extensionsCount = 0; + const char** extensions = glfwGetRequiredInstanceExtensions(&extensionsCount); + SetupVulkan(extensions, extensionsCount); + + // Create window surface + VkSurfaceKHR surface; + VkResult err = glfwCreateWindowSurface(mInstance, mWindow, mAllocator, &surface); + CheckVkResults(err); + + // Create framebuffers + int w, h; + glfwGetFramebufferSize(mWindow, &w, &h); + SetupVulkanWindow(&mMainWindowData, surface, w, h); + + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + + ImGui_ImplGlfw_InitForVulkan(mWindow, true); + ImGui_ImplVulkan_InitInfo init_info = {}; + init_info.Instance = mInstance; + init_info.PhysicalDevice = mPhysicalDevice; + init_info.Device = mDevice; + init_info.QueueFamily = mQueueFamily; + init_info.Queue = mQueue; + init_info.PipelineCache = mPipelineCache; + init_info.DescriptorPool = mDescriptorPool; + init_info.Allocator = mAllocator; + init_info.MinImageCount = mMinImageCount; + init_info.ImageCount = mMainWindowData.ImageCount; + init_info.CheckVkResultFn = CheckVkResults; + ImGui_ImplVulkan_Init(&init_info, mMainWindowData.RenderPass); + } + + virtual ~VulkanBackend() + { + auto err = vkDeviceWaitIdle(mDevice); + CheckVkResults(err); + ImGui_ImplVulkan_Shutdown(); + ImGui_ImplGlfw_Shutdown(); + ImGui::DestroyContext(); + + CleanupVulkanWindow(); + CleanupVulkan(); + + glfwDestroyWindow(mWindow); + glfwTerminate(); + } + + virtual void RunUntilWindowClose(void (*windowContent)()) override + { + // Upload Fonts + { + // Use any command queue + VkCommandPool commandPool = mMainWindowData.Frames[mMainWindowData.FrameIndex].CommandPool; + VkCommandBuffer commandBuffer = mMainWindowData.Frames[mMainWindowData.FrameIndex].CommandBuffer; + + CheckVkResults(vkResetCommandPool(mDevice, commandPool, 0)); + VkCommandBufferBeginInfo beginInfo = {}; + beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + beginInfo.flags |= VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + CheckVkResults(vkBeginCommandBuffer(commandBuffer, &beginInfo)); + + ImGui_ImplVulkan_CreateFontsTexture(commandBuffer); + + VkSubmitInfo endInfo = {}; + endInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + endInfo.commandBufferCount = 1; + endInfo.pCommandBuffers = &commandBuffer; + CheckVkResults(vkEndCommandBuffer(commandBuffer)); + CheckVkResults(vkQueueSubmit(mQueue, 1, &endInfo, VK_NULL_HANDLE)); + + CheckVkResults(vkDeviceWaitIdle(mDevice)); + ImGui_ImplVulkan_DestroyFontUploadObjects(); + } + + while (!glfwWindowShouldClose(mWindow)) { + glfwPollEvents(); + + // Resize swap chain? + if (mSwapChainRebuild) { + int width, height; + glfwGetFramebufferSize(mWindow, &width, &height); + if (width > 0 && height > 0) { + ImGui_ImplVulkan_SetMinImageCount(mMinImageCount); + ImGui_ImplVulkanH_CreateOrResizeWindow(mInstance, mPhysicalDevice, mDevice, &mMainWindowData, mQueueFamily, mAllocator, width, height, mMinImageCount); + mMainWindowData.FrameIndex = 0; + mSwapChainRebuild = false; + } + } + + // Start the Dear ImGui frame + ImGui_ImplVulkan_NewFrame(); + ImGui_ImplGlfw_NewFrame(); + ImGui::NewFrame(); + + windowContent(); + + ImGui::Render(); + ImDrawData* drawData = ImGui::GetDrawData(); + const bool isMinimized = (drawData->DisplaySize.x <= 0.0f || drawData->DisplaySize.y <= 0.0f); + if (!isMinimized) { + const ImVec4 kClearColor = ImVec4(0.45f, 0.55f, 0.60f, 1.00f); + mMainWindowData.ClearValue.color.float32[0] = kClearColor.x * kClearColor.w; + mMainWindowData.ClearValue.color.float32[1] = kClearColor.y * kClearColor.w; + mMainWindowData.ClearValue.color.float32[2] = kClearColor.z * kClearColor.w; + mMainWindowData.ClearValue.color.float32[3] = kClearColor.w; + FrameRender(&mMainWindowData, drawData); + FramePresent(&mMainWindowData); + } + } + } + +private: + void SetupVulkan(const char** extensions, uint32_t extensions_count) + { + VkResult err; + + // Create Vulkan Instance + { + VkInstanceCreateInfo createInfo = {}; + createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; + createInfo.enabledExtensionCount = extensions_count; + createInfo.ppEnabledExtensionNames = extensions; + // Create Vulkan Instance without any debug feature + err = vkCreateInstance(&createInfo, mAllocator, &mInstance); + CheckVkResults(err); + } + + // Select GPU + { + uint32_t gpuCount; + err = vkEnumeratePhysicalDevices(mInstance, &gpuCount, NULL); + CheckVkResults(err); + IM_ASSERT(gpuCount > 0); + + VkPhysicalDevice* gpus = (VkPhysicalDevice*)malloc(sizeof(VkPhysicalDevice) * gpuCount); + err = vkEnumeratePhysicalDevices(mInstance, &gpuCount, gpus); + CheckVkResults(err); + + // If a number >1 of GPUs got reported, find discrete GPU if present, or use first one available. This covers + // most common cases (multi-gpu/integrated+dedicated graphics). Handling more complicated setups (multiple + // dedicated GPUs) is out of scope of this sample. + int useGpu = 0; + for (int i = 0; i < (int)gpuCount; i++) + { + VkPhysicalDeviceProperties properties; + vkGetPhysicalDeviceProperties(gpus[i], &properties); + if (properties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) + { + useGpu = i; + break; + } + } + + mPhysicalDevice = gpus[useGpu]; + free(gpus); + } + + // Select graphics queue family + { + uint32_t count; + vkGetPhysicalDeviceQueueFamilyProperties(mPhysicalDevice, &count, NULL); + + auto queues = std::make_unique<VkQueueFamilyProperties[]>(count); + vkGetPhysicalDeviceQueueFamilyProperties(mPhysicalDevice, &count, queues.get()); + for (uint32_t i = 0; i < count; i++) { + if (queues[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) + { + mQueueFamily = i; + break; + } + } + + IM_ASSERT(mQueueFamily != (uint32_t)-1); + } + + // Create Logical Device (with 1 queue) + { + int deviceExtensionCount = 1; + const char* deviceExtensions[] = { "VK_KHR_swapchain" }; + const float queuePriority[] = { 1.0f }; + VkDeviceQueueCreateInfo queue_info[1] = {}; + queue_info[0].sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; + queue_info[0].queueFamilyIndex = mQueueFamily; + queue_info[0].queueCount = 1; + queue_info[0].pQueuePriorities = queuePriority; + VkDeviceCreateInfo createInfo = {}; + createInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; + createInfo.queueCreateInfoCount = sizeof(queue_info) / sizeof(queue_info[0]); + createInfo.pQueueCreateInfos = queue_info; + createInfo.enabledExtensionCount = deviceExtensionCount; + createInfo.ppEnabledExtensionNames = deviceExtensions; + err = vkCreateDevice(mPhysicalDevice, &createInfo, mAllocator, &mDevice); + CheckVkResults(err); + vkGetDeviceQueue(mDevice, mQueueFamily, 0, &mQueue); + } + + // Create Descriptor Pool + { + VkDescriptorPoolSize poolSizes[] = { + { VK_DESCRIPTOR_TYPE_SAMPLER, 1000 }, + { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1000 }, + { VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 1000 }, + { VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1000 }, + { VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER, 1000 }, + { VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER, 1000 }, + { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1000 }, + { VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1000 }, + { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, 1000 }, + { VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC, 1000 }, + { VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT, 1000 } + }; + VkDescriptorPoolCreateInfo poolInfo = {}; + poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; + poolInfo.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT; + poolInfo.maxSets = 1000 * IM_ARRAYSIZE(poolSizes); + poolInfo.poolSizeCount = (uint32_t)IM_ARRAYSIZE(poolSizes); + poolInfo.pPoolSizes = poolSizes; + err = vkCreateDescriptorPool(mDevice, &poolInfo, mAllocator, &mDescriptorPool); + CheckVkResults(err); + } + } + + void SetupVulkanWindow(ImGui_ImplVulkanH_Window* wd, VkSurfaceKHR surface, int width, int height) + { + wd->Surface = surface; + + // Check for WSI support + VkBool32 res; + vkGetPhysicalDeviceSurfaceSupportKHR(mPhysicalDevice, mQueueFamily, wd->Surface, &res); + if (res != VK_TRUE) { + throw "Error no WSI support on physical device 0."; + } + + // Select Surface Format + const VkFormat requestSurfaceImageFormat[] = { VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_B8G8R8_UNORM, VK_FORMAT_R8G8B8_UNORM }; + const VkColorSpaceKHR requestSurfaceColorSpace = VK_COLORSPACE_SRGB_NONLINEAR_KHR; + wd->SurfaceFormat = ImGui_ImplVulkanH_SelectSurfaceFormat(mPhysicalDevice, wd->Surface, requestSurfaceImageFormat, (size_t)IM_ARRAYSIZE(requestSurfaceImageFormat), requestSurfaceColorSpace); + + // Select Present Mode + VkPresentModeKHR present_modes[] = { VK_PRESENT_MODE_FIFO_KHR }; + wd->PresentMode = ImGui_ImplVulkanH_SelectPresentMode(mPhysicalDevice, wd->Surface, &present_modes[0], IM_ARRAYSIZE(present_modes)); + + // Create SwapChain, RenderPass, Framebuffer, etc. + IM_ASSERT(mMinImageCount >= 2); + ImGui_ImplVulkanH_CreateOrResizeWindow(mInstance, mPhysicalDevice, mDevice, wd, mQueueFamily, mAllocator, width, height, mMinImageCount); + } + + void FrameRender(ImGui_ImplVulkanH_Window* wd, ImDrawData* drawData) + { + VkResult err; + + VkSemaphore imageAcquiredSemaphore = wd->FrameSemaphores[wd->SemaphoreIndex].ImageAcquiredSemaphore; + VkSemaphore renderCompleteSemaphore = wd->FrameSemaphores[wd->SemaphoreIndex].RenderCompleteSemaphore; + err = vkAcquireNextImageKHR(mDevice, wd->Swapchain, UINT64_MAX, imageAcquiredSemaphore, VK_NULL_HANDLE, &wd->FrameIndex); + if (err == VK_ERROR_OUT_OF_DATE_KHR || err == VK_SUBOPTIMAL_KHR) { + mSwapChainRebuild = true; + return; + } + CheckVkResults(err); + + ImGui_ImplVulkanH_Frame* fd = &wd->Frames[wd->FrameIndex]; + { + err = vkWaitForFences(mDevice, 1, &fd->Fence, VK_TRUE, UINT64_MAX); // wait indefinitely instead of periodically checking + CheckVkResults(err); + + err = vkResetFences(mDevice, 1, &fd->Fence); + CheckVkResults(err); + } + { + err = vkResetCommandPool(mDevice, fd->CommandPool, 0); + CheckVkResults(err); + VkCommandBufferBeginInfo info = {}; + info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + info.flags |= VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + err = vkBeginCommandBuffer(fd->CommandBuffer, &info); + CheckVkResults(err); + } + { + VkRenderPassBeginInfo info = {}; + info.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; + info.renderPass = wd->RenderPass; + info.framebuffer = fd->Framebuffer; + info.renderArea.extent.width = wd->Width; + info.renderArea.extent.height = wd->Height; + info.clearValueCount = 1; + info.pClearValues = &wd->ClearValue; + vkCmdBeginRenderPass(fd->CommandBuffer, &info, VK_SUBPASS_CONTENTS_INLINE); + } + + // Record dear imgui primitives into command buffer + ImGui_ImplVulkan_RenderDrawData(drawData, fd->CommandBuffer); + + // Submit command buffer + vkCmdEndRenderPass(fd->CommandBuffer); + { + VkPipelineStageFlags wait_stage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + VkSubmitInfo info = {}; + info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + info.waitSemaphoreCount = 1; + info.pWaitSemaphores = &imageAcquiredSemaphore; + info.pWaitDstStageMask = &wait_stage; + info.commandBufferCount = 1; + info.pCommandBuffers = &fd->CommandBuffer; + info.signalSemaphoreCount = 1; + info.pSignalSemaphores = &renderCompleteSemaphore; + + err = vkEndCommandBuffer(fd->CommandBuffer); + CheckVkResults(err); + err = vkQueueSubmit(mQueue, 1, &info, fd->Fence); + CheckVkResults(err); + } + } + + void FramePresent(ImGui_ImplVulkanH_Window* wd) + { + if (mSwapChainRebuild) { + return; + } + + VkSemaphore renderCompleteSemaphore = wd->FrameSemaphores[wd->SemaphoreIndex].RenderCompleteSemaphore; + VkPresentInfoKHR info = {}; + info.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR; + info.waitSemaphoreCount = 1; + info.pWaitSemaphores = &renderCompleteSemaphore; + info.swapchainCount = 1; + info.pSwapchains = &wd->Swapchain; + info.pImageIndices = &wd->FrameIndex; + VkResult err = vkQueuePresentKHR(mQueue, &info); + if (err == VK_ERROR_OUT_OF_DATE_KHR || err == VK_SUBOPTIMAL_KHR) { + mSwapChainRebuild = true; + return; + } + CheckVkResults(err); + wd->SemaphoreIndex = (wd->SemaphoreIndex + 1) % wd->ImageCount; // Now we can use the next set of semaphores + } + + void CleanupVulkan() + { + vkDestroyDescriptorPool(mDevice, mDescriptorPool, mAllocator); + + vkDestroyDevice(mDevice, mAllocator); + vkDestroyInstance(mInstance, mAllocator); + } + + void CleanupVulkanWindow() + { + ImGui_ImplVulkanH_DestroyWindow(mInstance, mDevice, &mMainWindowData, mAllocator); + } + + static void CheckVkResults(VkResult err) + { + if (err == 0) return; + + std::string message; + message += "Vulkan error: VkResult = "; + message += err; + + if (err < 0) { + throw std::runtime_error(message); + } else { + std::cerr << message << '\n'; + } + } + static void GlfwErrorCallback(int errorCode, const char* message) + { + std::cerr << "GLFW Error " << errorCode << ": " << message << "\n"; + } +}; + +std::unique_ptr<RenderingBackend> RenderingBackend::CreateVulkanBackend() +{ + try { + return std::make_unique<VulkanBackend>(); + } catch (std::exception& e) { + return nullptr; + } +} + +#else // ^^ BUILD_CORE_WITH_VULKAN_BACKEND | ~BUILD_CORE_WITH_VULKAN_BACKEND vv + +std::unique_ptr<RenderingBackend> RenderingBackend::CreateVulkanBackend() +{ + return nullptr; +} + +#endif diff --git a/app/source/Cplt/Entrypoint/main.cpp b/app/source/Cplt/Entrypoint/main.cpp new file mode 100644 index 0000000..8f67d32 --- /dev/null +++ b/app/source/Cplt/Entrypoint/main.cpp @@ -0,0 +1,163 @@ +#include <Cplt/Entrypoint/Backend.hpp> +#include <Cplt/Model/GlobalStates.hpp> +#include <Cplt/UI/UI.hpp> +#include <Cplt/Utils/I18n.hpp> +#include <Cplt/Utils/ScopeGuard.hpp> +#include <Cplt/Utils/Sigslot.hpp> + +#include <IconsFontAwesome.h> +#include <imgui.h> +#include <argparse/argparse.hpp> +#include <filesystem> +#include <iostream> +#include <memory> +#include <stdexcept> +#include <string> +#include <string_view> + +namespace fs = std::filesystem; +using namespace std::literals::string_literals; +using namespace std::literals::string_view_literals; + +static std::unique_ptr<RenderingBackend> CreateDefaultBackend() +{ +#if defined(_WIN32) +# if BUILD_CORE_WITH_DX12_BACKEND + if (auto backend = RenderingBackend::CreateDx12Backend()) { + return backend; + } +# endif +# if BUILD_CORE_WITH_DX11_BACKEND + if (auto backend = RenderingBackend::CreateDx11Backend()) { + return backend; + } +# endif +# if BUILD_CORE_WITH_VULKAN_BACKEND + if (auto backend = RenderingBackend::CreateVulkanBackend()) { + return backend; + } +# endif +# if BUILD_CORE_WITH_OPENGL3_BACKEND + if (auto backend = RenderingBackend::CreateOpenGL3Backend()) { + return backend; + } +# endif +# if BUILD_CORE_WITH_OPENGL2_BACKEND + if (auto backend = RenderingBackend::CreateOpenGL2Backend()) { + return backend; + } +# endif +#elif defined(__APPLE__) + // We currently only support using metal on macos + return RenderingBackend::CreateMetalBackend(); +#elif defined(__linux__) +# if BUILD_CORE_WITH_VULKAN_BACKEND + if (auto backend = RenderingBackend::CreateVulkanBackend()) { + return backend; + } +# endif +# if BUILD_CORE_WITH_OPENGL3_BACKEND + if (auto backend = RenderingBackend::CreateOpenGL3Backend()) { + return backend; + } +# endif +# if BUILD_CORE_WITH_OPENGL2_BACKEND + if (auto backend = RenderingBackend::CreateOpenGL2Backend()) { + return backend; + } +# endif +#endif + + return nullptr; +} + +static std::unique_ptr<RenderingBackend> CreateBackend(std::string_view option) +{ + if (option == "default") { + return CreateDefaultBackend(); + } else if (option == "opengl2") { + return RenderingBackend::CreateOpenGL2Backend(); + } else if (option == "opengl3") { + return RenderingBackend::CreateOpenGL3Backend(); + } else if (option == "vulkan") { + return RenderingBackend::CreateVulkanBackend(); + } else if (option == "dx11") { + return RenderingBackend::CreateDx11Backend(); + } else if (option == "dx12") { + return RenderingBackend::CreateDx12Backend(); + } else if (option == "metal") { + return RenderingBackend::CreateMetalBackend(); + } else { + std::string message; + message += "Unknown backend '"; + message += option; + message += "'.\n"; + throw std::runtime_error(message); + } +} + +#ifdef DOCTEST_CONFIG_DISABLE +int main(int argc, char* argv[]) +{ + argparse::ArgumentParser parser; + parser.add_argument("--global-data-directory") + .help("Directory in which global data (such as recently used projects) are saved to. Use 'default' to use the default directory on each platform.") + .default_value("default"s); + parser.add_argument("--rendering-backend") + .help("Which rendering backend to use. If equals 'default', the preferred API for each platform will be used") + .default_value("default"s); + + try { + parser.parse_args(argc, argv); + } catch (const std::runtime_error& error) { + std::cout << error.what() << '\n'; + std::cout << parser; + return -1; + } + + auto backendOption = parser.get<std::string>("--rendering-backend"); + auto backend = CreateBackend(backendOption); + + auto& io = ImGui::GetIO(); + + // Disable saving window positions + io.IniFilename = nullptr; + // Disable log (dump widget tree) file, we don't trigger it but just to be safe + io.LogFilename = nullptr; + + // Light mode because all major OS's default theme is white + // TODO follow system theme + ImGui::StyleColorsLight(); + + // Configure default fonts + { + // Includes latin alphabet, although for some reason smaller than if rendered using 18 point NotoSans regular + io.Fonts->AddFontFromFileTTF("fonts/NotoSansSC-Regular.otf", 18, nullptr, io.Fonts->GetGlyphRangesChineseFull()); + + ImWchar iconRanges[] = { ICON_MIN_FA, ICON_MAX_FA }; + ImFontConfig config; + config.MergeMode = true; + io.Fonts->AddFontFromFileTTF("fonts/FontAwesome5-Solid.otf", 14, &config, iconRanges); + } + + auto dataDirOption = parser.get<std::string>("--global-data-directory"); + if (dataDirOption == "default") { + GlobalStates::Init(); + } else { + fs::path path(dataDirOption); + GlobalStates::Init(std::move(path)); + } + DEFER + { + GlobalStates::Shutdown(); + }; + + // Main loop + backend->RunUntilWindowClose(&UI::MainWindow); + + return 0; +} +#else +# define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +# include <doctest/doctest.h> +#endif diff --git a/app/source/Cplt/Locale/zh_CN.h b/app/source/Cplt/Locale/zh_CN.h new file mode 100644 index 0000000..f654464 --- /dev/null +++ b/app/source/Cplt/Locale/zh_CN.h @@ -0,0 +1,159 @@ +#pragma once + +#define L10N_ERROR "错误" +#define L10N_ADD "新建" +#define L10N_EDIT "编辑" +#define L10N_DELETE "删除" +#define L10N_RENAME "重命名" +#define L10N_DISCONNECT "断开连接" +#define L10N_OPEN "打开" +#define L10N_CLOSE "关闭" +#define L10N_CONFIRM "确定" +#define L10N_CANCEL "取消" +#define L10N_NAME "名称" +#define L10N_TYPE "类型" + +#define L10N_INVALID_PATH_ERROR "无效路径" +#define L10N_EMPTY_NAME_ERROR "名称不能为空" +#define L10N_DUPLICATE_NAME_ERROR "名称已被占用" + +#define L10N_MAIN_TAB_SETTINGS "设置" +#define L10N_MAIN_WINDOW_TAB_PROJECT "项目" +#define L10N_MAIN_WINDOW_TAB_DATABASE_VIEW "数据" +#define L10N_MAIN_WINDOW_TAB_ITEMS "物品" +#define L10N_MAIN_WINDOW_TAB_WORKFLOWS "工作流" +#define L10N_MAIN_WINDOW_TAB_TEMPLATES "模板" + +#define L10N_PROJECT_NEW "新建项目..." +#define L10N_PROJECT_NEW_DIALOG_TITLE "新建项目向导" +#define L10N_PROJECT_NAME "项目名称" +#define L10N_PROJECT_PATH "项目路径" +#define L10N_PROJECT_NEW_PATH_DIALOG_TITLE "项目路径" +#define L10N_PROJECT_OPEN "打开项目..." +#define L10N_PROJECT_OPEN_DIALOG_TITLE "打开项目" +#define L10N_PROJECT_RECENTS "最近使用" +#define L10N_PROJECT_RECENTS_CLEAR "清空" +#define L10N_PROJECT_RECENTS_NONE_PRESENT "(暂无最近使用的项目)" +#define L10N_PROJECT_RECENTS_OPEN_TOOLTIP "打开该项目" +#define L10N_PROJECT_RECENTS_DELETE_TOOLTIP "将该项目从最近使用列表中删除,项目本身将不受影响。" +#define L10N_PROJECT_INVALID_PROJECT_FORMAT "无效的项目文件" + +#define L10N_PROJECT_OPEN_IN_FILESYSTEM "在文件系统中打开" + +#define L10N_DATABASE_SALES_VIEW_TAB_NAME "销售" +#define L10N_DATABASE_SALES_VIEW_EDIT_DIALOG_TITLE "编辑销售记录" +#define L10N_DATABASE_PURCHASES_VIEW_TAB_NAME "采购" +#define L10N_DATABASE_PURCHASES_VIEW_EDIT_DIALOG_TITLE "编辑采购记录" +#define L10N_DATABASE_COLUMN_ITEMS "项目" +#define L10N_DATABASE_COLUMN_CUSTOMER "客户" +#define L10N_DATABASE_COLUMN_FACTORY "工厂" +/// 销售订单的交货期限 +#define L10N_DATABASE_COLUMN_DEADLINE "交货期限" +/// 采购订单的下单时间 +#define L10N_DATABASE_COLUMN_ORDER_TIME "下单时间" +/// 所有订单的“完成”时间。对于销售来说是实际交货时间,对于采购来说是收货时间。 +#define L10N_DATABASE_COLUMN_DELIVERY_TIME "交货时间" +/// 运输批次的发货时间,适用于采购和销售批次。 +#define L10N_DATABASE_COLUMN_SHIPMENT_TIME "发货时间" +/// 运输批次的收获时间,适用于采购和销售批次。 +#define L10N_DATABASE_COLUMN_ARRIVAL_TIME "实际到达时间" +#define L10N_DATABASE_MESSAGE_NO_ORDER_SELECTED "选择任意一个订单以查看与其相关的批次" +#define L10N_DATABASE_MESSAGE_NOT_DELIVERED "N/A" + +#define L10N_ITEM_ADD_DIALOG_TITLE "新建物品项" +#define L10N_ITEM_EDIT_DIALOG_TITLE "编辑物品项" +#define L10N_ITEM_DELETE_DIALOG_TITLE "删除物品项" +#define L10N_ITEM_DELETE_DIALOG_MESSAGE "确定删除该物品项吗?" +#define L10N_ITEM_CATEGORY_PRODUCT "产品" +#define L10N_ITEM_CATEGORY_FACTORY "工厂" +#define L10N_ITEM_CATEGORY_CUSTOMER "客户" +#define L10N_ITEM_COLUMN_NAME "名称" +#define L10N_ITEM_COLUMN_DESCRIPTION "描述" +#define L10N_ITEM_COLUMN_EMAIL "邮箱" +#define L10N_ITEM_COLUMN_STOCK "库存" +#define L10N_ITEM_COLUMN_PRICE "价格" + +#define L10N_ASSET_OPEN "打开资源..." +#define L10N_ASSET_OPEN_DIALOG_TITLE "打开资源" +#define L10N_ASSET_MANAGE "管理资源..." +#define L10N_ASSET_MANAGE_DIALOG_TITLE "管理资源" +#define L10N_ADD_ASSET_DIALOG_TITLE "新建资源向导" +#define L10N_DELETE_ASSET_DIALOG_TITLE "确认删除资源" +#define L10N_RENAME_ASSET_DIALOG_TITLE "重命名资源" +#define L10N_TEMPLATE_INVALID_TYPE_ERROR "无效的模板类型" + +#define L10N_VALUE_NUMERIC "数值" +#define L10N_VALUE_TEXT "文本" +#define L10N_VALUE_DATE_TIME "时间戳" +#define L10N_VALUE_ROW_ID "数据库表格行" +#define L10N_VALUE_LIST "列表" +#define L10N_VALUE_DICT "字典" +#define L10N_VALUE_OBJECT "对象" +#define L10N_VALUE_SALE_RECORD "销售记录" +#define L10N_VALUE_PURCHASE_RECORD "采购记录" + +#define L10N_VALUE_PROPERTY_CUSTOMER "客户" +#define L10N_VALUE_PROPERTY_DEADLINE "交货期限" +#define L10N_VALUE_PROPERTY_FACTORY "工厂" +#define L10N_VALUE_PROPERTY_ORDER_TIME "下单时间" +#define L10N_VALUE_PROPERTY_DELIVERY_TIME "交货时间" + +#define L10N_WORKFLOW_KIND_INPUT "输入节点" +#define L10N_WORKFLOW_KIND_TRANSFORM "计算节点" +#define L10N_WORKFLOW_KIND_OUTPUT "输出节点" + +#define L10N_WORKFLOW_ADD "加法" +#define L10N_WORKFLOW_SUB "减法" +#define L10N_WORKFLOW_MUL "乘法" +#define L10N_WORKFLOW_DIV "除法" +#define L10N_WORKFLOW_EVAL "对表达式求值" +#define L10N_WORKFLOW_FMT "格式化文本" +#define L10N_WORKFLOW_INSTANTIATE_TEMPLATE "实例化文档" +#define L10N_WORKFLOW_FORM_INPUT "表单输入" +#define L10N_WORKFLOW_DB_INPUT "数据库输入" + +#define L10N_WORKFLOW_CATEGORY_NUMERIC "数字" +#define L10N_WORKFLOW_CATEGORY_TEXT "文本" +#define L10N_WORKFLOW_CATEGORY_DOCUMENT "文档" +#define L10N_WORKFLOW_CATEGORY_USER_INPUT "用户输入" +#define L10N_WORKFLOW_CATEGORY_SYS_INPUT "环境输入" +#define L10N_WORKFLOW_CATEGORY_OUTPUT "输出" + +#define L10N_WORKFLOW_RTERROR_DIV_BY_0 "错误:除数为0" + +#define L10N_TEMPLATE_TABLE "表格模板" + +#define L10N_TABLE "表格" +#define L10N_TABLE_CONFIGURE_PROPERTIES "编辑表格属性..." +#define L10N_TABLE_PROPERTIES "表格属性" +#define L10N_TABLE_EDIT_TABLE "编辑表格" +#define L10N_TABLE_EDIT_RESIZE_COLS "调整列宽度" +#define L10N_TABLE_EDIT_RESIZE_ROWS "调整行高度" +#define L10N_TABLE_WIDTH "宽度" +#define L10N_TABLE_HEIGHT "长度" +#define L10N_TABLE_SINGLE_PARAMS "参数" +#define L10N_TABLE_ARRAY_GROUPS "列表参数组" +#define L10N_TABLE_CELL "单元格" +#define L10N_TABLE_CELL_POS "位置:%s%s" +#define L10N_TABLE_CELL_TYPE_CONST "类型:普通单元格" +#define L10N_TABLE_CELL_TYPE_PARAM "类型:参数单元格" +#define L10N_TABLE_CELL_TYPE_CREATE_AG "类型:列表参数组" +#define L10N_TABLE_CELL_CONV_CONST "转换为普通单元格" +#define L10N_TABLE_CELL_CONV_PARAM "转换为参数单元格" +#define L10N_TABLE_CELL_CONV_CREATE_AG "创建列表参数组" +#define L10N_TABLE_CELL_CONV_ADD_AG_LEFT "添加至左侧的列表参数组" +#define L10N_TABLE_CELL_CONV_ADD_AG_RIGHT "添加至右侧的列表参数组" +#define L10N_TABLE_CELL_SELECT_MSG "请单击选择一个单元格以编辑其属性" +#define L10N_TABLE_CELL_HORIZONTAL_ALIGNMENT "水平对齐" +#define L10N_TABLE_CELL_CONTENT "内容" +#define L10N_TABLE_CELL_VAR_NAME "变量名" +#define L10N_TABLE_CELL_VAR_TOOLTIP "参数单元格的唯一名称(表格内不得重复)。" +#define L10N_TABLE_CELL_ARRAY_VAR_TOOLTIP "列表参数组内单个参数单元格的名称,在组内唯一(不同的参数组可以包含相同的参数名)。" +#define L10N_TABLE_CELL_VAR_NAME_DUP "参数名已重复" +#define L10N_TABLE_CELL_VERTICAL_ALIGNMENT "垂直对齐" +#define L10N_TABLE_CELL_ALIGN_LEFT "左对齐" +#define L10N_TABLE_CELL_ALIGN_CENTER "居中" +#define L10N_TABLE_CELL_ALIGN_RIGHT "右对齐" +#define L10N_TABLE_CELL_ALIGN_TOP "顶部对齐" +#define L10N_TABLE_CELL_ALIGN_MIDDLE "居中" +#define L10N_TABLE_CELL_ALIGN_BOTTOM "底部对齐" diff --git a/app/source/Cplt/Model/Assets.cpp b/app/source/Cplt/Model/Assets.cpp new file mode 100644 index 0000000..0dfe847 --- /dev/null +++ b/app/source/Cplt/Model/Assets.cpp @@ -0,0 +1,306 @@ +#include "Assets.hpp" + +#include <Cplt/UI/UI.hpp> +#include <Cplt/Utils/I18n.hpp> +#include <Cplt/Utils/IO/DataStream.hpp> +#include <Cplt/Utils/IO/StringIntegration.hpp> +#include <Cplt/Utils/IO/UuidIntegration.hpp> + +#include <IconsFontAwesome.h> +#include <imgui.h> +#include <imgui_stdlib.h> +#include <tsl/array_map.h> +#include <string> +#include <utility> + +using namespace std::literals::string_view_literals; +namespace fs = std::filesystem; + +template <class TSavedAsset, class TStream> +void OperateStreamForSavedAsset(TSavedAsset& cell, TStream& proxy) +{ + proxy.template ObjectAdapted<DataStreamAdapters::String>(cell.Name); + proxy.template ObjectAdapted<DataStreamAdapters::Uuid>(cell.Uuid); + proxy.Value(cell.Payload); +} + +void SavedAsset::ReadFromDataStream(InputDataStream& stream) +{ + ::OperateStreamForSavedAsset(*this, stream); +} + +void SavedAsset::WriteToDataStream(OutputDataStream& stream) const +{ + ::OperateStreamForSavedAsset(*this, stream); +} + +Asset::Asset() = default; + +class AssetList::Private +{ +public: + Project* ConnectedProject; + tsl::array_map<char, SavedAsset> Assets; + tsl::array_map<char, std::unique_ptr<Asset>> Cache; + int CacheSizeLimit = 0; + + struct + { + std::string NewName; + NameSelectionError NewNameError = NameSelectionError::Empty; + + void ShowErrors() const + { + switch (NewNameError) { + case NameSelectionError::None: break; + case NameSelectionError::Duplicated: + ImGui::ErrorMessage(I18N_TEXT("Duplicate name", L10N_DUPLICATE_NAME_ERROR)); + break; + case NameSelectionError::Empty: + ImGui::ErrorMessage(I18N_TEXT("Name cannot be empty", L10N_EMPTY_NAME_ERROR)); + break; + } + } + + bool HasErrors() const + { + return NewNameError != NameSelectionError::None; + } + + void Validate(const AssetList& self) + { + if (NewName.empty()) { + NewNameError = NameSelectionError::Empty; + return; + } + + if (self.FindByName(NewName)) { + NewNameError = NameSelectionError::Duplicated; + return; + } + + NewNameError = NameSelectionError::None; + } + } PopupPrivateState; +}; + +AssetList::AssetList(Project& project) + : mPrivate{ std::make_unique<Private>() } +{ + mPrivate->ConnectedProject = &project; +} + +// Write an empty destructor here so std::unique_ptr's destructor can see AssetList::Private's implementation +AssetList::~AssetList() +{ +} + +Project& AssetList::GetConnectedProject() const +{ + return *mPrivate->ConnectedProject; +} + +void AssetList::Reload() +{ + // TODO fix asset dicovery loading + mPrivate->Assets.clear(); + mPrivate->Cache.clear(); + DiscoverFiles([this](SavedAsset asset) -> void { + mPrivate->Assets.insert(asset.Name, std::move(asset)); + }); +} + +int AssetList::GetCount() const +{ + return mPrivate->Assets.size(); +} + +const tsl::array_map<char, SavedAsset>& AssetList::GetAssets() const +{ + return mPrivate->Assets; +} + +const SavedAsset* AssetList::FindByName(std::string_view name) const +{ + auto iter = mPrivate->Assets.find(name); + if (iter != mPrivate->Assets.end()) { + return &iter.value(); + } else { + return nullptr; + } +} + +const SavedAsset& AssetList::Create(SavedAsset asset) +{ + auto [iter, DISCARD] = mPrivate->Assets.insert(asset.Name, SavedAsset{}); + auto& savedAsset = iter.value(); + + savedAsset = std::move(asset); + if (savedAsset.Uuid.is_nil()) { + savedAsset.Uuid = uuids::uuid_random_generator{}(); + } + + SaveInstance(savedAsset, nullptr); + + return savedAsset; +} + +std::unique_ptr<Asset> AssetList::CreateAndLoad(SavedAsset assetIn) +{ + auto& savedAsset = Create(std::move(assetIn)); + auto asset = std::unique_ptr<Asset>(CreateInstance(savedAsset)); + return asset; +} + +std::unique_ptr<Asset> AssetList::Load(std::string_view name) const +{ + if (auto savedAsset = FindByName(name)) { + auto asset = Load(*savedAsset); + return asset; + } else { + return nullptr; + } +} + +std::unique_ptr<Asset> AssetList::Load(const SavedAsset& asset) const +{ + return std::unique_ptr<Asset>(LoadInstance(asset)); +} + +const SavedAsset* AssetList::Rename(std::string_view oldName, std::string_view newName) +{ + auto iter = mPrivate->Assets.find(oldName); + if (iter == mPrivate->Assets.end()) return nullptr; + + auto info = std::move(iter.value()); + info.Name = newName; + + RenameInstanceOnDisk(info, oldName); + + mPrivate->Assets.erase(iter); + auto [newIter, DISCARD] = mPrivate->Assets.insert(newName, std::move(info)); + + return &newIter.value(); +} + +bool AssetList::Remove(std::string_view name) +{ + auto iter = mPrivate->Assets.find(name); + if (iter == mPrivate->Assets.end()) { + return false; + } + auto& asset = iter.value(); + + fs::remove(RetrievePathFromAsset(asset)); + mPrivate->Assets.erase(iter); + + return true; +} + +int AssetList::GetCacheSizeLimit() const +{ + return mPrivate->CacheSizeLimit; +} + +void AssetList::SetCacheSizeLimit(int limit) +{ + mPrivate->CacheSizeLimit = limit; +} + +void AssetList::DisplayIconsList(ListState& state) +{ + // TODO +} + +void AssetList::DisplayDetailsList(ListState& state) +{ + // Note: stub function remained here in case any state processing needs to be done before issuing to implementers + DisplayDetailsTable(state); +} + +void AssetList::DisplayControls(ListState& state) +{ + auto& ps = mPrivate->PopupPrivateState; + bool openedDummy = true; + + if (ImGui::Button(ICON_FA_PLUS " " I18N_TEXT("Add", L10N_ADD))) { + ImGui::OpenPopup(I18N_TEXT("Add asset wizard", L10N_ADD_ASSET_DIALOG_TITLE)); + } + if (ImGui::BeginPopupModal(I18N_TEXT("Add asset wizard", L10N_ADD_ASSET_DIALOG_TITLE), &openedDummy, ImGuiWindowFlags_AlwaysAutoResize)) { + DisplayAssetCreator(state); + ImGui::EndPopup(); + } + + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_I_CURSOR " " I18N_TEXT("Rename", L10N_RENAME), state.SelectedAsset == nullptr)) { + ImGui::OpenPopup(I18N_TEXT("Rename asset wizard", L10N_RENAME_ASSET_DIALOG_TITLE)); + } + if (ImGui::BeginPopupModal(I18N_TEXT("Rename asset wizard", L10N_RENAME_ASSET_DIALOG_TITLE), &openedDummy, ImGuiWindowFlags_AlwaysAutoResize)) { + if (ImGui::InputText(I18N_TEXT("Name", L10N_NAME), &ps.NewName)) { + ps.Validate(*this); + } + + if (ImGui::Button(I18N_TEXT("Confirm", L10N_CONFIRM), ps.HasErrors())) { + ImGui::CloseCurrentPopup(); + + auto movedAsset = Rename(state.SelectedAsset->Name, ps.NewName); + // Update the selected pointer to the new location (we mutated the map, the pointer may be invalid now) + state.SelectedAsset = movedAsset; + + ps = {}; + } + ImGui::SameLine(); + if (ImGui::Button(I18N_TEXT("Cancel", L10N_CANCEL))) { + ImGui::CloseCurrentPopup(); + } + + ps.ShowErrors(); + + ImGui::EndPopup(); + } + + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_TRASH " " I18N_TEXT("Delete", L10N_DELETE), state.SelectedAsset == nullptr)) { + ImGui::OpenPopup(I18N_TEXT("Delete asset", L10N_DELETE_ASSET_DIALOG_TITLE)); + } + if (ImGui::BeginPopupModal(I18N_TEXT("Delete asset", L10N_DELETE_ASSET_DIALOG_TITLE), &openedDummy, ImGuiWindowFlags_AlwaysAutoResize)) { + if (ImGui::Button(I18N_TEXT("Confirm", L10N_CONFIRM))) { + ImGui::CloseCurrentPopup(); + + auto& assetName = state.SelectedAsset->Name; + Remove(assetName); + + state.SelectedAsset = nullptr; + } + ImGui::SameLine(); + if (ImGui::Button(I18N_TEXT("Cancel", L10N_CANCEL))) { + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } +} + +void AssetList::DiscoverFilesByExtension(const std::function<void(SavedAsset)>& callback, const fs::path& containerDir, std::string_view extension) const +{ + for (auto entry : fs::directory_iterator(containerDir)) { + if (!entry.is_regular_file()) continue; + + // If the caller provided an extension to match against, and it doesn't equal to current file extension, skip + if (!extension.empty() && + entry.path().extension() != extension) + { + continue; + } + + callback(SavedAsset{ + .Name = RetrieveNameFromFile(entry.path()), + .Uuid = RetrieveUuidFromFile(entry.path()), + // TODO load payload + }); + } +} + +void AssetList::DiscoverFilesByHeader(const std::function<void(SavedAsset)>& callback, const fs::path& containerDir, const std::function<bool(std::istream&)>& validater) const +{ + // TODO +} diff --git a/app/source/Cplt/Model/Assets.hpp b/app/source/Cplt/Model/Assets.hpp new file mode 100644 index 0000000..d2f8570 --- /dev/null +++ b/app/source/Cplt/Model/Assets.hpp @@ -0,0 +1,130 @@ +#pragma once + +#include <Cplt/Utils/UUID.hpp> +#include <Cplt/fwd.hpp> + +#include <tsl/array_map.h> +#include <filesystem> +#include <iosfwd> +#include <memory> +#include <string_view> + +/// A structure representing a ready-to-be-loaded asset, locating on the disk. +/// Each asset should be identified by a unique uuid within the asset category (i.e. a workflow and a template can share the same uuid), +/// generated on insertion to an asset list if not given by the caller. +struct SavedAsset +{ + std::string Name; + /// UUID of this asset. This field is generated as a random UUID v4 upon insertion into an AssetList, if not already provided by the caller (indicated by !is_nil()). + uuids::uuid Uuid; + /// Extra data to be used by the AssetList/Asset implementation. + uint64_t Payload; + + void ReadFromDataStream(InputDataStream& stream); + void WriteToDataStream(OutputDataStream& stream) const; +}; + +class Asset +{ +public: + Asset(); + virtual ~Asset() = default; +}; + +enum class NameSelectionError +{ + None, + Duplicated, + Empty, +}; + +class AssetList +{ +private: + class Private; + std::unique_ptr<Private> mPrivate; + +public: + AssetList(Project& project); + virtual ~AssetList(); + + Project& GetConnectedProject() const; + + // TODO support file watches + void Reload(); + + int GetCount() const; + // TODO convert to custom iterable + const tsl::array_map<char, SavedAsset>& GetAssets() const; + + const SavedAsset* FindByName(std::string_view name) const; + const SavedAsset& Create(SavedAsset asset); + std::unique_ptr<Asset> CreateAndLoad(SavedAsset asset); + /// Load the asset on disk by its name. + std::unique_ptr<Asset> Load(std::string_view name) const; + /// Load the asset on disk by a reference to its SavedAsset instance. This function assumes that the given SavedAsset + /// is stored in AssetList, otherwise the behavior is undefined. + std::unique_ptr<Asset> Load(const SavedAsset& asset) const; + const SavedAsset* Rename(std::string_view oldName, std::string_view newName); + bool Remove(std::string_view name); + + int GetCacheSizeLimit() const; + void SetCacheSizeLimit(int limit); + + struct ListState + { + const SavedAsset* SelectedAsset = nullptr; + }; + void DisplayIconsList(ListState& state); + void DisplayDetailsList(ListState& state); + void DisplayControls(ListState& state); + +protected: + virtual void DiscoverFiles(const std::function<void(SavedAsset)>& callback) const = 0; + + // Helper + void DiscoverFilesByExtension(const std::function<void(SavedAsset)>& callback, const std::filesystem::path& containerDir, std::string_view extension) const; + void DiscoverFilesByHeader(const std::function<void(SavedAsset)>& callback, const std::filesystem::path& containerDir, const std::function<bool(std::istream&)>& validater) const; + + /// Create an empty/default instance of this asset type on disk, potentially qualified by SavedAsset::Payload. + /// Return `true` on success and `false` on failure. + virtual bool SaveInstance(const SavedAsset& assetInfo, const Asset* asset) const = 0; + /// The returned pointer indicate ownership to the object. + virtual Asset* LoadInstance(const SavedAsset& assetInfo) const = 0; + /// Create an empty/default instance of this asset type, potentially qualified by SavedAsset::Payload. + /// The returned pointer indicate ownership to the object. + virtual Asset* CreateInstance(const SavedAsset& assetInfo) const = 0; + virtual bool RenameInstanceOnDisk(const SavedAsset& assetInfo, std::string_view oldName) const = 0; + + virtual std::string RetrieveNameFromFile(const std::filesystem::path& file) const = 0; + virtual uuids::uuid RetrieveUuidFromFile(const std::filesystem::path& file) const = 0; + virtual std::filesystem::path RetrievePathFromAsset(const SavedAsset& asset) const = 0; + + virtual void DisplayAssetCreator(ListState& state) = 0; + virtual void DisplayDetailsTable(ListState& state) const = 0; +}; + +template <class T> +class AssetListTyped : public AssetList +{ +public: + using AssetList::AssetList; + +#pragma clang diagnostic push +#pragma ide diagnostic ignored "HidingNonVirtualFunction" + std::unique_ptr<T> CreateAndLoad(SavedAsset asset) + { + return std::unique_ptr<T>(static_cast<T*>(AssetList::CreateAndLoad(asset).release())); + } + + std::unique_ptr<T> Load(std::string_view name) const + { + return std::unique_ptr<T>(static_cast<T*>(AssetList::Load(name).release())); + } + + std::unique_ptr<T> Load(const SavedAsset& asset) const + { + return std::unique_ptr<T>(static_cast<T*>(AssetList::Load(asset).release())); + } +#pragma clang diagnostic pop +}; diff --git a/app/source/Cplt/Model/Database.cpp b/app/source/Cplt/Model/Database.cpp new file mode 100644 index 0000000..07c6e36 --- /dev/null +++ b/app/source/Cplt/Model/Database.cpp @@ -0,0 +1,163 @@ +#include "Database.hpp" + +#include <Cplt/Model/Project.hpp> + +#include <filesystem> +#include <stdexcept> + +namespace fs = std::filesystem; + +SalesTable::SalesTable(MainDatabase& db) + // language=SQLite + : GetRowCount(db.GetSQLite(), "SELECT Count(*) FROM Sales") + // language=SQLite + , GetRows(db.GetSQLite(), "SELECT * FROM Sales LIMIT ? OFFSET ?") + // language=SQLite + , GetItems(db.GetSQLite(), "SELECT * FROM SalesItems WHERE SaleId == ?") +{ +} + +PurchasesTable::PurchasesTable(MainDatabase& db) + // language=SQLite + : GetRowCount(db.GetSQLite(), "SELECT Count(*) FROM Purchases") + // language=SQLite + , GetRows(db.GetSQLite(), "SELECT * FROM Purchases LIMIT ? OFFSET ?") + // language=SQLite + , GetItems(db.GetSQLite(), "SELECT * FROM PurchasesItems WHERE PurchaseId == ?") +{ +} + +DeliveryTable::DeliveryTable(MainDatabase& db) + // language=SQLite + : FilterByTypeAndId(db.GetSQLite(), "SELECT * FROM Deliveries WHERE AssociatedOrder == ? AND Outgoing = ?") + // language=SQLite + , GetItems(db.GetSQLite(), "SELECT * FROM DeliveriesItems WHERE DeliveryId == ?") +{ +} + +static std::string GetDatabaseFilePath(const Project& project) +{ + auto dbsDir = project.GetPath() / "databases"; + fs::create_directories(dbsDir); + + auto dbFile = dbsDir / "transactions.sqlite3"; + return dbFile.string(); +} + +/// Wrapper for SQLite::Database that creates the default tables +MainDatabase::DatabaseWrapper::DatabaseWrapper(MainDatabase& self) + : mSqlite(GetDatabaseFilePath(*self.mProject), SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE) +{ + // If this table doesn't exist, the database probably just got initialized + if (mSqlite.tableExists("Sales")) { + return; + } + + // 'Sales' schema + // - Customer: the customer item ID + // - Deadline: unix epoch time of order deadline + // - DeliveryTime: the time this order was completed (through a set of deliveries) + + // 'Purchases' schema + // - Factory: the factory id, + // - OrderTime: the time this order was made + // - DeliveryTime: the time this order was completed (through a set of deliveries) + + // 'Deliveries' schema + // - ShipmentTime: unix epoch time stamp of sending to delivery + // - ArrivalTime: unix epoch time stamp of delivery arrived at warehouse; 0 if not arrived yet + // - AssociatedOrder: Id of the order that this delivery is completing (which table: Outgoing=true -> Sales, Outgoing=false -> Purchases) + // - Outgoing: true if the delivery is from warehouse to customer; false if the delivery is from factory to warehouse + + // Note: the 'Id' key would be unique (not recycled after row deletion) because it's explicit + // https://www.sqlite.org/rowidtable.html + + // language=SQLite + mSqlite.exec(R"""( +CREATE TABLE IF NOT EXISTS Sales( + Id INT PRIMARY KEY, + Customer INT, + Deadline DATETIME, + DeliveryTime DATETIME +); +CREATE TABLE IF NOT EXISTS SalesItems( + SaleId INT, + ItemId INT, + Count INT +); + +CREATE TABLE IF NOT EXISTS Purchases( + Id INT PRIMARY KEY, + Factory INT, + OrderTime DATETIME, + DeliveryTime DATETIME +); +CREATE TABLE IF NOT EXISTS PurchasesItems( + PurchaseId INT, + ItemId INT, + Count INT +); + +CREATE TABLE IF NOT EXISTS Deliveries( + Id INT PRIMARY KEY, + ShipmentTime DATETIME, + ArrivalTime DATETIME, + AssociatedOrder INT, + Outgoing BOOLEAN +); +CREATE TABLE IF NOT EXISTS DeliveriesItems( + DeliveryId INT, + ItemId INT, + Count INT +); +)"""); +} + +MainDatabase::MainDatabase(Project& project) + : mProject{ &project } + , mDbWrapper(*this) + , mSales(*this) + , mPurchases(*this) + , mDeliveries(*this) +{ +} + +const SQLite::Database& MainDatabase::GetSQLite() const +{ + return mDbWrapper.mSqlite; +} + +SQLite::Database& MainDatabase::GetSQLite() +{ + return mDbWrapper.mSqlite; +} + +const SalesTable& MainDatabase::GetSales() const +{ + return mSales; +} + +SalesTable& MainDatabase::GetSales() +{ + return mSales; +} + +const PurchasesTable& MainDatabase::GetPurchases() const +{ + return mPurchases; +} + +PurchasesTable& MainDatabase::GetPurchases() +{ + return mPurchases; +} + +const DeliveryTable& MainDatabase::GetDeliveries() const +{ + return mDeliveries; +} + +DeliveryTable& MainDatabase::GetDeliveries() +{ + return mDeliveries; +} diff --git a/app/source/Cplt/Model/Database.hpp b/app/source/Cplt/Model/Database.hpp new file mode 100644 index 0000000..222e43d --- /dev/null +++ b/app/source/Cplt/Model/Database.hpp @@ -0,0 +1,79 @@ +#pragma once + +#include <Cplt/fwd.hpp> + +#include <SQLiteCpp/Database.h> +#include <SQLiteCpp/Statement.h> +#include <cstdint> + +enum class TableKind +{ + Sales, + SalesItems, + Purchases, + PurchasesItems, + Deliveries, + DeliveriesItems, +}; + +class SalesTable +{ +public: + SQLite::Statement GetRowCount; + SQLite::Statement GetRows; + SQLite::Statement GetItems; + +public: + SalesTable(MainDatabase& db); +}; + +class PurchasesTable +{ +public: + SQLite::Statement GetRowCount; + SQLite::Statement GetRows; + SQLite::Statement GetItems; + +public: + PurchasesTable(MainDatabase& db); +}; + +class DeliveryTable +{ +public: + SQLite::Statement FilterByTypeAndId; + SQLite::Statement GetItems; + +public: + DeliveryTable(MainDatabase& db); +}; + +class MainDatabase +{ +private: + class DatabaseWrapper + { + public: + SQLite::Database mSqlite; + DatabaseWrapper(MainDatabase& self); + }; + + Project* mProject; + DatabaseWrapper mDbWrapper; + SalesTable mSales; + PurchasesTable mPurchases; + DeliveryTable mDeliveries; + +public: + MainDatabase(Project& project); + + const SQLite::Database& GetSQLite() const; + SQLite::Database& GetSQLite(); + + const SalesTable& GetSales() const; + SalesTable& GetSales(); + const PurchasesTable& GetPurchases() const; + PurchasesTable& GetPurchases(); + const DeliveryTable& GetDeliveries() const; + DeliveryTable& GetDeliveries(); +}; diff --git a/app/source/Cplt/Model/Filter.cpp b/app/source/Cplt/Model/Filter.cpp new file mode 100644 index 0000000..1e4b31b --- /dev/null +++ b/app/source/Cplt/Model/Filter.cpp @@ -0,0 +1 @@ +#include "Filter.hpp" diff --git a/app/source/Cplt/Model/Filter.hpp b/app/source/Cplt/Model/Filter.hpp new file mode 100644 index 0000000..1b923e1 --- /dev/null +++ b/app/source/Cplt/Model/Filter.hpp @@ -0,0 +1,6 @@ +#pragma once + +class TableRowsFilter +{ + // TODO +}; diff --git a/app/source/Cplt/Model/GlobalStates.cpp b/app/source/Cplt/Model/GlobalStates.cpp new file mode 100644 index 0000000..417514f --- /dev/null +++ b/app/source/Cplt/Model/GlobalStates.cpp @@ -0,0 +1,163 @@ +#include "GlobalStates.hpp" + +#include <Cplt/Model/Project.hpp> +#include <Cplt/Utils/StandardDirectories.hpp> + +#include <json/reader.h> +#include <json/value.h> +#include <json/writer.h> +#include <cassert> +#include <filesystem> +#include <fstream> +#include <memory> + +namespace fs = std::filesystem; + +static std::unique_ptr<GlobalStates> globalStateInstance; +static fs::path globalDataPath; + +void GlobalStates::Init() +{ + Init(StandardDirectories::UserData() / "cplt"); +} + +void GlobalStates::Init(std::filesystem::path userDataDir) +{ + globalStateInstance = std::make_unique<GlobalStates>(); + globalDataPath = userDataDir; + fs::create_directories(globalDataPath); + + // Reading recent projects + { + std::ifstream ifs(globalDataPath / "recents.json"); + if (!ifs) return; + + Json::Value root; + ifs >> root; + + if (!root.isObject()) return; + if (auto& recents = root["RecentProjects"]; recents.isArray()) { + for (auto& elm : recents) { + if (!elm.isString()) continue; + + fs::path path(elm.asCString()); + if (!fs::exists(path)) continue; + + auto utf8String = path.string(); + globalStateInstance->mRecentProjects.push_back(RecentProject{ + .Path = std::move(path), + .CachedUtf8String = std::move(utf8String), + }); + } + } + } +} + +void GlobalStates::Shutdown() +{ + if (!globalStateInstance) return; + + globalStateInstance->SetCurrentProject(nullptr); + + if (globalStateInstance->mDirty) { + globalStateInstance->WriteToDisk(); + } +} + +GlobalStates& GlobalStates::GetInstance() +{ + return *globalStateInstance; +} + +const std::filesystem::path& GlobalStates::GetUserDataPath() +{ + return globalDataPath; +} + +const std::vector<GlobalStates::RecentProject>& GlobalStates::GetRecentProjects() const +{ + return mRecentProjects; +} + +void GlobalStates::ClearRecentProjects() +{ + mRecentProjects.clear(); + MarkDirty(); +} + +void GlobalStates::AddRecentProject(const Project& project) +{ + mRecentProjects.push_back(RecentProject{ + .Path = project.GetPath(), + .CachedUtf8String = project.GetPath().string(), + }); + MarkDirty(); +} + +void GlobalStates::MoveProjectToTop(const Project& project) +{ + for (auto it = mRecentProjects.begin(); it != mRecentProjects.end(); ++it) { + if (it->Path == project.GetPath()) { + std::rotate(it, it + 1, mRecentProjects.end()); + MarkDirty(); + return; + } + } + AddRecentProject(project); +} + +void GlobalStates::RemoveRecentProject(int idx) +{ + assert(idx >= 0 && idx < mRecentProjects.size()); + + mRecentProjects.erase(mRecentProjects.begin() + idx); + MarkDirty(); +} + +bool GlobalStates::HasCurrentProject() const +{ + return mCurrentProject != nullptr; +} + +Project* GlobalStates::GetCurrentProject() const +{ + return mCurrentProject.get(); +} + +void GlobalStates::SetCurrentProject(std::unique_ptr<Project> project) +{ + if (mCurrentProject) { + mCurrentProject->WriteToDisk(); + mCurrentProject = nullptr; + } + if (project) { + MoveProjectToTop(*project); + } + mCurrentProject = std::move(project); +} + +void GlobalStates::WriteToDisk() const +{ + Json::Value root; + + auto& recentProjects = root["RecentProjects"] = Json::Value(Json::arrayValue); + for (auto& [path, _] : mRecentProjects) { + recentProjects.append(Json::Value(path.string())); + } + + std::ofstream ofs(globalDataPath / "recents.json"); + ofs << root; + + mDirty = false; +} + +bool GlobalStates::IsDirty() const +{ + return mDirty; +} + +void GlobalStates::MarkDirty() +{ + mDirty = true; + OnModified(); +} diff --git a/app/source/Cplt/Model/GlobalStates.hpp b/app/source/Cplt/Model/GlobalStates.hpp new file mode 100644 index 0000000..1eb47fb --- /dev/null +++ b/app/source/Cplt/Model/GlobalStates.hpp @@ -0,0 +1,55 @@ +#pragma once + +#include <Cplt/Utils/Sigslot.hpp> +#include <Cplt/fwd.hpp> + +#include <filesystem> +#include <string> +#include <vector> + +class GlobalStates +{ +public: + static void Init(); + static void Init(std::filesystem::path userDataDir); + static void Shutdown(); + + static GlobalStates& GetInstance(); + static const std::filesystem::path& GetUserDataPath(); + + struct RecentProject + { + std::filesystem::path Path; + std::string CachedUtf8String; + }; + +public: + Signal<> OnModified; + +private: + std::vector<RecentProject> mRecentProjects; + std::unique_ptr<Project> mCurrentProject; + mutable bool mDirty = false; + +public: + const std::vector<RecentProject>& GetRecentProjects() const; + void ClearRecentProjects(); + void AddRecentProject(const Project& project); + /// Move or add the project to end of the recent projects list. + /// If the project is not in the list of recently used projects, it will be appended, otherwise + /// it will be moved to the end. + void MoveProjectToTop(const Project& project); + void RemoveRecentProject(int idx); + + bool HasCurrentProject() const; + Project* GetCurrentProject() const; + void SetCurrentProject(std::unique_ptr<Project> project); + + // TODO async autosaving to prevent data loss on crash + void WriteToDisk() const; + + bool IsDirty() const; + +private: + void MarkDirty(); +}; diff --git a/app/source/Cplt/Model/Items.cpp b/app/source/Cplt/Model/Items.cpp new file mode 100644 index 0000000..9d2abc6 --- /dev/null +++ b/app/source/Cplt/Model/Items.cpp @@ -0,0 +1,114 @@ +#include "Items.hpp" + +const std::string& ProductItem::GetDescription() const +{ + return mDescription; +} + +void ProductItem::SetDescription(std::string description) +{ + mDescription = std::move(description); +} + +int ProductItem::GetPrice() const +{ + return mPrice; +} +void ProductItem::SetPrice(int price) +{ + mPrice = price; +} + +int ProductItem::GetStock() const +{ + return mStock; +} + +void ProductItem::SetStock(int stock) +{ + mStock = stock; +} + +Json::Value ProductItem::Serialize() const +{ + Json::Value elm; + elm["Description"] = mDescription; + elm["Price"] = mPrice; + elm["Stock"] = mStock; + return elm; +} + +void ProductItem::Deserialize(const Json::Value& elm) +{ + mDescription = elm["Description"].asString(); + mPrice = elm["Price"].asInt(); + mStock = elm["Stock"].asInt(); +} + +const std::string& FactoryItem::GetDescription() const +{ + return mDescription; +} + +void FactoryItem::SetDescription(std::string description) +{ + mDescription = std::move(description); +} + +const std::string& FactoryItem::GetEmail() const +{ + return mEmail; +} + +void FactoryItem::SetEmail(std::string email) +{ + mEmail = std::move(email); +} + +Json::Value FactoryItem::Serialize() const +{ + Json::Value elm; + elm["Description"] = mDescription; + elm["Email"] = mEmail; + return elm; +} + +void FactoryItem::Deserialize(const Json::Value& elm) +{ + mDescription = elm["Description"].asString(); + mEmail = elm["Email"].asString(); +} + +const std::string& CustomerItem::GetDescription() const +{ + return mDescription; +} + +void CustomerItem::SetDescription(std::string description) +{ + mDescription = std::move(description); +} + +const std::string& CustomerItem::GetEmail() const +{ + return mEmail; +} + +void CustomerItem::SetEmail(std::string email) +{ + mEmail = std::move(email); +} + +Json::Value CustomerItem::Serialize() const +{ + Json::Value elm; + elm["Description"] = mDescription; + elm["Email"] = mEmail; + return elm; +} + +void CustomerItem::Deserialize(const Json::Value& elm) +{ + mDescription = elm["Description"].asString(); + mEmail = elm["Email"].asString(); +} diff --git a/app/source/Cplt/Model/Items.hpp b/app/source/Cplt/Model/Items.hpp new file mode 100644 index 0000000..c00ee59 --- /dev/null +++ b/app/source/Cplt/Model/Items.hpp @@ -0,0 +1,253 @@ +#pragma once + +#include <Cplt/fwd.hpp> + +#include <json/reader.h> +#include <json/value.h> +#include <json/writer.h> +#include <tsl/array_map.h> +#include <cstddef> +#include <limits> +#include <stdexcept> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + +template <class T> +class ItemList +{ +private: + std::vector<T> mStorage; + tsl::array_map<char, size_t> mNameLookup; + +public: + template <class... Args> + T& Insert(std::string name, Args... args) + { + auto iter = mNameLookup.find(name); + if (iter != mNameLookup.end()) { + throw std::runtime_error("Duplicate key."); + } + + for (size_t i = 0; i < mStorage.size(); ++i) { + if (mStorage[i].IsInvalid()) { + mStorage[i] = T(*this, i, std::move(name), std::forward<Args>(args)...); + mNameLookup.insert(name, i); + return mStorage[i]; + } + } + + size_t id = mStorage.size(); + mNameLookup.insert(name, id); + mStorage.emplace_back(*this, id, std::move(name), std::forward<Args>(args)...); + return mStorage[id]; + } + + void Remove(size_t index) + { + auto& item = mStorage[index]; + mNameLookup.erase(item.GetName()); + mStorage[index] = T(*this); + } + + T* Find(size_t id) + { + return &mStorage[id]; + } + + const T* Find(size_t id) const + { + return &mStorage[id]; + } + + const T* Find(std::string_view name) const + { + auto iter = mNameLookup.find(name); + if (iter != mNameLookup.end()) { + return &mStorage[iter.value()]; + } else { + return nullptr; + } + } + + Json::Value Serialize() const + { + Json::Value items(Json::arrayValue); + for (auto& item : mStorage) { + if (!item.IsInvalid()) { + auto elm = item.Serialize(); + elm["Id"] = item.GetId(); + elm["Name"] = item.GetName(); + items.append(elm); + } + } + + Json::Value root; + root["MaxItemId"] = mStorage.size(); + root["Items"] = std::move(items); + + return root; + } + + ItemList() = default; + + ItemList(const Json::Value& root) + { + constexpr const char* kMessage = "Failed to load item list from JSON."; + + auto& itemCount = root["MaxItemId"]; + if (!itemCount.isIntegral()) throw std::runtime_error(kMessage); + + mStorage.resize(itemCount.asInt64(), T(*this)); + + auto& items = root["Items"]; + if (!items.isArray()) throw std::runtime_error(kMessage); + + for (auto& elm : items) { + if (!elm.isObject()) throw std::runtime_error(kMessage); + + auto& id = elm["Id"]; + if (!id.isIntegral()) throw std::runtime_error(kMessage); + auto& name = elm["Name"]; + if (!name.isString()) throw std::runtime_error(kMessage); + + size_t iid = id.asInt64(); + mStorage[iid] = T(*this, iid, name.asString()); + mStorage[iid].Deserialize(elm); + } + } + + typename decltype(mStorage)::iterator begin() + { + return mStorage.begin(); + } + + typename decltype(mStorage)::iterator end() + { + return mStorage.end(); + } + + typename decltype(mStorage)::const_iterator begin() const + { + return mStorage.begin(); + } + + typename decltype(mStorage)::const_iterator end() const + { + return mStorage.end(); + } + +private: + template <class TSelf> + friend class ItemBase; + + void UpdateItemName(const T& item, const std::string& newName) + { + mNameLookup.erase(item.GetName()); + mNameLookup.insert(newName, item.GetId()); + } +}; + +template <class TSelf> +class ItemBase +{ +private: + ItemList<TSelf>* mList; + size_t mId; + std::string mName; + +public: + ItemBase(ItemList<TSelf>& list, size_t id = std::numeric_limits<size_t>::max(), std::string name = "") + : mList{ &list } + , mId{ id } + , mName{ std::move(name) } + { + } + + bool IsInvalid() const + { + return mId == std::numeric_limits<size_t>::max(); + } + + ItemList<TSelf>& GetList() const + { + return *mList; + } + + size_t GetId() const + { + return mId; + } + + const std::string& GetName() const + { + return mName; + } + + void SetName(std::string name) + { + mList->UpdateItemName(static_cast<TSelf&>(*this), name); + mName = std::move(name); + } +}; + +class ProductItem : public ItemBase<ProductItem> +{ +private: + std::string mDescription; + int mPrice = 0; + int mStock = 0; + +public: + using ItemBase::ItemBase; + + const std::string& GetDescription() const; + void SetDescription(std::string description); + /// Get the price of this item in US cents. + int GetPrice() const; + void SetPrice(int price); + /// Get the current number of this product in warehouse. + /// This is a housekeeping field and shouldn't be editable by the user from the UI. + int GetStock() const; + void SetStock(int stock); + + Json::Value Serialize() const; + void Deserialize(const Json::Value& elm); +}; + +class FactoryItem : public ItemBase<FactoryItem> +{ +private: + std::string mDescription; + std::string mEmail; + +public: + using ItemBase::ItemBase; + + const std::string& GetDescription() const; + void SetDescription(std::string description); + const std::string& GetEmail() const; + void SetEmail(std::string email); + + Json::Value Serialize() const; + void Deserialize(const Json::Value& elm); +}; + +class CustomerItem : public ItemBase<CustomerItem> +{ +private: + std::string mDescription; + std::string mEmail; + +public: + using ItemBase::ItemBase; + + const std::string& GetDescription() const; + void SetDescription(std::string description); + const std::string& GetEmail() const; + void SetEmail(std::string email); + + Json::Value Serialize() const; + void Deserialize(const Json::Value& elm); +}; diff --git a/app/source/Cplt/Model/Project.cpp b/app/source/Cplt/Model/Project.cpp new file mode 100644 index 0000000..a1e9bab --- /dev/null +++ b/app/source/Cplt/Model/Project.cpp @@ -0,0 +1,168 @@ +#include "Project.hpp" + +#include <Cplt/Model/Workflow/Workflow.hpp> +#include <Cplt/Utils/Macros.hpp> + +#include <json/reader.h> +#include <json/value.h> +#include <json/writer.h> +#include <filesystem> +#include <fstream> +#include <stdexcept> +#include <utility> + +namespace fs = std::filesystem; + +template <class T> +static void ReadItemList(ItemList<T>& list, const fs::path& filePath) +{ + std::ifstream ifs(filePath); + if (ifs) { + Json::Value root; + ifs >> root; + + list = ItemList<T>(root); + } +} + +static void CreateProjectSubfolders(const Project& project) +{ + fs::create_directory(project.GetDatabasesDirectory()); + fs::create_directory(project.GetItemsDirectory()); + fs::create_directory(project.GetWorkflowsDirectory()); + fs::create_directory(project.GetTemplatesDirectory()); +} + +Project::Project(fs::path rootPath) + : mRootPath{ std::move(rootPath) } + , mRootPathString{ mRootPath.string() } + , Workflows(*this) + , Templates(*this) + , Database(*this) +{ + // TODO better diagnostic + const char* kInvalidFormatErr = "Failed to load project: invalid format."; + + std::ifstream ifs(mRootPath / "cplt_project.json"); + if (!ifs) { + std::string message; + message += "Failed to load project file at '"; + message += mRootPath.string(); + message += "'."; + throw std::runtime_error(message); + } + + { + Json::Value root; + ifs >> root; + + const auto& croot = root; // Use const reference so that accessors default to returning a null if not found, instead of silently creating new elements + if (!croot.isObject()) { + throw std::runtime_error(kInvalidFormatErr); + } + + if (auto& name = croot["Name"]; name.isString()) { + mName = name.asString(); + } else { + throw std::runtime_error(kInvalidFormatErr); + } + } + + CreateProjectSubfolders(*this); + + auto itemsDir = mRootPath / "items"; + ReadItemList(Products, itemsDir / "products.json"); + ReadItemList(Factories, itemsDir / "factories.json"); + ReadItemList(Customers, itemsDir / "customers.json"); + + Workflows.Reload(); + Templates.Reload(); +} + +Project::Project(fs::path rootPath, std::string name) + : mRootPath{ std::move(rootPath) } + , mRootPathString{ mRootPath.string() } + , mName{ std::move(name) } + , Workflows(*this) + , Templates(*this) + , Database(*this) +{ + CreateProjectSubfolders(*this); +} + +const fs::path& Project::GetPath() const +{ + return mRootPath; +} + +const std::string& Project::GetPathString() const +{ + return mRootPathString; +} + +fs::path Project::GetDatabasesDirectory() const +{ + return mRootPath / "databases"; +} + +fs::path Project::GetItemsDirectory() const +{ + return mRootPath / "items"; +} + +fs::path Project::GetWorkflowsDirectory() const +{ + return mRootPath / "workflows"; +} + +fs::path Project::GetWorkflowPath(std::string_view name) const +{ + return (mRootPath / "workflows" / name).concat(".cplt-workflow"); +} + +fs::path Project::GetTemplatesDirectory() const +{ + return mRootPath / "templates"; +} + +fs::path Project::GetTemplatePath(std::string_view name) const +{ + return (mRootPath / "templates" / name).concat(".cplt-template"); +} + +const std::string& Project::GetName() const +{ + return mName; +} + +void Project::SetName(std::string name) +{ + mName = std::move(name); +} + +Json::Value Project::Serialize() +{ + Json::Value root(Json::objectValue); + + root["Name"] = mName; + + return root; +} + +template <class T> +static void WriteItemList(ItemList<T>& list, const fs::path& filePath) +{ + std::ofstream ofs(filePath); + ofs << list.Serialize(); +} + +void Project::WriteToDisk() +{ + std::ofstream ofs(mRootPath / "cplt_project.json"); + ofs << this->Serialize(); + + auto itemsDir = GetItemsDirectory(); + WriteItemList(Products, itemsDir / "products.json"); + WriteItemList(Factories, itemsDir / "factories.json"); + WriteItemList(Customers, itemsDir / "customers.json"); +} diff --git a/app/source/Cplt/Model/Project.hpp b/app/source/Cplt/Model/Project.hpp new file mode 100644 index 0000000..8119a97 --- /dev/null +++ b/app/source/Cplt/Model/Project.hpp @@ -0,0 +1,57 @@ +#pragma once + +#include <Cplt/Model/Assets.hpp> +#include <Cplt/Model/Database.hpp> +#include <Cplt/Model/Items.hpp> +#include <Cplt/Model/Template/Template.hpp> +#include <Cplt/Model/Workflow/Workflow.hpp> + +#include <json/forwards.h> +#include <tsl/array_map.h> +#include <filesystem> +#include <string> +#include <string_view> + +class Project +{ +private: + std::filesystem::path mRootPath; + std::string mRootPathString; + std::string mName; + + // (Exception to style guidelines) + // This is put after the private fields, so that when XxxDatabase's constructor runs, all of them will be initialized +public: + WorkflowAssetList Workflows; + TemplateAssetList Templates; + ItemList<ProductItem> Products; + ItemList<FactoryItem> Factories; + ItemList<CustomerItem> Customers; + MainDatabase Database; + +public: + /// Load the project from a directory containing the cplt_project.json file. + /// This only loads the main project file, the caller needs to + Project(std::filesystem::path rootPath); + + /// Create a project with the given name in the given path. Note that the path should be a directory that will contain the project files once created. + /// This function assumes the given directory will exist and is empty. + Project(std::filesystem::path rootPath, std::string name); + + /// Path to a *directory* that contains the project file. + const std::filesystem::path& GetPath() const; + const std::string& GetPathString() const; + + std::filesystem::path GetDatabasesDirectory() const; + std::filesystem::path GetItemsDirectory() const; + std::filesystem::path GetWorkflowsDirectory() const; + std::filesystem::path GetWorkflowPath(std::string_view name) const; + std::filesystem::path GetTemplatesDirectory() const; + std::filesystem::path GetTemplatePath(std::string_view name) const; + + const std::string& GetName() const; + void SetName(std::string name); + + Json::Value Serialize(); + void WriteToDisk(); +}; diff --git a/app/source/Cplt/Model/Template/TableTemplate.cpp b/app/source/Cplt/Model/Template/TableTemplate.cpp new file mode 100644 index 0000000..5cd9ed8 --- /dev/null +++ b/app/source/Cplt/Model/Template/TableTemplate.cpp @@ -0,0 +1,591 @@ +#include "TableTemplate.hpp" + +#include <Cplt/Utils/IO/StringIntegration.hpp> +#include <Cplt/Utils/IO/TslArrayIntegration.hpp> +#include <Cplt/Utils/IO/VectorIntegration.hpp> + +#include <xlsxwriter.h> +#include <algorithm> +#include <charconv> +#include <cstddef> +#include <cstdint> +#include <iostream> +#include <map> + +bool TableCell::IsDataHoldingCell() const +{ + return IsPrimaryCell() || !IsMergedCell(); +} + +bool TableCell::IsPrimaryCell() const +{ + return PrimaryCellLocation == Location; +} + +bool TableCell::IsMergedCell() const +{ + return PrimaryCellLocation.x == -1 || PrimaryCellLocation.y == -1; +} + +template <class TTableCell, class TStream> +void OperateStreamForTableCell(TTableCell& cell, TStream& proxy) +{ + proxy.template ObjectAdapted<DataStreamAdapters::String>(cell.Content); + proxy.Object(cell.Location); + proxy.Object(cell.PrimaryCellLocation); + proxy.Value(cell.SpanX); + proxy.Value(cell.SpanY); + proxy.Enum(cell.HorizontalAlignment); + proxy.Enum(cell.VerticalAlignment); + proxy.Enum(cell.Type); + proxy.Value(cell.DataId); +} + +void TableCell::ReadFromDataStream(InputDataStream& stream) +{ + ::OperateStreamForTableCell(*this, stream); +} + +void TableCell::WriteToDataStream(OutputDataStream& stream) const +{ + ::OperateStreamForTableCell(*this, stream); +} + +Vec2i TableArrayGroup::GetLeftCell() const +{ + return { Row, LeftCell }; +} + +Vec2i TableArrayGroup::GetRightCell() const +{ + return { Row, RightCell }; +} + +int TableArrayGroup::GetCount() const +{ + return RightCell - LeftCell + 1; +} + +Vec2i TableArrayGroup::FindCell(std::string_view name) +{ + // TODO + return Vec2i{}; +} + +template <class TMap> +static bool UpdateElementName(TMap& map, std::string_view oldName, std::string_view newName) +{ + auto iter = map.find(oldName); + if (iter == map.end()) { + return false; + } + + auto elm = iter.value(); + auto [DISCARD, inserted] = map.insert(newName, elm); + if (!inserted) { + return false; + } + + map.erase(iter); + return true; +} + +bool TableArrayGroup::UpdateCellName(std::string_view oldName, std::string_view newName) +{ + return ::UpdateElementName(mName2Cell, oldName, newName); +} + +template <class TTableArrayGroup, class TStream> +void OperateStreamForTableArrayGroup(TTableArrayGroup& group, TStream& stream) +{ + stream.Value(group.Row); + stream.Value(group.LeftCell); + stream.Value(group.RightCell); +} + +void TableArrayGroup::ReadFromDataStream(InputDataStream& stream) +{ + ::OperateStreamForTableArrayGroup(*this, stream); +} + +void TableArrayGroup::WriteToDataStream(OutputDataStream& stream) const +{ + ::OperateStreamForTableArrayGroup(*this, stream); +} + +TableInstantiationParameters::TableInstantiationParameters(const TableTemplate& table) + : mTable{ &table } +{ +} + +TableInstantiationParameters& TableInstantiationParameters::ResetTable(const TableTemplate& newTable) +{ + mTable = &newTable; + return *this; +} + +TableInstantiationParameters TableInstantiationParameters::RebindTable(const TableTemplate& newTable) const +{ + TableInstantiationParameters result(newTable); + result.SingularCells = this->SingularCells; + result.ArrayGroups = this->ArrayGroups; + return result; +} + +const TableTemplate& TableInstantiationParameters::GetTable() const +{ + return *mTable; +} + +bool TableTemplate::IsInstance(const Template* tmpl) +{ + return tmpl->GetKind() == KD_Table; +} + +TableTemplate::TableTemplate() + : Template(KD_Table) +{ +} + +int TableTemplate::GetTableWidth() const +{ + return mColumnWidths.size(); +} + +int TableTemplate::GetTableHeight() const +{ + return mRowHeights.size(); +} + +void TableTemplate::Resize(int newWidth, int newHeight) +{ + // TODO this doesn't gracefully handle resizing to a smaller size which trims some merged cells + + std::vector<TableCell> cells; + cells.reserve(newWidth * newHeight); + + int tableWidth = GetTableWidth(); + int tableHeight = GetTableHeight(); + + for (int y = 0; y < newHeight; ++y) { + if (y >= tableHeight) { + for (int x = 0; x < newWidth; ++x) { + cells.push_back(TableCell{}); + } + continue; + } + + for (int x = 0; x < newWidth; ++x) { + if (x >= tableWidth) { + cells.push_back(TableCell{}); + } else { + auto& cell = GetCell({ x, y }); + cells.push_back(std::move(cell)); + } + } + } + + mCells = std::move(cells); + mColumnWidths.resize(newWidth, 80); + mRowHeights.resize(newHeight, 20); +} + +int TableTemplate::GetRowHeight(int row) const +{ + return mRowHeights[row]; +} + +void TableTemplate::SetRowHeight(int row, int height) +{ + mRowHeights[row] = height; +} + +int TableTemplate::GetColumnWidth(int column) const +{ + return mColumnWidths[column]; +} + +void TableTemplate::SetColumnWidth(int column, int width) +{ + mColumnWidths[column] = width; +} + +const TableCell& TableTemplate::GetCell(Vec2i pos) const +{ + int tableWidth = GetTableWidth(); + return mCells[pos.y * tableWidth + pos.x]; +} + +TableCell& TableTemplate::GetCell(Vec2i pos) +{ + return const_cast<TableCell&>(const_cast<const TableTemplate*>(this)->GetCell(pos)); +} + +void TableTemplate::SetCellType(Vec2i pos, TableCell::CellType type) +{ + auto& cell = GetCell(pos); + if (cell.Type == type) { + return; + } + + switch (cell.Type) { + // Nothing to change + case TableCell::ConstantCell: break; + + case TableCell::SingularParametricCell: + mName2Parameters.erase(cell.Content); + break; + + case TableCell::ArrayParametricCell: { + auto& ag = mArrayGroups[cell.DataId]; + if (pos.x == ag.LeftCell) { + ag.LeftCell++; + } else if (pos.x == ag.RightCell) { + ag.RightCell--; + } else { + } + } break; + } + + switch (type) { + // Nothing to do + case TableCell::ConstantCell: break; + + case TableCell::SingularParametricCell: { + int idx = pos.y * GetTableWidth() + pos.x; + auto [DISCARD, inserted] = mName2Parameters.insert(cell.Content, idx); + + // Duplicate name + if (!inserted) { + return; + } + } break; + + case TableCell::ArrayParametricCell: { + auto ptr = AddArrayGroup(pos.y, pos.x, pos.x); + + // Duplicate name + if (ptr == nullptr) { + return; + } + } break; + } + + cell.Type = type; +} + +bool TableTemplate::UpdateParameterName(std::string_view oldName, std::string_view newName) +{ + return ::UpdateElementName(mName2Parameters, oldName, newName); +} + +int TableTemplate::GetArrayGroupCount() const +{ + return mArrayGroups.size(); +} + +const TableArrayGroup& TableTemplate::GetArrayGroup(int id) const +{ + return mArrayGroups[id]; +} + +TableArrayGroup& TableTemplate::GetArrayGroup(int id) +{ + return mArrayGroups[id]; +} + +TableArrayGroup* TableTemplate::AddArrayGroup(int row, int left, int right) +{ + // size_t max value: 18446744073709551615 + // ^~~~~~~~~~~~~~~~~~~~ 20 chars + char name[20]; + auto res = std::to_chars(std::begin(name), std::end(name), mArrayGroups.size()); + std::string_view nameStr(name, res.ptr - name); + + return AddArrayGroup(nameStr, row, left, right); +} + +TableArrayGroup* TableTemplate::AddArrayGroup(std::string_view name, int row, int left, int right) +{ + assert(row >= 0 && row < GetTableHeight()); + assert(left >= 0 && left < GetTableWidth()); + assert(right >= 0 && right < GetTableWidth()); + + // TODO check for overlap + + if (left > right) { + std::swap(left, right); + } + + auto [DISCARD, inserted] = mName2ArrayGroups.insert(name, (int)mArrayGroups.size()); + if (!inserted) { + return nullptr; + } + + mArrayGroups.push_back(TableArrayGroup{ + .Row = row, + .LeftCell = left, + .RightCell = right, + }); + auto& ag = mArrayGroups.back(); + + for (int x = left; x <= right; x++) { + auto& cell = GetCell({ x, row }); + + // Update type + cell.Type = TableCell::ArrayParametricCell; + + // Insert parameter name lookup + while (true) { + auto [DISCARD, inserted] = ag.mName2Cell.insert(cell.Content, x); + if (inserted) { + break; + } + + cell.Content += "-"; + } + } + + return &ag; +} + +bool TableTemplate::UpdateArrayGroupName(std::string_view oldName, std::string_view newName) +{ + return ::UpdateElementName(mName2ArrayGroups, oldName, newName); +} + +bool TableTemplate::ExtendArrayGroupLeft(int id, int n) +{ + assert(n > 0); + + auto& ag = mArrayGroups[id]; + ag.LeftCell -= n; + + return false; +} + +bool TableTemplate::ExtendArrayGroupRight(int id, int n) +{ + assert(n > 0); + + auto& ag = mArrayGroups[id]; + ag.RightCell += n; + + return false; +} + +TableCell* TableTemplate::FindCell(std::string_view name) +{ + auto iter = mName2Parameters.find(name); + if (iter != mName2Parameters.end()) { + return &mCells[iter.value()]; + } else { + return nullptr; + } +} + +TableArrayGroup* TableTemplate::FindArrayGroup(std::string_view name) +{ + auto iter = mName2ArrayGroups.find(name); + if (iter != mName2ArrayGroups.end()) { + return &mArrayGroups[iter.value()]; + } else { + return nullptr; + } +} + +TableTemplate::MergeCellsResult TableTemplate::MergeCells(Vec2i topLeft, Vec2i bottomRight) +{ + auto SortTwo = [](int& a, int& b) { + if (a > b) { + std::swap(a, b); + } + }; + SortTwo(topLeft.x, bottomRight.x); + SortTwo(topLeft.y, bottomRight.y); + + auto ResetProgress = [&]() { + for (int y = topLeft.y; y < bottomRight.y; ++y) { + for (int x = topLeft.x; x < bottomRight.x; ++x) { + auto& cell = GetCell({ x, y }); + cell.PrimaryCellLocation = { -1, -1 }; + } + } + }; + + for (int y = topLeft.y; y < bottomRight.y; ++y) { + for (int x = topLeft.x; x < bottomRight.x; ++x) { + auto& cell = GetCell({ x, y }); + if (cell.IsMergedCell()) { + ResetProgress(); + return MCR_CellAlreadyMerged; + } + + cell.PrimaryCellLocation = topLeft; + } + } + + auto& primaryCell = GetCell(topLeft); + primaryCell.SpanX = bottomRight.x - topLeft.x; + primaryCell.SpanY = bottomRight.y - topLeft.y; + + return MCR_Success; +} + +TableTemplate::BreakCellsResult TableTemplate::BreakCells(Vec2i topLeft) +{ + auto& primaryCell = GetCell(topLeft); + if (!primaryCell.IsMergedCell()) { + return BCR_CellNotMerged; + } + + for (int dy = 0; dy < primaryCell.SpanY; ++dy) { + for (int dx = 0; dx < primaryCell.SpanX; ++dx) { + auto& cell = GetCell({ topLeft.x + dx, topLeft.y + dy }); + cell.PrimaryCellLocation = { -1, -1 }; + } + } + + primaryCell.SpanX = 1; + primaryCell.SpanY = 1; + + return BCR_Success; +} + +lxw_workbook* TableTemplate::InstantiateToExcelWorkbook(const TableInstantiationParameters& params) const +{ + auto workbook = workbook_new("Table.xlsx"); + InstantiateToExcelWorksheet(workbook, params); + return workbook; +} + +lxw_worksheet* TableTemplate::InstantiateToExcelWorksheet(lxw_workbook* workbook, const TableInstantiationParameters& params) const +{ + auto worksheet = workbook_add_worksheet(workbook, "CpltExport.xlsx"); + + // Map: row number -> length of generated ranges + std::map<int, int> generatedRanges; + + for (size_t i = 0; i < mArrayGroups.size(); ++i) { + auto& info = mArrayGroups[i]; + auto& param = params.ArrayGroups[i]; + + auto iter = generatedRanges.find(i); + if (iter != generatedRanges.end()) { + int available = iter->second; + if (available >= param.size()) { + // Current space is enough to fit in this array group, skip + continue; + } + } + + // Not enough space to fit in this array group, update (or insert) the appropriate amount of generated rows + int row = i; + int count = param.size(); + generatedRanges.try_emplace(row, count); + } + + auto GetOffset = [&](int y) -> int { + // std::find_if <values less than y> + int verticalOffset = 0; + for (auto it = generatedRanges.begin(); it != generatedRanges.end() && it->first < y; ++it) { + verticalOffset += it->second; + } + return verticalOffset; + }; + + auto WriteCell = [&](int row, int col, const TableCell& cell, const char* text) -> void { + if (cell.IsPrimaryCell()) { + int lastRow = row + cell.SpanY - 1; + int lastCol = col + cell.SpanX - 1; + // When both `string` and `format` are null, the top-left cell contents are untouched (what we just wrote in the above switch) + worksheet_merge_range(worksheet, row, col, lastRow, lastCol, text, nullptr); + } else { + worksheet_write_string(worksheet, row, col, text, nullptr); + } + }; + + // Write/instantiate all array groups + for (size_t i = 0; i < mArrayGroups.size(); ++i) { + auto& groupInfo = mArrayGroups[i]; + auto& groupParams = params.ArrayGroups[i]; + + int rowCellCount = groupInfo.GetCount(); + int rowCount = groupParams.size(); + int baseRowIdx = groupInfo.Row + GetOffset(groupInfo.Row); + + // For each row that would be generated + for (int rowIdx = 0; rowIdx < rowCount; ++rowIdx) { + auto& row = groupParams[rowIdx]; + + // For each cell in the row + for (int rowCellIdx = 0; rowCellIdx < rowCellCount; ++rowCellIdx) { + // TODO support merged cells in array groups + worksheet_write_string(worksheet, baseRowIdx + rowIdx, rowCellIdx, row[rowCellIdx].c_str(), nullptr); + } + } + } + + int tableWidth = GetTableWidth(); + int tableHeight = GetTableHeight(); + + // Write all regular and singular parameter cells + for (int y = 0; y < tableHeight; ++y) { + for (int x = 0; x < tableWidth; ++x) { + auto& cell = GetCell({ x, y }); + + if (!cell.IsDataHoldingCell()) { + continue; + } + + switch (cell.Type) { + case TableCell::ConstantCell: { + int row = y + GetOffset(y); + int col = x; + + WriteCell(row, col, cell, cell.Content.c_str()); + } break; + + case TableCell::SingularParametricCell: { + int row = y + GetOffset(y); + int col = x; + + auto iter = params.SingularCells.find({ x, y }); + if (iter != params.SingularCells.end()) { + WriteCell(row, col, cell, iter.value().c_str()); + } + } break; + + // See loop above that processes whole array groups at the same time + case TableCell::ArrayParametricCell: break; + } + } + } + + return worksheet; +} + +class TableTemplate::Private +{ +public: + template <class TTableTemplate, class TProxy> + static void OperateStream(TTableTemplate& table, TProxy& proxy) + { + proxy.template ObjectAdapted<DataStreamAdapters::Vector<>>(table.mColumnWidths); + proxy.template ObjectAdapted<DataStreamAdapters::Vector<>>(table.mRowHeights); + proxy.template ObjectAdapted<DataStreamAdapters::Vector<>>(table.mCells); + proxy.template ObjectAdapted<DataStreamAdapters::Vector<>>(table.mArrayGroups); + proxy.template ObjectAdapted<DataStreamAdapters::TslArrayMap<>>(table.mName2Parameters); + proxy.template ObjectAdapted<DataStreamAdapters::TslArrayMap<>>(table.mName2ArrayGroups); + } +}; + +void TableTemplate::ReadFromDataStream(InputDataStream& stream) +{ + Private::OperateStream(*this, stream); +} + +void TableTemplate::WriteToDataStream(OutputDataStream& stream) const +{ + Private::OperateStream(*this, stream); +} diff --git a/app/source/Cplt/Model/Template/TableTemplate.hpp b/app/source/Cplt/Model/Template/TableTemplate.hpp new file mode 100644 index 0000000..3e931d4 --- /dev/null +++ b/app/source/Cplt/Model/Template/TableTemplate.hpp @@ -0,0 +1,223 @@ +#pragma once + +#include <Cplt/Model/Template/Template.hpp> +#include <Cplt/Utils/Vector.hpp> +#include <Cplt/Utils/VectorHash.hpp> +#include <Cplt/fwd.hpp> + +#include <tsl/array_map.h> +#include <tsl/robin_map.h> +#include <string> +#include <string_view> +#include <vector> + +class TableCell +{ +public: + enum TextAlignment + { + /// For horizontal alignment, this means align left. For vertical alignment, this means align top. + AlignAxisMin, + /// Align middle of the text to the middle of the axis. + AlignCenter, + /// For horizontal alignment, this means align right. For vertical alignment, this means align bottom. + AlignAxisMax, + }; + + enum CellType + { + ConstantCell, + SingularParametricCell, + ArrayParametricCell, + }; + +public: + /// Display content of this cell. This doesn't necessarily have to line up with the parameter name (if this cell is one). + std::string Content; + Vec2i Location; + /// Location of the primary (top left) cell, if this cell is a part of a merged group. + /// Otherwise, either component of this field shall be -1. + Vec2i PrimaryCellLocation{ -1, -1 }; + int SpanX = 0; + int SpanY = 0; + TextAlignment HorizontalAlignment = AlignCenter; + TextAlignment VerticalAlignment = AlignCenter; + CellType Type = ConstantCell; + /// The id of the group description object, if this cell isn't a constant or singular parameter cell. Otherwise, this value is -1. + int DataId = -1; + +public: + /// Return whether this cell holds meaningful data, i.e. true when this cell is either unmerged or the primary cell of a merged range. + bool IsDataHoldingCell() const; + /// Return whether this cell is the primary (i.e. top left) cell of a merged range or not. + bool IsPrimaryCell() const; + /// Return whether this cell is a part of a merged range or not. Includes the primary cell. + bool IsMergedCell() const; + + void ReadFromDataStream(InputDataStream& stream); + void WriteToDataStream(OutputDataStream& stream) const; +}; + +// TODO support reverse (bottom to top) filling order +// TODO support horizontal filling order + +/// Parameter group information for a grouped array of cells. When instantiated, an array of 0 or more +/// elements shall be provided by the user, which will replace the group of templated cells with a list +/// of rows, each instantiated with the n-th element in the provided array. +/// \code +/// [["foo", "bar", "foobar"], +/// ["a", "b", c"], +/// ["1", "2", "3"], +/// ["x", "y", "z"]] +/// // ... may be more +/// \endcode +/// This would create 4 rows of data in the place of the original parameter group. +/// +/// If more than one array parameter groups are on the same row, they would share space between each other: +/// \code +/// | 2 elements was fed to it +/// | | 1 element was fed to it +/// V V +/// {~~~~~~~~~~~~~~~~}{~~~~~~~~~~~~~~} +/// +------+---------+---------------+ +/// | Foo | Example | Another group | +/// +------+---------+---------------+ +/// | Cool | Example | | +/// +------+---------+---------------+ +/// \endcode +/// +/// \see TableCell +/// \see TableInstantiationParameters +/// \see TableTemplate +class TableArrayGroup +{ +public: + /// Parameter name mapped to cell location (index from LeftCell). + tsl::array_map<char, int> mName2Cell; + int Row; + /// Leftmost cell in this group + int LeftCell; + /// Rightmost cell in this group + int RightCell; + +public: + Vec2i GetLeftCell() const; + Vec2i GetRightCell() const; + int GetCount() const; + + /// Find the location of the cell within this array group that has the given name. + Vec2i FindCell(std::string_view name); + bool UpdateCellName(std::string_view oldName, std::string_view newName); + + void ReadFromDataStream(InputDataStream& stream); + void WriteToDataStream(OutputDataStream& stream) const; +}; + +// Forward declaration of libxlsxwriter structs +struct lxw_workbook; +struct lxw_worksheet; + +/// An object containing the necessary information to instantiate a table template. +/// \see TableTemplate +class TableInstantiationParameters +{ +private: + const TableTemplate* mTable; + +public: + tsl::robin_map<Vec2i, std::string> SingularCells; + + using ArrayGroupRow = std::vector<std::string>; + using ArrayGroupData = std::vector<ArrayGroupRow>; + std::vector<ArrayGroupData> ArrayGroups; + +public: + TableInstantiationParameters(const TableTemplate& table); + + TableInstantiationParameters& ResetTable(const TableTemplate& newTable); + TableInstantiationParameters RebindTable(const TableTemplate& newTable) const; + + const TableTemplate& GetTable() const; +}; + +/// A table template, where individual cells can be filled by workflows instantiating this template. Merged cells, +/// parametric rows/columns, and grids are also supported. +/// +/// This current supports exporting to xlsx files. +class TableTemplate : public Template +{ + friend class TableSingleParamsIter; + friend class TableArrayGroupsIter; + class Private; + +private: + /// Map from parameter name to index of the parameter cell (stored in mCells). + tsl::array_map<char, int> mName2Parameters; + /// Map from array group name to the index of the array group (stored in mArrayGroups). + tsl::array_map<char, int> mName2ArrayGroups; + std::vector<TableCell> mCells; + std::vector<TableArrayGroup> mArrayGroups; + std::vector<int> mRowHeights; + std::vector<int> mColumnWidths; + +public: + static bool IsInstance(const Template* tmpl); + TableTemplate(); + + int GetTableWidth() const; + int GetTableHeight() const; + void Resize(int newWidth, int newHeight); + + int GetRowHeight(int row) const; + void SetRowHeight(int row, int height); + int GetColumnWidth(int column) const; + void SetColumnWidth(int column, int width); + + const TableCell& GetCell(Vec2i pos) const; + TableCell& GetCell(Vec2i pos); + /// <ul> + /// <li> In case of becoming a SingularParametricCell: the parameter name is filled with TableCell::Content. + /// <li> In case of becoming a ArrayGroupParametricCell: the array group name is automatically generated as the nth group it would be come. + /// i.e., if there aRe currently 3 groups, the newly created group would be named "4". + /// If this name collides with an existing group, hyphens \c - will be append to the name until no collision happens. + /// </ul> + void SetCellType(Vec2i pos, TableCell::CellType type); + + /// Updates the parameter cell to a new name. Returns true on success and false on failure (param not found or name duplicates). + bool UpdateParameterName(std::string_view oldName, std::string_view newName); + + int GetArrayGroupCount() const; + const TableArrayGroup& GetArrayGroup(int id) const; + TableArrayGroup& GetArrayGroup(int id); + TableArrayGroup* AddArrayGroup(int row, int left, int right); + TableArrayGroup* AddArrayGroup(std::string_view name, int row, int left, int right); + bool UpdateArrayGroupName(std::string_view oldName, std::string_view newName); + bool ExtendArrayGroupLeft(int id, int n); + bool ExtendArrayGroupRight(int id, int n); + + /// Find a singular parameter cell by its name. This does not include cells within an array group. + TableCell* FindCell(std::string_view name); + + /// Find an array group by its name. + TableArrayGroup* FindArrayGroup(std::string_view name); + + enum MergeCellsResult + { + MCR_CellAlreadyMerged, + MCR_Success, + }; + MergeCellsResult MergeCells(Vec2i topLeft, Vec2i bottomRight); + + enum BreakCellsResult + { + BCR_CellNotMerged, + BCR_Success, + }; + BreakCellsResult BreakCells(Vec2i topLeft); + + lxw_workbook* InstantiateToExcelWorkbook(const TableInstantiationParameters& params) const; + lxw_worksheet* InstantiateToExcelWorksheet(lxw_workbook* workbook, const TableInstantiationParameters& params) const; + + void ReadFromDataStream(InputDataStream& stream) override; + void WriteToDataStream(OutputDataStream& stream) const override; +}; diff --git a/app/source/Cplt/Model/Template/TableTemplateIterator.cpp b/app/source/Cplt/Model/Template/TableTemplateIterator.cpp new file mode 100644 index 0000000..19e30b9 --- /dev/null +++ b/app/source/Cplt/Model/Template/TableTemplateIterator.cpp @@ -0,0 +1,52 @@ +#include "TableTemplateIterator.hpp" + +TableSingleParamsIter::TableSingleParamsIter(TableTemplate& tmpl) + : mTemplate{ &tmpl } + , mIter{ tmpl.mName2Parameters.begin() } +{ +} + +bool TableSingleParamsIter::HasNext() const +{ + return mIter != mTemplate->mName2Parameters.end(); +} + +TableCell& TableSingleParamsIter::Next() +{ + int id = mIter.value(); + ++mIter; + + return mTemplate->mCells[id]; +} + +TableArrayGroupsIter::TableArrayGroupsIter(TableTemplate& tmpl) + : mTemplate{ &tmpl } + , mIter{ tmpl.mName2ArrayGroups.begin() } +{ +} + +bool TableArrayGroupsIter::HasNext() const +{ + return mIter != mTemplate->mName2ArrayGroups.end(); +} + +TableArrayGroup& TableArrayGroupsIter::Peek() const +{ + int id = mIter.value(); + return mTemplate->mArrayGroups[id]; +} + +std::string_view TableArrayGroupsIter::PeekName() const +{ + return mIter.key_sv(); +} + +const char* TableArrayGroupsIter::PeekNameCStr() const +{ + return mIter.key(); +} + +void TableArrayGroupsIter::Next() +{ + ++mIter; +} diff --git a/app/source/Cplt/Model/Template/TableTemplateIterator.hpp b/app/source/Cplt/Model/Template/TableTemplateIterator.hpp new file mode 100644 index 0000000..c4b5bf9 --- /dev/null +++ b/app/source/Cplt/Model/Template/TableTemplateIterator.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include <Cplt/Model/Template/TableTemplate.hpp> +#include <Cplt/Model/Template/Template.hpp> + +#include <string_view> + +class TableSingleParamsIter +{ +private: + TableTemplate* mTemplate; + tsl::array_map<char, int>::iterator mIter; + +public: + TableSingleParamsIter(TableTemplate& tmpl); + + bool HasNext() const; + TableCell& Next(); +}; + +class TableArrayGroupsIter +{ +private: + TableTemplate* mTemplate; + tsl::array_map<char, int>::iterator mIter; + +public: + TableArrayGroupsIter(TableTemplate& tmpl); + + bool HasNext() const; + TableArrayGroup& Peek() const; + std::string_view PeekName() const; + const char* PeekNameCStr() const; + void Next(); +}; diff --git a/app/source/Cplt/Model/Template/Template.hpp b/app/source/Cplt/Model/Template/Template.hpp new file mode 100644 index 0000000..cf926d0 --- /dev/null +++ b/app/source/Cplt/Model/Template/Template.hpp @@ -0,0 +1,68 @@ +#pragma once + +#include <Cplt/Model/Assets.hpp> +#include <Cplt/fwd.hpp> + +#include <filesystem> +#include <iosfwd> +#include <memory> +#include <string> + +class Template : public Asset +{ +public: + enum Kind + { + KD_Table, + + InvalidKind, + KindCount = InvalidKind, + }; + + using CategoryType = TemplateAssetList; + +private: + Kind mKind; + +public: + static const char* FormatKind(Kind kind); + static std::unique_ptr<Template> CreateByKind(Kind kind); + + static bool IsInstance(const Template* tmpl); + + Template(Kind kind); + ~Template() override = default; + + Kind GetKind() const; + + virtual void ReadFromDataStream(InputDataStream& stream) = 0; + virtual void WriteToDataStream(OutputDataStream& stream) const = 0; +}; + +class TemplateAssetList final : public AssetListTyped<Template> +{ +private: + // AC = Asset Creator + std::string mACNewName; + NameSelectionError mACNewNameError = NameSelectionError::Empty; + Template::Kind mACNewKind = Template::InvalidKind; + +public: + // Inherit constructors + using AssetListTyped::AssetListTyped; + +protected: + void DiscoverFiles(const std::function<void(SavedAsset)>& callback) const override; + + std::string RetrieveNameFromFile(const std::filesystem::path& file) const override; + uuids::uuid RetrieveUuidFromFile(const std::filesystem::path& file) const override; + std::filesystem::path RetrievePathFromAsset(const SavedAsset& asset) const override; + + bool SaveInstance(const SavedAsset& assetInfo, const Asset* asset) const override; + Template* LoadInstance(const SavedAsset& assetInfo) const override; + Template* CreateInstance(const SavedAsset& assetInfo) const override; + bool RenameInstanceOnDisk(const SavedAsset& assetInfo, std::string_view oldName) const override; + + void DisplayAssetCreator(ListState& state) override; + void DisplayDetailsTable(ListState& state) const override; +}; diff --git a/app/source/Cplt/Model/Template/Template_Main.cpp b/app/source/Cplt/Model/Template/Template_Main.cpp new file mode 100644 index 0000000..d658231 --- /dev/null +++ b/app/source/Cplt/Model/Template/Template_Main.cpp @@ -0,0 +1,214 @@ +#include "Template.hpp" + +#include <Cplt/Model/GlobalStates.hpp> +#include <Cplt/Model/Project.hpp> +#include <Cplt/UI/UI.hpp> +#include <Cplt/Utils/I18n.hpp> +#include <Cplt/Utils/IO/Archive.hpp> +#include <Cplt/Utils/UUID.hpp> + +#include <imgui.h> +#include <imgui_stdlib.h> +#include <algorithm> +#include <cstdint> +#include <fstream> + +using namespace std::literals::string_view_literals; +namespace fs = std::filesystem; + +Template::Template(Kind kind) + : mKind{ kind } +{ +} + +Template::Kind Template::GetKind() const +{ + return mKind; +} + +void TemplateAssetList::DiscoverFiles(const std::function<void(SavedAsset)>& callback) const +{ + auto dir = GetConnectedProject().GetTemplatesDirectory(); + DiscoverFilesByExtension(callback, dir, ".cplt-template"sv); +} + +std::string TemplateAssetList::RetrieveNameFromFile(const fs::path& file) const +{ + auto res = DataArchive::LoadFile(file); + if (!res) return ""; + auto& stream = res.value(); + + SavedAsset assetInfo; + stream.ReadObject(assetInfo); + + return assetInfo.Name; +} + +uuids::uuid TemplateAssetList::RetrieveUuidFromFile(const fs::path& file) const +{ + return uuids::uuid::from_string(file.stem().string()); +} + +fs::path TemplateAssetList::RetrievePathFromAsset(const SavedAsset& asset) const +{ + auto fileName = uuids::to_string(asset.Uuid); + return GetConnectedProject().GetTemplatePath(fileName); +} + +bool TemplateAssetList::SaveInstance(const SavedAsset& assetInfo, const Asset* asset) const +{ + auto path = RetrievePathFromAsset(assetInfo); + auto res = DataArchive::SaveFile(path); + if (!res) return false; + auto& stream = res.value(); + + stream.WriteObject(assetInfo); + // This cast is fine: calls to this class will always be wrapped in TypedAssetList<T>, which will ensure `asset` points to some Template + if (auto tmpl = static_cast<const Template*>(asset)) { // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast) + stream.WriteObject(*tmpl); + } + + return true; +} + +static std::unique_ptr<Template> LoadTemplateFromFile(const fs::path& path) +{ + auto res = DataArchive::LoadFile(path); + if (!res) return nullptr; + auto& stream = res.value(); + + SavedAsset assetInfo; + stream.ReadObject(assetInfo); + + auto kind = static_cast<Template::Kind>(assetInfo.Payload); + auto tmpl = Template::CreateByKind(kind); + stream.ReadObject(*tmpl); + + return tmpl; +} + +Template* TemplateAssetList::LoadInstance(const SavedAsset& assetInfo) const +{ + return ::LoadTemplateFromFile(RetrievePathFromAsset(assetInfo)).release(); +} + +Template* TemplateAssetList::CreateInstance(const SavedAsset& assetInfo) const +{ + auto kind = static_cast<Template::Kind>(assetInfo.Payload); + return Template::CreateByKind(kind).release(); +} + +bool TemplateAssetList::RenameInstanceOnDisk(const SavedAsset& assetInfo, std::string_view oldName) const +{ + // Get asset path, which is only dependent on UUID + auto path = RetrievePathFromAsset(assetInfo); + + auto tmpl = ::LoadTemplateFromFile(path); + if (!tmpl) return false; + + // Rewrite the asset with the updated name (note the given assetInfo already has the update name) + SaveInstance(assetInfo, tmpl.get()); + + return true; +} + +void TemplateAssetList::DisplayAssetCreator(ListState& state) +{ + auto ValidateNewName = [&]() -> void { + if (mACNewName.empty()) { + mACNewNameError = NameSelectionError::Empty; + return; + } + + if (FindByName(mACNewName)) { + mACNewNameError = NameSelectionError::Duplicated; + return; + } + + mACNewNameError = NameSelectionError::None; + }; + + auto ShowNewNameErrors = [&]() -> void { + switch (mACNewNameError) { + case NameSelectionError::None: break; + case NameSelectionError::Duplicated: + ImGui::ErrorMessage(I18N_TEXT("Duplicate name", L10N_DUPLICATE_NAME_ERROR)); + break; + case NameSelectionError::Empty: + ImGui::ErrorMessage(I18N_TEXT("Name cannot be empty", L10N_EMPTY_NAME_ERROR)); + break; + } + }; + + auto ShowNewKindErrors = [&]() -> void { + if (mACNewKind == Template::InvalidKind) { + ImGui::ErrorMessage(I18N_TEXT("Invalid template type", L10N_TEMPLATE_INVALID_TYPE_ERROR)); + } + }; + + auto IsInputValid = [&]() -> bool { + return mACNewNameError == NameSelectionError::None && + mACNewKind != Template::InvalidKind; + }; + + auto ResetState = [&]() -> void { + mACNewName.clear(); + mACNewKind = Template::InvalidKind; + ValidateNewName(); + }; + + if (ImGui::InputText(I18N_TEXT("Name", L10N_NAME), &mACNewName)) { + ValidateNewName(); + } + + if (ImGui::BeginCombo(I18N_TEXT("Type", L10N_TYPE), Template::FormatKind(mACNewKind))) { + for (int i = 0; i < Template::KindCount; ++i) { + auto kind = static_cast<Template::Kind>(i); + if (ImGui::Selectable(Template::FormatKind(kind), mACNewKind == kind)) { + mACNewKind = kind; + } + } + ImGui::EndCombo(); + } + + ShowNewNameErrors(); + ShowNewKindErrors(); + + if (ImGui::Button(I18N_TEXT("OK", L10N_CONFIRM), !IsInputValid())) { + ImGui::CloseCurrentPopup(); + + Create(SavedAsset{ + .Name = mACNewName, + .Payload = static_cast<uint64_t>(mACNewKind), + }); + ResetState(); + } + ImGui::SameLine(); + if (ImGui::Button(I18N_TEXT("Cancel", L10N_CANCEL))) { + ImGui::CloseCurrentPopup(); + } +} + +void TemplateAssetList::DisplayDetailsTable(ListState& state) const +{ + ImGui::BeginTable("AssetDetailsTable", 2, ImGuiTableFlags_Borders); + + ImGui::TableSetupColumn(I18N_TEXT("Name", L10N_NAME)); + ImGui::TableSetupColumn(I18N_TEXT("Type", L10N_TYPE)); + ImGui::TableHeadersRow(); + + for (auto& asset : this->GetAssets()) { + ImGui::TableNextRow(); + + ImGui::TableNextColumn(); + if (ImGui::Selectable(asset.Name.c_str(), state.SelectedAsset == &asset, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_DontClosePopups)) { + state.SelectedAsset = &asset; + } + + ImGui::TableNextColumn(); + auto kind = static_cast<Template::Kind>(asset.Payload); + ImGui::TextUnformatted(Template::FormatKind(kind)); + } + + ImGui::EndTable(); +} diff --git a/app/source/Cplt/Model/Template/Template_RTTI.cpp b/app/source/Cplt/Model/Template/Template_RTTI.cpp new file mode 100644 index 0000000..a96680b --- /dev/null +++ b/app/source/Cplt/Model/Template/Template_RTTI.cpp @@ -0,0 +1,29 @@ +#include "Template.hpp" + +#include <Cplt/Model/Template/TableTemplate.hpp> +#include <Cplt/Utils/I18n.hpp> + +const char* Template::FormatKind(Kind kind) +{ + switch (kind) { + case KD_Table: return I18N_TEXT("Table template", L10N_TEMPLATE_TABLE); + + case InvalidKind: break; + } + return ""; +} + +std::unique_ptr<Template> Template::CreateByKind(Kind kind) +{ + switch (kind) { + case KD_Table: return std::make_unique<TableTemplate>(); + + case InvalidKind: break; + } + return nullptr; +} + +bool Template::IsInstance(const Template* tmpl) +{ + return true; +} diff --git a/app/source/Cplt/Model/Template/fwd.hpp b/app/source/Cplt/Model/Template/fwd.hpp new file mode 100644 index 0000000..8378871 --- /dev/null +++ b/app/source/Cplt/Model/Template/fwd.hpp @@ -0,0 +1,11 @@ +#pragma once + +// TableTemplate.hpp +class TableCell; +class TableArrayGroup; +class TableInstantiationParameters; +class TableTemplate; + +// Template.hpp +class Template; +class TemplateAssetList; diff --git a/app/source/Cplt/Model/Workflow/Evaluation.cpp b/app/source/Cplt/Model/Workflow/Evaluation.cpp new file mode 100644 index 0000000..7035bf9 --- /dev/null +++ b/app/source/Cplt/Model/Workflow/Evaluation.cpp @@ -0,0 +1,174 @@ +#include "Evaluation.hpp" + +#include <queue> + +const char* WorkflowEvaluationError::FormatMessageType(enum MessageType messageType) +{ + switch (messageType) { + case Error: return "Error"; + case Warning: return "Warning"; + } +} + +const char* WorkflowEvaluationError::FormatPinType(enum PinType pinType) +{ + switch (pinType) { + case NoPin: return nullptr; + case InputPin: return "Input pin"; + case OutputPin: return "Output pin"; + } +} + +std::string WorkflowEvaluationError::Format() const +{ + // TODO convert to std::format + + std::string result; + result += FormatMessageType(this->Type); + result += " at "; + result += NodeId; + if (auto pinText = FormatPinType(this->PinType)) { + result += "/"; + result += pinText; + result += " "; + result += PinId; + } + result += ": "; + result += this->Message; + + return result; +} + +struct WorkflowEvaluationContext::RuntimeNode +{ + enum EvaluationStatus + { + ST_Unevaluated, + ST_Success, + ST_Failed, + }; + + EvaluationStatus Status = ST_Unevaluated; +}; + +struct WorkflowEvaluationContext::RuntimeConnection +{ + std::unique_ptr<BaseValue> Value; + + bool IsAvailableValue() const + { + return Value != nullptr; + } +}; + +WorkflowEvaluationContext::WorkflowEvaluationContext(Workflow& workflow) + : mWorkflow{ &workflow } +{ + mRuntimeNodes.resize(workflow.mNodes.size()); + mRuntimeConnections.resize(workflow.mConnections.size()); +} + +BaseValue* WorkflowEvaluationContext::GetConnectionValue(size_t id, bool constant) +{ + if (constant) { + return mWorkflow->GetConstantById(id); + } else { + return mRuntimeConnections[id].Value.get(); + } +} + +BaseValue* WorkflowEvaluationContext::GetConnectionValue(const WorkflowNode::InputPin& inputPin) +{ + if (inputPin.IsConnected()) { + return GetConnectionValue(inputPin.Connection, inputPin.IsConstantConnection()); + } else { + return nullptr; + } +} + +void WorkflowEvaluationContext::SetConnectionValue(size_t id, std::unique_ptr<BaseValue> value) +{ + mRuntimeConnections[id].Value = std::move(value); +} + +void WorkflowEvaluationContext::SetConnectionValue(const WorkflowNode::OutputPin& outputPin, std::unique_ptr<BaseValue> value) +{ + if (outputPin.IsConnected()) { + SetConnectionValue(outputPin.Connection, std::move(value)); + } +} + +void WorkflowEvaluationContext::Run() +{ + int evaluatedCount = 0; + int erroredCount = 0; + + for (auto& depthGroup : mWorkflow->GetDepthGroups()) { + for (size_t idx : depthGroup) { + auto& rn = mRuntimeNodes[idx]; + auto& n = *mWorkflow->mNodes[idx]; + + // TODO + + int preEvalErrors = mErrors.size(); + n.Evaluate(*this); + if (preEvalErrors != mErrors.size()) { + erroredCount++; + } else { + evaluatedCount++; + } + } + } + + for (size_t i = 0; i < mRuntimeNodes.size(); ++i) { + auto& rn = mRuntimeNodes[i]; + auto& n = *mWorkflow->mNodes[i]; + if (n.GetType() == WorkflowNode::OutputType) { + // TODO record outputs + } + } +} + +void WorkflowEvaluationContext::ReportError(std::string message, const WorkflowNode& node, int pinId, bool inputPin) +{ + mErrors.push_back(WorkflowEvaluationError{ + .Message = std::move(message), + .NodeId = node.GetId(), + .PinId = pinId, + .PinType = inputPin ? WorkflowEvaluationError::InputPin : WorkflowEvaluationError::OutputPin, + .Type = WorkflowEvaluationError::Error, + }); +} + +void WorkflowEvaluationContext::ReportError(std::string message, const WorkflowNode& node) +{ + mErrors.push_back(WorkflowEvaluationError{ + .Message = std::move(message), + .NodeId = node.GetId(), + .PinId = -1, + .PinType = WorkflowEvaluationError::NoPin, + .Type = WorkflowEvaluationError::Error, + }); +} + +void WorkflowEvaluationContext::ReportWarning(std::string message, const WorkflowNode& node, int pinId, bool inputPin) +{ + mErrors.push_back(WorkflowEvaluationError{ + .Message = std::move(message), + .NodeId = node.GetId(), + .PinId = pinId, + .PinType = inputPin ? WorkflowEvaluationError::InputPin : WorkflowEvaluationError::OutputPin, + .Type = WorkflowEvaluationError::Warning, + }); +} + +void WorkflowEvaluationContext::ReportWarning(std::string message, const WorkflowNode& node) +{ + mErrors.push_back(WorkflowEvaluationError{ + .Message = std::move(message), + .NodeId = node.GetId(), + .PinId = -1, + .PinType = WorkflowEvaluationError::NoPin, + .Type = WorkflowEvaluationError::Warning, + }); +} diff --git a/app/source/Cplt/Model/Workflow/Evaluation.hpp b/app/source/Cplt/Model/Workflow/Evaluation.hpp new file mode 100644 index 0000000..5b8c6cc --- /dev/null +++ b/app/source/Cplt/Model/Workflow/Evaluation.hpp @@ -0,0 +1,67 @@ +#pragma once + +#include <Cplt/Model/Workflow/Workflow.hpp> + +#include <cstddef> +#include <cstdint> +#include <string> +#include <vector> + +class WorkflowEvaluationError +{ +public: + enum MessageType : int16_t + { + Error, + Warning, + }; + + enum PinType : int16_t + { + NoPin, + InputPin, + OutputPin, + }; + +public: + std::string Message; + size_t NodeId; + int PinId; + PinType PinType; + MessageType Type; + +public: + static const char* FormatMessageType(enum MessageType messageType); + static const char* FormatPinType(enum PinType pinType); + + std::string Format() const; +}; + +class WorkflowEvaluationContext +{ +private: + struct RuntimeNode; + struct RuntimeConnection; + + Workflow* mWorkflow; + std::vector<RuntimeNode> mRuntimeNodes; + std::vector<RuntimeConnection> mRuntimeConnections; + std::vector<WorkflowEvaluationError> mErrors; + std::vector<WorkflowEvaluationError> mWarnings; + +public: + WorkflowEvaluationContext(Workflow& workflow); + + BaseValue* GetConnectionValue(size_t id, bool constant); + BaseValue* GetConnectionValue(const WorkflowNode::InputPin& inputPin); + void SetConnectionValue(size_t id, std::unique_ptr<BaseValue> value); + void SetConnectionValue(const WorkflowNode::OutputPin& outputPin, std::unique_ptr<BaseValue> value); + + void ReportError(std::string message, const WorkflowNode& node, int pinId, bool inputPin); + void ReportError(std::string message, const WorkflowNode& node); + void ReportWarning(std::string message, const WorkflowNode& node, int pinId, bool inputPin); + void ReportWarning(std::string message, const WorkflowNode& node); + + /// Run until all possible paths have been evaluated. + void Run(); +}; diff --git a/app/source/Cplt/Model/Workflow/Nodes/DocumentNodes.cpp b/app/source/Cplt/Model/Workflow/Nodes/DocumentNodes.cpp new file mode 100644 index 0000000..df4a8bb --- /dev/null +++ b/app/source/Cplt/Model/Workflow/Nodes/DocumentNodes.cpp @@ -0,0 +1,18 @@ +#include "DocumentNodes.hpp" + +#include <Cplt/Model/Workflow/Evaluation.hpp> +#include <Cplt/Model/Workflow/Values/Basic.hpp> + +bool DocumentTemplateExpansionNode::IsInstance(const WorkflowNode* node) +{ + return node->GetKind() == KD_DocumentTemplateExpansion; +} + +DocumentTemplateExpansionNode::DocumentTemplateExpansionNode() + : WorkflowNode(KD_DocumentTemplateExpansion, false) +{ +} + +void DocumentTemplateExpansionNode::Evaluate(WorkflowEvaluationContext& ctx) +{ +} diff --git a/app/source/Cplt/Model/Workflow/Nodes/DocumentNodes.hpp b/app/source/Cplt/Model/Workflow/Nodes/DocumentNodes.hpp new file mode 100644 index 0000000..a266b2c --- /dev/null +++ b/app/source/Cplt/Model/Workflow/Nodes/DocumentNodes.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include <Cplt/Model/Workflow/Workflow.hpp> + +class DocumentTemplateExpansionNode : public WorkflowNode +{ +public: + static bool IsInstance(const WorkflowNode* node); + DocumentTemplateExpansionNode(); + + // TODO + virtual void Evaluate(WorkflowEvaluationContext& ctx) override; +}; diff --git a/app/source/Cplt/Model/Workflow/Nodes/NumericNodes.cpp b/app/source/Cplt/Model/Workflow/Nodes/NumericNodes.cpp new file mode 100644 index 0000000..f8b29bb --- /dev/null +++ b/app/source/Cplt/Model/Workflow/Nodes/NumericNodes.cpp @@ -0,0 +1,94 @@ +#include "NumericNodes.hpp" + +#include <Cplt/Model/Workflow/Evaluation.hpp> +#include <Cplt/Model/Workflow/Values/Basic.hpp> +#include <Cplt/Utils/I18n.hpp> +#include <Cplt/Utils/Macros.hpp> +#include <Cplt/Utils/RTTI.hpp> + +#include <cassert> +#include <utility> + +WorkflowNode::Kind NumericOperationNode::OperationTypeToNodeKind(OperationType type) +{ + switch (type) { + case Addition: return KD_NumericAddition; + case Subtraction: return KD_NumericSubtraction; + case Multiplication: return KD_NumericMultiplication; + case Division: return KD_NumericDivision; + default: return InvalidKind; + } +} + +NumericOperationNode::OperationType NumericOperationNode::NodeKindToOperationType(Kind kind) +{ + switch (kind) { + case KD_NumericAddition: return Addition; + case KD_NumericSubtraction: return Subtraction; + case KD_NumericMultiplication: return Multiplication; + case KD_NumericDivision: return Division; + default: return InvalidType; + } +} + +bool NumericOperationNode::IsInstance(const WorkflowNode* node) +{ + return node->GetKind() >= KD_NumericAddition && node->GetKind() <= KD_NumericDivision; +} + +NumericOperationNode::NumericOperationNode(OperationType type) + : WorkflowNode(OperationTypeToNodeKind(type), false) + , mType{ type } +{ + mInputs.resize(2); + mInputs[0].MatchingType = BaseValue::KD_Numeric; + mInputs[1].MatchingType = BaseValue::KD_Numeric; + + mOutputs.resize(1); + mOutputs[0].MatchingType = BaseValue::KD_Numeric; +} + +void NumericOperationNode::Evaluate(WorkflowEvaluationContext& ctx) +{ + auto lhsVal = dyn_cast<NumericValue>(ctx.GetConnectionValue(mInputs[0])); + if (!lhsVal) return; + double lhs = lhsVal->GetValue(); + + auto rhsVal = dyn_cast<NumericValue>(ctx.GetConnectionValue(mInputs[1])); + if (!rhsVal) return; + double rhs = rhsVal->GetValue(); + + double res; + switch (mType) { + case Addition: res = lhs + rhs; break; + case Subtraction: res = lhs - rhs; break; + case Multiplication: res = lhs * rhs; break; + case Division: { + if (rhs == 0.0) { + ctx.ReportError(I18N_TEXT("Error: division by 0", L10N_WORKFLOW_RTERROR_DIV_BY_0), *this); + return; + } + res = lhs / rhs; + } break; + + default: return; + } + + auto value = std::make_unique<NumericValue>(); + value->SetValue(res); + ctx.SetConnectionValue(mOutputs[0], std::move(value)); +} + +bool NumericExpressionNode::IsInstance(const WorkflowNode* node) +{ + return node->GetKind() == KD_NumericExpression; +} + +NumericExpressionNode::NumericExpressionNode() + : WorkflowNode(KD_NumericExpression, false) +{ +} + +void NumericExpressionNode::Evaluate(WorkflowEvaluationContext& ctx) +{ +} diff --git a/app/source/Cplt/Model/Workflow/Nodes/NumericNodes.hpp b/app/source/Cplt/Model/Workflow/Nodes/NumericNodes.hpp new file mode 100644 index 0000000..3c89708 --- /dev/null +++ b/app/source/Cplt/Model/Workflow/Nodes/NumericNodes.hpp @@ -0,0 +1,44 @@ +#pragma once + +#include <Cplt/Model/Workflow/Workflow.hpp> + +#include <cstddef> +#include <memory> +#include <variant> +#include <vector> + +class NumericOperationNode : public WorkflowNode +{ +public: + enum OperationType + { + Addition, + Subtraction, + Multiplication, + Division, + + InvalidType, + TypeCount = InvalidType, + }; + +private: + OperationType mType; + +public: + static Kind OperationTypeToNodeKind(OperationType type); + static OperationType NodeKindToOperationType(Kind kind); + static bool IsInstance(const WorkflowNode* node); + NumericOperationNode(OperationType type); + + virtual void Evaluate(WorkflowEvaluationContext& ctx) override; +}; + +class NumericExpressionNode : public WorkflowNode +{ +public: + static bool IsInstance(const WorkflowNode* node); + NumericExpressionNode(); + + // TODO + virtual void Evaluate(WorkflowEvaluationContext& ctx) override; +};
\ No newline at end of file diff --git a/app/source/Cplt/Model/Workflow/Nodes/TextNodes.cpp b/app/source/Cplt/Model/Workflow/Nodes/TextNodes.cpp new file mode 100644 index 0000000..9b31f7a --- /dev/null +++ b/app/source/Cplt/Model/Workflow/Nodes/TextNodes.cpp @@ -0,0 +1,231 @@ +#include "TextNodes.hpp" + +#include <Cplt/Model/Workflow/Evaluation.hpp> +#include <Cplt/Model/Workflow/Values/Basic.hpp> +#include <Cplt/Utils/Macros.hpp> +#include <Cplt/Utils/RTTI.hpp> +#include <Cplt/Utils/Variant.hpp> + +#include <cassert> +#include <utility> +#include <variant> +#include <vector> + +class TextFormatterNode::Impl +{ +public: + template <class TFunction> + static void ForArguments(std::vector<Element>::iterator begin, std::vector<Element>::iterator end, const TFunction& func) + { + for (auto it = begin; it != end; ++it) { + auto& elm = *it; + if (auto arg = std::get_if<Argument>(&elm)) { + func(*arg); + } + } + } + + /// Find the pin index that the \c elmIdx -th element should have, based on the elements coming before it. + static int FindPinForElement(const std::vector<Element>& vec, int elmIdx) + { + for (int i = elmIdx; i >= 0; --i) { + auto& elm = vec[i]; + if (auto arg = std::get_if<Argument>(&elm)) { + return arg->PinIdx + 1; + } + } + return 0; + } +}; + +BaseValue::Kind TextFormatterNode::ArgumentTypeToValueKind(TextFormatterNode::ArgumentType arg) +{ + switch (arg) { + case NumericArgument: return BaseValue::KD_Numeric; + case TextArgument: return BaseValue::KD_Text; + case DateTimeArgument: return BaseValue::KD_DateTime; + } +} + +bool TextFormatterNode::IsInstance(const WorkflowNode* node) +{ + return node->GetKind() == KD_TextFormatting; +} + +TextFormatterNode::TextFormatterNode() + : WorkflowNode(KD_TextFormatting, false) +{ +} + +int TextFormatterNode::GetElementCount() const +{ + return mElements.size(); +} + +const TextFormatterNode::Element& TextFormatterNode::GetElement(int idx) const +{ + return mElements[idx]; +} + +void TextFormatterNode::SetElement(int idx, std::string text) +{ + assert(idx >= 0 && idx < mElements.size()); + + std::visit( + Overloaded{ + [&](const std::string& original) { mMinOutputChars -= original.size(); }, + [&](const Argument& original) { PreRemoveElement(idx); }, + }, + mElements[idx]); + + mMinOutputChars += text.size(); + mElements[idx] = std::move(text); +} + +void TextFormatterNode::SetElement(int idx, ArgumentType argument) +{ + assert(idx >= 0 && idx < mElements.size()); + + std::visit( + Overloaded{ + [&](const std::string& original) { + mMinOutputChars -= original.size(); + + mElements[idx] = Argument{ + .Type = argument, + .PinIdx = Impl::FindPinForElement(mElements, idx), + }; + /* `original` is invalid from this point */ + }, + [&](const Argument& original) { + int pinIdx = original.PinIdx; + + // Create pin + auto& pin = mInputs[pinIdx]; + pin.MatchingType = ArgumentTypeToValueKind(argument); + + // Create element + mElements[idx] = Argument{ + .Type = argument, + .PinIdx = pinIdx, + }; + /* `original` is invalid from this point */ + }, + }, + mElements[idx]); +} + +void TextFormatterNode::InsertElement(int idx, std::string text) +{ + assert(idx >= 0); + if (idx >= mElements.size()) AppendElement(std::move(text)); + + mMinOutputChars += text.size(); + mElements.insert(mElements.begin() + idx, std::move(text)); +} + +void TextFormatterNode::InsertElement(int idx, ArgumentType argument) +{ + assert(idx >= 0); + if (idx >= mElements.size()) AppendElement(argument); + + int pinIdx = Impl::FindPinForElement(mElements, idx); + + // Create pin + auto& pin = InsertInputPin(pinIdx); + pin.MatchingType = ArgumentTypeToValueKind(argument); + + // Create element + mElements.insert( + mElements.begin() + idx, + Argument{ + .Type = argument, + .PinIdx = pinIdx, + }); +} + +void TextFormatterNode::AppendElement(std::string text) +{ + mMinOutputChars += text.size(); + mElements.push_back(std::move(text)); +} + +void TextFormatterNode::AppendElement(ArgumentType argument) +{ + int pinIdx = mInputs.size(); + // Create pin + mInputs.push_back(InputPin{}); + mInputs.back().MatchingType = ArgumentTypeToValueKind(argument); + // Creat eelement + mElements.push_back(Argument{ + .Type = argument, + .PinIdx = pinIdx, + }); +} + +void TextFormatterNode::RemoveElement(int idx) +{ + assert(idx >= 0 && idx < mElements.size()); + + PreRemoveElement(idx); + if (auto arg = std::get_if<Argument>(&mElements[idx])) { + RemoveInputPin(arg->PinIdx); + } + mElements.erase(mElements.begin() + idx); +} + +void TextFormatterNode::Evaluate(WorkflowEvaluationContext& ctx) +{ + std::string result; + result.reserve((size_t)(mMinOutputChars * 1.5f)); + + auto HandleText = [&](const std::string& str) { + result += str; + }; + auto HandleArgument = [&](const Argument& arg) { + switch (arg.Type) { + case NumericArgument: { + if (auto val = dyn_cast<NumericValue>(ctx.GetConnectionValue(mInputs[arg.PinIdx]))) { + result += val->GetString(); + } else { + // TODO localize + ctx.ReportError("Non-numeric value connected to a numeric text format parameter.", *this); + } + } break; + case TextArgument: { + if (auto val = dyn_cast<TextValue>(ctx.GetConnectionValue(mInputs[arg.PinIdx]))) { + result += val->GetValue(); + } else { + // TODO localize + ctx.ReportError("Non-text value connected to a textual text format parameter.", *this); + } + } break; + case DateTimeArgument: { + if (auto val = dyn_cast<DateTimeValue>(ctx.GetConnectionValue(mInputs[arg.PinIdx]))) { + result += val->GetString(); + } else { + // TODO localize + ctx.ReportError("Non-date/time value connected to a date/time text format parameter.", *this); + } + } break; + } + }; + + for (auto& elm : mElements) { + std::visit(Overloaded{ HandleText, HandleArgument }, elm); + } +} + +void TextFormatterNode::PreRemoveElement(int idx) +{ + auto& elm = mElements[idx]; + if (auto arg = std::get_if<Argument>(&elm)) { + RemoveInputPin(arg->PinIdx); + Impl::ForArguments( + mElements.begin() + idx + 1, + mElements.end(), + [&](Argument& arg) { + arg.PinIdx--; + }); + } +} diff --git a/app/source/Cplt/Model/Workflow/Nodes/TextNodes.hpp b/app/source/Cplt/Model/Workflow/Nodes/TextNodes.hpp new file mode 100644 index 0000000..4689931 --- /dev/null +++ b/app/source/Cplt/Model/Workflow/Nodes/TextNodes.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include <Cplt/Model/Workflow/Workflow.hpp> + +#include <cstddef> +#include <memory> +#include <variant> +#include <vector> + +class TextFormatterNode : public WorkflowNode +{ +public: + enum ArgumentType + { + NumericArgument, + TextArgument, + DateTimeArgument, + }; + +private: + class Impl; + + struct Argument + { + ArgumentType Type; + int PinIdx; + }; + using Element = std::variant<std::string, Argument>; + + std::vector<Element> mElements; + int mMinOutputChars; + +public: + static BaseValue::Kind ArgumentTypeToValueKind(ArgumentType arg); + static bool IsInstance(const WorkflowNode* node); + TextFormatterNode(); + + int GetElementCount() const; + const Element& GetElement(int idx) const; + + void SetElement(int idx, std::string text); + void SetElement(int idx, ArgumentType argument); + void InsertElement(int idx, std::string text); + void InsertElement(int idx, ArgumentType argument); + void AppendElement(std::string text); + void AppendElement(ArgumentType argument); + void RemoveElement(int idx); + + virtual void Evaluate(WorkflowEvaluationContext& ctx) override; + +private: + void PreRemoveElement(int idx); +}; diff --git a/app/source/Cplt/Model/Workflow/Nodes/UserInputNodes.cpp b/app/source/Cplt/Model/Workflow/Nodes/UserInputNodes.cpp new file mode 100644 index 0000000..93d458c --- /dev/null +++ b/app/source/Cplt/Model/Workflow/Nodes/UserInputNodes.cpp @@ -0,0 +1,32 @@ +#include "UserInputNodes.hpp" + +#include <Cplt/Model/Workflow/Evaluation.hpp> +#include <Cplt/Model/Workflow/Values/Basic.hpp> + +bool FormInputNode::IsInstance(const WorkflowNode* node) +{ + return node->GetKind() == KD_FormInput; +} + +FormInputNode::FormInputNode() + : WorkflowNode(KD_FormInput, false) +{ +} + +void FormInputNode::Evaluate(WorkflowEvaluationContext& ctx) +{ +} + +bool DatabaseRowsInputNode::IsInstance(const WorkflowNode* node) +{ + return node->GetKind() == KD_DatabaseRowsInput; +} + +DatabaseRowsInputNode::DatabaseRowsInputNode() + : WorkflowNode(KD_DatabaseRowsInput, false) +{ +} + +void DatabaseRowsInputNode::Evaluate(WorkflowEvaluationContext& ctx) +{ +} diff --git a/app/source/Cplt/Model/Workflow/Nodes/UserInputNodes.hpp b/app/source/Cplt/Model/Workflow/Nodes/UserInputNodes.hpp new file mode 100644 index 0000000..f0b923c --- /dev/null +++ b/app/source/Cplt/Model/Workflow/Nodes/UserInputNodes.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include <Cplt/Model/Workflow/Workflow.hpp> + +class FormInputNode : public WorkflowNode +{ +public: + static bool IsInstance(const WorkflowNode* node); + FormInputNode(); + + // TODO + virtual void Evaluate(WorkflowEvaluationContext& ctx) override; +}; + +class DatabaseRowsInputNode : public WorkflowNode +{ +public: + static bool IsInstance(const WorkflowNode* node); + DatabaseRowsInputNode(); + + // TODO + virtual void Evaluate(WorkflowEvaluationContext& ctx) override; +}; diff --git a/app/source/Cplt/Model/Workflow/Nodes/fwd.hpp b/app/source/Cplt/Model/Workflow/Nodes/fwd.hpp new file mode 100644 index 0000000..4153825 --- /dev/null +++ b/app/source/Cplt/Model/Workflow/Nodes/fwd.hpp @@ -0,0 +1,15 @@ +#pragma once + +// DocumentNodes.hpp +class DocumentTemplateExpansionNode; + +// InputNodes.hpp +class FormInputNode; +class DatabaseRowsInputNode; + +// NumericNodes.hpp +class NumericOperationNode; +class NumericExpressionNode; + +// TextNodes.hpp +class TextFormatterNode; diff --git a/app/source/Cplt/Model/Workflow/Value.hpp b/app/source/Cplt/Model/Workflow/Value.hpp new file mode 100644 index 0000000..70fcb57 --- /dev/null +++ b/app/source/Cplt/Model/Workflow/Value.hpp @@ -0,0 +1,94 @@ +#pragma once + +#include <Cplt/Utils/Color.hpp> +#include <Cplt/fwd.hpp> + +#include <iosfwd> +#include <memory> +#include <string> +#include <vector> + +class BaseValue +{ +public: + enum Kind + { + KD_Numeric, + KD_Text, + KD_DateTime, + KD_DatabaseRowId, + KD_List, + KD_Dictionary, + + KD_BaseObject, + KD_SaleDatabaseRow, + KD_PurchaseDatabaseRow, + KD_BaseObjectLast = KD_PurchaseDatabaseRow, + + /// An unspecified type, otherwise known as "any" in some contexts. + InvalidKind, + KindCount = InvalidKind, + }; + + struct KindInfo + { + ImGui::IconType PinIcon; + RgbaColor PinColor; + }; + +private: + Kind mKind; + +public: + static const KindInfo& QueryInfo(Kind kind); + static const char* Format(Kind kind); + static std::unique_ptr<BaseValue> CreateByKind(Kind kind); + + static bool IsInstance(const BaseValue* value); + + BaseValue(Kind kind); + virtual ~BaseValue() = default; + + BaseValue(const BaseValue&) = delete; + BaseValue& operator=(const BaseValue&) = delete; + BaseValue(BaseValue&&) = default; + BaseValue& operator=(BaseValue&&) = default; + + Kind GetKind() const; + + // TODO get constant editor + + /// The functions \c ReadFrom, \c WriteTo will only be valid to call if this function returns true. + virtual bool SupportsConstant() const; + virtual void ReadFrom(std::istream& stream); + virtual void WriteTo(std::ostream& stream); +}; + +class BaseObjectDescription +{ +public: + struct Property + { + std::string Name; + BaseValue::Kind Kind; + bool Mutatable = true; + }; + +public: + std::vector<Property> Properties; +}; + +class BaseObjectValue : public BaseValue +{ +public: + /// \param kind A value kind enum, within the range of KD_BaseObject and KD_BaseObjectLast (both inclusive). + static const BaseObjectDescription& QueryObjectInfo(Kind kind); + + static bool IsInstance(const BaseValue* value); + BaseObjectValue(Kind kind); + + const BaseObjectDescription& GetObjectDescription() const; + + virtual const BaseValue* GetProperty(int idx) const = 0; + virtual bool SetProperty(int idx, std::unique_ptr<BaseValue> value) = 0; +}; diff --git a/app/source/Cplt/Model/Workflow/ValueInternals.hpp b/app/source/Cplt/Model/Workflow/ValueInternals.hpp new file mode 100644 index 0000000..45842db --- /dev/null +++ b/app/source/Cplt/Model/Workflow/ValueInternals.hpp @@ -0,0 +1,21 @@ +// This file contains utility classes and macros for implementing values +// As consumers, you should not include this header as it contains unnecessary symbols and can pollute your files +// for this reason, classes here aren't forward-declared in fwd.hpp either. + +#pragma once + +#include <Cplt/Utils/RTTI.hpp> + +#include <utility> + +#define CHECK_VALUE_TYPE(Type, value) \ + if (!is_a<Type>(value)) { \ + return false; \ + } + +#define CHECK_VALUE_TYPE_AND_MOVE(Type, dest, value) \ + if (auto ptr = dyn_cast<Type>(value)) { \ + dest = std::move(*ptr); \ + } else { \ + return false; \ + } diff --git a/app/source/Cplt/Model/Workflow/Value_Main.cpp b/app/source/Cplt/Model/Workflow/Value_Main.cpp new file mode 100644 index 0000000..ca972c4 --- /dev/null +++ b/app/source/Cplt/Model/Workflow/Value_Main.cpp @@ -0,0 +1,35 @@ +#include "Value.hpp" + +BaseValue::BaseValue(Kind kind) + : mKind{ kind } +{ +} + +BaseValue::Kind BaseValue::GetKind() const +{ + return mKind; +} + +bool BaseValue::SupportsConstant() const +{ + return false; +} + +void BaseValue::ReadFrom(std::istream& stream) +{ +} + +void BaseValue::WriteTo(std::ostream& stream) +{ +} + +BaseObjectValue::BaseObjectValue(Kind kind) + : BaseValue(kind) +{ + assert(kind >= KD_BaseObject && kind <= KD_BaseObjectLast); +} + +const BaseObjectDescription& BaseObjectValue::GetObjectDescription() const +{ + return QueryObjectInfo(this->GetKind()); +} diff --git a/app/source/Cplt/Model/Workflow/Value_RTTI.cpp b/app/source/Cplt/Model/Workflow/Value_RTTI.cpp new file mode 100644 index 0000000..a2a6960 --- /dev/null +++ b/app/source/Cplt/Model/Workflow/Value_RTTI.cpp @@ -0,0 +1,174 @@ +#include "Value.hpp" + +#include <Cplt/Model/Workflow/Values/Basic.hpp> +#include <Cplt/Model/Workflow/Values/Database.hpp> +#include <Cplt/Model/Workflow/Values/Dictionary.hpp> +#include <Cplt/Model/Workflow/Values/List.hpp> +#include <Cplt/UI/UI.hpp> +#include <Cplt/Utils/I18n.hpp> + +constexpr BaseValue::KindInfo kEmptyInfo{ + .PinIcon = ImGui::IconType::Circle, + .PinColor = RgbaColor(), +}; + +constexpr BaseValue::KindInfo kNumericInfo{ + .PinIcon = ImGui::IconType::Circle, + .PinColor = RgbaColor(147, 226, 74), +}; + +constexpr BaseValue::KindInfo kTextInfo{ + .PinIcon = ImGui::IconType::Circle, + .PinColor = RgbaColor(124, 21, 153), +}; + +constexpr BaseValue::KindInfo kDateTimeInfo{ + .PinIcon = ImGui::IconType::Circle, + .PinColor = RgbaColor(147, 226, 74), +}; + +constexpr BaseValue::KindInfo kDatabaseRowIdInfo{ + .PinIcon = ImGui::IconType::Circle, + .PinColor = RgbaColor(216, 42, 221), +}; + +constexpr BaseValue::KindInfo kListInfo{ + .PinIcon = ImGui::IconType::Diamond, + .PinColor = RgbaColor(58, 154, 214), +}; + +constexpr BaseValue::KindInfo kDictionaryInfo{ + .PinIcon = ImGui::IconType::Diamond, + .PinColor = RgbaColor(240, 240, 34), +}; + +constexpr BaseValue::KindInfo kDatabaseRowInfo{ + .PinIcon = ImGui::IconType::Square, + .PinColor = RgbaColor(15, 124, 196), +}; + +constexpr BaseValue::KindInfo kObjectInfo{ + .PinIcon = ImGui::IconType::Square, + .PinColor = RgbaColor(161, 161, 161), +}; + +const BaseValue::KindInfo& BaseValue::QueryInfo(BaseValue::Kind kind) +{ + switch (kind) { + case KD_Numeric: return kNumericInfo; + case KD_Text: return kTextInfo; + case KD_DateTime: return kDateTimeInfo; + case KD_DatabaseRowId: return kDatabaseRowIdInfo; + case KD_List: return kListInfo; + case KD_Dictionary: return kDictionaryInfo; + + case KD_BaseObject: return kObjectInfo; + case KD_SaleDatabaseRow: + case KD_PurchaseDatabaseRow: + return kDatabaseRowInfo; + + case InvalidKind: break; + } + return kEmptyInfo; +} + +const char* BaseValue::Format(Kind kind) +{ + switch (kind) { + case KD_Numeric: return I18N_TEXT("Numeric", L10N_VALUE_NUMERIC); + case KD_Text: return I18N_TEXT("Text", L10N_VALUE_TEXT); + case KD_DateTime: return I18N_TEXT("Date/time", L10N_VALUE_DATE_TIME); + case KD_DatabaseRowId: return I18N_TEXT("Row id", L10N_VALUE_ROW_ID); + case KD_List: return I18N_TEXT("List", L10N_VALUE_LIST); + case KD_Dictionary: return I18N_TEXT("Dictionary", L10N_VALUE_DICT); + + case KD_BaseObject: return I18N_TEXT("Object", L10N_VALUE_OBJECT); + case KD_SaleDatabaseRow: return I18N_TEXT("Sale record", L10N_VALUE_SALE_RECORD); + case KD_PurchaseDatabaseRow: return I18N_TEXT("Purchase record", L10N_VALUE_PURCHASE_RECORD); + + case InvalidKind: break; + } + return ""; +} + +std::unique_ptr<BaseValue> BaseValue::CreateByKind(BaseValue::Kind kind) +{ + switch (kind) { + case KD_Numeric: return std::make_unique<NumericValue>(); + case KD_Text: return std::make_unique<TextValue>(); + case KD_DateTime: return std::make_unique<DateTimeValue>(); + case KD_DatabaseRowId: return std::make_unique<DatabaseRowIdValue>(); + case KD_List: return std::make_unique<ListValue>(); + case KD_Dictionary: return std::make_unique<DictionaryValue>(); + + case KD_BaseObject: return nullptr; + case KD_SaleDatabaseRow: return std::make_unique<SaleDatabaseRowValue>(); + case KD_PurchaseDatabaseRow: return std::make_unique<PurchaseDatabaseRowValue>(); + + case InvalidKind: break; + } + return nullptr; +} + +bool BaseValue::IsInstance(const BaseValue* value) +{ + return true; +} + +const BaseObjectDescription kEmptyObjectInfo{ + .Properties = {}, +}; + +const BaseObjectDescription kSaleDbRowObject{ + .Properties = { + { + .Name = I18N_TEXT("Customer", L10N_VALUE_PROPERTY_CUSTOMER), + .Kind = BaseValue::KD_Text, + .Mutatable = false, + }, + { + .Name = I18N_TEXT("Deadline", L10N_VALUE_PROPERTY_DEADLINE), + .Kind = BaseValue::KD_DateTime, + }, + { + .Name = I18N_TEXT("Delivery time", L10N_VALUE_PROPERTY_DELIVERY_TIME), + .Kind = BaseValue::KD_DateTime, + }, + }, +}; + +const BaseObjectDescription kPurchaseDbRowObject{ + .Properties = { + { + .Name = I18N_TEXT("Factory", L10N_VALUE_PROPERTY_FACTORY), + .Kind = BaseValue::KD_Text, + .Mutatable = false, + }, + { + .Name = I18N_TEXT("Order time", L10N_VALUE_PROPERTY_ORDER_TIME), + .Kind = BaseValue::KD_DateTime, + }, + { + .Name = I18N_TEXT("Delivery time", L10N_VALUE_PROPERTY_DELIVERY_TIME), + .Kind = BaseValue::KD_DateTime, + }, + }, +}; + +const BaseObjectDescription& BaseObjectValue::QueryObjectInfo(Kind kind) +{ + switch (kind) { + case KD_BaseObject: return kEmptyObjectInfo; + case KD_SaleDatabaseRow: return kSaleDbRowObject; + case KD_PurchaseDatabaseRow: return kPurchaseDbRowObject; + + default: break; + } + return kEmptyObjectInfo; +} + +bool BaseObjectValue::IsInstance(const BaseValue* value) +{ + return value->GetKind() >= KD_BaseObject && + value->GetKind() <= KD_BaseObjectLast; +} diff --git a/app/source/Cplt/Model/Workflow/Values/Basic.cpp b/app/source/Cplt/Model/Workflow/Values/Basic.cpp new file mode 100644 index 0000000..198387c --- /dev/null +++ b/app/source/Cplt/Model/Workflow/Values/Basic.cpp @@ -0,0 +1,111 @@ +#include "Basic.hpp" + +#include <charconv> +#include <cmath> +#include <limits> + +bool NumericValue::IsInstance(const BaseValue* value) +{ + return value->GetKind() == KD_Numeric; +} + +NumericValue::NumericValue() + : BaseValue(BaseValue::KD_Numeric) +{ +} + +template <class T, int kMaxSize> +static std::string NumberToString(T value) +{ + char buf[kMaxSize]; + auto res = std::to_chars(buf, buf + kMaxSize, value); + if (res.ec == std::errc()) { + return std::string(buf, res.ptr); + } else { + return "<err>"; + } +} + +std::string NumericValue::GetTruncatedString() const +{ + constexpr auto kMaxSize = std::numeric_limits<int64_t>::digits10; + return ::NumberToString<int64_t, kMaxSize>((int64_t)mValue); +} + +std::string NumericValue::GetRoundedString() const +{ + constexpr auto kMaxSize = std::numeric_limits<int64_t>::digits10; + return ::NumberToString<int64_t, kMaxSize>((int64_t)std::round(mValue)); +} + +std::string NumericValue::GetString() const +{ + constexpr auto kMaxSize = std::numeric_limits<double>::max_digits10; + return ::NumberToString<double, kMaxSize>(mValue); +} + +int64_t NumericValue::GetInt() const +{ + return static_cast<int64_t>(mValue); +} + +double NumericValue::GetValue() const +{ + return mValue; +} + +void NumericValue::SetValue(double value) +{ + mValue = value; +} + +bool TextValue::IsInstance(const BaseValue* value) +{ + return value->GetKind() == KD_Text; +} + +TextValue::TextValue() + : BaseValue(BaseValue::KD_Text) +{ +} + +const std::string& TextValue::GetValue() const +{ + return mValue; +} + +void TextValue::SetValue(const std::string& value) +{ + mValue = value; +} + +bool DateTimeValue::IsInstance(const BaseValue* value) +{ + return value->GetKind() == KD_DateTime; +} + +DateTimeValue::DateTimeValue() + : BaseValue(BaseValue::KD_DateTime) +{ +} + +std::string DateTimeValue::GetString() const +{ + namespace chrono = std::chrono; + auto t = chrono::system_clock::to_time_t(mValue); + + char data[32]; + std::strftime(data, sizeof(data), "%Y-%m-%d %H:%M:%S", std::localtime(&t)); + + return std::string(data); +} + +const std::chrono::time_point<std::chrono::system_clock>& DateTimeValue::GetValue() const +{ + return mValue; +} + +void DateTimeValue::SetValue(const std::chrono::time_point<std::chrono::system_clock>& value) +{ + mValue = value; +} diff --git a/app/source/Cplt/Model/Workflow/Values/Basic.hpp b/app/source/Cplt/Model/Workflow/Values/Basic.hpp new file mode 100644 index 0000000..820fb13 --- /dev/null +++ b/app/source/Cplt/Model/Workflow/Values/Basic.hpp @@ -0,0 +1,67 @@ +#pragma once + +#include <Cplt/Model/Workflow/Value.hpp> + +#include <chrono> +#include <cstdint> +#include <string> + +class NumericValue : public BaseValue +{ +private: + double mValue; + +public: + static bool IsInstance(const BaseValue* value); + NumericValue(); + + NumericValue(const NumericValue&) = delete; + NumericValue& operator=(const NumericValue&) = delete; + NumericValue(NumericValue&&) = default; + NumericValue& operator=(NumericValue&&) = default; + + std::string GetTruncatedString() const; + std::string GetRoundedString() const; + std::string GetString() const; + + int64_t GetInt() const; + double GetValue() const; + void SetValue(double value); +}; + +class TextValue : public BaseValue +{ +private: + std::string mValue; + +public: + static bool IsInstance(const BaseValue* value); + TextValue(); + + TextValue(const TextValue&) = delete; + TextValue& operator=(const TextValue&) = delete; + TextValue(TextValue&&) = default; + TextValue& operator=(TextValue&&) = default; + + const std::string& GetValue() const; + void SetValue(const std::string& value); +}; + +class DateTimeValue : public BaseValue +{ +private: + std::chrono::time_point<std::chrono::system_clock> mValue; + +public: + static bool IsInstance(const BaseValue* value); + DateTimeValue(); + + DateTimeValue(const DateTimeValue&) = delete; + DateTimeValue& operator=(const DateTimeValue&) = delete; + DateTimeValue(DateTimeValue&&) = default; + DateTimeValue& operator=(DateTimeValue&&) = default; + + std::string GetString() const; + const std::chrono::time_point<std::chrono::system_clock>& GetValue() const; + void SetValue(const std::chrono::time_point<std::chrono::system_clock>& value); +}; diff --git a/app/source/Cplt/Model/Workflow/Values/Database.cpp b/app/source/Cplt/Model/Workflow/Values/Database.cpp new file mode 100644 index 0000000..25b77e9 --- /dev/null +++ b/app/source/Cplt/Model/Workflow/Values/Database.cpp @@ -0,0 +1,88 @@ +#include "Database.hpp" + +#include <Cplt/Model/Database.hpp> +#include <Cplt/Model/Workflow/ValueInternals.hpp> + +#include <limits> + +TableKind DatabaseRowIdValue::GetTable() const +{ + return mTable; +} + +int64_t DatabaseRowIdValue::GetRowId() const +{ + return mRowId; +} + +bool DatabaseRowIdValue::IsInstance(const BaseValue* value) +{ + return value->GetKind() == KD_DatabaseRowId; +} + +DatabaseRowIdValue::DatabaseRowIdValue() + : BaseValue(KD_DatabaseRowId) + , mTable{ TableKind::Sales } + , mRowId{ std::numeric_limits<int64_t>::max() } +{ +} + +bool SaleDatabaseRowValue::IsInstance(const BaseValue* value) +{ + return value->GetKind() == KD_SaleDatabaseRow; +} + +SaleDatabaseRowValue::SaleDatabaseRowValue() + : BaseObjectValue(KD_SaleDatabaseRow) +{ +} + +const BaseValue* SaleDatabaseRowValue::GetProperty(int idx) const +{ + switch (idx) { + case 0: return &mCustomerName; + case 1: return &mDeadline; + case 2: return &mDeliveryTime; + default: return nullptr; + } +} + +bool SaleDatabaseRowValue::SetProperty(int idx, std::unique_ptr<BaseValue> value) +{ + switch (idx) { + case 0: return false; + case 1: CHECK_VALUE_TYPE_AND_MOVE(DateTimeValue, mDeadline, value.get()); break; + case 2: CHECK_VALUE_TYPE_AND_MOVE(DateTimeValue, mDeliveryTime, value.get()); break; + } + return true; +} + +bool PurchaseDatabaseRowValue::IsInstance(const BaseValue* value) +{ + return value->GetKind() == KD_PurchaseDatabaseRow; +} + +PurchaseDatabaseRowValue::PurchaseDatabaseRowValue() + : BaseObjectValue(KD_PurchaseDatabaseRow) +{ +} + +const BaseValue* PurchaseDatabaseRowValue::GetProperty(int idx) const +{ + switch (idx) { + case 0: return &mFactoryName; + case 1: return &mOrderTime; + case 2: return &mDeliveryTime; + default: return nullptr; + } +} + +bool PurchaseDatabaseRowValue::SetProperty(int idx, std::unique_ptr<BaseValue> value) +{ + switch (idx) { + case 0: return false; + case 1: CHECK_VALUE_TYPE_AND_MOVE(DateTimeValue, mOrderTime, value.get()); break; + case 2: CHECK_VALUE_TYPE_AND_MOVE(DateTimeValue, mDeliveryTime, value.get()); break; + } + return true; +} diff --git a/app/source/Cplt/Model/Workflow/Values/Database.hpp b/app/source/Cplt/Model/Workflow/Values/Database.hpp new file mode 100644 index 0000000..f1c1571 --- /dev/null +++ b/app/source/Cplt/Model/Workflow/Values/Database.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include <Cplt/Model/Workflow/Value.hpp> +#include <Cplt/Model/Workflow/Values/Basic.hpp> +#include <Cplt/fwd.hpp> + +class DatabaseRowIdValue : public BaseValue +{ +private: + TableKind mTable; + int64_t mRowId; + +public: + static bool IsInstance(const BaseValue* value); + DatabaseRowIdValue(); + + TableKind GetTable() const; + int64_t GetRowId() const; +}; + +class SaleDatabaseRowValue : public BaseObjectValue +{ +private: + int mCustomerId; + TextValue mCustomerName; + DateTimeValue mDeadline; + DateTimeValue mDeliveryTime; + +public: + static bool IsInstance(const BaseValue* value); + SaleDatabaseRowValue(); + + virtual const BaseValue* GetProperty(int idx) const; + virtual bool SetProperty(int idx, std::unique_ptr<BaseValue> value); +}; + +class PurchaseDatabaseRowValue : public BaseObjectValue +{ +private: + int mFactoryId; + TextValue mFactoryName; + DateTimeValue mOrderTime; + DateTimeValue mDeliveryTime; + +public: + static bool IsInstance(const BaseValue* value); + PurchaseDatabaseRowValue(); + + virtual const BaseValue* GetProperty(int idx) const; + virtual bool SetProperty(int idx, std::unique_ptr<BaseValue> value); +}; diff --git a/app/source/Cplt/Model/Workflow/Values/Dictionary.cpp b/app/source/Cplt/Model/Workflow/Values/Dictionary.cpp new file mode 100644 index 0000000..97bf509 --- /dev/null +++ b/app/source/Cplt/Model/Workflow/Values/Dictionary.cpp @@ -0,0 +1,49 @@ +#include "Dictionary.hpp" + +#include <Cplt/Utils/Macros.hpp> + +bool DictionaryValue::IsInstance(const BaseValue* value) +{ + return value->GetKind() == KD_Dictionary; +} + +DictionaryValue::DictionaryValue() + : BaseValue(KD_Dictionary) +{ +} + +int DictionaryValue::GetCount() const +{ + return mElements.size(); +} + +BaseValue* DictionaryValue::Find(std::string_view key) +{ + auto iter = mElements.find(key); + if (iter != mElements.end()) { + return iter.value().get(); + } else { + return nullptr; + } +} + +BaseValue* DictionaryValue::Insert(std::string_view key, std::unique_ptr<BaseValue>& value) +{ + auto [iter, success] = mElements.insert(key, std::move(value)); + if (success) { + return iter.value().get(); + } else { + return nullptr; + } +} + +BaseValue& DictionaryValue::InsertOrReplace(std::string_view key, std::unique_ptr<BaseValue> value) +{ + auto [iter, DISCARD] = mElements.emplace(key, std::move(value)); + return *iter.value(); +} + +void DictionaryValue::Remove(std::string_view key) +{ + mElements.erase(mElements.find(key)); +} diff --git a/app/source/Cplt/Model/Workflow/Values/Dictionary.hpp b/app/source/Cplt/Model/Workflow/Values/Dictionary.hpp new file mode 100644 index 0000000..6eff308 --- /dev/null +++ b/app/source/Cplt/Model/Workflow/Values/Dictionary.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include <Cplt/Model/Workflow/Value.hpp> + +#include <tsl/array_map.h> +#include <memory> +#include <string> +#include <string_view> + +class DictionaryValue : public BaseValue +{ +private: + tsl::array_map<char, std::unique_ptr<BaseValue>> mElements; + +public: + static bool IsInstance(const BaseValue* value); + DictionaryValue(); + + int GetCount() const; + BaseValue* Find(std::string_view key); + + BaseValue* Insert(std::string_view key, std::unique_ptr<BaseValue>& value); + BaseValue& InsertOrReplace(std::string_view key, std::unique_ptr<BaseValue> value); + void Remove(std::string_view key); +}; diff --git a/app/source/Cplt/Model/Workflow/Values/List.cpp b/app/source/Cplt/Model/Workflow/Values/List.cpp new file mode 100644 index 0000000..9fd6bfd --- /dev/null +++ b/app/source/Cplt/Model/Workflow/Values/List.cpp @@ -0,0 +1,100 @@ +#include "List.hpp" + +#include <utility> + +BaseValue* ListValue::Iterator::operator*() const +{ + return mIter->get(); +} + +BaseValue* ListValue::Iterator::operator->() const +{ + return mIter->get(); +} + +ListValue::Iterator& ListValue::Iterator::operator++() +{ + ++mIter; + return *this; +} + +ListValue::Iterator ListValue::Iterator::operator++(int) const +{ + return Iterator(mIter + 1); +} + +ListValue::Iterator& ListValue::Iterator::operator--() +{ + --mIter; + return *this; +} + +ListValue::Iterator ListValue::Iterator::operator--(int) const +{ + return Iterator(mIter - 1); +} + +bool operator==(const ListValue::Iterator& a, const ListValue::Iterator& b) +{ + return a.mIter == b.mIter; +} + +ListValue::Iterator::Iterator(decltype(mIter) iter) + : mIter{ iter } +{ +} + +bool ListValue::IsInstance(const BaseValue* value) +{ + return value->GetKind() == KD_List; +} + +ListValue::ListValue() + : BaseValue(KD_List) +{ +} + +int ListValue::GetCount() const +{ + return mElements.size(); +} + +BaseValue* ListValue::GetElement(int i) const +{ + return mElements[i].get(); +} + +void ListValue::Append(std::unique_ptr<BaseValue> element) +{ + mElements.push_back(std::move(element)); +} + +void ListValue::Insert(int i, std::unique_ptr<BaseValue> element) +{ + mElements.insert(mElements.begin() + i, std::move(element)); +} + +void ListValue::Insert(Iterator iter, std::unique_ptr<BaseValue> element) +{ + mElements.insert(iter.mIter, std::move(element)); +} + +void ListValue::Remove(int i) +{ + mElements.erase(mElements.begin() + i); +} + +void ListValue::Remove(Iterator iter) +{ + mElements.erase(iter.mIter); +} + +ListValue::Iterator ListValue::begin() +{ + return Iterator(mElements.begin()); +} + +ListValue::Iterator ListValue::end() +{ + return Iterator(mElements.end()); +} diff --git a/app/source/Cplt/Model/Workflow/Values/List.hpp b/app/source/Cplt/Model/Workflow/Values/List.hpp new file mode 100644 index 0000000..cc8e061 --- /dev/null +++ b/app/source/Cplt/Model/Workflow/Values/List.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include <Cplt/Model/Workflow/Value.hpp> + +#include <memory> +#include <vector> + +class ListValue : public BaseValue +{ +public: + class Iterator + { + private: + std::vector<std::unique_ptr<BaseValue>>::iterator mIter; + + public: + BaseValue* operator*() const; + BaseValue* operator->() const; + + Iterator& operator++(); + Iterator operator++(int) const; + Iterator& operator--(); + Iterator operator--(int) const; + + friend bool operator==(const Iterator& a, const Iterator& b); + + private: + friend class ListValue; + Iterator(decltype(mIter) iter); + }; + +private: + std::vector<std::unique_ptr<BaseValue>> mElements; + +public: + static bool IsInstance(const BaseValue* value); + ListValue(); + + int GetCount() const; + BaseValue* GetElement(int i) const; + + void Append(std::unique_ptr<BaseValue> element); + void Insert(int i, std::unique_ptr<BaseValue> element); + void Insert(Iterator iter, std::unique_ptr<BaseValue> element); + void Remove(int i); + void Remove(Iterator iter); + + Iterator begin(); + Iterator end(); +}; diff --git a/app/source/Cplt/Model/Workflow/Values/fwd.hpp b/app/source/Cplt/Model/Workflow/Values/fwd.hpp new file mode 100644 index 0000000..51a04e9 --- /dev/null +++ b/app/source/Cplt/Model/Workflow/Values/fwd.hpp @@ -0,0 +1,17 @@ +#pragma once + +// Basic.hpp +class NumericValue; +class TextValue; +class DateTimeValue; + +// Database.hpp +class DatabaseRowIdValue; +class SaleDatabaseRowValue; +class PurchaseDatabaseRowValue; + +// Dictionary.hpp +class DictionaryValue; + +// List.hpp +class ListValue; diff --git a/app/source/Cplt/Model/Workflow/Workflow.hpp b/app/source/Cplt/Model/Workflow/Workflow.hpp new file mode 100644 index 0000000..e075e3c --- /dev/null +++ b/app/source/Cplt/Model/Workflow/Workflow.hpp @@ -0,0 +1,316 @@ +#pragma once + +#include <Cplt/Model/Assets.hpp> +#include <Cplt/Model/Workflow/Value.hpp> +#include <Cplt/Utils/Vector.hpp> +#include <Cplt/fwd.hpp> + +#include <imgui_node_editor.h> +#include <cstddef> +#include <cstdint> +#include <filesystem> +#include <functional> +#include <iosfwd> +#include <limits> +#include <memory> +#include <span> +#include <string> +#include <variant> +#include <vector> + +namespace ImNodes = ax::NodeEditor; + +class WorkflowConnection +{ +public: + static constexpr auto kInvalidId = std::numeric_limits<uint32_t>::max(); + + uint32_t Id; + uint32_t SourceNode; + uint32_t SourcePin; + uint32_t DestinationNode; + uint32_t DestinationPin; + +public: + WorkflowConnection(); + + bool IsValid() const; + + /// Used for `LinkId` when interfacing with imgui node editor. Runtime only (not saved to disk and generated when loading). + ImNodes::LinkId GetLinkId() const; + + void DrawDebugInfo() const; + void ReadFrom(std::istream& stream); + void WriteTo(std::ostream& stream) const; +}; + +class WorkflowNode +{ +public: + static constexpr auto kInvalidId = std::numeric_limits<uint32_t>::max(); + static constexpr auto kInvalidPinId = std::numeric_limits<uint32_t>::max(); + + enum Type + { + InputType, + TransformType, + OutputType, + }; + + enum Kind + { + KD_NumericAddition, + KD_NumericSubtraction, + KD_NumericMultiplication, + KD_NumericDivision, + KD_NumericExpression, + KD_TextFormatting, + KD_DocumentTemplateExpansion, + KD_FormInput, + KD_DatabaseRowsInput, + + InvalidKind, + KindCount = InvalidKind, + }; + + enum Category + { + CG_Numeric, + CG_Text, + CG_Document, + CG_UserInput, + CG_SystemInput, + CG_Output, + + InvalidCategory, + CategoryCount = InvalidCategory, + }; + + struct InputPin + { + uint32_t Connection = WorkflowConnection::kInvalidId; + BaseValue::Kind MatchingType = BaseValue::InvalidKind; + bool ConnectionToConst = false; + + /// A constant connection connects from a user-specified constant value, feeding to a valid \c DestinationNode and \c DestinationPin (i.e. input pins). + bool IsConstantConnection() const; + bool IsConnected() const; + BaseValue::Kind GetMatchingType() const; + }; + + struct OutputPin + { + uint32_t Connection = WorkflowConnection::kInvalidId; + BaseValue::Kind MatchingType = BaseValue::InvalidKind; + + bool IsConnected() const; + BaseValue::Kind GetMatchingType() const; + }; + +protected: + friend class Workflow; + friend class WorkflowEvaluationContext; + + Workflow* mWorkflow; + std::vector<InputPin> mInputs; + std::vector<OutputPin> mOutputs; + Vec2i mPosition; + uint32_t mId; + Kind mKind; + int mDepth; + bool mLocked; + +public: + static const char* FormatKind(Kind kind); + static const char* FormatCategory(Category category); + static const char* FormatType(Type type); + static Category QueryCategory(Kind kind); + static std::span<const Kind> QueryCategoryMembers(Category category); + static std::unique_ptr<WorkflowNode> CreateByKind(Kind kind); + + static bool IsInstance(const WorkflowNode* node); + + WorkflowNode(Kind kind, bool locked); + virtual ~WorkflowNode() = default; + + WorkflowNode(const WorkflowNode&) = delete; + WorkflowNode& operator=(const WorkflowNode&) = delete; + WorkflowNode(WorkflowNode&&) = default; + WorkflowNode& operator=(WorkflowNode&&) = default; + + void SetPosition(const Vec2i& position); + Vec2i GetPosition() const; + + uint32_t GetId() const; + /// Used for `NodeId` when interfacing with imgui node editor. Runtime only (not saved to disk and generated when loading). + ImNodes::NodeId GetNodeId() const; + Kind GetKind() const; + int GetDepth() const; + bool IsLocked() const; + + Type GetType() const; + bool IsInputNode() const; + bool IsOutputNode() const; + + void ConnectInput(uint32_t pinId, WorkflowNode& srcNode, uint32_t srcPinId); + void DisconnectInput(uint32_t pinId); + + void DrawInputPinDebugInfo(uint32_t pinId) const; + const InputPin& GetInputPin(uint32_t pinId) const; + ImNodes::PinId GetInputPinUniqueId(uint32_t pinId) const; + + void ConnectOutput(uint32_t pinId, WorkflowNode& dstNode, uint32_t dstPinId); + void DisconnectOutput(uint32_t pinId); + + void DrawOutputPinDebugInfo(uint32_t pinId) const; + const OutputPin& GetOutputPin(uint32_t pinId) const; + ImNodes::PinId GetOutputPinUniqueId(uint32_t pinId) const; + + virtual void Evaluate(WorkflowEvaluationContext& ctx) = 0; + + void Draw(); + virtual void DrawExtra() {} + + void DrawDebugInfo() const; + virtual void DrawExtraDebugInfo() const {} + + virtual void ReadFrom(std::istream& istream); + virtual void WriteTo(std::ostream& ostream); + +protected: + InputPin& InsertInputPin(int atIdx); + void RemoveInputPin(int pin); + void SwapInputPin(int a, int b); + OutputPin& InsertOutputPin(int atIdx); + void RemoveOutputPin(int pin); + void SwapOutputPin(int a, int b); + + /* For \c Workflow to invoke, override by implementations */ + + void OnAttach(Workflow& workflow, uint32_t newId); + void OnDetach(); +}; + +class Workflow : public Asset +{ + friend class WorkflowNode; + friend class WorkflowEvaluationContext; + class Private; + +public: + using CategoryType = WorkflowAssetList; + static constinit const WorkflowAssetList Category; + +private: + std::vector<WorkflowConnection> mConnections; + std::vector<std::unique_ptr<WorkflowNode>> mNodes; + std::vector<std::unique_ptr<BaseValue>> mConstants; + std::vector<std::vector<uint32_t>> mDepthGroups; + int mConnectionCount; + int mNodeCount; + int mConstantCount; + bool mDepthsDirty = true; + +public: + /* Graph access */ + + const std::vector<WorkflowConnection>& GetConnections() const; + std::vector<WorkflowConnection>& GetConnections(); + const std::vector<std::unique_ptr<WorkflowNode>>& GetNodes() const; + std::vector<std::unique_ptr<WorkflowNode>>& GetNodes(); + const std::vector<std::unique_ptr<BaseValue>>& GetConstants() const; + std::vector<std::unique_ptr<BaseValue>>& GetConstants(); + + WorkflowConnection* GetConnectionById(uint32_t id); + WorkflowConnection* GetConnectionByLinkId(ImNodes::LinkId linkId); + WorkflowNode* GetNodeById(uint32_t id); + WorkflowNode* GetNodeByNodeId(ImNodes::NodeId nodeId); + BaseValue* GetConstantById(uint32_t id); + + struct GlobalPinId + { + WorkflowNode* Node; + uint32_t PinId; + /// true => input pin + /// false => output pin + bool IsOutput; + }; + + /// `pinId` should be the `UniqueId` of a pin from a node that's within this workflow. + GlobalPinId DisassembleGlobalPinId(ImNodes::PinId id); + ImNodes::PinId FabricateGlobalPinId(const WorkflowNode& node, uint32_t pinId, bool isOutput) const; + + const std::vector<std::vector<uint32_t>>& GetDepthGroups() const; + bool DoesDepthNeedsUpdate() const; + + /* Graph mutation */ + + void AddNode(std::unique_ptr<WorkflowNode> step); + void RemoveNode(uint32_t id); + + void RemoveConnection(uint32_t id); + + bool Connect(WorkflowNode& sourceNode, uint32_t sourcePin, WorkflowNode& destinationNode, uint32_t destinationPin); + bool DisconnectBySource(WorkflowNode& sourceNode, uint32_t sourcePin); + bool DisconnectByDestination(WorkflowNode& destinationNode, uint32_t destinationPin); + + /* Graph rebuild */ + + enum GraphUpdateResult + { + /// Successfully rebuilt graph dependent data. + /// Details: nothing is written. + GUR_Success, + /// Nothing has changed since last time UpdateGraph() was called. + /// Details: nothing is written. + GUR_NoWorkToDo, + /// Details: list of nodes is written. + GUR_UnsatisfiedDependencies, + /// Details: list of nodes is written. + GUR_UnreachableNodes, + }; + + using GraphUpdateDetails = std::variant< + // Case: nothing + std::monostate, + // Case: list of nodes (ids) + std::vector<uint32_t>>; + + GraphUpdateResult UpdateGraph(GraphUpdateDetails* details = nullptr); + + /* Serialization */ + + void ReadFromDataStream(InputDataStream& stream); + void WriteToDataStream(OutputDataStream& stream) const; + +private: + std::pair<WorkflowConnection&, uint32_t> AllocWorkflowConnection(); + std::pair<std::unique_ptr<WorkflowNode>&, uint32_t> AllocWorkflowStep(); +}; + +class WorkflowAssetList final : public AssetListTyped<Workflow> +{ +private: + // AC = Asset Creator + std::string mACNewName; + NameSelectionError mACNewNameError = NameSelectionError::Empty; + +public: + // Inherit constructors + using AssetListTyped::AssetListTyped; + +protected: + void DiscoverFiles(const std::function<void(SavedAsset)>& callback) const override; + + std::string RetrieveNameFromFile(const std::filesystem::path& file) const override; + uuids::uuid RetrieveUuidFromFile(const std::filesystem::path& file) const override; + std::filesystem::path RetrievePathFromAsset(const SavedAsset& asset) const override; + + bool SaveInstance(const SavedAsset& assetInfo, const Asset* asset) const override; + Workflow* LoadInstance(const SavedAsset& assetInfo) const override; + Workflow* CreateInstance(const SavedAsset& assetInfo) const override; + bool RenameInstanceOnDisk(const SavedAsset& assetInfo, std::string_view oldName) const override; + + void DisplayAssetCreator(ListState& state) override; + void DisplayDetailsTable(ListState& state) const override; +}; diff --git a/app/source/Cplt/Model/Workflow/Workflow_Main.cpp b/app/source/Cplt/Model/Workflow/Workflow_Main.cpp new file mode 100644 index 0000000..0f35b32 --- /dev/null +++ b/app/source/Cplt/Model/Workflow/Workflow_Main.cpp @@ -0,0 +1,846 @@ +#include "Workflow.hpp" + +#include <Cplt/Model/GlobalStates.hpp> +#include <Cplt/Model/Project.hpp> +#include <Cplt/UI/UI.hpp> +#include <Cplt/Utils/I18n.hpp> +#include <Cplt/Utils/IO/Archive.hpp> +#include <Cplt/Utils/UUID.hpp> + +#include <imgui.h> +#include <imgui_node_editor.h> +#include <imgui_stdlib.h> +#include <tsl/robin_set.h> +#include <algorithm> +#include <cassert> +#include <cstdint> +#include <fstream> +#include <iostream> +#include <queue> +#include <utility> + +using namespace std::literals::string_view_literals; +namespace fs = std::filesystem; +namespace ImNodes = ax::NodeEditor; + +WorkflowConnection::WorkflowConnection() + : Id{ 0 } + , SourceNode{ WorkflowNode::kInvalidId } + , SourcePin{ WorkflowNode::kInvalidPinId } + , DestinationNode{ WorkflowNode::kInvalidId } + , DestinationPin{ WorkflowNode::kInvalidPinId } +{ +} + +bool WorkflowConnection::IsValid() const +{ + return Id != 0; +} + +ImNodes::LinkId WorkflowConnection::GetLinkId() const +{ + // Our id is 0-based (represents an index directly) + // but imgui-node-editor uses the value 0 to represent a null id, so we need to offset by 1 + return Id + 1; +} + +void WorkflowConnection::DrawDebugInfo() const +{ + ImGui::Text("Source (node with output pin):"); + ImGui::Text("{ Node = %u, Pin = %u }", SourceNode, SourcePin); + ImGui::Text("Destination (node with input pin):"); + ImGui::Text("{ Node = %u, Pin = %u }", DestinationNode, DestinationPin); +} + +void WorkflowConnection::ReadFrom(std::istream& stream) +{ + stream >> SourceNode >> SourcePin; + stream >> DestinationNode >> DestinationPin; +} + +void WorkflowConnection::WriteTo(std::ostream& stream) const +{ + stream << SourceNode << SourcePin; + stream << DestinationNode << DestinationPin; +} + +bool WorkflowNode::InputPin::IsConstantConnection() const +{ + return ConnectionToConst && IsConnected(); +} + +bool WorkflowNode::InputPin::IsConnected() const +{ + return Connection != WorkflowConnection::kInvalidId; +} + +BaseValue::Kind WorkflowNode::InputPin::GetMatchingType() const +{ + return MatchingType; +} + +bool WorkflowNode::OutputPin::IsConnected() const +{ + return Connection != WorkflowConnection::kInvalidId; +} + +BaseValue::Kind WorkflowNode::OutputPin::GetMatchingType() const +{ + return MatchingType; +} + +WorkflowNode::WorkflowNode(Kind kind, bool locked) + : mKind{ kind } + , mDepth{ -1 } + , mLocked(locked) +{ +} + +Vec2i WorkflowNode::GetPosition() const +{ + return mPosition; +} + +void WorkflowNode::SetPosition(const Vec2i& position) +{ + mPosition = position; +} + +uint32_t WorkflowNode::GetId() const +{ + return mId; +} + +ImNodes::NodeId WorkflowNode::GetNodeId() const +{ + // See WorkflowConnection::GetLinkId for the rationale + return mId + 1; +} + +WorkflowNode::Kind WorkflowNode::GetKind() const +{ + return mKind; +} + +int WorkflowNode::GetDepth() const +{ + return mDepth; +} + +bool WorkflowNode::IsLocked() const +{ + return mLocked; +} + +WorkflowNode::Type WorkflowNode::GetType() const +{ + if (IsInputNode()) { + return InputType; + } else if (IsOutputNode()) { + return OutputType; + } else { + return TransformType; + } +} + +bool WorkflowNode::IsInputNode() const +{ + return mInputs.size() == 0; +} + +bool WorkflowNode::IsOutputNode() const +{ + return mOutputs.size() == 0; +} + +void WorkflowNode::ConnectInput(uint32_t pinId, WorkflowNode& srcNode, uint32_t srcPinId) +{ + mWorkflow->Connect(*this, pinId, srcNode, srcPinId); +} + +void WorkflowNode::DisconnectInput(uint32_t pinId) +{ + mWorkflow->DisconnectByDestination(*this, pinId); +} + +void WorkflowNode::DrawInputPinDebugInfo(uint32_t pinId) const +{ + ImGui::Text("Node ID: %d", mId); + ImGui::Text("Pin ID: (input) %d", pinId); +} + +const WorkflowNode::InputPin& WorkflowNode::GetInputPin(uint32_t pinId) const +{ + return mInputs[pinId]; +} + +ImNodes::PinId WorkflowNode::GetInputPinUniqueId(uint32_t pinId) const +{ + return mWorkflow->FabricateGlobalPinId(*this, pinId, false); +} + +void WorkflowNode::ConnectOutput(uint32_t pinId, WorkflowNode& dstNode, uint32_t dstPinId) +{ + mWorkflow->Connect(dstNode, dstPinId, *this, pinId); +} + +void WorkflowNode::DisconnectOutput(uint32_t pinId) +{ + mWorkflow->DisconnectBySource(*this, pinId); +} + +void WorkflowNode::DrawOutputPinDebugInfo(uint32_t pinId) const +{ + ImGui::Text("Node ID: %d", mId); + ImGui::Text("Pin ID: (output) %d", pinId); +} + +const WorkflowNode::OutputPin& WorkflowNode::GetOutputPin(uint32_t pinId) const +{ + return mOutputs[pinId]; +} + +ImNodes::PinId WorkflowNode::GetOutputPinUniqueId(uint32_t pinId) const +{ + return mWorkflow->FabricateGlobalPinId(*this, pinId, true); +} + +void WorkflowNode::Draw() +{ + for (uint32_t i = 0; i < mInputs.size(); ++i) { + auto& pin = mInputs[i]; + auto& typeInfo = BaseValue::QueryInfo(pin.MatchingType); + ImNodes::BeginPin(GetInputPinUniqueId(i), ImNodes::PinKind::Input); + // TODO + ImNodes::EndPin(); + } + for (uint32_t i = 0; i < mOutputs.size(); ++i) { + auto& pin = mOutputs[i]; + auto& typeInfo = BaseValue::QueryInfo(pin.MatchingType); + ImNodes::BeginPin(GetOutputPinUniqueId(i), ImNodes::PinKind::Output); + // TODO + ImNodes::EndPin(); + } +} + +void WorkflowNode::DrawDebugInfo() const +{ + ImGui::Text("Node kind: %s", FormatKind(mKind)); + ImGui::Text("Node type: %s", FormatType(GetType())); + ImGui::Text("Node ID: %u", mId); + ImGui::Text("Depth: %d", mDepth); + DrawExtraDebugInfo(); +} + +void WorkflowNode::ReadFrom(std::istream& stream) +{ + stream >> mId; + stream >> mPosition.x >> mPosition.y; +} + +void WorkflowNode::WriteTo(std::ostream& stream) +{ + stream << mId; + stream << mPosition.x << mPosition.y; +} + +WorkflowNode::InputPin& WorkflowNode::InsertInputPin(int atIdx) +{ + assert(atIdx >= 0 && atIdx < mInputs.size()); + + mInputs.push_back(InputPin{}); + for (int i = (int)mInputs.size() - 1, end = atIdx + 1; i >= end; --i) { + SwapInputPin(i, i + 1); + } + + return mInputs[atIdx]; +} + +void WorkflowNode::RemoveInputPin(int pin) +{ + DisconnectInput(pin); + for (int i = 0, end = (int)mInputs.size() - 1; i < end; ++i) { + SwapInputPin(i, i + 1); + } + mInputs.resize(mInputs.size() - 1); +} + +void WorkflowNode::SwapInputPin(int a, int b) +{ + auto& pinA = mInputs[a]; + auto& pinB = mInputs[b]; + + if (mWorkflow) { + if (pinA.IsConnected() && !pinA.IsConstantConnection()) { + auto& conn = *mWorkflow->GetConnectionById(pinA.Connection); + conn.DestinationPin = b; + } + if (pinB.IsConnected() && !pinB.IsConstantConnection()) { + auto& conn = *mWorkflow->GetConnectionById(pinB.Connection); + conn.DestinationPin = a; + } + } + + std::swap(pinA, pinB); +} + +WorkflowNode::OutputPin& WorkflowNode::InsertOutputPin(int atIdx) +{ + assert(atIdx >= 0 && atIdx < mOutputs.size()); + + mOutputs.push_back(OutputPin{}); + for (int i = (int)mOutputs.size() - 1, end = atIdx + 1; i >= end; --i) { + SwapOutputPin(i, i + 1); + } + + return mOutputs[atIdx]; +} + +void WorkflowNode::RemoveOutputPin(int pin) +{ + DisconnectOutput(pin); + for (int i = 0, end = (int)mOutputs.size() - 1; i < end; ++i) { + SwapInputPin(i, i + 1); + } + mOutputs.resize(mOutputs.size() - 1); +} + +void WorkflowNode::SwapOutputPin(int a, int b) +{ + auto& pinA = mOutputs[a]; + auto& pinB = mOutputs[b]; + + if (mWorkflow) { + if (pinA.IsConnected()) { + auto& conn = *mWorkflow->GetConnectionById(pinA.Connection); + conn.SourcePin = b; + } + if (pinB.IsConnected()) { + auto& conn = *mWorkflow->GetConnectionById(pinB.Connection); + conn.SourcePin = a; + } + } + + std::swap(pinA, pinB); +} + +void WorkflowNode::OnAttach(Workflow& workflow, uint32_t newId) +{ +} + +void WorkflowNode::OnDetach() +{ +} + +const std::vector<WorkflowConnection>& Workflow::GetConnections() const +{ + return mConnections; +} + +std::vector<WorkflowConnection>& Workflow::GetConnections() +{ + return mConnections; +} + +const std::vector<std::unique_ptr<WorkflowNode>>& Workflow::GetNodes() const +{ + return mNodes; +} + +std::vector<std::unique_ptr<WorkflowNode>>& Workflow::GetNodes() +{ + return mNodes; +} + +const std::vector<std::unique_ptr<BaseValue>>& Workflow::GetConstants() const +{ + return mConstants; +} + +std::vector<std::unique_ptr<BaseValue>>& Workflow::GetConstants() +{ + return mConstants; +} + +WorkflowConnection* Workflow::GetConnectionById(uint32_t id) +{ + return &mConnections[id]; +} + +WorkflowConnection* Workflow::GetConnectionByLinkId(ImNodes::LinkId id) +{ + return &mConnections[(uint32_t)(size_t)id - 1]; +} + +WorkflowNode* Workflow::GetNodeById(uint32_t id) +{ + return mNodes[id].get(); +} + +WorkflowNode* Workflow::GetNodeByNodeId(ImNodes::NodeId id) +{ + return mNodes[(uint32_t)(size_t)id - 1].get(); +} + +BaseValue* Workflow::GetConstantById(uint32_t id) +{ + return mConstants[id].get(); +} + +Workflow::GlobalPinId Workflow::DisassembleGlobalPinId(ImNodes::PinId pinId) +{ + // imgui-node-editor requires all pins to have a global, unique id + // but in our model the pin are typed (input vs output) and associated with a node: there is no built-in global id + // Therefore we encode one ourselves + + // Global pin id format + // nnnnnnnn nnnnnnnn nnnnnnnn nnnnnnnn Tppppppp ppppppppp pppppppp pppppppp + // <------- (32 bits) node id -------> ^<------ (31 bits) pin id --------> + // | (1 bit) input (false) vs output (true) + + // 1 is added to pin id to prevent the 0th node's 0th input pin resulting in a 0 global pin id + // (this is problematic because imgui-node-editor use 0 to represent null) + + auto id = static_cast<uint64_t>(pinId); + GlobalPinId result; + + result.Node = mNodes[id >> 32].get(); + result.PinId = (uint32_t)(id & 0x000000001FFFFFFF) - 1; + result.IsOutput = id >> 31; + + return result; +} + +ImNodes::PinId Workflow::FabricateGlobalPinId(const WorkflowNode& node, uint32_t pinId, bool isOutput) const +{ + // See this->DisassembleGlobalPinId for format details and rationale + + uint64_t id = 0; + id |= ((uint64_t)node.GetId() << 32); + id |= (isOutput << 31); + id |= ((pinId + 1) & 0x1FFFFFFF); + + return id; +} + +const std::vector<std::vector<uint32_t>>& Workflow::GetDepthGroups() const +{ + return mDepthGroups; +} + +bool Workflow::DoesDepthNeedsUpdate() const +{ + return mDepthsDirty; +} + +void Workflow::AddNode(std::unique_ptr<WorkflowNode> step) +{ + auto [storage, id] = AllocWorkflowStep(); + storage = std::move(step); + storage->OnAttach(*this, id); + storage->mWorkflow = this; + storage->mId = id; +} + +void Workflow::RemoveNode(uint32_t id) +{ + auto& step = mNodes[id]; + if (step == nullptr) return; + + step->OnDetach(); + step->mWorkflow = nullptr; + step->mId = WorkflowNode::kInvalidId; +} + +void Workflow::RemoveConnection(uint32_t id) +{ + auto& conn = mConnections[id]; + if (!conn.IsValid()) return; + + mNodes[conn.SourceNode]->mInputs[conn.SourcePin].Connection = WorkflowNode::kInvalidId; + mNodes[conn.DestinationNode]->mInputs[conn.DestinationPin].Connection = WorkflowNode::kInvalidId; + + conn = {}; + mDepthsDirty = true; +} + +bool Workflow::Connect(WorkflowNode& sourceNode, uint32_t sourcePin, WorkflowNode& destinationNode, uint32_t destinationPin) +{ + auto& src = sourceNode.mOutputs[sourcePin]; + auto& dst = destinationNode.mInputs[destinationPin]; + + // TODO report error to user? + if (src.GetMatchingType() != dst.GetMatchingType()) { + return false; + } + + if (src.IsConnected()) { + DisconnectBySource(sourceNode, sourcePin); + } + + auto [conn, id] = AllocWorkflowConnection(); + conn.SourceNode = sourceNode.GetId(); + conn.SourcePin = sourcePin; + conn.DestinationNode = destinationNode.GetId(); + conn.DestinationPin = destinationPin; + + src.Connection = id; + dst.Connection = id; + + mDepthsDirty = true; + return true; +} + +bool Workflow::DisconnectBySource(WorkflowNode& sourceNode, uint32_t sourcePin) +{ + auto& sn = sourceNode.mOutputs[sourcePin]; + if (!sn.IsConnected()) return false; + + auto& conn = mConnections[sn.Connection]; + auto& dn = mNodes[conn.DestinationNode]->mInputs[conn.DestinationPin]; + + sn.Connection = WorkflowConnection::kInvalidId; + dn.Connection = WorkflowConnection::kInvalidId; + conn = {}; + + mDepthsDirty = true; + return true; +} + +bool Workflow::DisconnectByDestination(WorkflowNode& destinationNode, uint32_t destinationPin) +{ + auto& dn = destinationNode.mOutputs[destinationPin]; + if (!dn.IsConnected()) return false; + + auto& conn = mConnections[dn.Connection]; + auto& sn = mNodes[conn.SourceNode]->mInputs[conn.SourcePin]; + + sn.Connection = WorkflowConnection::kInvalidId; + dn.Connection = WorkflowConnection::kInvalidId; + conn = {}; + + mDepthsDirty = true; + return true; +} + +Workflow::GraphUpdateResult Workflow::UpdateGraph(GraphUpdateDetails* details) +{ + if (!mDepthsDirty) { + return GUR_NoWorkToDo; + } + + // Terminology: + // - Dependency = nodes its input pins are connected to + // - Dependents = nodes its output pins are connected to + + struct WorkingNode + { + // The max depth out of all dependency nodes, maintained during the traversal and committed as the actual depth + // when all dependencies of this node has been resolved. Add 1 to get the depth that will be assigned to the node. + int MaximumDepth = 0; + int FulfilledInputCount = 0; + }; + + std::vector<WorkingNode> workingNodes; + std::queue<uint32_t> q; + + // Check if all dependencies of this node is satisfied + auto CheckNodeDependencies = [&](WorkflowNode& node) -> bool { + for (auto& pin : node.mInputs) { + if (!pin.IsConnected()) { + return false; + } + } + return true; + }; + + workingNodes.reserve(mNodes.size()); + { + std::vector<uint32_t> unsatisfiedNodes; + for (uint32_t i = 0; i < mNodes.size(); ++i) { + auto& node = mNodes[i]; + workingNodes.push_back(WorkingNode{}); + + if (!node) continue; + + if (!CheckNodeDependencies(*node)) { + unsatisfiedNodes.push_back(i); + } + + node->mDepth = -1; + + // Start traversing with the input nodes + if (node->GetType() == WorkflowNode::InputType) { + q.push(i); + } + } + + if (!unsatisfiedNodes.empty()) { + if (details) { + details->emplace<decltype(unsatisfiedNodes)>(std::move(unsatisfiedNodes)); + } + return GUR_UnsatisfiedDependencies; + } + } + + auto ProcessNode = [&](WorkflowNode& node) -> void { + for (auto& pin : node.mOutputs) { + if (!pin.IsConnected()) continue; + auto& conn = mConnections[pin.Connection]; + + auto& wn = workingNodes[conn.DestinationNode]; + auto& n = *mNodes[conn.DestinationPin].get(); + + wn.FulfilledInputCount++; + wn.MaximumDepth = std::max(node.mDepth, wn.MaximumDepth); + + // Node's dependency is fulfilled, we can process its dependents next + // We use >= here because for a many-to-one pin, the dependency is an "or" relation ship, i.e. any of the nodes firing before this will fulfill the requirement + if (n.mInputs.size() >= wn.FulfilledInputCount) { + n.mDepth = wn.MaximumDepth + 1; + } + } + }; + + int processedNodes = 0; + while (!q.empty()) { + auto& wn = workingNodes[q.front()]; + auto& n = *mNodes[q.front()]; + q.pop(); + processedNodes++; + + ProcessNode(n); + } + + if (processedNodes < mNodes.size()) { + // There is unreachable nodes, collect them and report to the caller + + std::vector<uint32_t> unreachableNodes; + for (uint32_t i = 0; i < mNodes.size(); ++i) { + auto& wn = workingNodes[i]; + auto& n = *mNodes[i]; + + // This is a reachable node + if (n.mDepth != -1) continue; + + unreachableNodes.push_back(i); + } + + if (details) { + details->emplace<decltype(unreachableNodes)>(std::move(unreachableNodes)); + } + return GUR_UnreachableNodes; + } + + return GUR_Success; +} + +class Workflow::Private +{ +public: + template <class TSelf, class TProxy> + static void OperateStream(TSelf& self, TProxy& proxy) + { + // TODO + } +}; + +void Workflow::ReadFromDataStream(InputDataStream& stream) +{ + Private::OperateStream(*this, stream); +} + +void Workflow::WriteToDataStream(OutputDataStream& stream) const +{ + Private::OperateStream(*this, stream); +} + +std::pair<WorkflowConnection&, uint32_t> Workflow::AllocWorkflowConnection() +{ + for (size_t idx = 0; idx < mConnections.size(); ++idx) { + auto& elm = mConnections[idx]; + if (!elm.IsValid()) { + return { elm, (uint32_t)idx }; + } + } + + auto id = (uint32_t)mConnections.size(); + auto& conn = mConnections.emplace_back(WorkflowConnection{}); + conn.Id = id; + + return { conn, id }; +} + +std::pair<std::unique_ptr<WorkflowNode>&, uint32_t> Workflow::AllocWorkflowStep() +{ + for (size_t idx = 0; idx < mNodes.size(); ++idx) { + auto& elm = mNodes[idx]; + if (elm == nullptr) { + return { elm, (uint32_t)idx }; + } + } + + auto id = (uint32_t)mNodes.size(); + auto& node = mNodes.emplace_back(std::unique_ptr<WorkflowNode>()); + + return { node, id }; +} + +void WorkflowAssetList::DiscoverFiles(const std::function<void(SavedAsset)>& callback) const +{ + auto dir = GetConnectedProject().GetWorkflowsDirectory(); + DiscoverFilesByExtension(callback, dir, ".cplt-workflow"sv); +} + +std::string WorkflowAssetList::RetrieveNameFromFile(const fs::path& file) const +{ + auto res = DataArchive::LoadFile(file); + if (!res) return ""; + auto& stream = res.value(); + + SavedAsset assetInfo; + stream.ReadObject(assetInfo); + + return assetInfo.Name; +} + +uuids::uuid WorkflowAssetList::RetrieveUuidFromFile(const fs::path& file) const +{ + return uuids::uuid::from_string(file.stem().string()); +} + +fs::path WorkflowAssetList::RetrievePathFromAsset(const SavedAsset& asset) const +{ + auto fileName = uuids::to_string(asset.Uuid); + return GetConnectedProject().GetWorkflowPath(fileName); +} + +bool WorkflowAssetList::SaveInstance(const SavedAsset& assetInfo, const Asset* asset) const +{ + auto path = RetrievePathFromAsset(assetInfo); + auto res = DataArchive::SaveFile(path); + if (!res) return false; + auto& stream = res.value(); + + stream.WriteObject(assetInfo); + // This cast is fine: calls to this class will always be wrapped in TypedAssetList<T>, which will ensure `asset` points to some Workflow + if (auto workflow = static_cast<const Workflow*>(asset)) { // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast) + stream.WriteObject(*workflow); + } + + return true; +} + +static std::unique_ptr<Workflow> LoadWorkflowFromFile(const fs::path& path) +{ + auto res = DataArchive::LoadFile(path); + if (!res) return nullptr; + auto& stream = res.value(); + + // TODO this is currently unused + SavedAsset assetInfo; + stream.ReadObject(assetInfo); + + auto workflow = std::make_unique<Workflow>(); + stream.ReadObject(*workflow); + + return workflow; +} + +Workflow* WorkflowAssetList::LoadInstance(const SavedAsset& assetInfo) const +{ + return ::LoadWorkflowFromFile(RetrievePathFromAsset(assetInfo)).release(); +} + +Workflow* WorkflowAssetList::CreateInstance(const SavedAsset& assetInfo) const +{ + return new Workflow(); +} + +bool WorkflowAssetList::RenameInstanceOnDisk(const SavedAsset& assetInfo, std::string_view oldName) const +{ + auto path = RetrievePathFromAsset(assetInfo); + + auto workflow = ::LoadWorkflowFromFile(path); + if (!workflow) return false; + + SaveInstance(assetInfo, workflow.get()); + + return true; +} + +void WorkflowAssetList::DisplayAssetCreator(ListState& state) +{ + auto ValidateNewName = [&]() -> void { + if (mACNewName.empty()) { + mACNewNameError = NameSelectionError::Empty; + return; + } + + if (FindByName(mACNewName)) { + mACNewNameError = NameSelectionError::Duplicated; + return; + } + + mACNewNameError = NameSelectionError::None; + }; + + auto ShowNewNameErrors = [&]() -> void { + switch (mACNewNameError) { + case NameSelectionError::None: break; + case NameSelectionError::Duplicated: + ImGui::ErrorMessage(I18N_TEXT("Duplicate name", L10N_DUPLICATE_NAME_ERROR)); + break; + case NameSelectionError::Empty: + ImGui::ErrorMessage(I18N_TEXT("Name cannot be empty", L10N_EMPTY_NAME_ERROR)); + break; + } + }; + + auto IsInputValid = [&]() -> bool { + return mACNewNameError == NameSelectionError::None; + }; + + auto ResetState = [&]() -> void { + mACNewName.clear(); + ValidateNewName(); + }; + + if (ImGui::InputText(I18N_TEXT("Name", L10N_NAME), &mACNewName)) { + ValidateNewName(); + } + + ShowNewNameErrors(); + + if (ImGui::Button(I18N_TEXT("OK", L10N_CONFIRM), !IsInputValid())) { + ImGui::CloseCurrentPopup(); + + Create(SavedAsset{ + .Name = mACNewName, + }); + ResetState(); + } + ImGui::SameLine(); + if (ImGui::Button(I18N_TEXT("Cancel", L10N_CANCEL))) { + ImGui::CloseCurrentPopup(); + } +} + +void WorkflowAssetList::DisplayDetailsTable(ListState& state) const +{ + ImGui::BeginTable("AssetDetailsTable", 1, ImGuiTableFlags_Borders); + + ImGui::TableSetupColumn(I18N_TEXT("Name", L10N_NAME)); + ImGui::TableHeadersRow(); + + for (auto& asset : this->GetAssets()) { + ImGui::TableNextRow(); + + ImGui::TableNextColumn(); + if (ImGui::Selectable(asset.Name.c_str(), state.SelectedAsset == &asset, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_DontClosePopups)) { + state.SelectedAsset = &asset; + } + } + + ImGui::EndTable(); +} diff --git a/app/source/Cplt/Model/Workflow/Workflow_RTTI.cpp b/app/source/Cplt/Model/Workflow/Workflow_RTTI.cpp new file mode 100644 index 0000000..ee3da28 --- /dev/null +++ b/app/source/Cplt/Model/Workflow/Workflow_RTTI.cpp @@ -0,0 +1,143 @@ +#include "Workflow.hpp" + +#include <Cplt/Model/Workflow/Nodes/DocumentNodes.hpp> +#include <Cplt/Model/Workflow/Nodes/NumericNodes.hpp> +#include <Cplt/Model/Workflow/Nodes/TextNodes.hpp> +#include <Cplt/Model/Workflow/Nodes/UserInputNodes.hpp> +#include <Cplt/Utils/I18n.hpp> +#include <Cplt/Utils/Macros.hpp> + +#include <memory> + +const char* WorkflowNode::FormatKind(Kind kind) +{ + switch (kind) { + case KD_NumericAddition: return I18N_TEXT("Add", L10N_WORKFLOW_ADD); + case KD_NumericSubtraction: return I18N_TEXT("Subtract", L10N_WORKFLOW_SUB); + case KD_NumericMultiplication: return I18N_TEXT("Multiply", L10N_WORKFLOW_MUL); + case KD_NumericDivision: return I18N_TEXT("Divide", L10N_WORKFLOW_DIV); + case KD_NumericExpression: return I18N_TEXT("Evaluate expression", L10N_WORKFLOW_EVAL); + case KD_TextFormatting: return I18N_TEXT("Format text", L10N_WORKFLOW_FMT); + case KD_DocumentTemplateExpansion: return I18N_TEXT("Expand template", L10N_WORKFLOW_INSTANTIATE_TEMPLATE); + case KD_FormInput: return I18N_TEXT("Form input", L10N_WORKFLOW_FORM_INPUT); + case KD_DatabaseRowsInput: return I18N_TEXT("Database input", L10N_WORKFLOW_DB_INPUT); + + case InvalidKind: break; + } + return ""; +} + +const char* WorkflowNode::FormatCategory(WorkflowNode::Category category) +{ + switch (category) { + case CG_Numeric: return I18N_TEXT("Numeric", L10N_WORKFLOW_CATEGORY_NUMERIC); + case CG_Text: return I18N_TEXT("Text", L10N_WORKFLOW_CATEGORY_TEXT); + case CG_Document: return I18N_TEXT("Document", L10N_WORKFLOW_CATEGORY_DOCUMENT); + case CG_UserInput: return I18N_TEXT("User input", L10N_WORKFLOW_CATEGORY_USER_INPUT); + case CG_SystemInput: return I18N_TEXT("System input", L10N_WORKFLOW_CATEGORY_SYS_INPUT); + case CG_Output: return I18N_TEXT("Output", L10N_WORKFLOW_CATEGORY_OUTPUT); + + case InvalidCategory: break; + } + return ""; +} + +const char* WorkflowNode::FormatType(Type type) +{ + switch (type) { + case InputType: return I18N_TEXT("Input", L10N_WORKFLOW_KIND_INPUT); + case TransformType: return I18N_TEXT("Transform", L10N_WORKFLOW_KIND_TRANSFORM); + case OutputType: return I18N_TEXT("Output", L10N_WORKFLOW_KIND_OUTPUT); + } + return ""; +} + +WorkflowNode::Category WorkflowNode::QueryCategory(Kind kind) +{ + switch (kind) { + case KD_NumericAddition: + case KD_NumericSubtraction: + case KD_NumericMultiplication: + case KD_NumericDivision: + case KD_NumericExpression: + return CG_Numeric; + case KD_TextFormatting: + return CG_Text; + case KD_DocumentTemplateExpansion: + return CG_Document; + case KD_FormInput: + case KD_DatabaseRowsInput: + return CG_UserInput; + + case InvalidKind: break; + } + return InvalidCategory; +} + +std::span<const WorkflowNode::Kind> WorkflowNode::QueryCategoryMembers(Category category) +{ + constexpr WorkflowNode::Kind kNumeric[] = { + KD_NumericAddition, + KD_NumericSubtraction, + KD_NumericMultiplication, + KD_NumericDivision, + KD_NumericExpression, + }; + + constexpr WorkflowNode::Kind kText[] = { + KD_TextFormatting, + }; + + constexpr WorkflowNode::Kind kDocument[] = { + KD_DocumentTemplateExpansion, + }; + + constexpr WorkflowNode::Kind kUserInput[] = { + KD_FormInput, + KD_DatabaseRowsInput, + }; + + // TODO remove invalid kinds after we have nodes of these categories + constexpr WorkflowNode::Kind kSystemInput[] = { + InvalidKind, + }; + + constexpr WorkflowNode::Kind kOutput[] = { + InvalidKind, + }; + + switch (category) { + case CG_Numeric: return kNumeric; + case CG_Text: return kText; + case CG_Document: return kDocument; + case CG_UserInput: return kUserInput; + case CG_SystemInput: return kSystemInput; + case CG_Output: return kOutput; + + case InvalidCategory: break; + } + return {}; +} + +std::unique_ptr<WorkflowNode> WorkflowNode::CreateByKind(WorkflowNode::Kind kind) +{ + switch (kind) { + case KD_NumericAddition: return std::make_unique<NumericOperationNode>(NumericOperationNode::Addition); + case KD_NumericSubtraction: return std::make_unique<NumericOperationNode>(NumericOperationNode::Subtraction); + case KD_NumericMultiplication: return std::make_unique<NumericOperationNode>(NumericOperationNode::Multiplication); + case KD_NumericDivision: return std::make_unique<NumericOperationNode>(NumericOperationNode::Division); + case KD_NumericExpression: return std::make_unique<NumericExpressionNode>(); + case KD_TextFormatting: return std::make_unique<TextFormatterNode>(); + case KD_DocumentTemplateExpansion: return std::make_unique<DocumentTemplateExpansionNode>(); + case KD_FormInput: return std::make_unique<FormInputNode>(); + case KD_DatabaseRowsInput: return std::make_unique<DatabaseRowsInputNode>(); + + case InvalidKind: break; + } + return nullptr; +} + +bool WorkflowNode::IsInstance(const WorkflowNode* node) +{ + return true; +} diff --git a/app/source/Cplt/Model/Workflow/fwd.hpp b/app/source/Cplt/Model/Workflow/fwd.hpp new file mode 100644 index 0000000..ce5b6db --- /dev/null +++ b/app/source/Cplt/Model/Workflow/fwd.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include <Cplt/Model/Workflow/Nodes/fwd.hpp> +#include <Cplt/Model/Workflow/Values/fwd.hpp> + +// Evaluation.hpp +class WorkflowEvaluationError; +class WorkflowEvaluationContext; + +// SavedWorkflow.hpp +class SavedWorkflowCache; +class SavedWorkflow; + +// Value.hpp +class BaseValue; +class BaseObjectValue; + +// Workflow.hpp +class WorkflowConnection; +class WorkflowNode; +class Workflow; +class WorkflowAssetList; diff --git a/app/source/Cplt/Model/fwd.hpp b/app/source/Cplt/Model/fwd.hpp new file mode 100644 index 0000000..c7e44e6 --- /dev/null +++ b/app/source/Cplt/Model/fwd.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include <Cplt/Model/Template/fwd.hpp> +#include <Cplt/Model/Workflow/fwd.hpp> + +// Database.hpp +enum class TableKind; +class SalesTable; +class PurchasesTable; +class DeliveryTable; +class MainDatabase; + +// Assets.hpp +struct SavedAsset; +class Asset; +enum class NameSelectionError; +class AssetList; + +// Filter.hpp +class TableRowsFilter; + +// GlobalStates.hpp +class GlobalStates; + +// Items.hpp +template <class T> +class ItemList; +template <class TSelf> +class ItemBase; +class ProductItem; +class FactoryItem; +class CustomerItem; + +// Project.hpp +class Project; diff --git a/app/source/Cplt/UI/UI.hpp b/app/source/Cplt/UI/UI.hpp new file mode 100644 index 0000000..0a80b4c --- /dev/null +++ b/app/source/Cplt/UI/UI.hpp @@ -0,0 +1,48 @@ +#pragma once + +#include <imgui.h> + +namespace ImGui { + +void SetNextWindowSizeRelScreen(float xPercent, float yPercent, ImGuiCond cond = ImGuiCond_None); +void SetNextWindowCentered(ImGuiCond cond = ImGuiCond_None); + +void PushDisabled(); +void PopDisabled(); + +bool Button(const char* label, bool disabled); +bool Button(const char* label, const ImVec2& sizeArg, bool disabled); + +void ErrorIcon(); +void ErrorMessage(const char* fmt, ...); +void WarningIcon(); +void WarningMessage(const char* fmt, ...); + +enum class IconType +{ + Flow, + Circle, + Square, + Grid, + RoundSquare, + Diamond, +}; + +void DrawIcon(ImDrawList* drawList, const ImVec2& a, const ImVec2& b, IconType type, bool filled, ImU32 color, ImU32 innerColor); +void Icon(const ImVec2& size, IconType type, bool filled, const ImVec4& color = ImVec4(1, 1, 1, 1), const ImVec4& innerColor = ImVec4(0, 0, 0, 0)); + +bool Splitter(bool splitVertically, float thickness, float* size1, float* size2, float minSize1, float minSize2, float splitterLongAxisSize = -1.0f); + +} // namespace ImGui + +namespace UI { + +void MainWindow(); + +void SettingsTab(); +void DatabaseViewTab(); +void ItemsTab(); +void WorkflowsTab(); +void TemplatesTab(); + +} // namespace UI diff --git a/app/source/Cplt/UI/UI_DatabaseView.cpp b/app/source/Cplt/UI/UI_DatabaseView.cpp new file mode 100644 index 0000000..1e58eb0 --- /dev/null +++ b/app/source/Cplt/UI/UI_DatabaseView.cpp @@ -0,0 +1,668 @@ +#include "UI.hpp" + +#include <Cplt/Model/Filter.hpp> +#include <Cplt/Model/GlobalStates.hpp> +#include <Cplt/Model/Project.hpp> +#include <Cplt/Utils/I18n.hpp> +#include <Cplt/Utils/ScopeGuard.hpp> +#include <Cplt/Utils/Time.hpp> + +#include <IconsFontAwesome.h> +#include <SQLiteCpp/Statement.h> +#include <imgui.h> +#include <tsl/robin_map.h> +#include <cstdint> +#include <iostream> +#include <memory> +#include <vector> + +namespace CPLT_UNITY_ID { + +// TODO move to Settings +constexpr int kMaxEntriesPerPage = 32; +constexpr int kSummaryItemCount = 3; +constexpr int kSummaryMaxLength = 25; + +std::pair<int, int> SplitEntryIndex(int entryIdx) +{ + int page = entryIdx / kMaxEntriesPerPage; + int row = entryIdx % kMaxEntriesPerPage; + return { page, row }; +} + +enum class DeliveryDirection +{ + FactoryToWarehouse, + WarehouseToCustomer, +}; + +struct Item +{ + int ItemId; + int Count; +}; + +struct DeliveryEntry +{ + std::vector<Item> Items; + std::string ItemsSummary; + std::string ShipmentTime; + std::string ArriveTime; + DeliveryDirection Direction; + + const char* StringifyDirection() const + { + switch (Direction) { + case DeliveryDirection::FactoryToWarehouse: return "Factory to warehouse"; + case DeliveryDirection::WarehouseToCustomer: return "Warehouse to customer"; + } + } +}; + +struct SaleEntry +{ + static constexpr auto kType = DeliveryDirection::WarehouseToCustomer; + + std::vector<DeliveryEntry> AssociatedDeliveries; + std::vector<Item> Items; + std::string ItemsSummary; + std::string Customer; + std::string Deadline; + std::string DeliveryTime; + int Id; + bool DeliveriesCached = false; +}; + +struct PurchaseEntry +{ + static constexpr auto kType = DeliveryDirection::FactoryToWarehouse; + + std::vector<DeliveryEntry> AssociatedDeliveries; + std::vector<Item> Items; + std::string ItemsSummary; + std::string Factory; + std::string OrderTime; + std::string DeliveryTime; + int Id; + bool DeliveriesCached; +}; + +template <class T> +class GenericTableView +{ +public: + // clang-format off + static constexpr bool kHasItems = requires(T t) + { + t.Items; + t.ItemsSummary; + }; + static constexpr bool kHasCustomer = requires(T t) { t.Customer; }; + static constexpr bool kHasDeadline = requires(T t) { t.Deadline; }; + static constexpr bool kHasFactory = requires(T t) { t.Factory; }; + static constexpr bool kHasOrderTime = requires(T t) { t.OrderTime; }; + static constexpr bool kHasCompletionTime = requires(T t) { t.DeliveryTime; }; + static constexpr int kColumnCount = kHasItems + kHasCustomer + kHasDeadline + kHasFactory + kHasOrderTime + kHasCompletionTime; + // clang-format on + + using Page = std::vector<T>; + + struct QueryStatements + { + SQLite::Statement* GetRowCount; + SQLite::Statement* GetRows; + SQLite::Statement* GetItems; + SQLite::Statement* FilterRows; + } Statements; + +protected: + // Translation entries for implementer to fill out + const char* mEditDialogTitle; + + Project* mProject; + Page* mCurrentPage = nullptr; + + /// Current active filter object, or \c nullptr. + std::unique_ptr<TableRowsFilter> mActiveFilter; + + tsl::robin_map<int, Page> mPages; + + /// A vector of entry indices (in \c mEntries) that are visible under the current filter. + std::vector<int> mActiveEntries; + + /// Number of rows in the table. + int mRowCount; + /// Last possible page for the current set table and filter (inclusive). + int mLastPage; + + /// The current page the user is on. + int mCurrentPageNumber; + + /// Row index of the select entry + int mSelectRow; + +public: + /// Calculate the first visible row's entry index. + int GetFirstVisibleRowIdx() const + { + return mCurrentPageNumber * kMaxEntriesPerPage; + } + + Project* GetProject() const + { + return mProject; + } + + void OnProjectChanged(Project* newProject) + { + mProject = newProject; + + auto& stmt = *Statements.GetRowCount; + if (stmt.executeStep()) { + mRowCount = stmt.getColumn(0).getInt(); + } else { + std::cerr << "Failed to fetch row count from SQLite.\n"; + mRowCount = 0; + } + + mActiveFilter = nullptr; + mActiveEntries.clear(); + + mPages.clear(); + mCurrentPage = nullptr; + UpdateLastPage(); + SetPage(0); + + mSelectRow = -1; + } + + TableRowsFilter* GetFilter() const + { + return mActiveFilter.get(); + } + + void OnFilterChanged() + { + auto& stmt = *Statements.FilterRows; + // clang-format off + DEFER { stmt.reset(); }; + // clang-format on + + // TODO lazy loading when too many results + mActiveEntries.clear(); + int columnIdx = stmt.getColumnIndex("Id"); + while (stmt.executeStep()) { + mActiveEntries.push_back(stmt.getColumn(columnIdx).getInt()); + } + + UpdateLastPage(); + SetPage(0); + + mSelectRow = -1; + } + + void OnFilterChanged(std::unique_ptr<TableRowsFilter> filter) + { + mActiveFilter = std::move(filter); + OnFilterChanged(); + } + + void Display() + { + bool dummy = true; + + if (ImGui::Button(ICON_FA_ARROW_LEFT, mCurrentPageNumber == 0)) { + SetPage(mCurrentPageNumber - 1); + } + + ImGui::SameLine(); + // +1 to convert from 0-based indices to 1-based, for human legibility + ImGui::Text("%d/%d", mCurrentPageNumber + 1, mLastPage + 1); + + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_ARROW_RIGHT, mCurrentPageNumber == mLastPage)) { + SetPage(mCurrentPageNumber + 1); + } + + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_PLUS " " I18N_TEXT("Add", L10N_ADD))) { + // TODO + } + + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_EDIT " " I18N_TEXT("Edit", L10N_EDIT), mSelectRow == -1)) { + ImGui::OpenPopup(mEditDialogTitle); + } + if (ImGui::BeginPopupModal(mEditDialogTitle, &dummy, ImGuiWindowFlags_AlwaysAutoResize)) { + auto& entry = (*mCurrentPage)[mSelectRow]; + int entryIdx = GetFirstVisibleRowIdx() + mSelectRow; + EditEntry(entry, entryIdx, mSelectRow); + ImGui::EndPopup(); + } + + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_TRASH " " I18N_TEXT("Delete", L10N_DELETE), mSelectRow == -1)) { + // TODO + } + + ImGui::Columns(2); + { + DisplayMainTable(); + ImGui::NextColumn(); + + if (mSelectRow == -1) { + ImGui::TextWrapped("%s", I18N_TEXT("Select an entry to show associated deliveries", L10N_DATABASE_MESSAGE_NO_ORDER_SELECTED)); + } else { + DisplayDeliveriesTable(); + } + ImGui::NextColumn(); + } + ImGui::Columns(1); + } + + void SetPage(int page) + { + mCurrentPageNumber = page; + mCurrentPage = &LoadAndGetPage(page); + mSelectRow = -1; + } + +private: + static int CalcPageForRowId(int64_t entryIdx) + { + return entryIdx / kMaxEntriesPerPage; + } + + /// Calculate range [begin, end) of index for the list of entries that are currently visible that the path-th page would show. + /// i.e. when there is a filter, look into \c mActiveEntryIndices; when there is no filter, use directly. + static std::pair<int64_t, int64_t> CalcRangeForPage(int page) + { + int begin = page * kMaxEntriesPerPage; + return { begin, begin + kMaxEntriesPerPage }; + } + + void DisplayMainTable() + { + if (ImGui::BeginTable("DataTable", kColumnCount, ImGuiTableFlags_Borders | ImGuiTableFlags_ScrollX)) { + + if constexpr (kHasCustomer) ImGui::TableSetupColumn(I18N_TEXT("Customer", L10N_DATABASE_COLUMN_CUSTOMER)); + if constexpr (kHasDeadline) ImGui::TableSetupColumn(I18N_TEXT("Deadline", L10N_DATABASE_COLUMN_DEADLINE)); + if constexpr (kHasFactory) ImGui::TableSetupColumn(I18N_TEXT("Factory", L10N_DATABASE_COLUMN_FACTORY)); + if constexpr (kHasOrderTime) ImGui::TableSetupColumn(I18N_TEXT("Order time", L10N_DATABASE_COLUMN_ORDER_TIME)); + if constexpr (kHasCompletionTime) ImGui::TableSetupColumn(I18N_TEXT("Completion time", L10N_DATABASE_COLUMN_DELIVERY_TIME)); + if constexpr (kHasItems) ImGui::TableSetupColumn(I18N_TEXT("Items", L10N_DATABASE_COLUMN_ITEMS)); + ImGui::TableHeadersRow(); + + if (mActiveFilter) { + // TODO + auto [begin, end] = CalcRangeForPage(mCurrentPageNumber); + end = std::min(end, (int64_t)mActiveEntries.size() - 1); + for (int i = begin; i < end; ++i) { + int rowIdx = mActiveEntries[i]; + DisplayEntry(rowIdx); + } + } else { + int firstRowIdx = mCurrentPageNumber * kMaxEntriesPerPage; + auto& page = *mCurrentPage; + + int end = std::min((int)page.size(), kMaxEntriesPerPage); + for (int i = 0; i < end; ++i) { + DisplayEntry(page[i], i, firstRowIdx + i); + } + } + + ImGui::EndTable(); + } + } + + void DisplayEntry(int rowIdx) + { + // TODO + // auto [pageNumber, pageEntry] = SplitRowIndex(rowIdx); + // auto& entry = LoadAndGetPage(pageNumber)[pageEntry]; + // DisplayEntry(entry, rowIdx); + } + + void DisplayEntry(T& entry, int rowIdx, int entryIdx) + { + ImGui::PushID(rowIdx); + ImGui::TableNextRow(); + + if constexpr (kHasCustomer) { + ImGui::TableNextColumn(); + if (ImGui::Selectable(entry.Customer.c_str(), mSelectRow == rowIdx, ImGuiSelectableFlags_SpanAllColumns)) { + mSelectRow = rowIdx; + } + } + + if constexpr (kHasDeadline) { + ImGui::TableNextColumn(); + ImGui::TextUnformatted(entry.Deadline.c_str()); + } + + if constexpr (kHasFactory) { + ImGui::TableNextColumn(); + if (ImGui::Selectable(entry.Factory.c_str(), mSelectRow == rowIdx, ImGuiSelectableFlags_SpanAllColumns)) { + mSelectRow = rowIdx; + } + } + + if constexpr (kHasOrderTime) { + ImGui::TableNextColumn(); + ImGui::TextUnformatted(entry.OrderTime.c_str()); + } + + if constexpr (kHasCompletionTime) { + ImGui::TableNextColumn(); + if (entry.DeliveryTime.empty()) { + ImGui::TextUnformatted(I18N_TEXT("Not delivered", L10N_DATABASE_MESSAGE_NOT_DELIVERED)); + } else { + ImGui::TextUnformatted(entry.DeliveryTime.c_str()); + } + } + + if constexpr (kHasItems) { + ImGui::TableNextColumn(); + if (ImGui::TreeNode(entry.ItemsSummary.c_str())) { + DrawItems(entry.Items); + ImGui::TreePop(); + } + } + + ImGui::PopID(); + } + + void EditEntry(T& entry, int rowIdx, int entryIdx) + { + // TODO + } + + void DisplayDeliveriesTable() + { + if (ImGui::BeginTable("DeliveriesTable", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_ScrollX)) { + + ImGui::TableSetupColumn(I18N_TEXT("Shipment time", L10N_DATABASE_COLUMN_SHIPMENT_TIME)); + ImGui::TableSetupColumn(I18N_TEXT("Arrival time", L10N_DATABASE_COLUMN_ARRIVAL_TIME)); + ImGui::TableSetupColumn(I18N_TEXT("Items", L10N_DATABASE_COLUMN_ITEMS)); + ImGui::TableHeadersRow(); + + auto& entry = (*mCurrentPage)[mSelectRow]; + auto& deliveries = entry.AssociatedDeliveries; + for (auto& delivery : deliveries) { + ImGui::TableNextRow(); + + ImGui::TableNextColumn(); + ImGui::TextUnformatted(delivery.ShipmentTime.c_str()); + + ImGui::TableNextColumn(); + ImGui::TextUnformatted(delivery.ArriveTime.c_str()); + + ImGui::TableNextColumn(); + if (ImGui::TreeNode(delivery.ItemsSummary.c_str())) { + DrawItems(delivery.Items); + ImGui::TreePop(); + } + } + + ImGui::EndTable(); + } + } + + std::vector<Item> LoadItems(SQLite::Statement& stmt, int64_t id) + { + // clang-format off + DEFER { stmt.reset(); }; + // clang-format on + + stmt.bind(1, id); + + std::vector<Item> entries; + int itemIdCol = stmt.getColumnIndex("ItemId"); + int countCol = stmt.getColumnIndex("Count"); + while (stmt.executeStep()) { + entries.push_back(Item{ + .ItemId = stmt.getColumn(itemIdCol).getInt(), + .Count = stmt.getColumn(countCol).getInt(), + }); + } + + return entries; + } + + std::string CreateItemsSummary(const std::vector<Item>& items) + { + if (items.empty()) { + return "<empty>"; + } + + std::string result; + for (int i = 0, max = std::min((int)items.size(), kSummaryItemCount); i < max; ++i) { + auto& name = mProject->Products.Find(items[i].ItemId)->GetName(); + if (result.length() + name.length() > kSummaryMaxLength) { + break; + } + + result += name; + result += ", "; + } + + // Remove ", " + result.pop_back(); + result.pop_back(); + + result += "..."; + + return result; + } + + std::vector<DeliveryEntry> LoadDeliveriesEntries(int64_t orderId, DeliveryDirection type) + { + bool outgoingFlag; + switch (type) { + case DeliveryDirection::FactoryToWarehouse: outgoingFlag = false; break; + case DeliveryDirection::WarehouseToCustomer: outgoingFlag = true; break; + } + + auto& stmt = mProject->Database.GetDeliveries().FilterByTypeAndId; + // clang-format off + DEFER { stmt.reset(); }; + // clang-format on + + stmt.bind(1, orderId); + stmt.bind(2, outgoingFlag); + + std::vector<DeliveryEntry> entries; + int rowIdCol = stmt.getColumnIndex("Id"); + int sendTimeCol = stmt.getColumnIndex("ShipmentTime"); + int arrivalTimeCol = stmt.getColumnIndex("ArrivalTime"); + while (stmt.executeStep()) { + auto items = LoadItems( + mProject->Database.GetDeliveries().GetItems, + stmt.getColumn(rowIdCol).getInt64()); + auto summary = CreateItemsSummary(items); + + entries.push_back(DeliveryEntry{ + .Items = std::move(items), + .ItemsSummary = std::move(summary), + .ShipmentTime = TimeUtils::StringifyTimeStamp(stmt.getColumn(arrivalTimeCol).getInt64()), + .ArriveTime = TimeUtils::StringifyTimeStamp(stmt.getColumn(sendTimeCol).getInt64()), + .Direction = type, + }); + } + + return entries; + } + + Page& LoadAndGetPage(int page) + { + auto iter = mPages.find(page); + if (iter != mPages.end()) { + return iter.value(); + } + + auto& stmt = *Statements.GetRows; + // clang-format off + DEFER { stmt.reset(); }; + // clang-format on + + stmt.bind(1, kMaxEntriesPerPage); + stmt.bind(2, page * kMaxEntriesPerPage); + + // If a field flag is false, the column index won't be used (controlled by other if constexpr's downstream) + // so there is no UB here + + // This column is always necessary (and present) because the deliveries table require it + int idCol = stmt.getColumnIndex("Id"); + + int customerCol; + if constexpr (kHasCustomer) customerCol = stmt.getColumnIndex("Customer"); + + int deadlineCol; + if constexpr (kHasDeadline) deadlineCol = stmt.getColumnIndex("Deadline"); + + int factoryCol; + if constexpr (kHasFactory) factoryCol = stmt.getColumnIndex("Factory"); + + int orderTimeCol; + if constexpr (kHasOrderTime) orderTimeCol = stmt.getColumnIndex("OrderTime"); + + int deliveryTimeCol; + if constexpr (kHasCompletionTime) deliveryTimeCol = stmt.getColumnIndex("DeliveryTime"); + + Page entries; + while (stmt.executeStep()) { + auto& entry = entries.emplace_back(); + + auto id = stmt.getColumn(idCol).getInt64(); + entry.AssociatedDeliveries = LoadDeliveriesEntries(id, T::kType); + + if constexpr (kHasItems) { + auto items = LoadItems( + *Statements.GetItems, + id); + auto itemsSummary = CreateItemsSummary(items); + entry.Items = std::move(items); + entry.ItemsSummary = std::move(itemsSummary); + } + + if constexpr (kHasCustomer) { + auto customerId = stmt.getColumn(customerCol).getInt(); + entry.Customer = mProject->Customers.Find(customerId)->GetName(); + } + + if constexpr (kHasDeadline) { + auto timeStamp = stmt.getColumn(deadlineCol).getInt64(); + entry.Deadline = TimeUtils::StringifyTimeStamp(timeStamp); + } + + if constexpr (kHasFactory) { + auto factoryId = stmt.getColumn(factoryCol).getInt(); + entry.Factory = mProject->Factories.Find(factoryId)->GetName(); + } + + if constexpr (kHasOrderTime) { + auto timeStamp = stmt.getColumn(orderTimeCol).getInt64(); + entry.OrderTime = TimeUtils::StringifyTimeStamp(timeStamp); + } + + if constexpr (kHasCompletionTime) { + auto timeStamp = stmt.getColumn(deliveryTimeCol).getInt64(); + entry.DeliveryTime = TimeUtils::StringifyTimeStamp(timeStamp); + } + } + + auto [res, _] = mPages.try_emplace(page, std::move(entries)); + return res.value(); + } + + void DrawItems(const std::vector<Item>& items) + { + for (auto& item : items) { + auto& name = mProject->Products.Find(item.ItemId)->GetName(); + ImGui::Text("%s × %d", name.c_str(), item.Count); + } + } + + void UpdateLastPage() + { + mLastPage = mActiveEntries.empty() + ? CalcPageForRowId(mRowCount) + : CalcPageForRowId(mActiveEntries.back()); + } +}; + +class SalesTableView : public GenericTableView<SaleEntry> +{ +public: + SalesTableView() + { + mEditDialogTitle = I18N_TEXT("Edit sales entry", L10N_DATABASE_SALES_VIEW_EDIT_DIALOG_TITLE); + } + +#pragma clang diagnostic push +#pragma ide diagnostic ignored "HidingNonVirtualFunction" + void OnProjectChanged(Project* newProject) + { + auto& table = newProject->Database.GetSales(); + Statements.GetRowCount = &table.GetRowCount; + Statements.GetRows = &table.GetRows; + Statements.GetItems = &table.GetItems; + // TODO + // stmts.FilterRowsStatement = ; + + GenericTableView<SaleEntry>::OnProjectChanged(newProject); + } +#pragma clang diagnostic pop +}; + +class PurchasesTableView : public GenericTableView<PurchaseEntry> +{ +public: + PurchasesTableView() + { + mEditDialogTitle = I18N_TEXT("Edit purchase entry", L10N_DATABASE_PURCHASES_VIEW_EDIT_DIALOG_TITLE); + } + +#pragma clang diagnostic push +#pragma ide diagnostic ignored "HidingNonVirtualFunction" + void OnProjectChanged(Project* newProject) + { + auto& table = newProject->Database.GetPurchases(); + Statements.GetRowCount = &table.GetRowCount; + Statements.GetRows = &table.GetRows; + Statements.GetItems = &table.GetItems; + // TODO + // stmts.FilterRowsStatement = ; + + GenericTableView<PurchaseEntry>::OnProjectChanged(newProject); + } +#pragma clang diagnostic pop +}; +} // namespace CPLT_UNITY_ID + +void UI::DatabaseViewTab() +{ + auto& gs = GlobalStates::GetInstance(); + + static Project* currentProject = nullptr; + static CPLT_UNITY_ID::SalesTableView sales; + static CPLT_UNITY_ID::PurchasesTableView purchases; + + if (currentProject != gs.GetCurrentProject()) { + currentProject = gs.GetCurrentProject(); + sales.OnProjectChanged(currentProject); + purchases.OnProjectChanged(currentProject); + } + + if (ImGui::BeginTabBar("DatabaseViewTabs")) { + if (ImGui::BeginTabItem(I18N_TEXT("Sales", L10N_DATABASE_SALES_VIEW_TAB_NAME))) { + sales.Display(); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem(I18N_TEXT("Purchases", L10N_DATABASE_PURCHASES_VIEW_TAB_NAME))) { + purchases.Display(); + ImGui::EndTabItem(); + } + ImGui::EndTabBar(); + } +} diff --git a/app/source/Cplt/UI/UI_Items.cpp b/app/source/Cplt/UI/UI_Items.cpp new file mode 100644 index 0000000..0170e1a --- /dev/null +++ b/app/source/Cplt/UI/UI_Items.cpp @@ -0,0 +1,252 @@ +#include "UI.hpp" + +#include <Cplt/Model/GlobalStates.hpp> +#include <Cplt/Model/Project.hpp> +#include <Cplt/Utils/I18n.hpp> + +#include <IconsFontAwesome.h> +#include <imgui.h> +#include <imgui_stdlib.h> + +namespace CPLT_UNITY_ID { + +enum class ActionResult +{ + Confirmed, + Canceled, + Pending, +}; + +/// \param list Item list that the item is in. +/// \param item A non-null pointer to the currently being edited item. It should not change until this function returns a non-\c ActionResult::Pending value. +template <class T> +ActionResult ItemEditor(ItemList<T>& list, T* item) +{ + constexpr bool kHasDescription = requires(T t) + { + t.GetDescription(); + }; + constexpr bool kHasEmail = requires(T t) + { + t.GetEmail(); + }; + + static bool duplicateName = false; + + static std::string name; + static std::string description; + static std::string email; + if (name.empty()) { + name = item->GetName(); + if constexpr (kHasDescription) description = item->GetDescription(); + if constexpr (kHasEmail) email = item->GetEmail(); + } + + auto ClearStates = [&]() { + duplicateName = false; + name = {}; + description = {}; + }; + + if (ImGui::InputText(I18N_TEXT("Name", L10N_ITEM_COLUMN_NAME), &name)) { + duplicateName = name != item->GetName() && list.Find(name) != nullptr; + } + if constexpr (kHasDescription) ImGui::InputText(I18N_TEXT("Description", L10N_ITEM_COLUMN_DESCRIPTION), &description); + if constexpr (kHasEmail) ImGui::InputText(I18N_TEXT("Email", L10N_ITEM_COLUMN_EMAIL), &email); + + if (name.empty()) { + ImGui::ErrorMessage(I18N_TEXT("Name cannot be empty", L10N_EMPTY_NAME_ERROR)); + } + if (duplicateName) { + ImGui::ErrorMessage(I18N_TEXT("Duplicate name", L10N_DUPLICATE_NAME_ERROR)); + } + + // Return Value + auto rv = ActionResult::Pending; + + if (ImGui::Button(I18N_TEXT("Confirm", L10N_CONFIRM), name.empty() || duplicateName)) { + if (item->GetName() != name) { + item->SetName(std::move(name)); + } + if constexpr (kHasDescription) + if (item->GetDescription() != description) { + item->SetDescription(std::move(description)); + } + if constexpr (kHasEmail) + if (item->GetEmail() != email) { + item->SetEmail(std::move(email)); + } + + ImGui::CloseCurrentPopup(); + ClearStates(); + rv = ActionResult::Confirmed; + } + + ImGui::SameLine(); + if (ImGui::Button(I18N_TEXT("Cancel", L10N_CANCEL))) { + ImGui::CloseCurrentPopup(); + ClearStates(); + rv = ActionResult::Canceled; + } + + return rv; +} + +template <class T> +void ItemListEntries(ItemList<T>& list, int& selectedIdx) +{ + constexpr bool kHasDescription = requires(T t) + { + t.GetDescription(); + }; + constexpr bool kHasEmail = requires(T t) + { + t.GetEmail(); + }; + constexpr bool kHasStock = requires(T t) + { + t.GetPrice(); + }; + constexpr bool kHasPrice = requires(T t) + { + t.GetPrice(); + }; + constexpr int kColumns = 1 /* Name column */ + kHasDescription + kHasEmail + kHasStock + kHasPrice; + + if (ImGui::BeginTable("", kColumns, ImGuiTableFlags_Borders)) { + + ImGui::TableSetupColumn(I18N_TEXT("Name", L10N_ITEM_COLUMN_NAME)); + if constexpr (kHasDescription) ImGui::TableSetupColumn(I18N_TEXT("Description", L10N_ITEM_COLUMN_DESCRIPTION)); + if constexpr (kHasEmail) ImGui::TableSetupColumn(I18N_TEXT("Email", L10N_ITEM_COLUMN_EMAIL)); + if constexpr (kHasStock) ImGui::TableSetupColumn(I18N_TEXT("Stock", L10N_ITEM_COLUMN_STOCK)); + if constexpr (kHasPrice) ImGui::TableSetupColumn(I18N_TEXT("Price", L10N_ITEM_COLUMN_PRICE)); + ImGui::TableHeadersRow(); + + size_t idx = 0; + for (auto& entry : list) { + if (entry.IsInvalid()) { + continue; + } + + ImGui::TableNextRow(); + + ImGui::TableNextColumn(); + if (ImGui::Selectable(entry.GetName().c_str(), selectedIdx == idx, ImGuiSelectableFlags_SpanAllColumns)) { + selectedIdx = idx; + } + + if constexpr (kHasDescription) { + ImGui::TableNextColumn(); + ImGui::TextUnformatted(entry.GetDescription().c_str()); + } + + if constexpr (kHasEmail) { + ImGui::TableNextColumn(); + ImGui::TextUnformatted(entry.GetEmail().c_str()); + } + + if constexpr (kHasStock) { + ImGui::TableNextColumn(); + ImGui::Text("%d", entry.GetStock()); + } + + if constexpr (kHasPrice) { + ImGui::TableNextColumn(); + // TODO format in dollars + ImGui::Text("%d", entry.GetPrice()); + } + + idx++; + } + ImGui::EndTable(); + } +} + +template <class T> +void ItemListEditor(ItemList<T>& list) +{ + bool opened = true; + static int selectedIdx = -1; + static T* editingItem = nullptr; + + if (ImGui::Button(ICON_FA_PLUS " " I18N_TEXT("Add", L10N_ADD))) { + ImGui::SetNextWindowCentered(); + ImGui::OpenPopup(I18N_TEXT("Add item", L10N_ITEM_ADD_DIALOG_TITLE)); + + editingItem = &list.Insert(""); + } + if (ImGui::BeginPopupModal(I18N_TEXT("Add item", L10N_ITEM_ADD_DIALOG_TITLE), &opened, ImGuiWindowFlags_AlwaysAutoResize)) { + switch (ItemEditor(list, editingItem)) { + case ActionResult::Confirmed: + editingItem = nullptr; + break; + case ActionResult::Canceled: + list.Remove(editingItem->GetId()); + editingItem = nullptr; + break; + default: + break; + } + ImGui::EndPopup(); + } + + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_EDIT " " I18N_TEXT("Edit", L10N_EDIT), selectedIdx == -1)) { + ImGui::SetNextWindowCentered(); + ImGui::OpenPopup(I18N_TEXT("Edit item", L10N_ITEM_EDIT_DIALOG_TITLE)); + + editingItem = list.Find(selectedIdx); + } + if (ImGui::BeginPopupModal(I18N_TEXT("Edit item", L10N_ITEM_EDIT_DIALOG_TITLE), &opened, ImGuiWindowFlags_AlwaysAutoResize)) { + if (ItemEditor(list, editingItem) != ActionResult::Pending) { + editingItem = nullptr; + } + ImGui::EndPopup(); + } + + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_TRASH " " I18N_TEXT("Delete", L10N_DELETE), selectedIdx == -1)) { + ImGui::SetNextWindowCentered(); + ImGui::OpenPopup(I18N_TEXT("Delete item", L10N_ITEM_DELETE_DIALOG_TITLE)); + + list.Remove(selectedIdx); + } + if (ImGui::BeginPopupModal(I18N_TEXT("Delete item", L10N_ITEM_DELETE_DIALOG_TITLE), &opened, ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::TextUnformatted(I18N_TEXT("Are you sure you want to delete this item?", L10N_ITEM_DELETE_DIALOG_MESSAGE)); + + if (ImGui::Button(I18N_TEXT("Confirm", L10N_CONFIRM))) { + ImGui::CloseCurrentPopup(); + } + + ImGui::SameLine(); + if (ImGui::Button(I18N_TEXT("Cancel", L10N_CANCEL))) { + ImGui::CloseCurrentPopup(); + } + + ImGui::EndPopup(); + } + + ItemListEntries<T>(list, selectedIdx); +} +} // namespace CPLT_UNITY_ID + +void UI::ItemsTab() +{ + auto& gs = GlobalStates::GetInstance(); + + if (ImGui::BeginTabBar("ItemViewTabs")) { + if (ImGui::BeginTabItem(I18N_TEXT("Products", L10N_ITEM_CATEGORY_PRODUCT))) { + CPLT_UNITY_ID::ItemListEditor(gs.GetCurrentProject()->Products); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem(I18N_TEXT("Factories", L10N_ITEM_CATEGORY_FACTORY))) { + CPLT_UNITY_ID::ItemListEditor(gs.GetCurrentProject()->Factories); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem(I18N_TEXT("Customers", L10N_ITEM_CATEGORY_CUSTOMER))) { + CPLT_UNITY_ID::ItemListEditor(gs.GetCurrentProject()->Customers); + ImGui::EndTabItem(); + } + ImGui::EndTabBar(); + } +} diff --git a/app/source/Cplt/UI/UI_MainWindow.cpp b/app/source/Cplt/UI/UI_MainWindow.cpp new file mode 100644 index 0000000..4653f79 --- /dev/null +++ b/app/source/Cplt/UI/UI_MainWindow.cpp @@ -0,0 +1,237 @@ +#include "UI.hpp" + +#include <Cplt/Model/GlobalStates.hpp> +#include <Cplt/Model/Project.hpp> +#include <Cplt/Utils/I18n.hpp> + +#include <IconsFontAwesome.h> +#include <imgui.h> +#include <imgui_stdlib.h> +#include <portable-file-dialogs.h> +#include <filesystem> +#include <memory> + +namespace fs = std::filesystem; + +namespace CPLT_UNITY_ID { +void ProjectTab_Normal() +{ + auto& gs = GlobalStates::GetInstance(); + + if (ImGui::Button(ICON_FA_TIMES " " I18N_TEXT("Close", L10N_CLOSE))) { + gs.SetCurrentProject(nullptr); + return; + } + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_FOLDER " " I18N_TEXT("Open in filesystem", L10N_PROJECT_OPEN_IN_FILESYSTEM))) { + // TODO + } + + ImGui::Text("%s %s", I18N_TEXT("Project name", L10N_PROJECT_NAME), gs.GetCurrentProject()->GetName().c_str()); + ImGui::Text("%s %s", I18N_TEXT("Project path", L10N_PROJECT_PATH), gs.GetCurrentProject()->GetPathString().c_str()); +} + +void ProjectTab_NoProject() +{ + auto& gs = GlobalStates::GetInstance(); + + bool openedDummy = true; + bool openErrorDialog = false; + static std::string projectName; + static std::string dirName; + static fs::path dirPath; + static bool dirNameIsValid = false; + + auto TrySelectPath = [&](fs::path newPath) { + if (fs::exists(newPath)) { + dirNameIsValid = true; + dirName = newPath.string(); + dirPath = std::move(newPath); + } else { + dirNameIsValid = false; + } + }; + + if (ImGui::Button(I18N_TEXT("Create project....", L10N_PROJECT_NEW))) { + ImGui::SetNextWindowCentered(); + ImGui::OpenPopup(I18N_TEXT("Create project wizard", L10N_PROJECT_NEW_DIALOG_TITLE)); + } + + // Make it so that the modal dialog has a close button + if (ImGui::BeginPopupModal(I18N_TEXT("Create project wizard", L10N_PROJECT_NEW_DIALOG_TITLE), &openedDummy, ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::InputTextWithHint("##ProjectName", I18N_TEXT("Project name", L10N_PROJECT_NAME), &projectName); + + if (ImGui::InputTextWithHint("##ProjectPath", I18N_TEXT("Project path", L10N_PROJECT_PATH), &dirName)) { + // Changed, validate value + TrySelectPath(fs::path(dirName)); + } + ImGui::SameLine(); + if (ImGui::Button("...")) { + auto selection = pfd::select_folder(I18N_TEXT("Project path", L10N_PROJECT_NEW_PATH_DIALOG_TITLE)).result(); + if (!selection.empty()) { + TrySelectPath(fs::path(selection)); + } + } + + if (projectName.empty()) { + ImGui::ErrorMessage("%s", I18N_TEXT("Name cannot be empty", L10N_EMPTY_NAME_ERROR)); + } + if (!dirNameIsValid) { + ImGui::ErrorMessage("%s", I18N_TEXT("Invalid path", L10N_INVALID_PATH_ERROR)); + } + + ImGui::Spacing(); + + if (ImGui::Button(I18N_TEXT("Confirm", L10N_CONFIRM), !dirNameIsValid || projectName.empty())) { + ImGui::CloseCurrentPopup(); + + gs.SetCurrentProject(std::make_unique<Project>(std::move(dirPath), std::move(projectName))); + + // Dialog just got closed, reset states + projectName.clear(); + dirName.clear(); + dirPath.clear(); + dirNameIsValid = false; + } + + ImGui::SameLine(); + if (ImGui::Button(I18N_TEXT("Cancel", L10N_CANCEL))) { + ImGui::CloseCurrentPopup(); + } + + ImGui::EndPopup(); + } + + ImGui::SameLine(); + if (ImGui::Button(I18N_TEXT("Open project...", L10N_PROJECT_OPEN))) { + auto selection = pfd::open_file(I18N_TEXT("Open project", L10N_PROJECT_OPEN_DIALOG_TITLE)).result(); + if (!selection.empty()) { + fs::path path(selection[0]); + + try { + // Project's constructor wants a path to directory containing cplt_project.json + gs.SetCurrentProject(std::make_unique<Project>(path.parent_path())); + openErrorDialog = false; + } catch (const std::exception& e) { + openErrorDialog = true; + } + } + } + + // TODO cleanup UI + // Recent projects + + ImGui::Separator(); + ImGui::TextUnformatted(I18N_TEXT("Recent projects", L10N_PROJECT_RECENTS)); + + ImGui::SameLine(); + if (ImGui::Button(I18N_TEXT("Clear", L10N_PROJECT_RECENTS_CLEAR))) { + gs.ClearRecentProjects(); + } + + auto& rp = gs.GetRecentProjects(); + // End of vector is the most recently used, so that appending has less overhead + size_t toRemoveIdx = rp.size(); + + if (rp.empty()) { + ImGui::TextUnformatted(I18N_TEXT("No recent projects", L10N_PROJECT_RECENTS_NONE_PRESENT)); + } else { + for (auto it = rp.rbegin(); it != rp.rend(); ++it) { + auto& [path, recent] = *it; + ImGui::TextUnformatted(recent.c_str()); + + size_t idx = std::distance(it, rp.rend()) - 1; + ImGui::PushID(idx); + + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_FOLDER_OPEN)) { + try { + gs.SetCurrentProject(std::make_unique<Project>(path)); + openErrorDialog = false; + } catch (const std::exception& e) { + openErrorDialog = true; + } + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip(I18N_TEXT("Open this project", L10N_PROJECT_RECENTS_OPEN_TOOLTIP)); + } + + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_TRASH)) { + toRemoveIdx = idx; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip(I18N_TEXT("Delete this project from the Recent Projects list, the project itself will not be affected", L10N_PROJECT_RECENTS_DELETE_TOOLTIP)); + } + + ImGui::PopID(); + } + } + + if (toRemoveIdx != rp.size()) { + gs.RemoveRecentProject(toRemoveIdx); + } + + if (openErrorDialog) { + ImGui::SetNextWindowCentered(); + ImGui::OpenPopup(I18N_TEXT("Error", L10N_ERROR)); + } + if (ImGui::BeginPopupModal(I18N_TEXT("Error", L10N_ERROR), &openedDummy, ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::ErrorMessage("%s", I18N_TEXT("Invalid project file", L10N_PROJECT_INVALID_PROJECT_FORMAT)); + ImGui::EndPopup(); + } +} +} // namespace CPLT_UNITY_ID + +void UI::MainWindow() +{ + auto& gs = GlobalStates::GetInstance(); + + auto windowSize = ImGui::GetMainViewport()->Size; + ImGui::SetNextWindowSize({ windowSize.x, windowSize.y }); + ImGui::SetNextWindowPos({ 0, 0 }); + ImGui::Begin("##MainWindow", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize); + if (ImGui::BeginTabBar("MainWindowTabs")) { + if (ImGui::BeginTabItem(ICON_FA_COGS " " I18N_TEXT("Settings", L10N_MAIN_TAB_SETTINGS))) { + UI::SettingsTab(); + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem(ICON_FA_FILE " " I18N_TEXT("Project", L10N_MAIN_WINDOW_TAB_PROJECT), nullptr)) { + if (gs.HasCurrentProject()) { + CPLT_UNITY_ID::ProjectTab_Normal(); + } else { + CPLT_UNITY_ID::ProjectTab_NoProject(); + } + ImGui::EndTabItem(); + } + if (!gs.HasCurrentProject()) { + // No project open, simply skip all project specific tabs + goto endTab; + } + + if (ImGui::BeginTabItem(ICON_FA_DATABASE " " I18N_TEXT("Data", L10N_MAIN_WINDOW_TAB_DATABASE_VIEW))) { + UI::DatabaseViewTab(); + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem(ICON_FA_BOX " " I18N_TEXT("Items", L10N_MAIN_WINDOW_TAB_ITEMS))) { + UI::ItemsTab(); + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem(ICON_FA_SCROLL " " I18N_TEXT("Workflows", L10N_MAIN_WINDOW_TAB_WORKFLOWS))) { + UI::WorkflowsTab(); + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem(ICON_FA_TABLE " " I18N_TEXT("Templates", L10N_MAIN_WINDOW_TAB_TEMPLATES))) { + UI::TemplatesTab(); + ImGui::EndTabItem(); + } + + endTab: + ImGui::EndTabBar(); + } + ImGui::End(); +} diff --git a/app/source/Cplt/UI/UI_Settings.cpp b/app/source/Cplt/UI/UI_Settings.cpp new file mode 100644 index 0000000..71a752a --- /dev/null +++ b/app/source/Cplt/UI/UI_Settings.cpp @@ -0,0 +1,8 @@ +#include <Cplt/UI/UI.hpp> + +#include <imgui.h> + +void UI::SettingsTab() +{ + // TODO +} diff --git a/app/source/Cplt/UI/UI_Templates.cpp b/app/source/Cplt/UI/UI_Templates.cpp new file mode 100644 index 0000000..e01a97d --- /dev/null +++ b/app/source/Cplt/UI/UI_Templates.cpp @@ -0,0 +1,977 @@ +#include "UI.hpp" + +#include <Cplt/Model/GlobalStates.hpp> +#include <Cplt/Model/Project.hpp> +#include <Cplt/Model/Template/TableTemplate.hpp> +#include <Cplt/Model/Template/TableTemplateIterator.hpp> +#include <Cplt/Model/Template/Template.hpp> +#include <Cplt/Utils/I18n.hpp> + +#include <IconsFontAwesome.h> +#include <imgui.h> +#include <imgui_extra_math.h> +#include <imgui_internal.h> +#include <imgui_stdlib.h> +#include <charconv> +#include <fstream> +#include <iostream> +#include <utility> +#include <variant> + +namespace CPLT_UNITY_ID { +class TemplateUI +{ +public: + static std::unique_ptr<TemplateUI> CreateByKind(std::unique_ptr<Template> tmpl); + static std::unique_ptr<TemplateUI> CreateByKind(Template::Kind kind); + + virtual ~TemplateUI() = default; + virtual void Display() = 0; + virtual void Close() = 0; +}; + +// Table template styles +constexpr ImU32 kSingleParamOutline = IM_COL32(255, 255, 0, 255); +constexpr ImU32 kArrayGroupOutline = IM_COL32(255, 0, 0, 255); + +class TableTemplateUI : public TemplateUI +{ +private: + std::unique_ptr<TableTemplate> mTable; + + struct UICell + { + bool Hovered = false; + bool Held = false; + bool Selected = false; + }; + std::vector<UICell> mUICells; + + struct UIArrayGroup + { + ImVec2 Pos; + ImVec2 Size; + }; + std::vector<UIArrayGroup> mUIArrayGroups; + + struct Sizer + { + bool Hovered = false; + bool Held = false; + }; + std::vector<Sizer> mRowSizers; + std::vector<Sizer> mColSizers; + + /* Selection range */ + Vec2i mSelectionTL; + Vec2i mSelectionBR; + + /* Selection states */ + + /// "CStates" stands for "Constant cell selection States" + struct CStates + { + }; + + /// "SStates" stands for "Singular parameter selection States". + struct SStates + { + std::string EditBuffer; + bool ErrorDuplicateVarName; + bool HasLeftAG; + bool HasRightAG; + }; + + /// "AStates" stands for "Array group parameter selection States". + struct AStates + { + std::string EditBuffer; + bool ErrorDuplicateVarName; + }; + + // "RStates" stands for "Range selection States". + struct RStates + { + }; + + union + { + // Initialize to this element + std::monostate mIdleState{}; + CStates mCS; + SStates mSS; + AStates mAS; + RStates mRS; + }; + + /* Table resizer dialog states */ + int mNewTableWidth; + int mNewTableHeight; + + /* Table states */ + enum EditMode + { + ModeEditing, + ModeColumnResizing, + ModeRowResizing, + }; + EditMode mMode = ModeEditing; + + float mStartDragDim; + /// Depending on row/column sizer being dragged, this will be the y/x coordinate + float mStartDragMouseCoordinate; + + bool mDirty = false; + bool mFirstDraw = true; + +public: + TableTemplateUI(std::unique_ptr<TableTemplate> table) + : mTable{ std::move(table) } + , mSelectionTL{ -1, -1 } + , mSelectionBR{ -1, -1 } + { + // TODO debug code + Resize(6, 5); + } + + ~TableTemplateUI() override + { + // We can't move this to be a destructor of the union + // because that way it would run after the destruction of mTable + if (!IsSelected()) { + // Case: mIdleState + // Noop + } else if (mSelectionTL == mSelectionBR) { + switch (mTable->GetCell(mSelectionTL).Type) { + case TableCell::ConstantCell: + // Case: mCS + // Noop + break; + + case TableCell::SingularParametricCell: + // Case: mSS + mSS.EditBuffer.std::string::~string(); + break; + + case TableCell::ArrayParametricCell: + // Case: mAS + mAS.EditBuffer.std::string::~string(); + break; + } + } else { + // Case: mRS + // Noop + } + } + + void Display() override + { + ImGui::Columns(2); + if (mFirstDraw) { + mFirstDraw = false; + ImGui::SetColumnWidth(0, ImGui::GetWindowWidth() * 0.15f); + } + + DisplayInspector(); + ImGui::NextColumn(); + + auto initialPos = ImGui::GetCursorPos(); + DisplayTable(); + DisplayTableResizers(initialPos); + ImGui::NextColumn(); + + ImGui::Columns(1); + } + + void Close() override + { + // TODO + } + + void Resize(int width, int height) + { + mTable->Resize(width, height); + mUICells.resize(width * height); + mUIArrayGroups.resize(mTable->GetArrayGroupCount()); + mRowSizers.resize(width); + mColSizers.resize(height); + + for (size_t i = 0; i < mUIArrayGroups.size(); ++i) { + auto& ag = mTable->GetArrayGroup(i); + auto& uag = mUIArrayGroups[i]; + + auto itemSpacing = ImGui::GetStyle().ItemSpacing; + uag.Pos.x = CalcTablePixelWidth() + itemSpacing.x; + uag.Pos.y = CalcTablePixelHeight() + itemSpacing.y; + + uag.Size.x = mTable->GetRowHeight(ag.Row); + uag.Size.y = 0; + for (int x = ag.LeftCell; x <= ag.RightCell; ++x) { + uag.Size.y += mTable->GetColumnWidth(x); + } + } + + mSelectionTL = { 0, 0 }; + mSelectionBR = { 0, 0 }; + } + +private: + void DisplayInspector() + { + bool openedDummy = true; + + // This is an id, no need to localize + if (ImGui::BeginTabBar("Inspector")) { + if (ImGui::BeginTabItem(I18N_TEXT("Cell", L10N_TABLE_CELL))) { + if (!IsSelected()) { + ImGui::Text(I18N_TEXT("Select a cell to edit", L10N_TABLE_CELL_SELECT_MSG)); + } else if (mSelectionTL == mSelectionBR) { + DisplayCellProperties(mSelectionTL); + } else { + DisplayRangeProperties(mSelectionTL, mSelectionBR); + } + ImGui::EndTabItem(); + } + + auto OpenPopup = [](const char* name) { + // Act as if ImGui::OpenPopup is executed in the previous id stack frame (tab bar level) + // Note: we can't simply use ImGui::GetItemID() here, because that would return the id of the ImGui::Button + auto tabBar = ImGui::GetCurrentContext()->CurrentTabBar; + auto id = tabBar->Tabs[tabBar->LastTabItemIdx].ID; + ImGui::PopID(); + ImGui::OpenPopup(name); + ImGui::PushOverrideID(id); + }; + if (ImGui::BeginTabItem(I18N_TEXT("Table", L10N_TABLE))) { + if (ImGui::Button(I18N_TEXT("Configure table properties...", L10N_TABLE_CONFIGURE_PROPERTIES))) { + mNewTableWidth = mTable->GetTableWidth(); + mNewTableHeight = mTable->GetTableHeight(); + OpenPopup(I18N_TEXT("Table properties", L10N_TABLE_PROPERTIES)); + } + + int mode = mMode; + ImGui::RadioButton(I18N_TEXT("Edit table", L10N_TABLE_EDIT_TABLE), &mode, ModeEditing); + ImGui::RadioButton(I18N_TEXT("Resize column widths", L10N_TABLE_EDIT_RESIZE_COLS), &mode, ModeColumnResizing); + ImGui::RadioButton(I18N_TEXT("Resize rows heights", L10N_TABLE_EDIT_RESIZE_ROWS), &mode, ModeRowResizing); + mMode = static_cast<EditMode>(mode); + + // Table contents + DisplayTableContents(); + + ImGui::EndTabItem(); + } + if (ImGui::BeginPopupModal(I18N_TEXT("Table properties", L10N_TABLE_PROPERTIES), &openedDummy, ImGuiWindowFlags_AlwaysAutoResize)) { + DisplayTableProperties(); + ImGui::EndPopup(); + } + + ImGui::EndTabBar(); + } + } + + static char NthUppercaseLetter(int n) + { + return (char)((int)'A' + n); + } + + static void ExcelRow(int row, char* bufferBegin, char* bufferEnd) + { + auto res = std::to_chars(bufferBegin, bufferEnd, row); + if (res.ec != std::errc()) { + return; + } + } + + static char* ExcelColumn(int column, char* bufferBegin, char* bufferEnd) + { + // https://stackoverflow.com/a/182924/11323702 + + int dividend = column; + int modulo; + + char* writeHead = bufferEnd - 1; + *writeHead = '\0'; + --writeHead; + + while (dividend > 0) { + if (writeHead < bufferBegin) { + return nullptr; + } + + modulo = (dividend - 1) % 26; + + *writeHead = NthUppercaseLetter(modulo); + --writeHead; + + dividend = (dividend - modulo) / 26; + } + + // `writeHead` at this point would be a one-past-the-bufferEnd reverse iterator (i.e. one-past-the-(text)beginning in the bufferBegin) + // add 1 to get to the actual beginning of the text + return writeHead + 1; + } + + void DisplayCellProperties(Vec2i pos) + { + auto& cell = mTable->GetCell(pos); + auto& uiCell = mUICells[pos.y * mTable->GetTableWidth() + pos.x]; + + char colStr[8]; // 2147483647 -> FXSHRXW, len == 7, along with \0 + char* colBegin = ExcelColumn(pos.x + 1, std::begin(colStr), std::end(colStr)); + char rowStr[11]; // len(2147483647) == 10, along with \0 + ExcelRow(pos.y + 1, std::begin(rowStr), std::end(rowStr)); + ImGui::Text(I18N_TEXT("Location: %s%s", L10N_TABLE_CELL_POS), colBegin, rowStr); + + switch (cell.Type) { + case TableCell::ConstantCell: + ImGui::Text(I18N_TEXT("Type: Constant", L10N_TABLE_CELL_TYPE_CONST)); + break; + case TableCell::SingularParametricCell: + ImGui::Text(I18N_TEXT("Type: Single parameter", L10N_TABLE_CELL_TYPE_PARAM)); + break; + case TableCell::ArrayParametricCell: + ImGui::Text(I18N_TEXT("Type: Array group", L10N_TABLE_CELL_TYPE_CREATE_AG)); + break; + } + + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_EDIT)) { + ImGui::OpenPopup("ConvertCtxMenu"); + } + if (ImGui::BeginPopup("ConvertCtxMenu")) { + bool constantEnabled = cell.Type != TableCell::ConstantCell; + if (ImGui::MenuItem(I18N_TEXT("Convert to regular cell", L10N_TABLE_CELL_CONV_CONST), nullptr, false, constantEnabled)) { + mTable->SetCellType(pos, TableCell::ConstantCell); + ResetCS(); + } + + bool singleEnabled = cell.Type != TableCell::SingularParametricCell; + if (ImGui::MenuItem(I18N_TEXT("Convert to parameter cell", L10N_TABLE_CELL_CONV_PARAM), nullptr, false, singleEnabled)) { + mTable->SetCellType(pos, TableCell::SingularParametricCell); + ResetSS(pos); + } + + bool arrayEnabled = cell.Type != TableCell::ArrayParametricCell; + if (ImGui::MenuItem(I18N_TEXT("Add to a new array group", L10N_TABLE_CELL_CONV_CREATE_AG), nullptr, false, arrayEnabled)) { + mTable->AddArrayGroup(pos.y, pos.x, pos.x); // Use automatically generated name + ResetAS(pos); + } + + bool leftEnabled = mSS.HasLeftAG && arrayEnabled; + if (ImGui::MenuItem(I18N_TEXT("Add to the array group to the left", L10N_TABLE_CELL_CONV_ADD_AG_LEFT), nullptr, false, leftEnabled)) { + auto& leftCell = mTable->GetCell({ pos.x - 1, pos.y }); + mTable->ExtendArrayGroupRight(leftCell.DataId, 1); + ResetAS(pos); + } + + bool rightEnabled = mSS.HasRightAG && arrayEnabled; + if (ImGui::MenuItem(I18N_TEXT("Add to the array group to the right", L10N_TABLE_CELL_CONV_ADD_AG_RIGHT), nullptr, false, rightEnabled)) { + auto& rightCell = mTable->GetCell({ pos.x + 1, pos.y }); + mTable->ExtendArrayGroupLeft(rightCell.DataId, 1); + ResetAS(pos); + } + + ImGui::EndPopup(); + } + + ImGui::Spacing(); + + constexpr auto kLeft = I18N_TEXT("Left", L10N_TABLE_CELL_ALIGN_LEFT); + constexpr auto kCenter = I18N_TEXT("Center", L10N_TABLE_CELL_ALIGN_CENTER); + constexpr auto kRight = I18N_TEXT("Right", L10N_TABLE_CELL_ALIGN_RIGHT); + + const char* horizontalText; + switch (cell.HorizontalAlignment) { + case TableCell::AlignAxisMin: horizontalText = kLeft; break; + case TableCell::AlignCenter: horizontalText = kCenter; break; + case TableCell::AlignAxisMax: horizontalText = kRight; break; + } + + if (ImGui::BeginCombo(I18N_TEXT("Horizontal alignment", L10N_TABLE_CELL_HORIZONTAL_ALIGNMENT), horizontalText)) { + if (ImGui::Selectable(kLeft, cell.HorizontalAlignment == TableCell::AlignAxisMin)) { + cell.HorizontalAlignment = TableCell::AlignAxisMin; + } + if (ImGui::Selectable(kCenter, cell.HorizontalAlignment == TableCell::AlignCenter)) { + cell.HorizontalAlignment = TableCell::AlignCenter; + } + if (ImGui::Selectable(kRight, cell.HorizontalAlignment == TableCell::AlignAxisMax)) { + cell.HorizontalAlignment = TableCell::AlignAxisMax; + } + ImGui::EndCombo(); + } + + constexpr auto kTop = I18N_TEXT("Left", L10N_TABLE_CELL_ALIGN_TOP); + constexpr auto kMiddle = I18N_TEXT("Middle", L10N_TABLE_CELL_ALIGN_MIDDLE); + constexpr auto kBottom = I18N_TEXT("Right", L10N_TABLE_CELL_ALIGN_BOTTOM); + + const char* verticalText; + switch (cell.VerticalAlignment) { + case TableCell::AlignAxisMin: verticalText = kTop; break; + case TableCell::AlignCenter: verticalText = kMiddle; break; + case TableCell::AlignAxisMax: verticalText = kBottom; break; + } + + if (ImGui::BeginCombo(I18N_TEXT("Vertical alignment", L10N_TABLE_CELL_VERTICAL_ALIGNMENT), verticalText)) { + if (ImGui::Selectable(kTop, cell.VerticalAlignment == TableCell::AlignAxisMin)) { + cell.VerticalAlignment = TableCell::AlignAxisMin; + } + if (ImGui::Selectable(kMiddle, cell.VerticalAlignment == TableCell::AlignCenter)) { + cell.VerticalAlignment = TableCell::AlignCenter; + } + if (ImGui::Selectable(kBottom, cell.VerticalAlignment == TableCell::AlignAxisMax)) { + cell.VerticalAlignment = TableCell::AlignAxisMax; + } + ImGui::EndCombo(); + } + + switch (cell.Type) { + case TableCell::ConstantCell: + ImGui::InputText(I18N_TEXT("Content", L10N_TABLE_CELL_CONTENT), &cell.Content); + break; + + case TableCell::SingularParametricCell: + if (ImGui::InputText(I18N_TEXT("Variable name", L10N_TABLE_CELL_VAR_NAME), &mSS.EditBuffer)) { + // Sync name change to table + bool success = mTable->UpdateParameterName(cell.Content, mSS.EditBuffer); + if (success) { + // Flush name to display content + cell.Content = mSS.EditBuffer; + mSS.ErrorDuplicateVarName = false; + } else { + mSS.ErrorDuplicateVarName = true; + } + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip(I18N_TEXT("Name of the parameter link to this cell.", L10N_TABLE_CELL_VAR_TOOLTIP)); + } + + if (mSS.ErrorDuplicateVarName) { + ImGui::ErrorMessage(I18N_TEXT("Variable name duplicated.", L10N_TABLE_CELL_VAR_NAME_DUP)); + } + break; + + case TableCell::ArrayParametricCell: + if (ImGui::InputText(I18N_TEXT("Variable name", L10N_TABLE_CELL_VAR_NAME), &mAS.EditBuffer)) { + auto ag = mTable->GetArrayGroup(cell.DataId); + bool success = ag.UpdateCellName(cell.Content, mAS.EditBuffer); + if (success) { + cell.Content = mAS.EditBuffer; + mAS.ErrorDuplicateVarName = false; + } else { + mAS.ErrorDuplicateVarName = true; + } + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip(I18N_TEXT("Name of the parameter link to this cell; local within the array group.", L10N_TABLE_CELL_ARRAY_VAR_TOOLTIP)); + } + + if (mAS.ErrorDuplicateVarName) { + ImGui::ErrorMessage(I18N_TEXT("Variable name duplicated.", L10N_TABLE_CELL_VAR_NAME_DUP)); + } + break; + } + } + + void DisplayRangeProperties(Vec2i tl, Vec2i br) + { + // TODO + } + + void DisplayTableContents() + { + if (ImGui::TreeNode(ICON_FA_BONG " " I18N_TEXT("Parameters", L10N_TABLE_SINGLE_PARAMS))) { + TableSingleParamsIter iter(*mTable); + while (iter.HasNext()) { + auto& cell = iter.Next(); + if (ImGui::Selectable(cell.Content.c_str())) { + SelectCell(cell.Location); + } + } + ImGui::TreePop(); + } + if (ImGui::TreeNode(ICON_FA_LIST " " I18N_TEXT("Array groups", L10N_TABLE_ARRAY_GROUPS))) { + TableArrayGroupsIter iter(*mTable); + // For each array group + while (iter.HasNext()) { + if (ImGui::TreeNode(iter.PeekNameCStr())) { + auto& ag = iter.Peek(); + // For each cell in the array group + for (int x = ag.LeftCell; x <= ag.RightCell; ++x) { + Vec2i pos{ x, ag.Row }; + auto& cell = mTable->GetCell(pos); + if (ImGui::Selectable(cell.Content.c_str())) { + SelectCell(pos); + } + } + ImGui::TreePop(); + } + iter.Next(); + } + ImGui::TreePop(); + } + } + + void DisplayTableProperties() + { + ImGui::InputInt(I18N_TEXT("Width", L10N_TABLE_WIDTH), &mNewTableWidth); + ImGui::InputInt(I18N_TEXT("Height", L10N_TABLE_HEIGHT), &mNewTableHeight); + + if (ImGui::Button(I18N_TEXT("Confirm", L10N_CONFIRM))) { + ImGui::CloseCurrentPopup(); + Resize(mNewTableWidth, mNewTableHeight); + } + ImGui::SameLine(); + if (ImGui::Button(I18N_TEXT("Cancel", L10N_CANCEL))) { + ImGui::CloseCurrentPopup(); + } + + // TODO + } + + void DisplayTable() + { + struct CellPalette + { + ImU32 Regular; + ImU32 Hovered; + ImU32 Active; + + ImU32 GetColorFor(const UICell& cell) const + { + if (cell.Held) { + return Active; + } else if (cell.Hovered) { + return Hovered; + } else { + return Regular; + } + } + }; + + CellPalette constantPalette{ + .Regular = ImGui::GetColorU32(ImGuiCol_Button), + .Hovered = ImGui::GetColorU32(ImGuiCol_ButtonHovered), + .Active = ImGui::GetColorU32(ImGuiCol_ButtonActive), + }; + CellPalette paramPalette{ + .Regular = IM_COL32(0, 214, 4, 102), + .Hovered = IM_COL32(0, 214, 4, 255), + .Active = IM_COL32(0, 191, 2, 255), + }; + + // TODO array group color + + auto navHighlight = ImGui::GetColorU32(ImGuiCol_NavHighlight); + + int colCount = mTable->GetTableWidth(); + int rowCount = mTable->GetTableHeight(); + for (int rowIdx = 0; rowIdx < rowCount; ++rowIdx) { + int rowHeight = mTable->GetRowHeight(rowIdx); + + for (int colIdx = 0; colIdx < colCount; ++colIdx) { + int colWidth = mTable->GetColumnWidth(colIdx); + + int i = rowIdx * colCount + colIdx; + auto window = ImGui::GetCurrentWindow(); + auto id = window->GetID(i); + + Vec2i cellLoc{ colIdx, rowIdx }; + auto& cell = mTable->GetCell(cellLoc); + auto& uiCell = mUICells[i]; + + ImVec2 size(colWidth, rowHeight); + ImRect rect{ + window->DC.CursorPos, + window->DC.CursorPos + ImGui::CalcItemSize(size, 0.0f, 0.0f), + }; + + /* Draw cell selection */ + + if (uiCell.Selected) { + constexpr int mt = 2; // Marker Thickness + constexpr int ms = 8; // Marker Size + + ImVec2 outerTL(rect.Min - ImVec2(mt, mt)); + ImVec2 outerBR(rect.Max + ImVec2(mt, mt)); + + // Top left + window->DrawList->AddRectFilled(outerTL + ImVec2(0, 0), outerTL + ImVec2(ms, mt), navHighlight); + window->DrawList->AddRectFilled(outerTL + ImVec2(0, mt), outerTL + ImVec2(mt, ms), navHighlight); + + // Top right + ImVec2 outerTR(outerBR.x, outerTL.y); + window->DrawList->AddRectFilled(outerTR + ImVec2(-ms, 0), outerTR + ImVec2(0, mt), navHighlight); + window->DrawList->AddRectFilled(outerTR + ImVec2(-mt, mt), outerTR + ImVec2(0, ms), navHighlight); + + // Bottom right + window->DrawList->AddRectFilled(outerBR + ImVec2(-ms, -mt), outerBR + ImVec2(0, 0), navHighlight); + window->DrawList->AddRectFilled(outerBR + ImVec2(-mt, -ms), outerBR + ImVec2(0, -mt), navHighlight); + + // Bottom left + ImVec2 outerBL(outerTL.x, outerBR.y); + window->DrawList->AddRectFilled(outerBL + ImVec2(0, -mt), outerBL + ImVec2(ms, 0), navHighlight); + window->DrawList->AddRectFilled(outerBL + ImVec2(0, -ms), outerBL + ImVec2(mt, -mt), navHighlight); + } + + /* Draw cell body */ + + CellPalette* palette; + switch (cell.Type) { + case TableCell::ConstantCell: + palette = &constantPalette; + break; + + case TableCell::SingularParametricCell: + case TableCell::ArrayParametricCell: + palette = ¶mPalette; + break; + } + + window->DrawList->AddRectFilled(rect.Min, rect.Max, palette->GetColorFor(uiCell)); + + /* Draw cell content */ + + auto content = cell.Content.c_str(); + auto contentEnd = content + cell.Content.size(); + auto textSize = ImGui::CalcTextSize(content, contentEnd); + + ImVec2 textRenderPos; + switch (cell.HorizontalAlignment) { + case TableCell::AlignAxisMin: textRenderPos.x = rect.Min.x; break; + case TableCell::AlignCenter: textRenderPos.x = rect.Min.x + colWidth / 2 - textSize.x / 2; break; + case TableCell::AlignAxisMax: textRenderPos.x = rect.Max.x - textSize.x; break; + } + switch (cell.VerticalAlignment) { + case TableCell::AlignAxisMin: textRenderPos.y = rect.Min.y; break; + case TableCell::AlignCenter: textRenderPos.y = rect.Min.y + rowHeight / 2 - textSize.y / 2; break; + case TableCell::AlignAxisMax: textRenderPos.y = rect.Max.y - textSize.y; break; + } + window->DrawList->AddText(textRenderPos, IM_COL32(0, 0, 0, 255), content, contentEnd); + + /* Update ImGui cursor */ + + ImGui::ItemSize(size); + if (!ImGui::ItemAdd(rect, id)) { + goto logicEnd; + } + + if (mMode != ModeEditing) { + goto logicEnd; + } + if (ImGui::ButtonBehavior(rect, id, &uiCell.Hovered, &uiCell.Held)) { + if (ImGui::GetIO().KeyShift && IsSelected()) { + SelectRange(mSelectionTL, { colIdx, rowIdx }); + } else { + SelectCell({ colIdx, rowIdx }); + } + } + + logicEnd: + // Don't SameLine() if we are on the last cell in the row + if (colIdx != colCount - 1) { + ImGui::SameLine(); + } + } + } + + for (auto& uag : mUIArrayGroups) { + ImGui::GetCurrentWindow()->DrawList->AddRect( + uag.Pos - ImVec2(1, 1), + uag.Pos + uag.Size + ImVec2(1, 1), + kArrayGroupOutline); + } + } + + void DisplayResizers( + ImVec2 pos, + ImVec2 sizerDim, + ImVec2 sizerOffset, + std::type_identity_t<float ImVec2::*> vecCompGetter, + std::type_identity_t<int (TableTemplate::*)() const> lenGetter, + std::type_identity_t<int (TableTemplate::*)(int) const> dimGetter, + std::type_identity_t<void (TableTemplate::*)(int, int)> dimSetter) + { + auto window = ImGui::GetCurrentWindow(); + auto spacing = ImGui::GetStyle().ItemSpacing.*vecCompGetter; + + auto regularColor = ImGui::GetColorU32(ImGuiCol_Button); + auto hoveredColor = ImGui::GetColorU32(ImGuiCol_ButtonHovered); + auto activeColor = ImGui::GetColorU32(ImGuiCol_ButtonActive); + + auto GetColor = [&](const Sizer& sizer) -> ImU32 { + if (sizer.Held) { + return activeColor; + } else if (sizer.Hovered) { + return hoveredColor; + } else { + return regularColor; + } + }; + + for (int ix = 0; ix < (mTable.get()->*lenGetter)(); ++ix) { + // ImGui uses float for sizes, our table uses int (because excel uses int) + // Convert here to avoid mountains of narrowing warnings below + auto dim = (float)(mTable.get()->*dimGetter)(ix); + + pos.*vecCompGetter += dim; + ImRect rect{ + pos - sizerOffset, + pos - sizerOffset + ImGui::CalcItemSize(sizerDim, 0.0f, 0.0f), + }; + pos.*vecCompGetter += spacing; + + auto& sizer = mColSizers[ix]; + auto id = window->GetID(ix); + window->DrawList->AddRectFilled(rect.Min, rect.Max, GetColor(sizer)); + + if (ImGui::ButtonBehavior(rect, id, &sizer.Hovered, &sizer.Held, ImGuiButtonFlags_PressedOnClick)) { + mStartDragDim = dim; + mStartDragMouseCoordinate = ImGui::GetMousePos().*vecCompGetter; + } + if (sizer.Held) { + float change = ImGui::GetMousePos().*vecCompGetter - mStartDragMouseCoordinate; + float colWidth = std::max(mStartDragDim + change, 1.0f); + (mTable.get()->*dimSetter)(ix, (int)colWidth); + } + } + } + + void DisplayTableResizers(ImVec2 topLeftPixelPos) + { + constexpr float kExtraSideLength = 5.0f; + constexpr float kExtraAxialLength = 2.0f; + + switch (mMode) { + case ModeEditing: break; + + case ModeColumnResizing: + ImGui::PushID("Cols"); + DisplayResizers( + topLeftPixelPos, + ImVec2( + ImGui::GetStyle().ItemSpacing.x + kExtraSideLength * 2, + CalcTablePixelHeight() + kExtraAxialLength * 2), + ImVec2(kExtraSideLength, kExtraAxialLength), + &ImVec2::x, + &TableTemplate::GetTableWidth, + &TableTemplate::GetColumnWidth, + &TableTemplate::SetColumnWidth); + ImGui::PopID(); + break; + + case ModeRowResizing: + ImGui::PushID("Rows"); + DisplayResizers( + topLeftPixelPos, + ImVec2( + CalcTablePixelWidth() + kExtraAxialLength * 2, + ImGui::GetStyle().ItemSpacing.y + kExtraSideLength * 2), + ImVec2(kExtraAxialLength, kExtraSideLength), + &ImVec2::y, + &TableTemplate::GetTableHeight, + &TableTemplate::GetRowHeight, + &TableTemplate::SetRowHeight); + ImGui::PopID(); + break; + } + } + + float CalcTablePixelWidth() const + { + float horizontalSpacing = ImGui::GetStyle().ItemSpacing.x; + float width = 0; + for (int x = 0; x < mTable->GetTableWidth(); ++x) { + width += mTable->GetColumnWidth(x); + width += horizontalSpacing; + } + return width - horizontalSpacing; + } + + float CalcTablePixelHeight() const + { + float verticalSpacing = ImGui::GetStyle().ItemSpacing.y; + float height = 0; + for (int y = 0; y < mTable->GetTableHeight(); ++y) { + height += mTable->GetRowHeight(y); + height += verticalSpacing; + } + return height - verticalSpacing; + } + + template <class TFunction> + void ForeachSelectedCell(const TFunction& func) + { + for (int y = mSelectionTL.y; y <= mSelectionBR.y; ++y) { + for (int x = mSelectionTL.x; x <= mSelectionBR.x; ++x) { + int i = y * mTable->GetTableWidth() + x; + func(i, x, y); + } + } + } + + bool IsSelected() const + { + return mSelectionTL.x != -1; + } + + void ClearSelection() + { + if (IsSelected()) { + ForeachSelectedCell([this](int i, int, int) { + auto& uiCell = mUICells[i]; + uiCell.Selected = false; + }); + } + + mSelectionTL = { -1, -1 }; + mSelectionBR = { -1, -1 }; + + ResetIdleState(); + } + + void ResetIdleState() + { + mIdleState = {}; + } + + void SelectRange(Vec2i p1, Vec2i p2) + { + ClearSelection(); + + if (p2.x < p1.x) { + std::swap(p2.x, p1.x); + } + if (p2.y < p1.y) { + std::swap(p2.y, p1.y); + } + + mSelectionTL = p1; + mSelectionBR = p2; + + ForeachSelectedCell([this](int i, int, int) { + auto& uiCell = mUICells[i]; + uiCell.Selected = true; + }); + + ResetRS(); + } + + void ResetRS() + { + mRS = {}; + } + + void SelectCell(Vec2i pos) + { + ClearSelection(); + + mSelectionTL = pos; + mSelectionBR = pos; + + int i = pos.y * mTable->GetTableWidth() + pos.x; + mUICells[i].Selected = true; + + switch (mTable->GetCell(pos).Type) { + case TableCell::ConstantCell: ResetCS(); break; + case TableCell::SingularParametricCell: ResetSS(pos); break; + case TableCell::ArrayParametricCell: ResetAS(pos); break; + } + } + + void ResetCS() + { + mCS = {}; + } + + void ResetSS(Vec2i pos) + { + new (&mSS) SStates{ + .EditBuffer = mTable->GetCell(pos).Content, + .ErrorDuplicateVarName = false, + .HasLeftAG = pos.x > 1 && mTable->GetCell({ pos.x - 1, pos.y }).Type == TableCell::ArrayParametricCell, + .HasRightAG = pos.x < mTable->GetTableWidth() - 1 && mTable->GetCell({ pos.x + 1, pos.y }).Type == TableCell::ArrayParametricCell, + }; + } + + void ResetAS(Vec2i pos) + { + new (&mAS) AStates{ + .EditBuffer = mTable->GetCell(pos).Content, + .ErrorDuplicateVarName = false, + }; + } +}; + +template <class TTarget> +static auto CastTemplateAs(std::unique_ptr<Template>& input) requires std::is_base_of_v<Template, TTarget> +{ + return std::unique_ptr<TTarget>(static_cast<TTarget*>(input.release())); +} + +std::unique_ptr<TemplateUI> TemplateUI::CreateByKind(std::unique_ptr<Template> tmpl) +{ + switch (tmpl->GetKind()) { + case Template::KD_Table: return std::make_unique<TableTemplateUI>(CastTemplateAs<TableTemplate>(tmpl)); + case Template::InvalidKind: break; + } + return nullptr; +} + +std::unique_ptr<TemplateUI> TemplateUI::CreateByKind(Template::Kind kind) +{ + switch (kind) { + case Template::KD_Table: return std::make_unique<TableTemplateUI>(std::make_unique<TableTemplate>()); + case Template::InvalidKind: break; + } + return nullptr; +} +} // namespace CPLT_UNITY_ID + +void UI::TemplatesTab() +{ + auto& project = *GlobalStates::GetInstance().GetCurrentProject(); + + static std::unique_ptr<CPLT_UNITY_ID::TemplateUI> openTemplate; + static AssetList::ListState state; + bool openedDummy = true; + + // Toolbar item: close + if (ImGui::Button(ICON_FA_TIMES " " I18N_TEXT("Close", L10N_CLOSE), openTemplate == nullptr)) { + openTemplate->Close(); + openTemplate = nullptr; + } + + // Toolbar item: open... + ImGui::SameLine(); + if (ImGui::Button(I18N_TEXT("Open asset...", L10N_ASSET_OPEN))) { + ImGui::OpenPopup(I18N_TEXT("Open asset", L10N_ASSET_OPEN_DIALOG_TITLE)); + } + if (ImGui::BeginPopupModal(I18N_TEXT("Open asset", L10N_ASSET_OPEN_DIALOG_TITLE), &openedDummy, ImGuiWindowFlags_AlwaysAutoResize)) { + if (ImGui::Button(ICON_FA_FOLDER_OPEN " " I18N_TEXT("Open", L10N_OPEN), state.SelectedAsset == nullptr)) { + ImGui::CloseCurrentPopup(); + + auto tmpl = project.Templates.Load(*state.SelectedAsset); + openTemplate = CPLT_UNITY_ID::TemplateUI::CreateByKind(std::move(tmpl)); + } + ImGui::SameLine(); + project.Templates.DisplayControls(state); + project.Templates.DisplayDetailsList(state); + + ImGui::EndPopup(); + } + + // Toolbar item: manage... + ImGui::SameLine(); + if (ImGui::Button(I18N_TEXT("Manage assets...", L10N_ASSET_MANAGE))) { + ImGui::OpenPopup(I18N_TEXT("Manage assets", L10N_ASSET_MANAGE_DIALOG_TITLE)); + } + if (ImGui::BeginPopupModal(I18N_TEXT("Manage assets", L10N_ASSET_MANAGE_DIALOG_TITLE), &openedDummy, ImGuiWindowFlags_AlwaysAutoResize)) { + project.Templates.DisplayControls(state); + project.Templates.DisplayDetailsList(state); + ImGui::EndPopup(); + } + + if (openTemplate) { + openTemplate->Display(); + } +} diff --git a/app/source/Cplt/UI/UI_Utils.cpp b/app/source/Cplt/UI/UI_Utils.cpp new file mode 100644 index 0000000..a2bf692 --- /dev/null +++ b/app/source/Cplt/UI/UI_Utils.cpp @@ -0,0 +1,315 @@ +#include "UI.hpp" + +#include <IconsFontAwesome.h> +#include <imgui.h> + +#define IMGUI_DEFINE_MATH_OPERATORS +#include <imgui_internal.h> + +void ImGui::SetNextWindowSizeRelScreen(float xPercent, float yPercent, ImGuiCond cond) +{ + auto vs = ImGui::GetMainViewport()->Size; + ImGui::SetNextWindowSize({ vs.x * xPercent, vs.y * yPercent }, cond); +} + +void ImGui::SetNextWindowCentered(ImGuiCond cond) +{ + auto vs = ImGui::GetMainViewport()->Size; + ImGui::SetNextWindowPos({ vs.x / 2, vs.y / 2 }, cond, { 0.5f, 0.5f }); +} + +void ImGui::PushDisabled() +{ + ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true); + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.5f * ImGui::GetStyle().Alpha); +} + +void ImGui::PopDisabled() +{ + ImGui::PopItemFlag(); + ImGui::PopStyleVar(); +} + +bool ImGui::Button(const char* label, bool disabled) +{ + return Button(label, ImVec2{}, disabled); +} + +bool ImGui::Button(const char* label, const ImVec2& sizeArg, bool disabled) +{ + if (disabled) PushDisabled(); + bool res = ImGui::Button(label, sizeArg); + if (disabled) PopDisabled(); + + // Help clang-tidy's static analyzer: if the button is disabled, res should always be false + assert(!disabled || (disabled && !res)); + + return res; +} + +void ImGui::ErrorIcon() +{ + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4{ 237 / 255.0f, 67 / 255.0f, 55 / 255.0f, 1.0f }); // #ED4337 + ImGui::Text(ICON_FA_EXCLAMATION_CIRCLE); + ImGui::PopStyleColor(); +} + +void ImGui::ErrorMessage(const char* fmt, ...) +{ + ErrorIcon(); + SameLine(); + + va_list args; + va_start(args, fmt); + TextV(fmt, args); + va_end(args); +} + +void ImGui::WarningIcon() +{ + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4{ 255 / 255.0f, 184 / 255.0f, 24 / 255.0f, 1.0f }); // #FFB818 + ImGui::Text(ICON_FA_EXCLAMATION_TRIANGLE); + ImGui::PopStyleColor(); +} + +void ImGui::WarningMessage(const char* fmt, ...) +{ + WarningIcon(); + SameLine(); + + va_list args; + va_start(args, fmt); + TextV(fmt, args); + va_end(args); +} + +void ImGui::DrawIcon(ImDrawList* drawList, const ImVec2& a, const ImVec2& b, IconType type, bool filled, ImU32 color, ImU32 innerColor) +{ + // Taken from https://github.com/thedmd/imgui-node-editor/blob/master/examples/blueprints-example/utilities/drawing.cpp + // ax::NodeEditor::DrawIcon + + // Brace style was adapted but no names are changed + + auto rect = ImRect(a, b); + auto rect_x = rect.Min.x; + auto rect_y = rect.Min.y; + auto rect_w = rect.Max.x - rect.Min.x; + auto rect_h = rect.Max.y - rect.Min.y; + auto rect_center_x = (rect.Min.x + rect.Max.x) * 0.5f; + auto rect_center_y = (rect.Min.y + rect.Max.y) * 0.5f; + auto rect_center = ImVec2(rect_center_x, rect_center_y); + const auto outline_scale = rect_w / 24.0f; + const auto extra_segments = static_cast<int>(2 * outline_scale); // for full circle + + if (type == IconType::Flow) { + const auto origin_scale = rect_w / 24.0f; + + const auto offset_x = 1.0f * origin_scale; + const auto offset_y = 0.0f * origin_scale; + const auto margin = 2.0f * origin_scale; + const auto rounding = 0.1f * origin_scale; + const auto tip_round = 0.7f; // percentage of triangle edge (for tip) + //const auto edge_round = 0.7f; // percentage of triangle edge (for corner) + const auto canvas = ImRect( + rect.Min.x + margin + offset_x, + rect.Min.y + margin + offset_y, + rect.Max.x - margin + offset_x, + rect.Max.y - margin + offset_y); + const auto canvas_x = canvas.Min.x; + const auto canvas_y = canvas.Min.y; + const auto canvas_w = canvas.Max.x - canvas.Min.x; + const auto canvas_h = canvas.Max.y - canvas.Min.y; + + const auto left = canvas_x + canvas_w * 0.5f * 0.3f; + const auto right = canvas_x + canvas_w - canvas_w * 0.5f * 0.3f; + const auto top = canvas_y + canvas_h * 0.5f * 0.2f; + const auto bottom = canvas_y + canvas_h - canvas_h * 0.5f * 0.2f; + const auto center_y = (top + bottom) * 0.5f; + //const auto angle = AX_PI * 0.5f * 0.5f * 0.5f; + + const auto tip_top = ImVec2(canvas_x + canvas_w * 0.5f, top); + const auto tip_right = ImVec2(right, center_y); + const auto tip_bottom = ImVec2(canvas_x + canvas_w * 0.5f, bottom); + + drawList->PathLineTo(ImVec2(left, top) + ImVec2(0, rounding)); + drawList->PathBezierCurveTo( + ImVec2(left, top), + ImVec2(left, top), + ImVec2(left, top) + ImVec2(rounding, 0)); + drawList->PathLineTo(tip_top); + drawList->PathLineTo(tip_top + (tip_right - tip_top) * tip_round); + drawList->PathBezierCurveTo( + tip_right, + tip_right, + tip_bottom + (tip_right - tip_bottom) * tip_round); + drawList->PathLineTo(tip_bottom); + drawList->PathLineTo(ImVec2(left, bottom) + ImVec2(rounding, 0)); + drawList->PathBezierCurveTo( + ImVec2(left, bottom), + ImVec2(left, bottom), + ImVec2(left, bottom) - ImVec2(0, rounding)); + + if (!filled) { + if (innerColor & 0xFF000000) { + drawList->AddConvexPolyFilled(drawList->_Path.Data, drawList->_Path.Size, innerColor); + } + + drawList->PathStroke(color, true, 2.0f * outline_scale); + } else { + drawList->PathFillConvex(color); + } + } else { + auto triangleStart = rect_center_x + 0.32f * rect_w; + + auto rect_offset = -static_cast<int>(rect_w * 0.25f * 0.25f); + + rect.Min.x += rect_offset; + rect.Max.x += rect_offset; + rect_x += rect_offset; + rect_center_x += rect_offset * 0.5f; + rect_center.x += rect_offset * 0.5f; + + if (type == IconType::Circle) { + const auto c = rect_center; + + if (!filled) { + const auto r = 0.5f * rect_w / 2.0f - 0.5f; + + if (innerColor & 0xFF000000) + drawList->AddCircleFilled(c, r, innerColor, 12 + extra_segments); + drawList->AddCircle(c, r, color, 12 + extra_segments, 2.0f * outline_scale); + } else { + drawList->AddCircleFilled(c, 0.5f * rect_w / 2.0f, color, 12 + extra_segments); + } + } + + if (type == IconType::Square) { + if (filled) { + const auto r = 0.5f * rect_w / 2.0f; + const auto p0 = rect_center - ImVec2(r, r); + const auto p1 = rect_center + ImVec2(r, r); + + drawList->AddRectFilled(p0, p1, color, 0, 15 + extra_segments); + } else { + const auto r = 0.5f * rect_w / 2.0f - 0.5f; + const auto p0 = rect_center - ImVec2(r, r); + const auto p1 = rect_center + ImVec2(r, r); + + if (innerColor & 0xFF000000) + drawList->AddRectFilled(p0, p1, innerColor, 0, 15 + extra_segments); + + drawList->AddRect(p0, p1, color, 0, 15 + extra_segments, 2.0f * outline_scale); + } + } + + if (type == IconType::Grid) { + const auto r = 0.5f * rect_w / 2.0f; + const auto w = ceilf(r / 3.0f); + + const auto baseTl = ImVec2(floorf(rect_center_x - w * 2.5f), floorf(rect_center_y - w * 2.5f)); + const auto baseBr = ImVec2(floorf(baseTl.x + w), floorf(baseTl.y + w)); + + auto tl = baseTl; + auto br = baseBr; + for (int i = 0; i < 3; ++i) { + tl.x = baseTl.x; + br.x = baseBr.x; + drawList->AddRectFilled(tl, br, color); + tl.x += w * 2; + br.x += w * 2; + if (i != 1 || filled) + drawList->AddRectFilled(tl, br, color); + tl.x += w * 2; + br.x += w * 2; + drawList->AddRectFilled(tl, br, color); + + tl.y += w * 2; + br.y += w * 2; + } + + triangleStart = br.x + w + 1.0f / 24.0f * rect_w; + } + + if (type == IconType::RoundSquare) { + if (filled) { + const auto r = 0.5f * rect_w / 2.0f; + const auto cr = r * 0.5f; + const auto p0 = rect_center - ImVec2(r, r); + const auto p1 = rect_center + ImVec2(r, r); + + drawList->AddRectFilled(p0, p1, color, cr, 15); + } else { + const auto r = 0.5f * rect_w / 2.0f - 0.5f; + const auto cr = r * 0.5f; + const auto p0 = rect_center - ImVec2(r, r); + const auto p1 = rect_center + ImVec2(r, r); + + if (innerColor & 0xFF000000) + drawList->AddRectFilled(p0, p1, innerColor, cr, 15); + + drawList->AddRect(p0, p1, color, cr, 15, 2.0f * outline_scale); + } + } else if (type == IconType::Diamond) { + if (filled) { + const auto r = 0.607f * rect_w / 2.0f; + const auto c = rect_center; + + drawList->PathLineTo(c + ImVec2(0, -r)); + drawList->PathLineTo(c + ImVec2(r, 0)); + drawList->PathLineTo(c + ImVec2(0, r)); + drawList->PathLineTo(c + ImVec2(-r, 0)); + drawList->PathFillConvex(color); + } else { + const auto r = 0.607f * rect_w / 2.0f - 0.5f; + const auto c = rect_center; + + drawList->PathLineTo(c + ImVec2(0, -r)); + drawList->PathLineTo(c + ImVec2(r, 0)); + drawList->PathLineTo(c + ImVec2(0, r)); + drawList->PathLineTo(c + ImVec2(-r, 0)); + + if (innerColor & 0xFF000000) + drawList->AddConvexPolyFilled(drawList->_Path.Data, drawList->_Path.Size, innerColor); + + drawList->PathStroke(color, true, 2.0f * outline_scale); + } + } else { + const auto triangleTip = triangleStart + rect_w * (0.45f - 0.32f); + + drawList->AddTriangleFilled( + ImVec2(ceilf(triangleTip), rect_y + rect_h * 0.5f), + ImVec2(triangleStart, rect_center_y + 0.15f * rect_h), + ImVec2(triangleStart, rect_center_y - 0.15f * rect_h), + color); + } + } +} + +void ImGui::Icon(const ImVec2& size, IconType type, bool filled, const ImVec4& color, const ImVec4& innerColor) +{ + // Taken from https://github.com/thedmd/imgui-node-editor/blob/master/examples/blueprints-example/utilities/widgets.cpp + // ax::NodeEditor::Icon + + if (ImGui::IsRectVisible(size)) + { + auto cursorPos = ImGui::GetCursorScreenPos(); + auto drawList = ImGui::GetWindowDrawList(); + DrawIcon(drawList, cursorPos, cursorPos + size, type, filled, ImColor(color), ImColor(innerColor)); + } + + ImGui::Dummy(size); +} + +bool ImGui::Splitter(bool splitVertically, float thickness, float* size1, float* size2, float minSize1, float minSize2, float splitterLongAxisSize) +{ + // Taken from https://github.com/thedmd/imgui-node-editor/blob/master/examples/blueprints-example/blueprints-example.cpp + // ::Splitter + + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiID id = window->GetID("##Splitter"); + ImRect bb; + bb.Min = window->DC.CursorPos + (splitVertically ? ImVec2(*size1, 0.0f) : ImVec2(0.0f, *size1)); + bb.Max = bb.Min + CalcItemSize(splitVertically ? ImVec2(thickness, splitterLongAxisSize) : ImVec2(splitterLongAxisSize, thickness), 0.0f, 0.0f); + return SplitterBehavior(bb, id, splitVertically ? ImGuiAxis_X : ImGuiAxis_Y, size1, size2, minSize1, minSize2, 0.0f); +} diff --git a/app/source/Cplt/UI/UI_Workflows.cpp b/app/source/Cplt/UI/UI_Workflows.cpp new file mode 100644 index 0000000..5eea53a --- /dev/null +++ b/app/source/Cplt/UI/UI_Workflows.cpp @@ -0,0 +1,293 @@ +#include "UI.hpp" + +#include <Cplt/Model/GlobalStates.hpp> +#include <Cplt/Model/Project.hpp> +#include <Cplt/Model/Workflow/Nodes/DocumentNodes.hpp> +#include <Cplt/Model/Workflow/Nodes/NumericNodes.hpp> +#include <Cplt/Model/Workflow/Nodes/TextNodes.hpp> +#include <Cplt/Model/Workflow/Nodes/UserInputNodes.hpp> +#include <Cplt/Model/Workflow/Workflow.hpp> +#include <Cplt/Utils/I18n.hpp> +#include <Cplt/Utils/Macros.hpp> + +#include <IconsFontAwesome.h> +#include <imgui.h> +#include <imgui_node_editor.h> +#include <imgui_stdlib.h> +#include <memory> +#include <span> +#include <vector> + +namespace ImNodes = ax::NodeEditor; + +namespace CPLT_UNITY_ID { +class WorkflowUI +{ +private: + std::unique_ptr<Workflow> mWorkflow; + + ImNodes::EditorContext* mContext; + + ImNodes::NodeId mContextMenuNodeId = 0; + ImNodes::PinId mContextMenuPinId = 0; + ImNodes::LinkId mContextMenuLinkId = 0; + +public: + WorkflowUI(std::unique_ptr<Workflow> workflow) + : mWorkflow{ std::move(workflow) } + { + mContext = ImNodes::CreateEditor(); + } + + ~WorkflowUI() + { + ImNodes::DestroyEditor(mContext); + } + + void Display() + { + ImNodes::SetCurrentEditor(mContext); + ImNodes::Begin(""); + + // Defer creation of tooltip because within the node editor, cursor positioning is going to be off + const char* tooltipMessage = nullptr; + + for (auto& node : mWorkflow->GetNodes()) { + if (!node) continue; + + ImNodes::BeginNode(node->GetId()); + node->Draw(); + ImNodes::EndNode(); + } + + for (auto& conn : mWorkflow->GetConnections()) { + if (!conn.IsValid()) continue; + + auto srcId = mWorkflow->GetNodes()[conn.SourceNode]->GetOutputPinUniqueId(conn.SourcePin); + auto dstId = mWorkflow->GetNodes()[conn.DestinationNode]->GetInputPinUniqueId(conn.DestinationPin); + ImNodes::Link(conn.GetLinkId(), srcId, dstId); + } + + if (ImNodes::BeginCreate()) { + ImNodes::PinId src = 0, dst = 0; + if (ImNodes::QueryNewLink(&src, &dst)) { + if (!src || !dst) { + goto createError; + } + + auto [srcNode, srcPinId, srcIsOutput] = mWorkflow->DisassembleGlobalPinId(src); + auto [dstNode, dstPinId, dstIsOutput] = mWorkflow->DisassembleGlobalPinId(dst); + + if (srcNode == dstNode) { + ImNodes::RejectNewItem(); + goto createError; + } + + if (srcIsOutput == dstIsOutput) { + ImNodes::RejectNewItem(); + goto createError; + } + + auto srcPin = srcNode->GetOutputPin(srcPinId); + auto dstPin = dstNode->GetOutputPin(dstPinId); + + if (srcPin.MatchingType != dstPin.MatchingType) { + ImNodes::RejectNewItem(); + goto createError; + } + + if (ImNodes::AcceptNewItem()) { + mWorkflow->Connect(*srcNode, srcPinId, *dstNode, dstPinId); + } + } + + ImNodes::PinId newNodePin = 0; + if (ImNodes::QueryNewNode(&newNodePin)) { + auto [node, pinId, isOutput] = mWorkflow->DisassembleGlobalPinId(newNodePin); + + if ((isOutput && node->GetOutputPin(pinId).IsConnected()) || + (!isOutput && node->GetInputPin(pinId).IsConnected())) + { + ImNodes::RejectNewItem(); + goto createError; + } + + if (ImNodes::AcceptNewItem()) { + ImNodes::Suspend(); + ImGui::BeginPopup("CreateNodeCtxMenu"); + ImNodes::Resume(); + } + } + } + createError: + ImNodes::EndCreate(); + + if (ImNodes::BeginDelete()) { + ImNodes::LinkId deletedLinkId; + if (ImNodes::QueryDeletedLink(&deletedLinkId)) { + auto& conn = *mWorkflow->GetConnectionByLinkId(deletedLinkId); + mWorkflow->RemoveConnection(conn.Id); + } + + ImNodes::NodeId deletedNodeId; + if (ImNodes::QueryDeletedNode(&deletedNodeId)) { + auto node = mWorkflow->GetNodeByNodeId(deletedNodeId); + if (!node) { + ImNodes::RejectDeletedItem(); + goto deleteError; + } + + if (node->IsLocked()) { + ImNodes::RejectDeletedItem(); + goto deleteError; + } + } + } + deleteError: + ImNodes::EndDelete(); + + // Popups + ImNodes::Suspend(); + if (ImNodes::ShowNodeContextMenu(&mContextMenuNodeId)) { + ImGui::OpenPopup("NodeCtxMenu"); + } else if (ImNodes::ShowPinContextMenu(&mContextMenuPinId)) { + ImGui::OpenPopup("PinCtxMenu"); + } else if (ImNodes::ShowLinkContextMenu(&mContextMenuLinkId)) { + ImGui::OpenPopup("LinkCtxMenu"); + } + + if (ImGui::BeginPopup("NodeCtxMenu")) { + auto& node = *mWorkflow->GetNodeByNodeId(mContextMenuNodeId); + node.DrawDebugInfo(); + + if (ImGui::MenuItem(ICON_FA_TRASH " " I18N_TEXT("Delete", L10N_DELETE))) { + ImNodes::DeleteNode(mContextMenuNodeId); + } + + ImGui::EndPopup(); + } + + if (ImGui::BeginPopup("PinCtxMenu")) { + auto [node, pinId, isOutput] = mWorkflow->DisassembleGlobalPinId(mContextMenuPinId); + if (isOutput) { + node->DrawOutputPinDebugInfo(pinId); + } else { + node->DrawInputPinDebugInfo(pinId); + } + + if (ImGui::MenuItem(ICON_FA_UNLINK " " I18N_TEXT("Disconnect", L10N_DISCONNECT))) { + if (isOutput) { + auto& pin = node->GetOutputPin(pinId); + if (pin.IsConnected()) { + auto linkId = mWorkflow->GetConnectionById(pin.Connection)->GetLinkId(); + ImNodes::DeleteLink(linkId); + } + } else { + auto& pin = node->GetInputPin(pinId); + if (pin.IsConstantConnection()) { + // TODO + } else if (pin.IsConnected()) { + auto linkId = mWorkflow->GetConnectionById(pin.Connection)->GetLinkId(); + ImNodes::DeleteLink(linkId); + } + } + } + + ImGui::EndPopup(); + } + + if (ImGui::BeginPopup("LinkCtxMenu")) { + auto& conn = *mWorkflow->GetConnectionByLinkId(mContextMenuLinkId); + conn.DrawDebugInfo(); + + if (ImGui::MenuItem(ICON_FA_TRASH " " I18N_TEXT("Delete", L10N_DELETE))) { + ImNodes::DeleteLink(mContextMenuLinkId); + } + + ImGui::EndPopup(); + } + + if (ImGui::BeginPopup("CreateNodeCtxMenu")) { + for (int i = WorkflowNode::CG_Numeric; i < WorkflowNode::InvalidCategory; ++i) { + auto category = (WorkflowNode::Category)i; + auto members = WorkflowNode::QueryCategoryMembers(category); + + if (ImGui::BeginMenu(WorkflowNode::FormatCategory(category))) { + for (auto member : members) { + if (ImGui::MenuItem(WorkflowNode::FormatKind(member))) { + // Create node + auto uptr = WorkflowNode::CreateByKind(member); + mWorkflow->AddNode(std::move(uptr)); + } + } + ImGui::EndMenu(); + } + } + ImGui::EndPopup(); + } + + if (tooltipMessage) { + ImGui::BeginTooltip(); + ImGui::TextUnformatted(tooltipMessage); + ImGui::EndTooltip(); + } + ImNodes::Resume(); + + ImNodes::End(); + } + + void Close() + { + // TODO + } +}; +} // namespace CPLT_UNITY_ID + +void UI::WorkflowsTab() +{ + auto& project = *GlobalStates::GetInstance().GetCurrentProject(); + + static std::unique_ptr<CPLT_UNITY_ID::WorkflowUI> openWorkflow; + static AssetList::ListState state; + bool openedDummy = true; + + // Toolbar item: close + if (ImGui::Button(ICON_FA_TIMES " " I18N_TEXT("Close", L10N_CLOSE), openWorkflow == nullptr)) { + openWorkflow->Close(); + openWorkflow = nullptr; + } + + // Toolbar item: open... + ImGui::SameLine(); + if (ImGui::Button((I18N_TEXT("Open asset...", L10N_ASSET_OPEN)))) { + ImGui::OpenPopup(I18N_TEXT("Open asset", L10N_ASSET_OPEN_DIALOG_TITLE)); + } + if (ImGui::BeginPopupModal(I18N_TEXT("Open asset", L10N_ASSET_OPEN_DIALOG_TITLE), &openedDummy, ImGuiWindowFlags_AlwaysAutoResize)) { + if (ImGui::Button(ICON_FA_FOLDER_OPEN " " I18N_TEXT("Open", L10N_OPEN), state.SelectedAsset == nullptr)) { + ImGui::CloseCurrentPopup(); + + auto workflow = project.Workflows.Load(*state.SelectedAsset); + openWorkflow = std::make_unique<CPLT_UNITY_ID::WorkflowUI>(std::move(workflow)); + } + ImGui::SameLine(); + project.Workflows.DisplayControls(state); + project.Workflows.DisplayDetailsList(state); + + ImGui::EndPopup(); + } + + // Toolbar item: manage... + ImGui::SameLine(); + if (ImGui::Button(I18N_TEXT("Manage assets...", L10N_ASSET_MANAGE))) { + ImGui::OpenPopup(I18N_TEXT("Manage assets", L10N_ASSET_MANAGE_DIALOG_TITLE)); + } + if (ImGui::BeginPopupModal(I18N_TEXT("Manage assets", L10N_ASSET_MANAGE_DIALOG_TITLE), &openedDummy, ImGuiWindowFlags_AlwaysAutoResize)) { + project.Workflows.DisplayControls(state); + project.Workflows.DisplayDetailsList(state); + ImGui::EndPopup(); + } + + if (openWorkflow) { + openWorkflow->Display(); + } +} diff --git a/app/source/Cplt/UI/fwd.hpp b/app/source/Cplt/UI/fwd.hpp new file mode 100644 index 0000000..756e567 --- /dev/null +++ b/app/source/Cplt/UI/fwd.hpp @@ -0,0 +1,6 @@ +#pragma once + +// UI.hpp +namespace ImGui { +enum class IconType; +} diff --git a/app/source/Cplt/Utils/Color.hpp b/app/source/Cplt/Utils/Color.hpp new file mode 100644 index 0000000..15fe6a1 --- /dev/null +++ b/app/source/Cplt/Utils/Color.hpp @@ -0,0 +1,202 @@ +#pragma once + +#include <Cplt/Utils/Math.hpp> +#include <Cplt/Utils/Vector.hpp> +#include <Cplt/Utils/fwd.hpp> + +#include <imgui.h> +#include <algorithm> +#include <cstdint> +#include <limits> + +class RgbaColor +{ +public: + uint8_t r; + uint8_t g; + uint8_t b; + uint8_t a; + +public: + constexpr RgbaColor() noexcept + : r{ 255 } + , g{ 255 } + , b{ 255 } + , a{ 255 } + { + } + + constexpr RgbaColor(float r, float g, float b, float a = 1.0f) noexcept + : r{ static_cast<uint8_t>(r * 255.0f) } + , g{ static_cast<uint8_t>(g * 255.0f) } + , b{ static_cast<uint8_t>(b * 255.0f) } + , a{ static_cast<uint8_t>(a * 255.0f) } + { + } + + constexpr RgbaColor(int r, int g, int b, int a = 255) noexcept + : r{ static_cast<uint8_t>(r & 0xFF) } + , g{ static_cast<uint8_t>(g & 0xFF) } + , b{ static_cast<uint8_t>(b & 0xFF) } + , a{ static_cast<uint8_t>(a & 0xFF) } + { + } + + constexpr RgbaColor(uint32_t rgba) noexcept + : r{ static_cast<uint8_t>((rgba >> 0) & 0xFF) } + , g{ static_cast<uint8_t>((rgba >> 8) & 0xFF) } + , b{ static_cast<uint8_t>((rgba >> 16) & 0xFF) } + , a{ static_cast<uint8_t>((rgba >> 24) & 0xFF) } + { + } + + constexpr uint32_t GetScalar() const noexcept + { + uint32_t res = 0; + res |= r << 24; + res |= g << 16; + res |= b << 8; + res |= a; + return res; + } + + constexpr void SetScalar(uint32_t scalar) noexcept + { + r = (scalar >> 0) & 0xFF; + g = (scalar >> 8) & 0xFF; + b = (scalar >> 16) & 0xFF; + a = (scalar >> 24) & 0xFF; + } + + constexpr float GetNormalizedRed() const noexcept + { + return r / 255.0f; + } + + constexpr float GetNormalizedGreen() const noexcept + { + return g / 255.0f; + } + + constexpr float GetNormalizedBlue() const noexcept + { + return b / 255.0f; + } + + constexpr float GetNormalizedAlpha() const noexcept + { + return a / 255.0f; + } + + constexpr Vec4i AsVec4i() const noexcept + { + return Vec4i{ r, g, b, a }; + } + + constexpr Vec4f AsVec4f() const noexcept + { + return Vec4f{ + GetNormalizedRed(), + GetNormalizedGreen(), + GetNormalizedBlue(), + GetNormalizedAlpha(), + }; + } + + ImVec4 AsImVec() const + { + auto v = AsVec4f(); + return ImVec4{ v.x, v.y, v.z, v.w }; + } + + ImColor AsImColor() const + { + auto v = AsVec4f(); + return ImColor{ v.x, v.y, v.z, v.w }; + } + + ImU32 AsImU32() const + { + ImU32 res; + res |= r << IM_COL32_R_SHIFT; + res |= g << IM_COL32_G_SHIFT; + res |= b << IM_COL32_B_SHIFT; + res |= a << IM_COL32_A_SHIFT; + return res; + } + + constexpr void SetVec(const Vec4f& vec) noexcept + { + r = (uint8_t)(vec.x * 255.0f); + g = (uint8_t)(vec.y * 255.0f); + b = (uint8_t)(vec.z * 255.0f); + a = (uint8_t)(vec.w * 255.0f); + } + + // Forward declaring because cyclic reference between RgbaColor and HsvColor + constexpr HsvColor ToHsv() const noexcept; + + friend constexpr bool operator==(const RgbaColor&, const RgbaColor&) noexcept = default; +}; + +class HsvColor +{ +public: + float h; + float s; + float v; + float a; + +public: + constexpr HsvColor() noexcept + : h{ 0.0f } + , s{ 0.0f } + , v{ 1.0f } + , a{ 1.0f } + { + } + + constexpr HsvColor(float h, float s, float v, float a) noexcept + : h{ h } + , s{ s } + , v{ v } + , a{ a } + { + } + + // Forward declaring because cyclic reference between RgbaColor and HsvColor + constexpr RgbaColor ToRgba() const noexcept; +}; + +constexpr HsvColor RgbaColor::ToHsv() const noexcept +{ + float fr = GetNormalizedRed(); + float fg = GetNormalizedBlue(); + float fb = GetNormalizedGreen(); + float fa = GetNormalizedAlpha(); + + auto p = fg < fb ? Vec4f{ fb, fg, -1, 2.0f / 3.0f } : Vec4f{ fg, fb, 0, -1.0f / 3.0f }; + auto q = fr < p.x ? Vec4f{ p.x, p.y, p.w, fr } : Vec4f{ fr, p.y, p.z, p.x }; + float c = q.x - std::min(q.w, q.y); + float h = MathUtils::Abs((q.w - q.y) / (6 * c + std::numeric_limits<float>::epsilon()) + q.z); + + Vec3f hcv{ h, c, q.x }; + float s = hcv.y / (hcv.z + std::numeric_limits<float>::epsilon()); + return HsvColor(hcv.x, s, hcv.z, fa); +} + +constexpr RgbaColor HsvColor::ToRgba() const noexcept +{ + float r = MathUtils::Abs(h * 6 - 3) - 1; + float g = 2 - MathUtils::Abs(h * 6 - 2); + float b = 2 - MathUtils::Abs(h * 6 - 4); + + auto rgb = Vec3f{ + std::clamp(r, 0.0f, 1.0f), + std::clamp(g, 0.0f, 1.0f), + std::clamp(b, 0.0f, 1.0f), + }; + auto vc = (rgb - Vec3f{ 0, 0, 0 }) * s + Vec3f{ 1, 1, 1 } * v; + + return RgbaColor(vc.x, vc.y, vc.z, a); +} diff --git a/app/source/Cplt/Utils/Hash.hpp b/app/source/Cplt/Utils/Hash.hpp new file mode 100644 index 0000000..cf7713a --- /dev/null +++ b/app/source/Cplt/Utils/Hash.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include <cstddef> +#include <functional> + +namespace HashUtils { + +template <class T> +void Combine(size_t& seed, const T& v) +{ + std::hash<T> hasher; + seed ^= hasher(v) + 0x9e3779b9 + (seed << 6) + (seed >> 2); +} + +} // namespace HashUtils diff --git a/app/source/Cplt/Utils/I18n.hpp b/app/source/Cplt/Utils/I18n.hpp new file mode 100644 index 0000000..895856a --- /dev/null +++ b/app/source/Cplt/Utils/I18n.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include <Cplt/Utils/Macros.hpp> + +#if !defined(TARGET_LOCALE) +# define I18N_TEXT(defaultText, name) defaultText +#else +# include TARGET_LOCALE_FILE +# define I18N_TEXT(defaultText, name) name +#endif diff --git a/app/source/Cplt/Utils/IO/Archive.cpp b/app/source/Cplt/Utils/IO/Archive.cpp new file mode 100644 index 0000000..f6e7b27 --- /dev/null +++ b/app/source/Cplt/Utils/IO/Archive.cpp @@ -0,0 +1,57 @@ +#include "Archive.hpp" + +constexpr uint8_t kMagicNumbers[] = { 0x98, 0xd8, 0xa4, 0x65, 0x18, 0xa2, 0xd6, 0xa0 }; +constexpr size_t kMagicNumberCount = std::size(kMagicNumbers); + +constexpr uint8_t kByteOrderMark = []() { + switch (std::endian::native) { + case std::endian::little: return 0; + case std::endian::big: return 1; + } +}(); + +std::span<const uint8_t, 8> DataArchive::GetMagicNumbers() +{ + return std::span<const uint8_t, 8>{ kMagicNumbers }; +} + +std::optional<InputDataStream> DataArchive::LoadFile(const std::filesystem::path& path) +{ + InputFileStream fileStream(path); + fileStream.SetReadInSize(1024); + InputDataStream stream(std::move(fileStream)); + + uint8_t magicNumbers[kMagicNumberCount]; + stream.ReadBytes(kMagicNumberCount, magicNumbers); + + for (size_t i = 0; i < kMagicNumberCount; ++i) { + if (magicNumbers[i] != kMagicNumbers[i]) { + return {}; + } + } + + uint8_t byteOrderMark; + stream.Read(byteOrderMark); + + switch (byteOrderMark) { + case 0: stream.SetEndianness(std::endian::little); break; + case 1: stream.SetEndianness(std::endian::big); break; + default: std::abort(); + } + + return stream; +} + +std::optional<OutputDataStream> DataArchive::SaveFile(const std::filesystem::path& path) +{ + OutputFileStream fileStream(path, OutputFileStream::TruncateFile); + fileStream.SetMaxBufferSize(1024); + OutputDataStream stream(std::move(fileStream)); + + stream.WriteBytes(kMagicNumberCount, kMagicNumbers); + stream.Write(kByteOrderMark); + + stream.SetEndianness(std::endian::native); + + return stream; +} diff --git a/app/source/Cplt/Utils/IO/Archive.hpp b/app/source/Cplt/Utils/IO/Archive.hpp new file mode 100644 index 0000000..7632f3b --- /dev/null +++ b/app/source/Cplt/Utils/IO/Archive.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include <Cplt/Utils/IO/DataStream.hpp> + +#include <cstdint> +#include <filesystem> +#include <optional> +#include <span> + +class DataArchive +{ +public: + static std::span<const uint8_t, 8> GetMagicNumbers(); + + // TODO more complete impl + static std::optional<InputDataStream> LoadFile(const std::filesystem::path& path); + static std::optional<OutputDataStream> SaveFile(const std::filesystem::path& path); +}; diff --git a/app/source/Cplt/Utils/IO/CstdioFile.cpp b/app/source/Cplt/Utils/IO/CstdioFile.cpp new file mode 100644 index 0000000..c414dfb --- /dev/null +++ b/app/source/Cplt/Utils/IO/CstdioFile.cpp @@ -0,0 +1,36 @@ +#include "CstdioFile.hpp" + +#include <Cplt/Utils/Macros.hpp> + +#pragma push_macro("MODE_STRING") +#undef MODE_STRING + +#if defined(_WIN32) +# define MODE_STRING(x) L##x +#else +# define MODE_STRING(x) x +#endif + +namespace CPLT_UNITY_ID { +auto GetModeString(FileUtils::IoMode mode) +{ + switch (mode) { + case FileUtils::IM_Read: return MODE_STRING("rb"); + case FileUtils::IM_WriteAppend: return MODE_STRING("ab"); + case FileUtils::IM_WriteTruncate: return MODE_STRING("wb"); + } + return MODE_STRING(""); +} +} // namespace CPLT_UNITY_ID + +#pragma pop_macro("MODE_STRING") + +FILE* FileUtils::OpenCstdioFile(const std::filesystem::path& path, IoMode mode) +{ +#ifdef _WIN32 + // std::filesystem::path::c_str() returns `const wchar_t*` under Windows, because NT uses UTF-16 natively + return _wfopen(path.c_str(), ::GetModeString(mode)); +#else + return fopen(path.c_str(), CPLT_UNITY_ID::GetModeString(mode)); +#endif +} diff --git a/app/source/Cplt/Utils/IO/CstdioFile.hpp b/app/source/Cplt/Utils/IO/CstdioFile.hpp new file mode 100644 index 0000000..e863dd5 --- /dev/null +++ b/app/source/Cplt/Utils/IO/CstdioFile.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include <cstdio> +#include <filesystem> + +namespace FileUtils { + +enum IoMode +{ + IM_Read, + IM_WriteAppend, + IM_WriteTruncate, +}; + +FILE* OpenCstdioFile(const std::filesystem::path& path, IoMode mode); + +} // namespace FileUtils diff --git a/app/source/Cplt/Utils/IO/DataStream.cpp b/app/source/Cplt/Utils/IO/DataStream.cpp new file mode 100644 index 0000000..c0797e3 --- /dev/null +++ b/app/source/Cplt/Utils/IO/DataStream.cpp @@ -0,0 +1,283 @@ +#include "DataStream.hpp" + +#include <bit> +#include <limits> +#include <utility> + +static_assert(std::numeric_limits<float>::is_iec559, "Non IEE754/IEC559 'float' is not supported."); +static_assert(std::numeric_limits<double>::is_iec559, "Non IEE754/IEC559 'double' is not supported."); + +static_assert(std::endian::native == std::endian::little || std::endian::native == std::endian::big, "Mixed endian is not supported."); + +// Reading/writing signed integer byte-by-byte is fine, since the representation got fixed to 2's complements in C++20 + +static uint16_t ByteSwap(uint16_t n) +{ + auto bytes = reinterpret_cast<std::byte*>(n); + std::swap(bytes[0], bytes[1]); + return n; +} + +static uint32_t ByteSwap(uint32_t n) +{ +#ifdef _MSC_VER + return _byteswap_ulong(n); +#else + return __builtin_bswap32(n); +#endif +} + +static uint64_t ByteSwap(uint64_t n) +{ +#ifdef _MSC_VER + return _byteswap_uint64(n); +#else + return __builtin_bswap64(n); +#endif +} + +template <class TSigned> +static TSigned ByteSwap(TSigned n) +{ + using Unsigned = std::make_unsigned_t<TSigned>; + + auto swapped = ::ByteSwap(std::bit_cast<Unsigned>(n)); + return std::bit_cast<TSigned>(swapped); +} + +std::endian BaseDataStream::GetEndianness() const +{ + return mEndian; +} + +void BaseDataStream::SetEndianness(std::endian endianness) +{ + mEndian = endianness; +} + +InputDataStream::InputDataStream(InputFileStream stream) + : mBackend{ std::move(stream) } +{ +} + +void InputDataStream::ReadBytes(size_t byteCount, std::byte* buffer) +{ + mBackend.ReadBytes(static_cast<std::streamsize>(byteCount), reinterpret_cast<std::byte*>(buffer)); +} + +void InputDataStream::ReadBytes(size_t byteCount, char* buffer) +{ + mBackend.ReadBytes(static_cast<std::streamsize>(byteCount), reinterpret_cast<std::byte*>(buffer)); +} + +void InputDataStream::ReadBytes(size_t byteCount, signed char* buffer) +{ + mBackend.ReadBytes(static_cast<std::streamsize>(byteCount), reinterpret_cast<std::byte*>(buffer)); +} + +void InputDataStream::ReadBytes(size_t byteCount, unsigned char* buffer) +{ + mBackend.ReadBytes(static_cast<std::streamsize>(byteCount), reinterpret_cast<std::byte*>(buffer)); +} + +void InputDataStream::Read(int8_t& n) +{ + // sizeof() of a reference type yields the size of the reference + mBackend.ReadBytes(sizeof(n), reinterpret_cast<std::byte*>(&n)); +} + +void InputDataStream::Read(int16_t& n) +{ + int16_t tmp; + mBackend.ReadBytes(sizeof(tmp), reinterpret_cast<std::byte*>(&tmp)); + if (GetEndianness() != std::endian::native) { + n = ::ByteSwap(tmp); + } else { + n = tmp; + } +} + +void InputDataStream::Read(int32_t& n) +{ + int32_t tmp; + mBackend.ReadBytes(sizeof(tmp), reinterpret_cast<std::byte*>(&tmp)); + if (GetEndianness() != std::endian::native) { + n = ::ByteSwap(tmp); + } else { + n = tmp; + } +} + +void InputDataStream::Read(int64_t& n) +{ + int64_t tmp; + mBackend.ReadBytes(sizeof(tmp), reinterpret_cast<std::byte*>(&tmp)); + if (GetEndianness() != std::endian::native) { + n = ::ByteSwap(tmp); + } else { + n = tmp; + } +} + +void InputDataStream::Read(uint8_t& n) +{ + mBackend.ReadBytes(sizeof(n), reinterpret_cast<std::byte*>(&n)); +} + +void InputDataStream::Read(uint16_t& n) +{ + uint16_t tmp; + mBackend.ReadBytes(sizeof(tmp), reinterpret_cast<std::byte*>(&tmp)); + if (GetEndianness() != std::endian::native) { + n = ::ByteSwap(tmp); + } else { + n = tmp; + } +} + +void InputDataStream::Read(uint32_t& n) +{ + uint32_t tmp; + mBackend.ReadBytes(sizeof(tmp), reinterpret_cast<std::byte*>(&tmp)); + if (GetEndianness() != std::endian::native) { + n = ::ByteSwap(tmp); + } else { + n = tmp; + } +} + +void InputDataStream::Read(uint64_t& n) +{ + uint64_t tmp; + mBackend.ReadBytes(sizeof(tmp), reinterpret_cast<std::byte*>(&tmp)); + if (GetEndianness() != std::endian::native) { + n = ::ByteSwap(tmp); + } else { + n = tmp; + } +} + +void InputDataStream::Read(float& n) +{ + uint32_t buffer; + Read(buffer); + + if (GetEndianness() != std::endian::native) { + buffer = ::ByteSwap(buffer); + } + + n = std::bit_cast<float>(buffer); +} + +void InputDataStream::Read(double& n) +{ + uint64_t buffer; + Read(buffer); + + if (GetEndianness() != std::endian::native) { + buffer = ::ByteSwap(buffer); + } + + n = std::bit_cast<double>(buffer); +} + +OutputDataStream::OutputDataStream(OutputFileStream stream) + : mBackend{ std::move(stream) } +{ +} + +void OutputDataStream::WriteBytes(size_t byteCount, const std::byte* buffer) +{ + mBackend.WriteBytes(static_cast<std::streamsize>(byteCount), reinterpret_cast<const std::byte*>(buffer)); +} + +void OutputDataStream::WriteBytes(size_t byteCount, const char* buffer) +{ + mBackend.WriteBytes(static_cast<std::streamsize>(byteCount), reinterpret_cast<const std::byte*>(buffer)); +} + +void OutputDataStream::WriteBytes(size_t byteCount, const signed char* buffer) +{ + mBackend.WriteBytes(static_cast<std::streamsize>(byteCount), reinterpret_cast<const std::byte*>(buffer)); +} + +void OutputDataStream::WriteBytes(size_t byteCount, const unsigned char* buffer) +{ + mBackend.WriteBytes(static_cast<std::streamsize>(byteCount), reinterpret_cast<const std::byte*>(buffer)); +} + +void OutputDataStream::Write(int8_t n) +{ + mBackend.WriteBytes(sizeof(n), reinterpret_cast<const std::byte*>(&n)); +} + +void OutputDataStream::Write(int16_t n) +{ + if (GetEndianness() != std::endian::native) { + n = ::ByteSwap(n); + } + mBackend.WriteBytes(sizeof(n), reinterpret_cast<const std::byte*>(&n)); +} + +void OutputDataStream::Write(int32_t n) +{ + if (GetEndianness() != std::endian::native) { + n = ::ByteSwap(n); + } + mBackend.WriteBytes(sizeof(n), reinterpret_cast<const std::byte*>(&n)); +} + +void OutputDataStream::Write(int64_t n) +{ + if (GetEndianness() != std::endian::native) { + n = ::ByteSwap(n); + } + mBackend.WriteBytes(sizeof(n), reinterpret_cast<const std::byte*>(&n)); +} + +void OutputDataStream::Write(uint8_t n) +{ + mBackend.WriteBytes(sizeof(n), reinterpret_cast<const std::byte*>(&n)); +} + +void OutputDataStream::Write(uint16_t n) +{ + if (GetEndianness() != std::endian::native) { + n = ::ByteSwap(n); + } + mBackend.WriteBytes(sizeof(n), reinterpret_cast<const std::byte*>(&n)); +} + +void OutputDataStream::Write(uint32_t n) +{ + if (GetEndianness() != std::endian::native) { + n = ::ByteSwap(n); + } + mBackend.WriteBytes(sizeof(n), reinterpret_cast<const std::byte*>(&n)); +} + +void OutputDataStream::Write(uint64_t n) +{ + if (GetEndianness() != std::endian::native) { + n = ::ByteSwap(n); + } + mBackend.WriteBytes(sizeof(n), reinterpret_cast<const std::byte*>(&n)); +} + +void OutputDataStream::Write(float n) +{ + auto buffer = std::bit_cast<uint32_t>(n); + if (GetEndianness() != std::endian::native) { + buffer = ::ByteSwap(buffer); + } + mBackend.WriteBytes(sizeof(buffer), reinterpret_cast<const std::byte*>(&buffer)); +} + +void OutputDataStream::Write(double n) +{ + auto buffer = std::bit_cast<uint64_t>(n); + if (GetEndianness() != std::endian::native) { + buffer = ::ByteSwap(buffer); + } + mBackend.WriteBytes(sizeof(buffer), reinterpret_cast<const std::byte*>(&buffer)); +} diff --git a/app/source/Cplt/Utils/IO/DataStream.hpp b/app/source/Cplt/Utils/IO/DataStream.hpp new file mode 100644 index 0000000..133adc2 --- /dev/null +++ b/app/source/Cplt/Utils/IO/DataStream.hpp @@ -0,0 +1,210 @@ +#pragma once + +#include "FileStream.hpp" +#include <Cplt/fwd.hpp> + +#include <bit> +#include <cstddef> +#include <cstdint> +#include <span> + +class BaseDataStream +{ +private: + std::endian mEndian = std::endian::big; + +public: + std::endian GetEndianness() const; + void SetEndianness(std::endian endianness); +}; + +class InputDataStream : public BaseDataStream +{ +private: + InputFileStream mBackend; + +public: + static constexpr bool IsSerializer() + { + return false; + } + + InputDataStream(InputFileStream stream); + + void ReadBytes(size_t byteCount, std::byte* buffer); + void ReadBytes(size_t byteCount, char* buffer); + void ReadBytes(size_t byteCount, signed char* buffer); + void ReadBytes(size_t byteCount, unsigned char* buffer); + + template <class TInserter> + void ReadBytes(size_t byteCount, TInserter&& inserter) + { + for (size_t i = 0; i < byteCount; ++i) { + uint8_t byte; + Read(byte); + + inserter = byte; + } + } + + void Read(int8_t& n); + void Read(int16_t& n); + void Read(int32_t& n); + void Read(int64_t& n); + + void Read(uint8_t& n); + void Read(uint16_t& n); + void Read(uint32_t& n); + void Read(uint64_t& n); + + void Read(float& n); + void Read(double& n); + + template <class TEnum> + requires std::is_enum_v<TEnum> + void ReadEnum(TEnum& e) + { + std::underlying_type_t<TEnum> n; + Read(n); + e = static_cast<TEnum>(e); + } + + template <class TObject> + void ReadObject(TObject& obj) + { + obj.ReadFromDataStream(*this); + } + + template <class TAdapter, class TObject> + void ReadObjectAdapted(TObject& obj) + { + TAdapter::ReadFromDataStream(*this, obj); + } + +public: + // Proxy functions for writing templated IO functions + + template <class T> + void Bytes(size_t byteCount, T* buffer) + { + ReadBytes(byteCount, buffer); + } + + template <class T> + void Value(T& t) + { + Read(t); + } + + template <class T> + void Enum(T& t) + { + ReadEnum(t); + } + + template <class T> + void Object(T& obj) + { + ReadObject(obj); + } + + template <class TAdapter, class TObject> + void ObjectAdapted(TObject& obj) + { + ReadObjectAdapted<TAdapter>(obj); + } +}; + +class OutputDataStream : public BaseDataStream +{ +private: + OutputFileStream mBackend; + +public: + static constexpr bool IsSerializer() + { + return true; + } + + OutputDataStream(OutputFileStream stream); + + void WriteBytes(size_t byteCount, const std::byte* buffer); + void WriteBytes(size_t byteCount, const char* buffer); + void WriteBytes(size_t byteCount, const signed char* buffer); + void WriteBytes(size_t byteCount, const unsigned char* buffer); + + template <class TIterator> + void WriteBytes(TIterator&& begin, TIterator&& end) + { + for (; begin != end; ++begin) { + uint8_t byte = *begin; + Write(byte); + } + } + + void Write(int8_t n); + void Write(int16_t n); + void Write(int32_t n); + void Write(int64_t n); + + void Write(uint8_t n); + void Write(uint16_t n); + void Write(uint32_t n); + void Write(uint64_t n); + + void Write(float n); + void Write(double n); + + template <class TEnum> + requires std::is_enum_v<TEnum> + void WriteEnum(TEnum e) + { + auto n = static_cast<std::underlying_type_t<TEnum>>(e); + Write(n); + } + + template <class TObject> + void WriteObject(const TObject& obj) + { + obj.WriteToDataStream(*this); + } + + template <class TAdapter, class TObject> + void WriteObjectAdapted(const TObject& obj) + { + TAdapter::WriteToDataStream(*this, obj); + } + +public: + // Proxy functions for writing templated IO functions + + template <class T> + void Bytes(size_t byteCount, T* buffer) + { + WriteBytes(byteCount, buffer); + } + + template <class T> + void Value(T t) + { + Write(t); + } + + template <class T> + void Enum(T t) + { + WriteEnum(t); + } + + template <class T> + void Object(T& obj) + { + WriteObject(obj); + } + + template <class TAdapter, class TObject> + void ObjectAdapted(TObject& obj) + { + WriteObjectAdapted<TAdapter>(obj); + } +}; diff --git a/app/source/Cplt/Utils/IO/FileStream.cpp b/app/source/Cplt/Utils/IO/FileStream.cpp new file mode 100644 index 0000000..8b83712 --- /dev/null +++ b/app/source/Cplt/Utils/IO/FileStream.cpp @@ -0,0 +1,7 @@ +#include "FileStream.hpp" + +#if defined(CPLT_FILESTREAM_USE_CSTDIO) +# include "FileStream_Cstdio.inl" +#else +# include "FileStream_Custom.inl" +#endif diff --git a/app/source/Cplt/Utils/IO/FileStream.hpp b/app/source/Cplt/Utils/IO/FileStream.hpp new file mode 100644 index 0000000..453ddbe --- /dev/null +++ b/app/source/Cplt/Utils/IO/FileStream.hpp @@ -0,0 +1,97 @@ +#pragma once + +#include <cstddef> +#include <cstdint> +#include <filesystem> +#include <memory> + +// TODO switch to custom when unit tests are ready and bugs are fixed +#define CPLT_FILESTREAM_USE_CSTDIO + +struct IoResult +{ + enum ErrorKind + { + ERR_None, + ERR_PermissionDenied, + ERR_UnexpectedEof, + ERR_Unsupported, + ERR_OutOfSpace, + ERR_Other, + }; + + ErrorKind Error; + uint32_t SystemError; + size_t BytesMoved; +}; + +class InputFileStream +{ +private: +#if defined(CPLT_FILESTREAM_USE_CSTDIO) + FILE* mFile; +#else + alignas(void*) char mOsFileHandle[sizeof(void*)]; + + // mBuffer is always mReadInSize size + std::unique_ptr<std::byte[]> mBuffer; + int mReadInSize = 1024; + + int mFirstByteIdx = 0; + int mAvailableBytes = 0; + + bool mEof = false; +#endif + +public: + InputFileStream(const std::filesystem::path& path); + ~InputFileStream(); + + InputFileStream(const InputFileStream&) = delete; + InputFileStream& operator=(const InputFileStream&) = delete; + InputFileStream(InputFileStream&&); + InputFileStream& operator=(InputFileStream&&); + + int GetReadInSize() const; + void SetReadInSize(int size); + + bool IsEof() const; + + IoResult ReadBytes(size_t bufferLength, std::byte* buffer); +}; + +class OutputFileStream +{ +public: + enum WriteMode + { + AppendFile, + TruncateFile, + }; + +private: +#if defined(CPLT_FILESTREAM_USE_CSTDIO) + FILE* mFile; +#else + alignas(void*) char mOsFileHandle[sizeof(void*)]; + std::unique_ptr<std::byte[]> mBuffer; + int mMaxBufferSize = 1024; + int mCurrentBufferSize = 0; +#endif + +public: + OutputFileStream(const std::filesystem::path& path, WriteMode mode); + ~OutputFileStream(); + + OutputFileStream(const OutputFileStream&) = delete; + OutputFileStream& operator=(const OutputFileStream&) = delete; + OutputFileStream(OutputFileStream&&); + OutputFileStream& operator=(OutputFileStream&&); + + int GetMaxBufferSize() const; + void SetMaxBufferSize(int maxSize); + + IoResult WriteBytes(size_t bufferLength, const std::byte* buffer); + + void FlushBuffer(); +}; diff --git a/app/source/Cplt/Utils/IO/FileStream_Cstdio.inl b/app/source/Cplt/Utils/IO/FileStream_Cstdio.inl new file mode 100644 index 0000000..ff2ca01 --- /dev/null +++ b/app/source/Cplt/Utils/IO/FileStream_Cstdio.inl @@ -0,0 +1,126 @@ +// Note: included by FileStream.cpp conditionally, not compiled separately +#include "FileStream.hpp" + +#include <Cplt/Utils/IO/CstdioFile.hpp> + +#include <cstdio> +#include <filesystem> + +namespace fs = std::filesystem; + +InputFileStream::InputFileStream(const fs::path& path) + : mFile{ FileUtils::OpenCstdioFile(path, FileUtils::IM_Read) } +{ +} + +InputFileStream::~InputFileStream() +{ + if (mFile) { + std::fclose(mFile); + } +} + +InputFileStream::InputFileStream(InputFileStream&& that) + : mFile{ that.mFile } +{ + that.mFile = nullptr; +} + +InputFileStream& InputFileStream::operator=(InputFileStream&& that) +{ + if (this == &that) return *this; + + if (mFile) { + std::fclose(mFile); + } + mFile = that.mFile; + that.mFile = nullptr; + + return *this; +} + +OutputFileStream::OutputFileStream(const fs::path& path, WriteMode mode) +{ + switch (mode) { + case AppendFile: mFile = FileUtils::OpenCstdioFile(path, FileUtils::IM_WriteAppend); break; + case TruncateFile: mFile = FileUtils::OpenCstdioFile(path, FileUtils::IM_WriteTruncate); break; + } +} + +OutputFileStream::~OutputFileStream() +{ + if (mFile) { + std::fclose(mFile); + } +} + +OutputFileStream::OutputFileStream(OutputFileStream&& that) + : mFile{ that.mFile } +{ + that.mFile = nullptr; +} + +OutputFileStream& OutputFileStream::operator=(OutputFileStream&& that) +{ + if (this == &that) return *this; + + if (mFile) { + std::fclose(mFile); + } + mFile = that.mFile; + that.mFile = nullptr; + + return *this; +} + +int InputFileStream::GetReadInSize() const +{ + return 0; +} + +void InputFileStream::SetReadInSize(int size) +{ + // No-op +} + +bool InputFileStream::IsEof() const +{ + return std::feof(mFile); +} + +IoResult InputFileStream::ReadBytes(size_t bufferLength, std::byte* buffer) +{ + auto bytesRead = std::fread(buffer, 1, bufferLength, mFile); + + return { + .Error = IoResult::ERR_None, + .SystemError = 0, + .BytesMoved = bytesRead, + }; +} + +int OutputFileStream::GetMaxBufferSize() const +{ + return 0; +} + +void OutputFileStream::SetMaxBufferSize(int maxSize) +{ + // No-op +} + +IoResult OutputFileStream::WriteBytes(size_t bufferLength, const std::byte* buffer) +{ + auto bytesWritten = std::fwrite(buffer, 1, bufferLength, mFile); + + return IoResult{ + .Error = IoResult::ERR_None, + .SystemError = 0, + .BytesMoved = bytesWritten, + }; +} + +void OutputFileStream::FlushBuffer() +{ + // No-op +} diff --git a/app/source/Cplt/Utils/IO/FileStream_Custom.inl b/app/source/Cplt/Utils/IO/FileStream_Custom.inl new file mode 100644 index 0000000..004dd01 --- /dev/null +++ b/app/source/Cplt/Utils/IO/FileStream_Custom.inl @@ -0,0 +1,358 @@ +// Note: included by FileStream.cpp conditionally, not compiled separately +#include "FileStream.hpp" + +#include <cstring> +#include <filesystem> +#include <iostream> + +namespace fs = std::filesystem; + +#if defined(_WIN32) +# define WIN32_LEAN_AND_MEAN +# define NOMINMAX +# include <Windows.h> + +InputFileStream::InputFileStream(const fs::path& path) + : mOsFileHandle{ 0 } +{ + auto handle = reinterpret_cast<HANDLE*>(mOsFileHandle); + + *handle = CreateFileW( + path.c_str(), // fs::path::c_str() returns a wide string on Windows + GENERIC_READ, + /* No sharing */ 0, + /* Use default security*/ nullptr, + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, + /* No attribute template */ nullptr); + + // TODO handle error +} + +InputFileStream::~InputFileStream() +{ + auto handle = reinterpret_cast<HANDLE*>(mOsFileHandle); + CloseHandle(*handle); +} + +OutputFileStream::OutputFileStream(const fs::path& path, WriteMode mode) + : mOsFileHandle{ 0 } +{ + auto handle = reinterpret_cast<HANDLE*>(mOsFileHandle); + + DWORD creationDisposition; + switch (mode) { + case AppendFile: creationDisposition = OPEN_ALWAYS; break; + case TruncateFile: creationDisposition = CREATE_ALWAYS; break; + } + + *handle = CreateFileW( + path.c_str(), + GENERIC_WRITE, + /* No sharing */ 0, + /* Use default security*/ nullptr, + creationDisposition, + FILE_ATTRIBUTE_NORMAL, + /* No attribute template */ nullptr); + + // TODO handle error +} + +OutputFileStream::~OutputFileStream() +{ + auto handle = reinterpret_cast<HANDLE*>(mOsFileHandle); + CloseHandle(*handle); +} + +static IoResult::ErrorKind MapErrorCodeToIoResult(DWORD error) +{ + switch (error) { + // TODO + + default: + std::cerr << "Unimplemented win32 error code " << error << ", report bug immediately.\n"; + std::abort(); + } +} + +static IoResult ReadBytesDirect(HANDLE hFile, size_t byteCount, std::byte* bytes) +{ + DWORD bytesRead; + BOOL result = ReadFile(hFile, bytes, byteCount, &bytesRead, nullptr); + + if (result) { + return IoResult{ + .Error = IoResult::ERR_None, + .SystemError = 0, + .BytesMoved = bytesRead, + }; + } else { + DWORD errorCode = GetLastError(); + return IoResult{ + .Error = ::MapErrorCodeToIoResult(errorCode), + .SystemError = errorCode, + .BytesMoved = bytesRead, + }; + } +} + +static IoResult WriteBytesDirect(HANDLE hFile, size_t byteCount, const std::byte* bytes) +{ + DWORD bytesWritten; + BOOL result = WriteFile(hFile, bytes, byteCount, &bytesWritten, nullptr); + + if (result) { + return IoResult{ + .Error = IoResult::ERR_None, + .SystemError = 0, + .BytesMoved = bytesWritten, + }; + } else { + DWORD errorCode = GetLastError(); + return IoResult{ + .Error = ::MapErrorCodeToIoResult(errorCode), + .SystemError = errorCode, + .BytesMoved = bytesWritten, + }; + } +} + +#elif defined(__APPLE__) || defined(__linux__) +# include <fcntl.h> +# include <sys/stat.h> +# include <sys/types.h> +# include <unistd.h> + +InputFileStream::InputFileStream(const fs::path& path) + : mOsFileHandle{ 0 } +{ + auto fd = reinterpret_cast<int*>(mOsFileHandle); + *fd = open(path.c_str(), O_RDONLY); +} + +InputFileStream::~InputFileStream() +{ + auto fd = reinterpret_cast<int*>(mOsFileHandle); + close(*fd); +} + +OutputFileStream::OutputFileStream(const fs::path& path, WriteMode mode) + : mOsFileHandle{ 0 } +{ + auto fd = reinterpret_cast<int*>(mOsFileHandle); + + int flags = O_WRONLY | O_CREAT; + switch (mode) { + case AppendFile: flags |= O_APPEND; break; + case TruncateFile: flags |= O_TRUNC; break; + } + + *fd = open(path.c_str(), flags, 0644); +} + +OutputFileStream::~OutputFileStream() +{ + auto fd = reinterpret_cast<int*>(mOsFileHandle); + close(*fd); +} + +static IoResult::ErrorKind MapErrnoToIoResult(int err) +{ + switch (err) { + // TODO + case EFAULT: return IoResult::ERR_UnexpectedEof; + case EPERM: return IoResult::ERR_PermissionDenied; + case ENOSPC: return IoResult::ERR_OutOfSpace; + case EIO: return IoResult::ERR_Other; + + default: + std::cerr << "Unimplemented POSIX errno " << err << ", report bug immediately.\n"; + std::abort(); + } +} + +static IoResult ReadBytesDirect(const char* osFileHandle, size_t byteCount, std::byte* bytes) +{ + int fd = *reinterpret_cast<const int*>(osFileHandle); + int status = read(fd, bytes, byteCount); + + if (status == -1) { + int err = errno; + return IoResult{ + .Error = ::MapErrnoToIoResult(err), + .SystemError = (uint32_t)err, + .BytesMoved = 0, + }; + } else { + return IoResult{ + .Error = IoResult::ERR_None, + .SystemError = 0, + .BytesMoved = (size_t)status, // Equal to number of bytes read + }; + } +} + +static IoResult WriteBytesDirect(const char* osFileHandle, size_t byteCount, const std::byte* bytes) +{ + int fd = *reinterpret_cast<const int*>(osFileHandle); + int status = write(fd, bytes, byteCount); + + if (status == -1) { + int err = errno; + return IoResult{ + .Error = ::MapErrnoToIoResult(err), + .SystemError = (uint32_t)err, + .BytesMoved = 0, + }; + } else { + return IoResult{ + .Error = IoResult::ERR_None, + .SystemError = 0, + .BytesMoved = (size_t)status, // Equal to number of bytes read + }; + } +} + +#else +# error "Unsupported target platform." +#endif + +int InputFileStream::GetReadInSize() const +{ + return mReadInSize; +} + +void InputFileStream::SetReadInSize(int size) +{ + if (size > mReadInSize) { + mReadInSize = size; + mBuffer = std::make_unique<std::byte[]>(size); + } +} + +bool InputFileStream::IsEof() const +{ + return mEof; +} + +IoResult InputFileStream::ReadBytes(size_t bufferLength, std::byte* buffer) +{ + // TODO reduce duplicated code + + auto bytesMoved = std::min<size_t>(mAvailableBytes, bufferLength); + + // On first call after construction, mFirstByteIdx will equal to mReadInSize, i.e. bytesAvailable == 0 + // and this call to std::memcpy will be no-op + std::memcpy(buffer, &mBuffer[mFirstByteIdx], bytesMoved); + mFirstByteIdx += (int)bytesMoved; + mAvailableBytes -= (int)bytesMoved; + buffer += bytesMoved; + + size_t bytesLeft = bufferLength - bytesMoved; + if (bytesLeft > mReadInSize) { + // Our buffer can't handle rest of the request, just skip the buffering step + + // Read rest of the data into buffer + { + auto result = ::ReadBytesDirect(mOsFileHandle, bytesLeft, buffer); + bytesMoved += result.BytesMoved; + + if (result.Error == IoResult::ERR_None) { + if (result.BytesMoved < mReadInSize) { + mEof = true; + } + } else { + goto end; + } + } + + // Refill our buffer + { + auto result = ::ReadBytesDirect(mOsFileHandle, mReadInSize, mBuffer.get()); + mFirstByteIdx = 0; + mAvailableBytes = (int)result.BytesMoved; + + if (result.Error == IoResult::ERR_None) { + if (result.BytesMoved < mReadInSize) { + mEof = true; + } + } else { + goto end; + } + } + } else if (bytesLeft > 0) { + // Our buffer can handle rest of the request, first buffer than supply the requested data + + // Refill our buffer + { + auto result = ::ReadBytesDirect(mOsFileHandle, mReadInSize, mBuffer.get()); + mFirstByteIdx = 0; + mAvailableBytes = (int)result.BytesMoved; + + if (result.Error == IoResult::ERR_None) { + if (result.BytesMoved < mReadInSize) { + mEof = true; + } + } else { + goto end; + } + } + + // Copy data into buffer + { + std::memcpy(buffer, &mBuffer[mFirstByteIdx], bytesLeft); + mFirstByteIdx += (int)bytesLeft; + bytesMoved += bytesLeft; + buffer += bytesLeft; + } + } else { + // Request completed already + } + +end: + return IoResult{ + .Error = IoResult::ERR_None, + .SystemError = 0, + .BytesMoved = bytesMoved, + }; +} + +int OutputFileStream::GetMaxBufferSize() const +{ + return mMaxBufferSize; +} + +void OutputFileStream::SetMaxBufferSize(int maxSize) +{ + FlushBuffer(); + if (maxSize > mMaxBufferSize) { + mMaxBufferSize = maxSize; + mBuffer = std::make_unique<std::byte[]>(maxSize); + } +} + +IoResult OutputFileStream::WriteBytes(size_t bufferLength, const std::byte* buffer) +{ + if (bufferLength + mCurrentBufferSize > mMaxBufferSize) { + FlushBuffer(); + + if (bufferLength > mMaxBufferSize) { + return ::WriteBytesDirect(mOsFileHandle, bufferLength, buffer); + } + } + + std::memcpy(mBuffer.get() + mCurrentBufferSize, buffer, bufferLength); + mCurrentBufferSize += (int)bufferLength; + + return IoResult{ + .Error = IoResult::ERR_None, + .SystemError = 0, + .BytesMoved = bufferLength, + }; +} + +void OutputFileStream::FlushBuffer() +{ + ::WriteBytesDirect(mOsFileHandle, mCurrentBufferSize, mBuffer.get()); + mCurrentBufferSize = 0; +} diff --git a/app/source/Cplt/Utils/IO/Helper.hpp b/app/source/Cplt/Utils/IO/Helper.hpp new file mode 100644 index 0000000..7a84103 --- /dev/null +++ b/app/source/Cplt/Utils/IO/Helper.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include <Cplt/Utils/IO/DataStream.hpp> + +namespace DataStreamAdapters { + +/// Helper to invoke either Read() or ReadObject(). +/// This is intended for writing IO adapters, users that's writing IO logic shouldn't using this - it increases compile time while reducing readability. +template <class TAdapter, class T> +void ReadHelper(InputDataStream& stream, T& t) +{ + if constexpr (!std::is_same_v<TAdapter, void>) { + stream.ReadObjectAdapted<TAdapter>(t); + } else if constexpr (requires(T tt, InputDataStream ss) { ss.Read(tt); }) { + stream.Read(t); + } else if constexpr (requires(T tt, InputDataStream ss) { ss.ReadEnum(tt); }) { + stream.ReadEnum(t); + } else if constexpr (requires(T tt, InputDataStream ss) { ss.ReadObject(tt); }) { + stream.ReadObject(t); + } else { + static_assert(false && sizeof(T), "This type is neither a 'value' nor an 'object'."); + } +} + +/// Helper to invoke either Write() or WriteObject(). +/// This is intended for writing IO adapters, users that's writing IO logic shouldn't using this - it increases compile time while reducing readability. +template <class TAdapter, class T> +void WriteHelper(OutputDataStream& stream, T& t) +{ + if constexpr (!std::is_same_v<TAdapter, void>) { + stream.WriteObjectAdapted<TAdapter>(t); + } else if constexpr (requires(T tt, OutputDataStream ss) { ss.Write(tt); }) { + stream.Write(t); + } else if constexpr (requires(T tt, OutputDataStream ss) { ss.WriteEnum(tt); }) { + stream.WriteEnum(t); + } else if constexpr (requires(T tt, OutputDataStream ss) { ss.WriteObject(tt); }) { + stream.WriteObject(t); + } else { + static_assert(false && sizeof(T), "This type is neither a 'value' nor an 'object'."); + } +} + +} // namespace DataStreamAdapters diff --git a/app/source/Cplt/Utils/IO/StringIntegration.hpp b/app/source/Cplt/Utils/IO/StringIntegration.hpp new file mode 100644 index 0000000..66f42b0 --- /dev/null +++ b/app/source/Cplt/Utils/IO/StringIntegration.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include <Cplt/Utils/IO/DataStream.hpp> + +#include <iterator> +#include <string> +#include <string_view> + +namespace DataStreamAdapters { +struct String +{ + static void ReadFromDataStream(InputDataStream& stream, std::string& str) + { + uint64_t size; + stream.Read(size); + + str = {}; + str.reserve(size); + stream.ReadBytes(size, std::back_inserter(str)); + } + + static void WriteToDataStream(OutputDataStream& stream, const std::string& str) + { + stream.Write((uint64_t)str.size()); + stream.WriteBytes(str.size(), str.data()); + } +}; + +struct StringView +{ + static void WriteToDataStream(OutputDataStream& stream, const std::string_view& str) + { + stream.Write((uint64_t)str.size()); + stream.WriteBytes(str.size(), str.data()); + } +}; +} // namespace DataStreamAdapters diff --git a/app/source/Cplt/Utils/IO/TslArrayIntegration.hpp b/app/source/Cplt/Utils/IO/TslArrayIntegration.hpp new file mode 100644 index 0000000..b585bee --- /dev/null +++ b/app/source/Cplt/Utils/IO/TslArrayIntegration.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include <Cplt/Utils/IO/DataStream.hpp> +#include <Cplt/Utils/IO/Helper.hpp> +#include <Cplt/Utils/IO/StringIntegration.hpp> + +#include <tsl/array_map.h> +#include <tsl/array_set.h> +#include <string> +#include <type_traits> + +// TODO support custom key types + +namespace DataStreamAdapters { +template <class TAdapter = void> +struct TslArrayMap +{ + template <class TValue> + static void ReadFromDataStream(InputDataStream& stream, tsl::array_map<char, TValue>& map) + { + static_assert(std::is_default_constructible_v<TValue>); + static_assert(std::is_move_constructible_v<TValue>); + + uint64_t size; + stream.Read(size); + map.reserve(size); + + for (uint64_t i = 0; i < size; ++i) { + std::string key; + stream.ReadObjectAdapted<DataStreamAdapters::String>(key); + + TValue value; + ReadHelper<TAdapter>(stream, value); + + map.insert(key, std::move(value)); + } + } + + template <class TValue> + static void WriteToDataStream(OutputDataStream& stream, const tsl::array_map<char, TValue>& map) + { + stream.Write((uint64_t)map.size()); + + for (auto it = map.begin(); it != map.end(); ++it) { + stream.WriteObjectAdapted<DataStreamAdapters::StringView>(it.key_sv()); + WriteHelper<TAdapter>(stream, it.value()); + } + } +}; +} // namespace DataStreamAdapters diff --git a/app/source/Cplt/Utils/IO/TslRobinIntegration.hpp b/app/source/Cplt/Utils/IO/TslRobinIntegration.hpp new file mode 100644 index 0000000..bdea505 --- /dev/null +++ b/app/source/Cplt/Utils/IO/TslRobinIntegration.hpp @@ -0,0 +1,78 @@ +#pragma once + +#include <Cplt/Utils/IO/DataStream.hpp> +#include <Cplt/Utils/IO/Helper.hpp> + +#include <tsl/robin_map.h> +#include <tsl/robin_set.h> +#include <type_traits> + +namespace DataStreamAdapters { +template <class TKeyAdapter = void, class TValueAdapter = void> +struct TslRobinMap +{ + template <class TKey, class TValue> + static void ReadFromDataStream(InputDataStream& stream, tsl::robin_map<TKey, TValue>& map) + { + static_assert(std::is_default_constructible_v<TValue>); + static_assert(std::is_move_constructible_v<TValue>); + + uint64_t size; + stream.Read(size); + map.reserve(size); + + for (uint64_t i = 0; i < size; ++i) { + TKey key; + ReadHelper<TKeyAdapter>(stream, key); + + TValue value; + ReadHelper<TValueAdapter>(stream, value); + + map.insert(std::move(key), std::move(value)); + } + } + + template <class TKey, class TValue> + static void WriteToDataStream(OutputDataStream& stream, const tsl::robin_map<TKey, TValue>& map) + { + stream.Write((uint64_t)map.size()); + + for (auto it = map.begin(); it != map.end(); ++it) { + WriteHelper<TKeyAdapter>(stream, it.key()); + WriteHelper<TValueAdapter>(stream, it.value()); + } + } +}; + +template <class TAdapter = void> +struct TslRobinSet +{ + template <class TElement> + static void ReadFromDataStream(InputDataStream& stream, tsl::robin_set<TElement>& set) + { + static_assert(std::is_default_constructible_v<TElement>); + static_assert(std::is_move_constructible_v<TElement>); + + uint64_t size; + stream.Read(size); + set.reserve(size); + + for (uint64_t i = 0; i < size; ++i) { + TElement element; + ReadHelper<TAdapter>(stream, element); + + set.insert(std::move(element)); + } + } + + template <class TElement> + static void WriteToDataStream(OutputDataStream& stream, const tsl::robin_set<TElement>& set) + { + stream.Write((uint64_t)set.size()); + + for (auto& element : set) { + WriteHelper<TAdapter>(stream, element); + } + } +}; +} // namespace DataStreamAdapters diff --git a/app/source/Cplt/Utils/IO/UuidIntegration.hpp b/app/source/Cplt/Utils/IO/UuidIntegration.hpp new file mode 100644 index 0000000..20c1e7e --- /dev/null +++ b/app/source/Cplt/Utils/IO/UuidIntegration.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include <Cplt/Utils/IO/DataStream.hpp> +#include <Cplt/Utils/UUID.hpp> + +#include <cstddef> +#include <cstdint> +#include <iterator> + +namespace DataStreamAdapters { +struct Uuid +{ + static void ReadFromDataStream(InputDataStream& stream, uuids::uuid& uuid) + { + uint8_t buffer[16]; + stream.ReadBytes(16, buffer); + + uuid = uuids::uuid(gsl::span<uint8_t, 16>{ buffer }); + } + + static void WriteToDataStream(OutputDataStream& stream, const uuids::uuid& uuid) + { + auto gslSpan = uuid.as_bytes(); + stream.WriteBytes(gslSpan.size(), gslSpan.data()); + } +}; +} // namespace DataStreamAdapters diff --git a/app/source/Cplt/Utils/IO/VectorIntegration.hpp b/app/source/Cplt/Utils/IO/VectorIntegration.hpp new file mode 100644 index 0000000..93967f6 --- /dev/null +++ b/app/source/Cplt/Utils/IO/VectorIntegration.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include <Cplt/Utils/IO/DataStream.hpp> +#include <Cplt/Utils/IO/Helper.hpp> + +#include <type_traits> +#include <vector> + +namespace DataStreamAdapters { +template <class TAdapter = void> +struct Vector +{ + template <class TElement> + static void ReadFromDataStream(InputDataStream& stream, std::vector<TElement>& vec) + { + static_assert(std::is_default_constructible_v<TElement>); + static_assert(std::is_move_constructible_v<TElement>); + + uint64_t size; + stream.Read(size); + + vec.clear(); + vec.reserve(size); + + for (uint64_t i = 0; i < size; ++i) { + TElement element; + ReadHelper<TAdapter>(stream, element); + + vec.push_back(std::move(element)); + } + } + + template <class TElement> + static void WriteToDataStream(OutputDataStream& stream, const std::vector<TElement>& vec) + { + stream.Write((uint64_t)vec.size()); + for (auto& element : vec) { + WriteHelper<TAdapter>(stream, element); + } + } +}; +} // namespace DataStreamAdapters diff --git a/app/source/Cplt/Utils/IO/fwd.hpp b/app/source/Cplt/Utils/IO/fwd.hpp new file mode 100644 index 0000000..9f1492b --- /dev/null +++ b/app/source/Cplt/Utils/IO/fwd.hpp @@ -0,0 +1,13 @@ +#pragma once + +// Archive.hpp +class DataArchive; + +// BaseDataStream.hpp +class BaseDataStream; +class InputDataStream; +class OutputDataStream; + +// FileStream.hpp +class InputFileStream; +class OutputFileStream; diff --git a/app/source/Cplt/Utils/Macros.hpp b/app/source/Cplt/Utils/Macros.hpp new file mode 100644 index 0000000..6958ed1 --- /dev/null +++ b/app/source/Cplt/Utils/Macros.hpp @@ -0,0 +1,13 @@ +#pragma once + +#define STRINGIFY_IMPL(text) #text +#define STRINGIFY(text) STRINGIFY_IMPL(text) + +#define CONCAT_IMPL(a, b) a##b +#define CONCAT(a, b) CONCAT_IMPL(a, b) +#define CONCAT_3(a, b, c) CONCAT(a, CONCAT(b, c)) +#define CONCAT_4(a, b, c, d) CONCAT(CONCAT(a, b), CONCAT(c, d)) + +#define UNIQUE_NAME(prefix) CONCAT(prefix, __COUNTER__) +#define UNIQUE_NAME_LINE(prefix) CONCAT(prefix, __LINE__) +#define DISCARD UNIQUE_NAME(_discard) diff --git a/app/source/Cplt/Utils/Math.hpp b/app/source/Cplt/Utils/Math.hpp new file mode 100644 index 0000000..da53da2 --- /dev/null +++ b/app/source/Cplt/Utils/Math.hpp @@ -0,0 +1,11 @@ +#pragma once + +namespace MathUtils { + +template <class T> +constexpr T Abs(T t) +{ + return t < 0 ? -t : t; +} + +} // namespace MathUtils diff --git a/app/source/Cplt/Utils/RTTI.hpp b/app/source/Cplt/Utils/RTTI.hpp new file mode 100644 index 0000000..86b1e2c --- /dev/null +++ b/app/source/Cplt/Utils/RTTI.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include <cassert> + +template <class T, class TBase> +bool is_a(TBase* t) +{ + assert(t != nullptr); + return T::IsInstance(t); +} + +template <class T, class TBase> +bool is_a_nullable(TBase* t) +{ + if (t) { + return is_a<T, TBase>(t); + } else { + return false; + } +} + +template <class T, class TBase> +T* dyn_cast(TBase* t) +{ + assert(t != nullptr); + if (T::IsInstance(t)) { + return static_cast<T*>(t); + } else { + return nullptr; + } +} + +template <class T, class TBase> +const T* dyn_cast(const TBase* t) +{ + assert(t != nullptr); + if (T::IsInstance(t)) { + return static_cast<const T*>(t); + } else { + return nullptr; + } +} + +template <class T, class TBase> +T* dyn_cast_nullable(TBase* t) +{ + if (!t) return nullptr; + return dyn_cast<T, TBase>(t); +} diff --git a/app/source/Cplt/Utils/ScopeGuard.hpp b/app/source/Cplt/Utils/ScopeGuard.hpp new file mode 100644 index 0000000..f2b7f46 --- /dev/null +++ b/app/source/Cplt/Utils/ScopeGuard.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include <Cplt/Utils/Macros.hpp> + +#include <utility> + +template <class TCleanupFunc> +class ScopeGuard +{ +private: + TCleanupFunc mFunc; + bool mDismissed = false; + +public: + /// Specifically left this implicit so that constructs like + /// \code + /// ScopeGuard sg = [&]() { res.Cleanup(); }; + /// \endcode + /// would work. It is highly discourage and unlikely that one would want to use ScopeGuard as a function + /// parameter, so the normal argument that implicit conversion are harmful doesn't really apply here. + ScopeGuard(TCleanupFunc func) + : mFunc{ std::move(func) } + { + } + + ~ScopeGuard() + { + if (!mDismissed) { + mFunc(); + } + } + + void Dismiss() noexcept + { + mDismissed = true; + } +}; + +#define DEFER ScopeGuard UNIQUE_NAME(scopeGuard) = [&]() diff --git a/app/source/Cplt/Utils/Sigslot.cpp b/app/source/Cplt/Utils/Sigslot.cpp new file mode 100644 index 0000000..1132dfb --- /dev/null +++ b/app/source/Cplt/Utils/Sigslot.cpp @@ -0,0 +1,233 @@ +#include "Sigslot.hpp" + +#include <doctest/doctest.h> + +bool SignalStub::Connection::IsOccupied() const +{ + return id != InvalidId; +} + +SignalStub::SignalStub(IWrapper& wrapper) + : mWrapper{ &wrapper } +{ +} + +SignalStub::~SignalStub() +{ + RemoveAllConnections(); +} + +std::span<const SignalStub::Connection> SignalStub::GetConnections() const +{ + return mConnections; +} + +SignalStub::Connection& SignalStub::InsertConnection(SlotGuard* guard) +{ + Connection* result; + int size = static_cast<int>(mConnections.size()); + for (int i = 0; i < size; ++i) { + auto& conn = mConnections[i]; + if (!conn.IsOccupied()) { + result = &conn; + result->id = i; + goto setup; + } + } + + mConnections.push_back(Connection{}); + result = &mConnections.back(); + result->id = size; + +setup: + if (guard) { + result->guard = guard; + result->slotId = guard->InsertConnection(*this, result->id); + } + return *result; +} + +void SignalStub::RemoveConnection(int id) +{ + if (id >= 0 && id < mConnections.size()) { + auto& conn = mConnections[id]; + if (conn.IsOccupied()) { + mWrapper->RemoveFunction(conn.id); + if (conn.guard) { + conn.guard->RemoveConnection(conn.slotId); + } + + conn.guard = nullptr; + conn.slotId = SignalStub::InvalidId; + conn.id = SignalStub::InvalidId; + } + } +} + +void SignalStub::RemoveConnectionFor(SlotGuard& guard) +{ + guard.RemoveConnectionFor(*this); +} + +void SignalStub::RemoveAllConnections() +{ + for (size_t i = 0; i < mConnections.size(); ++i) { + RemoveConnection(i); + } +} + +SlotGuard::SlotGuard() +{ +} + +SlotGuard::~SlotGuard() +{ + DisconnectAll(); +} + +void SlotGuard::DisconnectAll() +{ + for (auto& conn : mConnections) { + if (conn.stub) { + // Also calls SlotGuard::removeConnection, our copy of the data will be cleared in it + conn.stub->RemoveConnection(conn.stubId); + } + } +} + +int SlotGuard::InsertConnection(SignalStub& stub, int stubId) +{ + int size = static_cast<int>(mConnections.size()); + for (int i = 0; i < size; ++i) { + auto& conn = mConnections[i]; + if (!conn.stub) { + conn.stub = &stub; + conn.stubId = stubId; + return i; + } + } + + mConnections.push_back(Connection{}); + auto& conn = mConnections.back(); + conn.stub = &stub; + conn.stubId = stubId; + return size; +} + +void SlotGuard::RemoveConnectionFor(SignalStub& stub) +{ + for (auto& conn : mConnections) { + if (conn.stub == &stub) { + conn.stub->RemoveConnection(conn.stubId); + } + } +} + +void SlotGuard::RemoveConnection(int slotId) +{ + mConnections[slotId] = {}; +} + +TEST_CASE("Signal connect and disconnect") +{ + Signal<> sig; + + int counter = 0; + int id = sig.Connect([&]() { counter++; }); + + sig(); + CHECK(counter == 1); + + sig(); + CHECK(counter == 2); + + sig.Disconnect(id); + sig(); + CHECK(counter == 2); +} + +TEST_CASE("Signal with parameters") +{ + Signal<int> sig; + + int counter = 0; + int id = sig.Connect([&](int i) { counter += i; }); + + sig(1); + CHECK(counter == 1); + + sig(0); + CHECK(counter == 1); + + sig(4); + CHECK(counter == 5); + + sig.Disconnect(id); + sig(1); + CHECK(counter == 5); +} + +TEST_CASE("Signal disconnectAll()") +{ + Signal<> sig; + + int counter1 = 0; + int counter2 = 0; + sig.Connect([&]() { counter1++; }); + sig.Connect([&]() { counter2++; }); + + sig(); + CHECK(counter1 == 1); + CHECK(counter2 == 1); + + sig(); + CHECK(counter1 == 2); + CHECK(counter2 == 2); + + sig.DisconnectAll(); + sig(); + CHECK(counter1 == 2); + CHECK(counter2 == 2); +} + +TEST_CASE("SlotGuard auto-disconnection") +{ + int counter1 = 0; + int counter2 = 0; + Signal<> sig; + + { + SlotGuard guard; + sig.Connect(guard, [&]() { counter1 += 1; }); + sig.Connect(guard, [&]() { counter2 += 1; }); + + sig(); + CHECK(counter1 == 1); + CHECK(counter2 == 1); + + sig(); + CHECK(counter1 == 2); + CHECK(counter2 == 2); + } + + sig(); + CHECK(counter1 == 2); + CHECK(counter2 == 2); +} + +TEST_CASE("Signal destruct before SlotGuard") +{ + int counter = 0; + SlotGuard guard; + + { + Signal<> sig2; + sig2.Connect(guard, [&]() { counter++; }); + + sig2(); + CHECK(counter == 1); + } + + // Shouldn't error + guard.DisconnectAll(); +} diff --git a/app/source/Cplt/Utils/Sigslot.hpp b/app/source/Cplt/Utils/Sigslot.hpp new file mode 100644 index 0000000..a4ab94e --- /dev/null +++ b/app/source/Cplt/Utils/Sigslot.hpp @@ -0,0 +1,165 @@ +#pragma once + +#include <Cplt/Utils/fwd.hpp> + +#include <cstddef> +#include <functional> +#include <span> +#include <utility> +#include <vector> + +class SignalStub +{ +public: + /// Non-template interface for Signal<T...> to implement (a barrier to stop template + /// arguments propagation). + class IWrapper + { + public: + virtual ~IWrapper() = default; + virtual void RemoveFunction(int id) = 0; + }; + + enum + { + InvalidId = -1, + }; + + struct Connection + { + SlotGuard* guard; + int slotId; + int id = InvalidId; // If `InvalidId`, then this "spot" is unused + + bool IsOccupied() const; + }; + +private: + std::vector<Connection> mConnections; + IWrapper* mWrapper; + +private: + template <class...> + friend class Signal; + friend class SlotGuard; + + SignalStub(IWrapper& wrapper); + ~SignalStub(); + + SignalStub(const SignalStub&) = delete; + SignalStub& operator=(const SignalStub&) = delete; + SignalStub(SignalStub&&) = default; + SignalStub& operator=(SignalStub&&) = default; + + std::span<const Connection> GetConnections() const; + Connection& InsertConnection(SlotGuard* guard = nullptr); + void RemoveConnection(int id); + void RemoveConnectionFor(SlotGuard& guard); + void RemoveAllConnections(); +}; + +template <class... TArgs> +class Signal : public SignalStub::IWrapper +{ +private: + // Must be in this order so that mFunctions is still intact when mStub's destructor runs + std::vector<std::function<void(TArgs...)>> mFunctions; + SignalStub mStub; + +public: + Signal() + : mStub(*this) + { + } + + virtual ~Signal() = default; + + Signal(const Signal&) = delete; + Signal& operator=(const Signal&) = delete; + Signal(Signal&&) = default; + Signal& operator=(Signal&&) = default; + + void operator()(TArgs... args) + { + for (auto& conn : mStub.GetConnections()) { + if (conn.IsOccupied()) { + mFunctions[conn.id](std::forward<TArgs>(args)...); + } + } + } + + template <class TFunction> + int Connect(TFunction slot) + { + auto& conn = mStub.InsertConnection(); + mFunctions.resize(std::max(mFunctions.size(), (size_t)conn.id + 1)); + mFunctions[conn.id] = std::move(slot); + return conn.id; + } + + template <class TFunction> + int Connect(SlotGuard& guard, TFunction slot) + { + auto& conn = mStub.InsertConnection(&guard); + mFunctions.resize(std::max(mFunctions.size(), (size_t)conn.id + 1)); + mFunctions[conn.id] = std::move(slot); + return conn.id; + } + + void Disconnect(int id) + { + mStub.RemoveConnection(id); + } + + void DisconnectFor(SlotGuard& guard) + { + mStub.RemoveConnectionFor(guard); + } + + void DisconnectAll() + { + mStub.RemoveAllConnections(); + } + + virtual void RemoveFunction(int id) + { + mFunctions[id] = {}; + } +}; + +/// Automatic disconnection mechanism for Signal<>. +/// Bind connection to this guard by using the Connect(SlotGuard&, TFunction) overload. +/// Either DisconnectAll() or the destructor disconnects all connections bound to this guard. +class SlotGuard +{ +private: + struct Connection + { + SignalStub* stub = nullptr; + int stubId = SignalStub::InvalidId; + }; + std::vector<Connection> mConnections; + +public: + friend class SignalStub; + SlotGuard(); + ~SlotGuard(); + + SlotGuard(const SlotGuard&) = delete; + SlotGuard& operator=(const SlotGuard&) = delete; + SlotGuard(SlotGuard&&) = default; + SlotGuard& operator=(SlotGuard&&) = default; + + /// DisconnectBySource all connection associated with this SlotGuard. + void DisconnectAll(); + +private: + /// \return Slot id. + int InsertConnection(SignalStub& stub, int stubId); + /// Remove the connection data in this associated with slotId. This does not invoke + /// the connections' stub's RemoveConnection function. + void RemoveConnection(int slotId); + /// DisconnectBySource all connections from the given stub associated with this SlotGuard. + /// Implementation for SignalStub::RemoveConnectionsFor(SlotGuard&) + void RemoveConnectionFor(SignalStub& stub); +}; diff --git a/app/source/Cplt/Utils/Size.hpp b/app/source/Cplt/Utils/Size.hpp new file mode 100644 index 0000000..ae38e8a --- /dev/null +++ b/app/source/Cplt/Utils/Size.hpp @@ -0,0 +1,65 @@ +#pragma once + +#include <Cplt/Utils/Vector.hpp> + +template <class T> +class Size2 { +public: + T width; + T height; + +public: + Size2() + : width{ 0 }, height{ 0 } { + } + + Size2(T width, T height) + : width{ width }, height{ height } { + } + + Size2(Vec2<T> vec) + : width{ vec.x }, height{ vec.y } + { + } + + operator Vec2<T>() const + { + return { width, height }; + } + + Vec2<T> AsVec() const + { + return { width, height }; + } + + friend bool operator==(const Size2<T>&, const Size2<T>&) = default; + + template <class TTarget> + Size2<TTarget> Cast() const + { + return { + static_cast<TTarget>(width), + static_cast<TTarget>(height), + }; + } +}; + +template <class T> +Size2<T> operator+(Size2<T> a, Size2<T> b) { + return { a.width + b.width, a.height + b.height }; +} + +template <class T> +Size2<T> operator-(Size2<T> a, Size2<T> b) { + return { a.width - b.width, a.height - b.height }; +} + +template <class T, class N> +auto operator*(Size2<T> a, N mult) -> Size2<decltype(a.width * mult)> { + return { a.width * mult, a.height * mult }; +} + +template <class T, class N> +auto operator/(Size2<T> a, N mult) -> Size2<decltype(a.width / mult)> { + return { a.width / mult, a.height / mult }; +} diff --git a/app/source/Cplt/Utils/StandardDirectories.cpp b/app/source/Cplt/Utils/StandardDirectories.cpp new file mode 100644 index 0000000..2202f51 --- /dev/null +++ b/app/source/Cplt/Utils/StandardDirectories.cpp @@ -0,0 +1,78 @@ +#include "StandardDirectories.hpp" + +#include <filesystem> +#include <stdexcept> + +namespace fs = std::filesystem; + +#if defined(_WIN32) +// https://stackoverflow.com/questions/54499256/how-to-find-the-saved-games-folder-programmatically-in-c-c +# include <ShlObj_core.h> +# include <objbase.h> +# pragma comment(lib, "shell32.lib") +# pragma comment(lib, "ole32.lib") + +static fs::path GetAppDataRoaming() +{ + PWSTR path = nullptr; + HRESULT hr = SHGetKnownFolderPath(FOLDERID_RoamingAppData, KF_FLAG_CREATE, nullptr, &path); + if (SUCCEEDED(hr)) { + auto dataDir = fs::path(path); + CoTaskMemFree(path); + + fs::create_directories(dataDir); + return dataDir; + } else { + fs::path dataDir("~/AppData/Roaming"); + fs::create_directories(dataDir); + return dataDir; + } +} + +#elif defined(__APPLE__) +// TODO +#elif defined(__linux__) +# include <cstdlib> + +static fs::path GetEnvVar(const char* name, const char* backup) +{ + if (const char* path = std::getenv(name)) { + fs::path dataDir(path); + fs::create_directories(dataDir); + return dataDir; + } else { + fs::path dataDir(backup); + fs::create_directories(dataDir); + return dataDir; + } +} + +#endif + +const std::filesystem::path& StandardDirectories::UserData() +{ + static auto userDataDir = []() -> fs::path { +#if defined(_WIN32) + return GetAppDataRoaming(); +#elif defined(__APPLE__) + // TODO where? +#elif defined(__linux__) + return GetEnvVar("XDG_DATA_HOME", "~/.local/share"); +#endif + }(); + return userDataDir; +} + +const std::filesystem::path& StandardDirectories::UserConfig() +{ + static auto userConfigDir = []() -> fs::path { +#if defined(_WIN32) + return GetAppDataRoaming(); +#elif defined(__APPLE__) + // TODO where? +#elif defined(__linux__) + return GetEnvVar("XDG_CONFIG_HOME", "~/.config"); +#endif + }(); + return userConfigDir; +} diff --git a/app/source/Cplt/Utils/StandardDirectories.hpp b/app/source/Cplt/Utils/StandardDirectories.hpp new file mode 100644 index 0000000..4f7e5e2 --- /dev/null +++ b/app/source/Cplt/Utils/StandardDirectories.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include <filesystem> + +namespace StandardDirectories { + +const std::filesystem::path& UserData(); +const std::filesystem::path& UserConfig(); + +} // namespace StandardDirectories diff --git a/app/source/Cplt/Utils/Time.cpp b/app/source/Cplt/Utils/Time.cpp new file mode 100644 index 0000000..4e79ffa --- /dev/null +++ b/app/source/Cplt/Utils/Time.cpp @@ -0,0 +1,29 @@ +#include "Time.hpp" + +#include <ctime> + +std::string TimeUtils::StringifyTimePoint(std::chrono::time_point<std::chrono::system_clock> tp) +{ + auto t = std::chrono::system_clock::to_time_t(tp); + + char data[32]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" // C++ doesn't have std::localtime_s + std::strftime(data, sizeof(data), "%Y-%m-%d %H:%M:%S", std::localtime(&t)); +#pragma clang diagnostic pop + + return std::string(data); +} + +std::string TimeUtils::StringifyTimeStamp(int64_t timeStamp) +{ + if (timeStamp == 0) { + return ""; + } + + namespace chrono = std::chrono; + chrono::milliseconds d{ timeStamp }; + chrono::time_point<chrono::system_clock> tp{ d }; + + return StringifyTimePoint(tp); +} diff --git a/app/source/Cplt/Utils/Time.hpp b/app/source/Cplt/Utils/Time.hpp new file mode 100644 index 0000000..fbbd3b2 --- /dev/null +++ b/app/source/Cplt/Utils/Time.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include <chrono> +#include <string> + +namespace TimeUtils { + +std::string StringifyTimePoint(std::chrono::time_point<std::chrono::system_clock> tp); +std::string StringifyTimeStamp(int64_t timeStamp); + +} // namespace TimeUtils diff --git a/app/source/Cplt/Utils/UUID.hpp b/app/source/Cplt/Utils/UUID.hpp new file mode 100644 index 0000000..9044aa6 --- /dev/null +++ b/app/source/Cplt/Utils/UUID.hpp @@ -0,0 +1,5 @@ +#pragma once + +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include <uuid.h> diff --git a/app/source/Cplt/Utils/Variant.hpp b/app/source/Cplt/Utils/Variant.hpp new file mode 100644 index 0000000..df2f882 --- /dev/null +++ b/app/source/Cplt/Utils/Variant.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include <utility> +#include <variant> + +template <class... Ts> +struct Overloaded : Ts... +{ + using Ts::operator()...; +}; +template <class... Ts> +Overloaded(Ts...) -> Overloaded<Ts...>; + +template <class... Args> +struct VariantCastProxy +{ + std::variant<Args...> v; + + template <class... ToArgs> + operator std::variant<ToArgs...>() const + { + return std::visit( + [](auto&& arg) -> std::variant<ToArgs...> { return arg; }, + v); + } +}; + +/// Use snake_case naming to align with `static_cast`, `dynamic_cast`, etc.. +template <class... Args> +auto variant_cast(std::variant<Args...> v) -> VariantCastProxy<Args...> +{ + return { std::move(v) }; +} diff --git a/app/source/Cplt/Utils/Vector.hpp b/app/source/Cplt/Utils/Vector.hpp new file mode 100644 index 0000000..79f4ea2 --- /dev/null +++ b/app/source/Cplt/Utils/Vector.hpp @@ -0,0 +1,144 @@ +#pragma once + +#include <Cplt/Utils/IO/DataStream.hpp> + +template <class T> +struct Vec2 +{ + T x = 0; + T y = 0; + + template <class TTarget> + Vec2<TTarget> Cast() const + { + return { + static_cast<TTarget>(x), + static_cast<TTarget>(y), + }; + } + + void ReadFromDataStream(InputDataStream& stream) + { + stream.Value(x); + stream.Value(y); + } + + void WriteToDataStream(OutputDataStream& stream) const + { + stream.Value(x); + stream.Value(y); + } + + friend constexpr bool operator==(const Vec2& a, const Vec2& b) = default; + + friend constexpr Vec2 operator+(const Vec2& a, const Vec2& b) { return { a.x + b.x, a.y + b.y }; } + friend constexpr Vec2 operator-(const Vec2& a, const Vec2& b) { return { a.x - b.x, a.y - b.y }; } + friend constexpr Vec2 operator*(const Vec2& a, const Vec2& b) { return { a.x * b.x, a.y * b.y }; } + friend constexpr Vec2 operator/(const Vec2& a, const Vec2& b) { return { a.x / b.x, a.y / b.y }; } + + friend constexpr Vec2 operator+(const Vec2& a, T n) { return { a.x + n, a.y + n }; } + friend constexpr Vec2 operator-(const Vec2& a, T n) { return { a.x - n, a.y - n }; } + friend constexpr Vec2 operator*(const Vec2& a, T n) { return { a.x * n, a.y * n }; } + friend constexpr Vec2 operator/(const Vec2& a, T n) { return { a.x / n, a.y / n }; } +}; + +using Vec2i = Vec2<int>; +using Vec2f = Vec2<float>; + +template <class T> +struct Vec3 +{ + T x = 0; + T y = 0; + T z = 0; + + template <class TTarget> + Vec3<TTarget> Cast() const + { + return { + static_cast<TTarget>(x), + static_cast<TTarget>(y), + static_cast<TTarget>(z), + }; + } + + void ReadFromDataStream(InputDataStream& stream) + { + stream.Value(x); + stream.Value(y); + stream.Value(z); + } + + void WriteToDataStream(OutputDataStream& stream) const + { + stream.Value(x); + stream.Value(y); + stream.Value(z); + } + + friend constexpr bool operator==(const Vec3& a, const Vec3& b) = default; + + friend constexpr Vec3 operator+(const Vec3& a, const Vec3& b) { return { a.x + b.x, a.y + b.y, a.z + b.z }; } + friend constexpr Vec3 operator-(const Vec3& a, const Vec3& b) { return { a.x - b.x, a.y - b.y, a.z - b.z }; } + friend constexpr Vec3 operator*(const Vec3& a, const Vec3& b) { return { a.x * b.x, a.y * b.y, a.z * b.z }; } + friend constexpr Vec3 operator/(const Vec3& a, const Vec3& b) { return { a.x / b.x, a.y / b.y, a.z / b.z }; } + + friend constexpr Vec3 operator+(const Vec3& a, T n) { return { a.x + n, a.y + n, a.z + n }; } + friend constexpr Vec3 operator-(const Vec3& a, T n) { return { a.x - n, a.y - n, a.z - n }; } + friend constexpr Vec3 operator*(const Vec3& a, T n) { return { a.x * n, a.y * n, a.z * n }; } + friend constexpr Vec3 operator/(const Vec3& a, T n) { return { a.x / n, a.y / n, a.z / n }; } +}; + +using Vec3i = Vec3<int>; +using Vec3f = Vec3<float>; + +template <class T> +struct Vec4 +{ + T x = 0; + T y = 0; + T z = 0; + T w = 0; + + template <class TTarget> + Vec4<TTarget> Cast() const + { + return { + static_cast<TTarget>(x), + static_cast<TTarget>(y), + static_cast<TTarget>(z), + static_cast<TTarget>(w), + }; + } + + void ReadFromDataStream(InputDataStream& stream) + { + stream.Value(x); + stream.Value(y); + stream.Value(z); + stream.Value(w); + } + + void WriteToDataStream(OutputDataStream& stream) const + { + stream.Value(x); + stream.Value(y); + stream.Value(z); + stream.Value(w); + } + + friend constexpr bool operator==(const Vec4& a, const Vec4& b) = default; + + friend constexpr Vec4 operator+(const Vec4& a, const Vec4& b) { return { a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w }; } + friend constexpr Vec4 operator-(const Vec4& a, const Vec4& b) { return { a.x - b.x, a.y - b.y, a.z - b.z, a.w - b.w }; } + friend constexpr Vec4 operator*(const Vec4& a, const Vec4& b) { return { a.x * b.x, a.y * b.y, a.z * b.z, a.w * b.w }; } + friend constexpr Vec4 operator/(const Vec4& a, const Vec4& b) { return { a.x / b.x, a.y / b.y, a.z / b.z, a.w / b.w }; } + + friend constexpr Vec4 operator+(const Vec4& a, T n) { return { a.x + n, a.y + n, a.z + n, a.w + n }; } + friend constexpr Vec4 operator-(const Vec4& a, T n) { return { a.x - n, a.y - n, a.z - n, a.w - n }; } + friend constexpr Vec4 operator*(const Vec4& a, T n) { return { a.x * n, a.y * n, a.z * n, a.w * n }; } + friend constexpr Vec4 operator/(const Vec4& a, T n) { return { a.x / n, a.y / n, a.z / n, a.w / n }; } +}; + +using Vec4i = Vec4<int>; +using Vec4f = Vec4<float>; diff --git a/app/source/Cplt/Utils/VectorHash.hpp b/app/source/Cplt/Utils/VectorHash.hpp new file mode 100644 index 0000000..f649367 --- /dev/null +++ b/app/source/Cplt/Utils/VectorHash.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include <Cplt/Utils/Hash.hpp> +#include <Cplt/Utils/Vector.hpp> + +#include <cstddef> +#include <functional> + +template <class T> +struct std::hash<Vec2<T>> +{ + size_t operator()(const Vec2<T>& vec) const + { + size_t result; + HashUtils::Combine(result, vec.x); + HashUtils::Combine(result, vec.y); + return result; + } +}; + +template <class T> +struct std::hash<Vec3<T>> +{ + size_t operator()(const Vec3<T>& vec) const + { + size_t result; + HashUtils::Combine(result, vec.x); + HashUtils::Combine(result, vec.y); + HashUtils::Combine(result, vec.z); + return result; + } +}; + +template <class T> +struct std::hash<Vec4<T>> +{ + size_t operator()(const Vec4<T>& vec) const + { + size_t result; + HashUtils::Combine(result, vec.x); + HashUtils::Combine(result, vec.y); + HashUtils::Combine(result, vec.z); + HashUtils::Combine(result, vec.w); + return result; + } +}; diff --git a/app/source/Cplt/Utils/fwd.hpp b/app/source/Cplt/Utils/fwd.hpp new file mode 100644 index 0000000..366cacc --- /dev/null +++ b/app/source/Cplt/Utils/fwd.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include <Cplt/Utils/IO/fwd.hpp> + +// Color.hpp +class RgbaColor; +class HsvColor; + +// Sigslot.hpp +class SignalStub; +template <class... TArgs> +class Signal; +class SlotGuard; + +// String.hpp +class Utf8Iterator; +class Utf8IterableString; diff --git a/app/source/Cplt/fwd.hpp b/app/source/Cplt/fwd.hpp new file mode 100644 index 0000000..97f669d --- /dev/null +++ b/app/source/Cplt/fwd.hpp @@ -0,0 +1,5 @@ +#pragma once + +#include <Cplt/Model/fwd.hpp> +#include <Cplt/UI/fwd.hpp> +#include <Cplt/Utils/fwd.hpp> |