diff --git a/.gitignore b/.gitignore index 6694de9f1..3af4797c4 100644 --- a/.gitignore +++ b/.gitignore @@ -428,3 +428,4 @@ SampleProject/ # Presentation exports *.pdf *.pptx +.claude/worktrees/ diff --git a/Resources/Shaders/imgui.vert b/Resources/Shaders/imgui.vert deleted file mode 100644 index b9ad2957f..000000000 --- a/Resources/Shaders/imgui.vert +++ /dev/null @@ -1,26 +0,0 @@ -#version 460 core -layout(location = 0) in vec2 aPos; -layout(location = 1) in vec2 aUV; -layout(location = 2) in vec4 aColor; - -layout(push_constant) uniform uPushConstant -{ - vec2 uScale; - vec2 uTranslate; - uint index; - uint _padding; -} -pc; - -layout(location = 0) out struct -{ - vec4 Color; - vec4 TexData; -} Out; - -void main() -{ - Out.Color = aColor; - Out.TexData = vec4(aUV, pc.index, 0.0); - gl_Position = vec4(aPos * pc.uScale + pc.uTranslate, 0, 1); -} \ No newline at end of file diff --git a/Resources/Shaders/imgui.frag b/Resources/Shaders/zui_draw.frag similarity index 80% rename from Resources/Shaders/imgui.frag rename to Resources/Shaders/zui_draw.frag index 56b7532a7..5b05daac4 100644 --- a/Resources/Shaders/imgui.frag +++ b/Resources/Shaders/zui_draw.frag @@ -8,12 +8,11 @@ layout(set = 1, binding = 2) uniform sampler LinearClampSampler; layout(location = 0) in struct { vec4 Color; - vec4 TexData; + vec4 TexData; // xy=UV, z=texIdx } In; void main() { - // texId derives from pc.index (push constant) - dynamically uniform; nonuniformEXT not needed. uint texId = uint(floor(In.TexData.z + 0.5)); vec4 texVal = texture(sampler2D(TextureArray[texId], LinearClampSampler), In.TexData.xy); fColor = In.Color * texVal; diff --git a/Resources/Shaders/zui_draw.vert b/Resources/Shaders/zui_draw.vert new file mode 100644 index 000000000..cc8fd9497 --- /dev/null +++ b/Resources/Shaders/zui_draw.vert @@ -0,0 +1,32 @@ +#version 460 core + +layout(location = 0) in vec2 aPos; +layout(location = 1) in vec2 aUV; +layout(location = 2) in vec4 aColor; // RGBA8 UNORM — hardware unpacks packed uint32 + +layout(push_constant) uniform PC +{ + vec2 uScale; + vec2 uTranslate; + uint uTexIdx; + float uFbScale; // UIScale = fb/win; snaps vertices to nearest physical pixel +} +pc; + +layout(location = 0) out struct +{ + vec4 Color; + vec4 TexData; // xy=UV, z=texIdx +} Out; + +void main() +{ + Out.Color = aColor; + Out.TexData = vec4(aUV, float(pc.uTexIdx), 0.0); + + // Snap to the nearest physical pixel before projecting. + // Prevents sub-pixel drift that blurs glyph quads on non-Retina displays. + float fs = max(pc.uFbScale, 1.0); + vec2 snapped = round(aPos * fs) / fs; + gl_Position = vec4(snapped * pc.uScale + pc.uTranslate, 0.0, 1.0); +} diff --git a/Tetragrama/Components/AboutUIComponent.h b/Tetragrama/Components/AboutUIComponent.h deleted file mode 100644 index 4d2621786..000000000 --- a/Tetragrama/Components/AboutUIComponent.h +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once -#include -#include - -namespace Tetragrama::Components -{ - class AboutUIComponent : public UIComponent - { - public: - AboutUIComponent() {} - virtual ~AboutUIComponent() = default; - - void Initialize(Layers::ImguiLayer* parent = nullptr, const char* name = "AboutUIComponent", bool visibility = true, bool closed = false) override - { - UIComponent::Initialize(parent, name, visibility, closed); - } - - virtual void Render(ZEngine::Rendering::Renderers::GraphicRenderer* const renderer, ZEngine::Hardwares::CommandBuffer* const command_buffer) override - { - ImGui::ShowAboutWindow(&m_is_open); - } - - void Update(ZEngine::Core::TimeStep dt) override {} - - private: - bool m_is_open{true}; - }; -} // namespace Tetragrama::Components diff --git a/Tetragrama/Components/AssetImporterUIComponent.cpp b/Tetragrama/Components/AssetImporterUIComponent.cpp deleted file mode 100644 index aa8e00be0..000000000 --- a/Tetragrama/Components/AssetImporterUIComponent.cpp +++ /dev/null @@ -1,631 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -using namespace ZEngine::Core::VFS; -using namespace ZEngine::Helpers; -using namespace ZEngine::Importers; - -namespace Tetragrama::Components -{ - void AssetImporterUIComponent::Initialize(Layers::ImguiLayer* parent, cstring name, bool visibility, bool closed) - { - UIComponent::Initialize(parent, name, visibility, closed); - - parent->LocalArena.CreateSubArena(ZMega(8), &LocalArena); - parent->LocalArena.CreateSubArena(ZMega(4), &LocalStringArena); - - // Importer arenas carved from the engine's ImportPipeline budget so all - // import memory — engine importers and editor importers — is budget-tracked. - auto* import_arena = &ZEngine::Engine::GetContext()->ImportPipelineArena; - import_arena->CreateSubArena(ZMega(64), &GltfImporterArena); - import_arena->CreateSubArena(ZMega(128), &AssimpImporterArena); - - m_gltf_importer = ZPushStructCtor(import_arena, ZEngine::Importers::GltfImporter); - m_fbx_importer = ZPushStructCtor(import_arena, ZEngine::Importers::FbxImporter); - m_assimp_importer = ZPushStructCtor(import_arena, ZEngine::Importers::AssimpImporter); - m_gltf_importer->Initialize(&GltfImporterArena); - m_fbx_importer->Initialize(import_arena); - m_assimp_importer->Initialize(&AssimpImporterArena); - - m_path_buf.init(&LocalStringArena, 1024); - } - - void AssetImporterUIComponent::Update(ZEngine::Core::TimeStep /*dt*/) {} - - void AssetImporterUIComponent::PushLog(cstring text, const float color[4]) - { - std::lock_guard lock(m_log_mutex); - auto& e = m_log[m_log_head]; - secure_strncpy(e.Text, sizeof(e.Text), text, sizeof(e.Text) - 1); - e.Color[0] = color[0]; - e.Color[1] = color[1]; - e.Color[2] = color[2]; - e.Color[3] = color[3]; - m_log_head = (m_log_head + 1) % kLogMax; - if (m_log_count < kLogMax) - ++m_log_count; - m_scroll_log = true; - } - - void AssetImporterUIComponent::PushHistory(cstring name, bool success, cstring msg) - { - if (m_history_count < kHistMax) - { - auto& e = m_history[m_history_count++]; - secure_strncpy(e.Name, sizeof(e.Name), name, sizeof(e.Name) - 1); - secure_strncpy(e.Message, sizeof(e.Message), msg, sizeof(e.Message) - 1); - e.Success = success; - } - else - { - for (int i = 0; i < kHistMax - 1; ++i) - m_history[i] = m_history[i + 1]; - auto& e = m_history[kHistMax - 1]; - secure_strncpy(e.Name, sizeof(e.Name), name, sizeof(e.Name) - 1); - secure_strncpy(e.Message, sizeof(e.Message), msg, sizeof(e.Message) - 1); - e.Success = success; - } - } - - void AssetImporterUIComponent::TriggerScan() - { - // Consume any pending Actor creation from the importer background thread. - // This runs on the main thread, so ECS operations are safe here. - if (m_pending_actor.valid) - { - auto* app = ParentLayer ? reinterpret_cast(ParentLayer->CurrentApp) : nullptr; - auto* scene = app ? reinterpret_cast(app->CurrentScene) : nullptr; - auto* ctx = ZEngine::Engine::GetContext(); - if (scene && ctx && ctx->ActorManager) - { - ZEngine::ECS::ActorHandle handle = ctx->ActorManager->Create(); - ZEngine::ECS::Actor* actor = ctx->ActorManager->Access(handle); - if (actor) - { - using namespace ZEngine::ECS::Components; - NameComponent nc = {}; - ZEngine::Helpers::secure_strncpy(nc.Value, sizeof(nc.Value), m_pending_actor.name, ZEngine::Helpers::secure_strlen(m_pending_actor.name)); - actor->AddComponent(nc); - actor->AddComponent({}); - MeshComponent mc = {}; - mc.MeshUUID = m_pending_actor.uuid; - mc.RenderInstanceId = m_pending_actor.render_id; - actor->AddComponent(mc); - } - } - m_pending_actor = {}; - } - - auto* vfs = reinterpret_cast(ZEngine::Engine::GetContext()->VFS); - if (vfs && ParentLayer) - ParentLayer->Scanner.Scan(vfs, ZEngine::Core::VFS::VFSPath::Root(), &ParentLayer->Cache); - } - - std::future AssetImporterUIComponent::BrowseFileAsync() - { - if (!ParentLayer || !ParentLayer->CurrentApp) - co_return; - auto window = ParentLayer->CurrentApp->CurrentWindow; - std::vector filters = {".glb", ".gltf", ".fbx", ".obj"}; - std::string picked = co_await window->OpenFileDialogAsync(filters); - if (!picked.empty()) - { - m_path_buf.clear(); - m_path_buf.append(picked.c_str()); - if (m_same_settings) - StartImport(); - else - m_state.value.store(ImporterState::Options); - } - } - - void AssetImporterUIComponent::BrowseFile() - { - ZEngine::Core::MainThreadScheduler::Post(this, [](void* context) { reinterpret_cast(context)->BrowseFileAsync(); }); - } - - void AssetImporterUIComponent::StartImport() - { - if (!m_gltf_importer || !m_fbx_importer || !m_assimp_importer) - return; - - auto* app = reinterpret_cast(ParentLayer->CurrentApp); - if (!app || !app->Configuration) - return; - - static constexpr float kWhite[] = {0.8f, 0.8f, 0.8f, 1.0f}; - - // Build ImportConfiguration from project settings - const auto& cfg = *app->Configuration; - auto vfs_result = VFSPath::Parse(m_path_buf.c_str()); - if (vfs_result.Failed()) - { - return; - } - auto& vfs_value = vfs_result.Value(); - auto asset_name = vfs_value.Stem(); - auto parent_dir = vfs_value.Parent(); - - char asset_file_buf[256]; - snprintf(asset_file_buf, sizeof(asset_file_buf), "%.*s.zemesh", (int) asset_name.Length, asset_name.Data); - - // Use the arena-local config (arena will be cleared after import) - LocalArena.Clear(); - auto* config = ZPushStruct(&LocalArena, AssetCodec::ImportConfiguration); - config->OutputWorkingSpacePath.init(&LocalArena, cfg.WorkingSpacePath.c_str()); - config->OutputTextureFilesPath.init(&LocalArena, cfg.TexturePath.c_str()); - config->OutputAssetsPath.init(&LocalArena, cfg.MeshPath.c_str()); - config->OutputMaterialPath.init(&LocalArena, cfg.MaterialPath.c_str()); - if (!m_use_source_name && m_instance_name[0] != '\0') - config->AssetName.init(&LocalArena, m_instance_name); - else - config->AssetName.init(&LocalArena, asset_name.Data); - config->OutputAssetFile.init(&LocalArena, asset_file_buf); - config->InputBaseAssetFilePath.init(&LocalArena, parent_dir.CStr()); - config->VFS = reinterpret_cast(ZEngine::Engine::GetContext()->VFS); - config->Options.UniformScale = m_scale; - config->Options.AxisUpIsZ = m_axis_index == 1; - config->Options.NormalsMode = static_cast(m_normals_mode); - config->Options.MergeVertices = m_merge_vertices; - config->Options.ImportMaterials = m_import_materials; - config->Options.ImportTextures = m_import_textures; - config->Options.FlipUVs = m_flip_uvs; - - char msg[512]; - snprintf(msg, sizeof(msg), "Importing %s", vfs_value.Filename().Data); - PushLog(msg, kWhite); - - m_state.value.store(ImporterState::Importing); - m_progress.value.store(0.0f); - - // Route by extension to the appropriate importer - auto ext = vfs_value.Extension(); - auto cfg_copy = *config; - - if (secure_strcmp(ext.Data, ".glb") == 0 || secure_strcmp(ext.Data, ".gltf") == 0) - { - ZEngine::Helpers::ThreadPoolHelper::Submit([this, src = m_path_buf, cfg_copy, arena = &LocalArena, app]() mutable { m_gltf_importer->ImportFile(src.c_str(), cfg_copy, arena, this, OnImportFileComplete, OnImportProgress, OnImportError, OnImportLog); }); - } - else if (secure_strcmp(ext.Data, ".fbx") == 0) - { - ZEngine::Helpers::ThreadPoolHelper::Submit([this, src = m_path_buf, cfg_copy, arena = &LocalArena, app]() mutable { m_fbx_importer->ImportFile(src.c_str(), cfg_copy, arena, this, OnImportFileComplete, OnImportProgress, OnImportError, OnImportLog); }); - } - else - { - ZEngine::Helpers::ThreadPoolHelper::Submit([this, src = m_path_buf, cfg_copy, arena = &LocalArena, app]() mutable { m_assimp_importer->ImportFile(src.c_str(), cfg_copy, arena, this, OnImportFileComplete, OnImportProgress, OnImportError, OnImportLog); }); - } - } - - void AssetImporterUIComponent::OnImportFileComplete(void* ctx, ZEngine::Core::Containers::ArrayView outputs) - { - auto* self = reinterpret_cast(ctx); - - static constexpr float kGreen[] = {0.3f, 1.0f, 0.4f, 1.0f}; - static constexpr float kRed[] = {1.0f, 0.3f, 0.3f, 1.0f}; - - bool has_mesh = false; - cstring mesh_path = nullptr; - for (unsigned i = 0; i < outputs.size(); ++i) - { - if (outputs[i].Type == ZEngine::Importers::AssetFileType::MESH) - { - has_mesh = true; - mesh_path = outputs[i].Path.c_str(); - } - } - - if (has_mesh) - { - // Write meta file with SourcePath so reimport is possible later - auto* ctx_engine = ZEngine::Engine::GetContext(); - if (ctx_engine && ctx_engine->VFS && mesh_path) - { - auto* vfs = reinterpret_cast(ctx_engine->VFS); - auto* app = reinterpret_cast(self->ParentLayer->CurrentApp); - cstring ws = app ? app->WorkingSpacePath : ""; - size_t ws_len = secure_strlen(ws); - - // Build VFSPath for the .zemesh artifact - if (ws_len > 0 && strncmp(mesh_path, ws, ws_len) == 0) - { - auto rel_result = VFSPath::Parse(mesh_path + ws_len); - if (rel_result.Succeeded()) - { - // Read existing meta or create a new one - auto meta_result = ZEngine::Core::VFS::MetaFileIO::Read(*vfs, rel_result.Value()); - ZEngine::Core::VFS::MetaFileData meta = meta_result.Succeeded() ? meta_result.Value() : ZEngine::Core::VFS::MetaFileData{}; - - // Store source path, artifact path, importer - ZEngine::Helpers::secure_strncpy(meta.SourcePath, sizeof(meta.SourcePath), self->m_path_buf.c_str(), sizeof(meta.SourcePath) - 1); - ZEngine::Helpers::secure_strncpy(meta.ArtifactPath, sizeof(meta.ArtifactPath), mesh_path, sizeof(meta.ArtifactPath) - 1); - ZEngine::Helpers::secure_strncpy(meta.ImporterName, sizeof(meta.ImporterName), "GltfImporter/AssimpImporter", sizeof(meta.ImporterName) - 1); - - ZEngine::Core::VFS::MetaFileIO::Write(*vfs, rel_result.Value(), meta); - } - } - } - - // If triggered by viewport drag-drop, add the mesh instance to the scene - if (self->m_add_to_scene && mesh_path) - { - ZEngine::Importers::AssetCodec::AssetMeshFileHeader header{}; - if (ZEngine::Importers::AssetCodec::ReadAssetMeshFileHeader(mesh_path, header)) - { - auto* app = reinterpret_cast(self->ParentLayer->CurrentApp); - auto* scene = app ? reinterpret_cast(app->CurrentScene) : nullptr; - if (scene) - { - char iname_buf[256] = {}; - if (self->m_instance_name[0]) - secure_strncpy(iname_buf, sizeof(iname_buf), self->m_instance_name, sizeof(iname_buf) - 1); - else - { - auto pr = VFSPath::Parse(self->m_path_buf.c_str()); - if (pr.Succeeded()) - { - auto stem = pr.Value().Stem(); - snprintf(iname_buf, sizeof(iname_buf), "%.*s", (int) stem.Length, stem.Data); - } - } - cstring iname = iname_buf; - // AddMeshInstance is seqlock-protected — safe from this background thread. - // Actor creation (ECS, not thread-safe) is deferred to TriggerScan on the main thread. - uint32_t render_id = scene->AddMeshInstance(header.Id, iname); - self->m_pending_actor.uuid = header.Id; - self->m_pending_actor.render_id = render_id; - self->m_pending_actor.valid = true; - ZEngine::Helpers::secure_strncpy(self->m_pending_actor.name, sizeof(self->m_pending_actor.name), iname, ZEngine::Helpers::secure_strlen(iname)); - } - } - self->m_add_to_scene = false; - self->m_instance_name[0] = '\0'; - } - - self->PushLog("Completed", kGreen); - char fn_buf[256] = {}; - { - auto pr = VFSPath::Parse(self->m_path_buf.c_str()); - if (pr.Succeeded()) - { - auto fn = pr.Value().Filename(); - snprintf(fn_buf, sizeof(fn_buf), "%.*s", (int) fn.Length, fn.Data); - } - } - self->PushHistory(fn_buf, true, "Completed"); - } - else - { - self->m_add_to_scene = false; - self->m_instance_name[0] = '\0'; - self->PushLog("Import failed — no mesh output", kRed); - char fn_buf2[256] = {}; - { - auto pr = VFSPath::Parse(self->m_path_buf.c_str()); - if (pr.Succeeded()) - { - auto fn = pr.Value().Filename(); - snprintf(fn_buf2, sizeof(fn_buf2), "%.*s", (int) fn.Length, fn.Data); - } - } - self->PushHistory(fn_buf2, false, "No mesh output"); - } - - self->m_progress.value.store(1.0f); - self->m_state.value.store(ImporterState::Idle); - ZEngine::Core::MainThreadScheduler::Post(self, [](void* ctx) { reinterpret_cast(ctx)->TriggerScan(); }); - } - - void AssetImporterUIComponent::OnImportProgress(void* ctx, float pct) - { - auto* self = reinterpret_cast(ctx); - static constexpr float kWhite[] = {0.8f, 0.8f, 0.8f, 1.0f}; - char msg[128]; - snprintf(msg, sizeof(msg), "Processing… %.0f%%", pct * 100.0f); - self->PushLog(msg, kWhite); - self->m_progress.value.store(pct); - } - - void AssetImporterUIComponent::OnImportError(void* ctx, std::string_view err) - { - auto* self = reinterpret_cast(ctx); - static constexpr float kRed[] = {1.0f, 0.3f, 0.3f, 1.0f}; - char msg[512]; - snprintf(msg, sizeof(msg), "Error: %.*s", static_cast(err.size()), err.data()); - self->PushLog(msg, kRed); - char fn_buf[256] = {}; - { - auto pr = VFSPath::Parse(self->m_path_buf.c_str()); - if (pr.Succeeded()) - { - auto fn = pr.Value().Filename(); - snprintf(fn_buf, sizeof(fn_buf), "%.*s", (int) fn.Length, fn.Data); - } - } - self->PushHistory(fn_buf, false, msg); - self->m_state.value.store(ImporterState::Idle); - ZEngine::Core::MainThreadScheduler::Post(self, [](void* ctx) { reinterpret_cast(ctx)->TriggerScan(); }); - } - - void AssetImporterUIComponent::OnImportLog(void* ctx, std::string_view msg) - { - auto* self = reinterpret_cast(ctx); - static constexpr float kWhite[] = {0.8f, 0.8f, 0.8f, 1.0f}; - char buf[256]; - snprintf(buf, sizeof(buf), "%.*s", static_cast(msg.size()), msg.data()); - self->PushLog(buf, kWhite); - } - - void AssetImporterUIComponent::RenderIdle() - { - if (ImGui::Button("+ Import File", ImVec2(-1, 0))) - BrowseFile(); - - ImGui::TextDisabled("or drag a 3D file here"); - - if (ImGui::BeginDragDropTarget()) - { - if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("CONTENT_BROWSER_FILE_DRAG_OP")) - { - char buf[1024] = {}; - ZEngine::Helpers::secure_memcpy(buf, sizeof(buf), payload->Data, payload->DataSize); - auto pr = VFSPath::Parse(buf); - if (pr.Succeeded()) - { - auto ext = pr.Value().Extension(); - if (ext.Equals(".glb") || ext.Equals(".gltf") || ext.Equals(".fbx") || ext.Equals(".obj")) - { - m_path_buf.clear(); - m_path_buf.append(buf); - if (m_same_settings) - StartImport(); - else - m_state.value.store(ImporterState::Options); - } - } - } - ImGui::EndDragDropTarget(); - } - - if (m_history_count > 0) - { - ImGui::Separator(); - ImGui::TextDisabled("Recent Imports"); - for (int i = m_history_count - 1; i >= 0; --i) - { - const auto& h = m_history[i]; - if (h.Success) - ImGui::TextColored({0.3f, 1.0f, 0.4f, 1.0f}, "[OK] %s", h.Name); - else - { - ImGui::TextColored({1.0f, 0.3f, 0.3f, 1.0f}, "[ERR] %s", h.Name); - ImGui::SameLine(); - ImGui::TextDisabled("(%s)", h.Message); - } - } - } - } - - void AssetImporterUIComponent::RenderOptions() - { - char fn_buf[256] = {}; - { - auto pr = VFSPath::Parse(m_path_buf.c_str()); - if (pr.Succeeded()) - { - auto fn = pr.Value().Filename(); - snprintf(fn_buf, sizeof(fn_buf), "%.*s", (int) fn.Length, fn.Data); - } - } - ImGui::TextUnformatted(fn_buf); - ImGui::SameLine(ImGui::GetContentRegionAvail().x + ImGui::GetCursorPosX() - 22.0f); - if (ImGui::SmallButton("X")) - { - m_path_buf.clear(); - m_state.value.store(ImporterState::Idle); - return; - } - - ImGui::Separator(); - - if (ImGui::BeginTabBar("##import_tabs")) - { - // General tab - if (ImGui::BeginTabItem("General")) - { - ImGui::Checkbox("Use Source Name", &m_use_source_name); - if (!m_use_source_name) - { - ImGui::SetNextItemWidth(-1.f); - ImGui::InputText("Asset Name", m_instance_name, sizeof(m_instance_name)); - } - else - { - ImGui::BeginDisabled(true); - ImGui::SetNextItemWidth(-1.f); - ImGui::InputText("Asset Name", fn_buf, sizeof(fn_buf)); - ImGui::EndDisabled(); - } - ImGui::Spacing(); - ImGui::SetNextItemWidth(100.f); - ImGui::DragFloat("Uniform Scale", &m_scale, 0.01f, 0.001f, 100.f, "%.3f"); - ImGui::SetNextItemWidth(100.f); - static constexpr cstring kAxes[] = {"Y-Up", "Z-Up"}; - ImGui::Combo("Axis Up", &m_axis_index, kAxes, 2); - ImGui::EndTabItem(); - } - - // Mesh tab - if (ImGui::BeginTabItem("Mesh")) - { - static constexpr cstring kNormals[] = {"Off", "Flat", "Smooth"}; - ImGui::SetNextItemWidth(120.f); - ImGui::Combo("Normals", &m_normals_mode, kNormals, 3); - ImGui::Checkbox("Merge Identical Vertices", &m_merge_vertices); - ImGui::Checkbox("Flip UVs", &m_flip_uvs); - ImGui::BeginDisabled(true); - static bool s_keep_sections = false; - ImGui::Checkbox("Keep Sections Separate", &s_keep_sections); - if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) - ImGui::SetTooltip("Not yet supported"); - ImGui::EndDisabled(); - ImGui::EndTabItem(); - } - - // Material tab - if (ImGui::BeginTabItem("Material")) - { - ImGui::Checkbox("Import Materials", &m_import_materials); - ImGui::Checkbox("Import Textures", &m_import_textures); - ImGui::EndTabItem(); - } - - // Animation tab — placeholder, all disabled - if (ImGui::BeginTabItem("Animation")) - { - ImGui::BeginDisabled(true); - static bool s_import_anims = true, s_only_anims = false, s_bone_tracks = true; - ImGui::Checkbox("Import Animations", &s_import_anims); - ImGui::Checkbox("Import Only Animations", &s_only_anims); - ImGui::Checkbox("Import Bone Tracks", &s_bone_tracks); - ImGui::EndDisabled(); - ImGui::Spacing(); - ImGui::TextDisabled("Animation import requires skeletal mesh support."); - ImGui::EndTabItem(); - } - - // LOD tab — placeholder, all disabled - if (ImGui::BeginTabItem("LOD")) - { - ImGui::BeginDisabled(true); - static bool s_import_lods = false; - static int s_max_lods = 4; - ImGui::Checkbox("Import LODs", &s_import_lods); - ImGui::SetNextItemWidth(120.f); - ImGui::SliderInt("Max LOD Count", &s_max_lods, 1, 8); - ImGui::EndDisabled(); - ImGui::Spacing(); - ImGui::TextDisabled("LOD support requires virtual geometry streaming."); - ImGui::EndTabItem(); - } - - ImGui::EndTabBar(); - } - - ImGui::Separator(); - ImGui::Checkbox("Use same settings for subsequent files", &m_same_settings); - ImGui::Spacing(); - - if (ImGui::Button("Import All", ImVec2(-168.f, 0))) - StartImport(); - ImGui::SameLine(); - ImGui::BeginDisabled(true); - ImGui::Button("Preview...", ImVec2(-88.f, 0)); - if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) - ImGui::SetTooltip("Not yet supported"); - ImGui::EndDisabled(); - ImGui::SameLine(); - if (ImGui::Button("Cancel", ImVec2(-1, 0))) - { - m_path_buf.clear(); - m_state.value.store(ImporterState::Idle); - } - } - - void AssetImporterUIComponent::RenderImporting() - { - char fn_buf[256] = {}; - { - auto pr = VFSPath::Parse(m_path_buf.c_str()); - if (pr.Succeeded()) - { - auto fn = pr.Value().Filename(); - snprintf(fn_buf, sizeof(fn_buf), "%.*s", (int) fn.Length, fn.Data); - } - } - ImGui::Text("Importing %s", fn_buf); - ImGui::ProgressBar(m_progress.value.load(), ImVec2(-1, 0)); - - ImGui::Separator(); - - ImGui::BeginChild("##imp_log", ImVec2(0, 0), false, ImGuiWindowFlags_HorizontalScrollbar); - { - std::lock_guard lock(m_log_mutex); - int count = m_log_count < kLogMax ? m_log_count : kLogMax; - int start = (m_log_count >= kLogMax) ? m_log_head : 0; - for (int i = 0; i < count; ++i) - { - const auto& e = m_log[(start + i) % kLogMax]; - ImGui::TextColored({e.Color[0], e.Color[1], e.Color[2], e.Color[3]}, "> %s", e.Text); - } - if (m_scroll_log) - { - ImGui::SetScrollHereY(1.0f); - m_scroll_log = false; - } - } - ImGui::EndChild(); - } - - void AssetImporterUIComponent::Render(ZEngine::Rendering::Renderers::GraphicRenderer* const, ZEngine::Hardwares::CommandBuffer* const) - { - if (!ParentLayer || !ParentLayer->CurrentApp) - return; - - auto* app = reinterpret_cast(ParentLayer->CurrentApp); - if (!app || !app->Configuration->ShowImporter) - return; - - if (app->Configuration->FocusImporter) - { - ImGui::SetNextWindowFocus(); - app->Configuration->FocusImporter = false; - } - - if (!ImGui::Begin(Name, &app->Configuration->ShowImporter, ImGuiWindowFlags_NoCollapse)) - { - ImGui::End(); - return; - } - - // Consume viewport drag-drop: auto-import and add mesh to scene when done - if (app->Configuration->PendingImportPath[0] != '\0' && m_state.value.load() == ImporterState::Idle) - { - m_path_buf.clear(); - m_path_buf.append(app->Configuration->PendingImportPath); - secure_strncpy(m_instance_name, sizeof(m_instance_name), app->Configuration->PendingImportName, sizeof(m_instance_name) - 1); - app->Configuration->PendingImportPath[0] = '\0'; - app->Configuration->PendingImportName[0] = '\0'; - m_add_to_scene = true; - StartImport(); - } - - switch (m_state.value.load()) - { - case ImporterState::Idle: - RenderIdle(); - break; - case ImporterState::Options: - RenderOptions(); - break; - case ImporterState::Importing: - RenderImporting(); - break; - } - - ImGui::End(); - } -} // namespace Tetragrama::Components diff --git a/Tetragrama/Components/AssetImporterUIComponent.h b/Tetragrama/Components/AssetImporterUIComponent.h deleted file mode 100644 index f6f0bd0f6..000000000 --- a/Tetragrama/Components/AssetImporterUIComponent.h +++ /dev/null @@ -1,113 +0,0 @@ -#pragma once -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace Tetragrama::Components -{ - enum class ImporterState : uint8_t - { - Idle = 0, - Options = 1, - Importing = 2, - }; - - class AssetImporterUIComponent : public UIComponent - { - public: - AssetImporterUIComponent() = default; - ~AssetImporterUIComponent() override = default; - - ZEngine::Core::Memory::ArenaAllocator LocalArena = {}; - ZEngine::Core::Memory::ArenaAllocator LocalStringArena = {}; - ZEngine::Core::Memory::ArenaAllocator GltfImporterArena = {}; // 64 MB scratch for GltfImporter - ZEngine::Core::Memory::ArenaAllocator AssimpImporterArena = {}; // 350 MB scratch for AssimpImporter - - void Initialize(Layers::ImguiLayer* parent = nullptr, cstring name = "Asset Importer", bool visibility = true, bool closed = false) override; - void Update(ZEngine::Core::TimeStep dt) override; - virtual void Render(ZEngine::Rendering::Renderers::GraphicRenderer* const renderer, ZEngine::Hardwares::CommandBuffer* const command_buffer) override; - void TriggerScan(); // main-thread only - - private: - PaddedAtomic m_state{}; // default = Idle (0) - PaddedAtomic m_progress{}; - - // Selected file - ZEngine::Core::Containers::String m_path_buf = {}; - bool m_add_to_scene = false; // import was triggered by viewport drag-drop - char m_instance_name[256] = {}; - - // Pending Actor creation — set by OnImportFileComplete (background thread), - // consumed by TriggerScan (main thread). Avoids calling ECS from a worker. - struct PendingActor - { - uuids::uuid uuid = {}; - uint32_t render_id = UINT32_MAX; - char name[128] = {}; - bool valid = false; - } m_pending_actor = {}; - - // Import settings (shown in Options state) - float m_scale = 1.0f; - int m_axis_index = 0; // 0 = Y-Up, 1 = Z-Up - int m_normals_mode = 1; // 0 = Off, 1 = Flat, 2 = Smooth - bool m_flip_uvs = false; - bool m_merge_vertices = true; - bool m_import_materials = true; - bool m_import_textures = true; - bool m_use_source_name = true; - bool m_same_settings = false; - int m_active_tab = 0; - - // Compact import log (Importing state — ring buffer) - struct LogEntry - { - char Text[256] = {}; - float Color[4] = {0.8f, 0.8f, 0.8f, 1.0f}; - }; - static constexpr int kLogMax = 64; - LogEntry m_log[kLogMax] = {}; - int m_log_head = 0; - int m_log_count = 0; - std::mutex m_log_mutex; - bool m_scroll_log = false; - - // Recent imports history (Idle state) - struct HistoryEntry - { - char Name[256] = {}; - char Message[256] = {}; - bool Success = false; - }; - static constexpr int kHistMax = 16; - HistoryEntry m_history[kHistMax] = {}; - int m_history_count = 0; - - // Importers — allocated from parent arena in Initialize() - ZEngine::Importers::GltfImporter* m_gltf_importer = nullptr; - ZEngine::Importers::FbxImporter* m_fbx_importer = nullptr; - ZEngine::Importers::AssimpImporter* m_assimp_importer = nullptr; - - void PushLog(cstring text, const float color[4]); - void PushHistory(cstring name, bool success, cstring msg); - void StartImport(); - std::future BrowseFileAsync(); - void BrowseFile(); - void RenderIdle(); - void RenderOptions(); - void RenderImporting(); - - static void OnImportFileComplete(void* ctx, ZEngine::Core::Containers::ArrayView outputs); - static void OnImportProgress(void* ctx, float pct); - static void OnImportError(void* ctx, std::string_view msg); - static void OnImportLog(void* ctx, std::string_view msg); - }; -} // namespace Tetragrama::Components diff --git a/Tetragrama/Components/ContentBrowserIcons.h b/Tetragrama/Components/ContentBrowserIcons.h deleted file mode 100644 index 8ffdf8e66..000000000 --- a/Tetragrama/Components/ContentBrowserIcons.h +++ /dev/null @@ -1,273 +0,0 @@ -#pragma once -#include -#include - -namespace Tetragrama::Components -{ - enum class ContentIconType - { - Folder, - Texture, // .png .jpg .jpeg .tga .bmp .dds .hdr .exr - Mesh, // .obj .fbx .gltf .glb .ply .stl .zemesh - Shader, // .glsl .vert .frag .geom .comp .hlsl .tesc .tese .rgen .rchit .rmiss - Material, // .mat .zemat - Audio, // .wav .mp3 .ogg .flac .aiff - Scene, // .scene .zescene .level - Config, // .json .yaml .yml .xml .toml .ini .cfg - CppSource, // .cpp .cc .cxx .c .h .hpp .hxx .inl - Meta, // .meta - Generic, - }; - - inline ContentIconType GetContentIconType(bool is_dir, const ZEngine::Core::VFS::VFSPathComponent& ext) - { - if (is_dir) - return ContentIconType::Folder; - if (ext.Empty()) - return ContentIconType::Generic; - - if (ext.Equals(".png") || ext.Equals(".jpg") || ext.Equals(".jpeg") || ext.Equals(".tga") || ext.Equals(".bmp") || ext.Equals(".dds") || ext.Equals(".hdr") || ext.Equals(".exr")) - return ContentIconType::Texture; - - if (ext.Equals(".obj") || ext.Equals(".fbx") || ext.Equals(".gltf") || ext.Equals(".glb") || ext.Equals(".ply") || ext.Equals(".stl") || ext.Equals(".zemesh")) - return ContentIconType::Mesh; - - if (ext.Equals(".glsl") || ext.Equals(".vert") || ext.Equals(".frag") || ext.Equals(".geom") || ext.Equals(".comp") || ext.Equals(".hlsl") || ext.Equals(".tesc") || ext.Equals(".tese") || ext.Equals(".rgen") || ext.Equals(".rchit") || ext.Equals(".rmiss")) - return ContentIconType::Shader; - - if (ext.Equals(".mat") || ext.Equals(".zemat")) - return ContentIconType::Material; - - if (ext.Equals(".wav") || ext.Equals(".mp3") || ext.Equals(".ogg") || ext.Equals(".flac") || ext.Equals(".aiff")) - return ContentIconType::Audio; - - if (ext.Equals(".scene") || ext.Equals(".zescene") || ext.Equals(".level")) - return ContentIconType::Scene; - - if (ext.Equals(".json") || ext.Equals(".yaml") || ext.Equals(".yml") || ext.Equals(".xml") || ext.Equals(".toml") || ext.Equals(".ini") || ext.Equals(".cfg")) - return ContentIconType::Config; - - if (ext.Equals(".cpp") || ext.Equals(".cc") || ext.Equals(".cxx") || ext.Equals(".c") || ext.Equals(".h") || ext.Equals(".hpp") || ext.Equals(".hxx") || ext.Equals(".inl")) - return ContentIconType::CppSource; - - if (ext.Equals(".meta")) - return ContentIconType::Meta; - - return ContentIconType::Generic; - } - - // Draws a flat/minimal content-browser icon into dl. - // ixo : top-left of the icon area (same as the caller's ixo) - // ic : icon size (sz * 0.85f) - // dark_theme : true for dark editor backgrounds - // - // When a per-asset thumbnail is available, call dl->AddImage() instead and - // skip this function. This icon acts as the no-thumbnail fallback. - inline void DrawContentIcon(ImDrawList* dl, ImVec2 ixo, float ic, ContentIconType type, bool dark_theme) - { - if (type == ContentIconType::Folder) - { - const ImU32 body = dark_theme ? IM_COL32(200, 175, 100, 255) : IM_COL32(185, 155, 75, 255); - const ImU32 tab = dark_theme ? IM_COL32(220, 195, 120, 255) : IM_COL32(205, 175, 95, 255); - const float tw = ic * 0.42f; - const float th = ic * 0.14f; - const float by = ixo.y + th; - dl->AddRectFilled({ixo.x, ixo.y}, {ixo.x + tw, by + 1.0f}, tab, 2.0f); - dl->AddRectFilled({ixo.x, by}, {ixo.x + ic, ixo.y + ic * 0.92f}, body, 2.0f); - return; - } - - ImU32 body_col, fold_col, sym_col; - switch (type) - { - case ContentIconType::Texture: - body_col = dark_theme ? IM_COL32(230, 130, 50, 255) : IM_COL32(210, 110, 35, 255); - fold_col = dark_theme ? IM_COL32(180, 100, 30, 255) : IM_COL32(165, 85, 20, 255); - sym_col = dark_theme ? IM_COL32(255, 230, 190, 255) : IM_COL32(100, 55, 10, 255); - break; - case ContentIconType::Mesh: - body_col = dark_theme ? IM_COL32(80, 150, 230, 255) : IM_COL32(55, 125, 210, 255); - fold_col = dark_theme ? IM_COL32(50, 110, 190, 255) : IM_COL32(35, 95, 170, 255); - sym_col = dark_theme ? IM_COL32(205, 225, 255, 255) : IM_COL32(20, 60, 130, 255); - break; - case ContentIconType::Shader: - body_col = dark_theme ? IM_COL32(155, 85, 220, 255) : IM_COL32(135, 65, 200, 255); - fold_col = dark_theme ? IM_COL32(115, 55, 180, 255) : IM_COL32(100, 40, 165, 255); - sym_col = dark_theme ? IM_COL32(230, 205, 255, 255) : IM_COL32(75, 25, 140, 255); - break; - case ContentIconType::Material: - body_col = dark_theme ? IM_COL32(50, 195, 175, 255) : IM_COL32(35, 170, 155, 255); - fold_col = dark_theme ? IM_COL32(30, 150, 135, 255) : IM_COL32(20, 130, 118, 255); - sym_col = dark_theme ? IM_COL32(190, 255, 245, 255) : IM_COL32(10, 85, 75, 255); - break; - case ContentIconType::Audio: - body_col = dark_theme ? IM_COL32(75, 200, 105, 255) : IM_COL32(50, 175, 80, 255); - fold_col = dark_theme ? IM_COL32(45, 160, 75, 255) : IM_COL32(30, 140, 55, 255); - sym_col = dark_theme ? IM_COL32(200, 255, 215, 255) : IM_COL32(15, 90, 35, 255); - break; - case ContentIconType::Scene: - body_col = dark_theme ? IM_COL32(230, 200, 50, 255) : IM_COL32(205, 175, 30, 255); - fold_col = dark_theme ? IM_COL32(185, 160, 30, 255) : IM_COL32(165, 140, 15, 255); - sym_col = dark_theme ? IM_COL32(255, 245, 185, 255) : IM_COL32(105, 85, 5, 255); - break; - case ContentIconType::Config: - body_col = dark_theme ? IM_COL32(125, 160, 205, 255) : IM_COL32(95, 130, 180, 255); - fold_col = dark_theme ? IM_COL32(90, 120, 170, 255) : IM_COL32(65, 98, 150, 255); - sym_col = dark_theme ? IM_COL32(220, 230, 248, 255) : IM_COL32(40, 65, 115, 255); - break; - case ContentIconType::CppSource: - body_col = dark_theme ? IM_COL32(220, 80, 80, 255) : IM_COL32(195, 55, 55, 255); - fold_col = dark_theme ? IM_COL32(175, 50, 50, 255) : IM_COL32(155, 30, 30, 255); - sym_col = dark_theme ? IM_COL32(255, 210, 210, 255) : IM_COL32(105, 15, 15, 255); - break; - case ContentIconType::Meta: - body_col = dark_theme ? IM_COL32(130, 120, 200, 255) : IM_COL32(105, 95, 175, 255); - fold_col = dark_theme ? IM_COL32(95, 85, 160, 255) : IM_COL32(75, 65, 140, 255); - sym_col = dark_theme ? IM_COL32(220, 215, 255, 255) : IM_COL32(50, 40, 120, 255); - break; - default: // Generic - body_col = dark_theme ? IM_COL32(160, 160, 165, 255) : IM_COL32(130, 130, 135, 255); - fold_col = dark_theme ? IM_COL32(120, 120, 125, 255) : IM_COL32(95, 95, 100, 255); - sym_col = dark_theme ? IM_COL32(220, 220, 225, 255) : IM_COL32(55, 55, 60, 255); - break; - } - - // Document base with dog-ear fold - const float f = ic * 0.22f; - const ImVec2 tl = {ixo.x + ic * 0.08f, ixo.y + ic * 0.04f}; - const ImVec2 br = {ixo.x + ic * 0.92f, ixo.y + ic * 0.96f}; - dl->AddRectFilled({tl.x, tl.y + f}, br, body_col, 2.0f); - dl->AddRectFilled(tl, {br.x - f, tl.y + f}, body_col); - dl->AddTriangleFilled({br.x - f, tl.y}, {br.x, tl.y + f}, {br.x - f, tl.y + f}, fold_col); - - // Symbol drawn in the body area below the fold line - const float cx = (tl.x + br.x) * 0.5f; - const float area_t = tl.y + f; - const float area_b = br.y; - const float cy = (area_t + area_b) * 0.5f; - const float area_h = area_b - area_t; - const float area_w = br.x - tl.x; - - switch (type) - { - case ContentIconType::Texture: - { - // Sun circle + mountain triangle - const float sr = area_h * 0.16f; - const float scx = cx - area_w * 0.12f; - const float scy = area_t + area_h * 0.28f; - dl->AddCircleFilled({scx, scy}, sr, sym_col, 10); - const float my = area_t + area_h * 0.82f; - dl->AddTriangleFilled({tl.x + area_w * 0.10f, my}, {tl.x + area_w * 0.90f, my}, {cx, area_t + area_h * 0.42f}, sym_col); - break; - } - case ContentIconType::Mesh: - { - // Isometric cube: flat-top hexagon + 3 inner Y-lines - const float r = area_h * 0.30f; - const float rx = r * 0.866f; // cos(30) - const float ry = r * 0.5f; // sin(30) - const ImVec2 vtop = {cx, cy - r}; - const ImVec2 vtr = {cx + rx, cy - ry}; - const ImVec2 vbr = {cx + rx, cy + ry}; - const ImVec2 vbot = {cx, cy + r}; - const ImVec2 vbl = {cx - rx, cy + ry}; - const ImVec2 vtl = {cx - rx, cy - ry}; - const ImVec2 vctr = {cx, cy}; - const float lw = 1.5f; - ImVec2 hex[6] = {vtop, vtr, vbr, vbot, vbl, vtl}; - dl->AddPolyline(hex, 6, sym_col, ImDrawFlags_Closed, lw); - dl->AddLine(vtop, vctr, sym_col, lw); - dl->AddLine(vbl, vctr, sym_col, lw); - dl->AddLine(vbr, vctr, sym_col, lw); - break; - } - case ContentIconType::Shader: - { - // GPU render triangle - const float th = area_h * 0.55f; - const float tw = area_w * 0.58f; - dl->AddTriangleFilled({cx, cy - th * 0.5f}, {cx + tw * 0.5f, cy + th * 0.5f}, {cx - tw * 0.5f, cy + th * 0.5f}, sym_col); - break; - } - case ContentIconType::Material: - { - // Sphere with specular highlight - const float r = area_h * 0.32f; - const ImU32 hi = IM_COL32(255, 255, 255, dark_theme ? 55 : 80); - dl->AddCircleFilled({cx, cy}, r, sym_col, 20); - dl->AddCircleFilled({cx - r * 0.28f, cy - r * 0.28f}, r * 0.35f, hi, 12); - break; - } - case ContentIconType::Audio: - { - // Quarter note: filled head + vertical stem - const float nr = area_h * 0.18f; - const float nx = cx - nr * 0.3f; - const float ny = area_t + area_h * 0.72f; - dl->AddCircleFilled({nx, ny}, nr, sym_col, 10); - dl->AddLine({nx + nr * 0.9f, ny - nr * 0.2f}, {nx + nr * 0.9f, area_t + area_h * 0.18f}, sym_col, 1.5f); - break; - } - case ContentIconType::Scene: - { - // Ground line + building rectangle + sun circle - const float gy = area_t + area_h * 0.78f; - dl->AddLine({tl.x + area_w * 0.06f, gy}, {br.x - area_w * 0.06f, gy}, sym_col, 1.5f); - const float bw = area_w * 0.32f; - const float bh = area_h * 0.40f; - dl->AddRectFilled({cx - bw * 0.5f, gy - bh}, {cx + bw * 0.5f, gy}, sym_col, 1.5f); - const float sr = area_h * 0.10f; - const float scx = tl.x + area_w * 0.76f; - const float scy = area_t + area_h * 0.22f; - dl->AddCircleFilled({scx, scy}, sr, sym_col, 8); - break; - } - case ContentIconType::Config: - { - // Three horizontal data lines (full / short / full) - const float lh = 1.5f; - const float gap = area_h * 0.20f; - const float lx0 = tl.x + area_w * 0.10f; - const float lx1 = br.x - area_w * 0.10f; - float ly = area_t + area_h * 0.22f; - for (int i = 0; i < 3; ++i) - { - const float rx1 = (i == 1) ? lx0 + (lx1 - lx0) * 0.62f : lx1; - dl->AddRectFilled({lx0, ly}, {rx1, ly + lh + 0.5f}, sym_col); - ly += gap; - } - break; - } - case ContentIconType::Meta: - { - // Info "i": dot above a vertical bar - const float bar_w = area_w * 0.12f; - const float bar_h = area_h * 0.38f; - const float dot_r = bar_w * 1.1f; - const float bar_x0 = cx - bar_w * 0.5f; - const float bar_y0 = cy - bar_h * 0.1f; - dl->AddCircleFilled({cx, cy - bar_h * 0.52f - dot_r}, dot_r, sym_col, 10); - dl->AddRectFilled({bar_x0, bar_y0}, {bar_x0 + bar_w, bar_y0 + bar_h}, sym_col, 1.0f); - break; - } - case ContentIconType::CppSource: - { - // < > angle brackets - const float aw = area_w * 0.22f; - const float ah = area_h * 0.38f; - const float lw = 2.0f; - const float lx = cx - area_w * 0.32f; - const float rx = cx + area_w * 0.32f; - dl->AddLine({lx + aw, cy - ah * 0.5f}, {lx, cy}, sym_col, lw); - dl->AddLine({lx, cy}, {lx + aw, cy + ah * 0.5f}, sym_col, lw); - dl->AddLine({rx - aw, cy - ah * 0.5f}, {rx, cy}, sym_col, lw); - dl->AddLine({rx, cy}, {rx - aw, cy + ah * 0.5f}, sym_col, lw); - break; - } - default: - break; - } - } - -} // namespace Tetragrama::Components diff --git a/Tetragrama/Components/DemoUIComponent.h b/Tetragrama/Components/DemoUIComponent.h deleted file mode 100644 index 86f06750c..000000000 --- a/Tetragrama/Components/DemoUIComponent.h +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once -#include -#include - -namespace Tetragrama::Components -{ - class DemoUIComponent : public UIComponent - { - public: - DemoUIComponent() {} - virtual ~DemoUIComponent() = default; - - void Initialize(Layers::ImguiLayer* parent = nullptr, const char* name = "DemoUIComponent", bool visibility = true, bool closed = false) override - { - UIComponent::Initialize(parent, name, visibility, closed); - } - - virtual void Render(ZEngine::Rendering::Renderers::GraphicRenderer* const renderer, ZEngine::Hardwares::CommandBuffer* const command_buffer) override - { - ImGui::ShowDemoWindow(&m_is_open); - } - - void Update(ZEngine::Core::TimeStep dt) override {} - - private: - bool m_is_open{true}; - }; -} // namespace Tetragrama::Components diff --git a/Tetragrama/Components/DockspaceUIComponent.cpp b/Tetragrama/Components/DockspaceUIComponent.cpp deleted file mode 100644 index 4a9132b0c..000000000 --- a/Tetragrama/Components/DockspaceUIComponent.cpp +++ /dev/null @@ -1,1264 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace fs = std::filesystem; -using ZEngine::Core::VFS::VFSPath; - -using namespace ZEngine::Helpers; - -namespace Tetragrama::Components -{ - char DockspaceUIComponent::s_save_as_input_buffer[1024] = {0}; - float DockspaceUIComponent::s_editor_scene_serializer_progress = 0.0f; - - static bool s_is_scene_loading = false; - static char s_scene_serializer_log[DEFAULT_STR_BUFFER] = {0}; - static ImVec4 s_scene_serializer_log_color = {1, 1, 1, 1}; - - static constexpr cstring kLayoutsDir = "ZodiacEngine/Settings/Layouts"; - - // Serialize the live dock tree as a pre-order sequence of SPLIT/LEAF lines. - // Format: SPLIT or LEAF [window_name ...] - static void WriteNodeToFile(FILE* f, ImGuiDockNode* node) - { - if (!node) - return; - if (node->IsSplitNode()) - { - float ratio = 0.5f; - if (node->SplitAxis == ImGuiAxis_X && node->Size.x > 0.0f) - ratio = node->ChildNodes[0]->Size.x / node->Size.x; - else if (node->SplitAxis == ImGuiAxis_Y && node->Size.y > 0.0f) - ratio = node->ChildNodes[0]->Size.y / node->Size.y; - fprintf(f, "SPLIT %c %f\n", node->SplitAxis == ImGuiAxis_X ? 'X' : 'Y', ratio); - WriteNodeToFile(f, node->ChildNodes[0]); - WriteNodeToFile(f, node->ChildNodes[1]); - } - else - { - fprintf(f, "LEAF"); - for (int i = 0; i < node->Windows.Size; ++i) - { - cstring name = node->Windows[i]->Name; - if (name[0] != '#') // skip internal/popup windows - fprintf(f, " %s", name); - } - fprintf(f, "\n"); - } - } - - // Replay one node from the file into the given dock node ID. - // Returns true if a record was consumed. - static bool LoadNodeFromFile(FILE* f, ImGuiID node_id) - { - char line[512]; - while (fgets(line, sizeof(line), f)) - { - if (line[0] == '\n' || line[0] == '\0') - continue; - - if (strncmp(line, "SPLIT", 5) == 0) - { - char axis = 'X'; - float ratio = 0.5f; - sscanf(line, "SPLIT %c %f", &axis, &ratio); - ImGuiID child0 = 0, child1 = 0; - ImGuiDir dir = (axis == 'X') ? ImGuiDir_Left : ImGuiDir_Up; - ImGui::DockBuilderSplitNode(node_id, dir, ratio, &child0, &child1); - LoadNodeFromFile(f, child0); - LoadNodeFromFile(f, child1); - return true; - } - if (strncmp(line, "LEAF", 4) == 0) - { - char* nl = strchr(line, '\n'); - if (nl) - *nl = '\0'; - char* tok = strtok(line + 4, " "); - while (tok) - { - if (tok[0] != '\0') - ImGui::DockBuilderDockWindow(tok, node_id); - tok = strtok(nullptr, " "); - } - return true; - } - } - return false; - } - - static ImGuiID BuildLayout_Default(ImGuiID root) - { - ImGuiID main = root; - ImGuiID left = ImGui::DockBuilderSplitNode(main, ImGuiDir_Left, 0.18f, nullptr, &main); - ImGuiID right = ImGui::DockBuilderSplitNode(main, ImGuiDir_Right, 0.22f, nullptr, &main); - ImGuiID down = ImGui::DockBuilderSplitNode(main, ImGuiDir_Down, 0.25f, nullptr, &main); - ImGuiID down_right = ImGui::DockBuilderSplitNode(down, ImGuiDir_Right, 0.60f, nullptr, &down); - ImGui::DockBuilderDockWindow("Hierarchy", left); - ImGui::DockBuilderDockWindow("Inspector", right); - ImGui::DockBuilderDockWindow("Scene", main); - ImGui::DockBuilderDockWindow("Project", down_right); - ImGui::DockBuilderDockWindow("Console", down); - ImGui::DockBuilderDockWindow("Asset Importer", down); - return down; - } - - ImGuiID DockspaceUIComponent::ApplyBuiltinLayout(ImGuiID root, EditorLayout /*layout*/) - { - ImGui::DockBuilderRemoveNode(root); - ImGui::DockBuilderAddNode(root, ImGuiDockNodeFlags_None); - ImGui::DockBuilderSetNodeSize(root, ImGui::GetMainViewport()->Size); - ImGuiID console_dock = BuildLayout_Default(root); - ImGui::DockBuilderFinish(root); - return console_dock; - } - - void DockspaceUIComponent::ScanCustomLayouts() - { - m_custom_layout_count = 0; - std::error_code ec; - if (!fs::exists(kLayoutsDir, ec)) - return; - for (auto& entry : fs::directory_iterator(kLayoutsDir, ec)) - { - if (m_custom_layout_count >= kMaxCustomLayouts) - break; - if (entry.path().extension() != ".zlayout") - continue; - auto& slot = m_custom_layouts[m_custom_layout_count++]; - auto stem = entry.path().stem().string(); - auto path = entry.path().string(); - ZEngine::Helpers::secure_strncpy(slot.Name, sizeof(slot.Name), stem.c_str(), stem.size()); - ZEngine::Helpers::secure_strncpy(slot.Path, sizeof(slot.Path), path.c_str(), path.size()); - } - } - - void DockspaceUIComponent::SaveCurrentLayout(cstring name) - { - if (!ParentLayer->DockspaceId) - return; - ImGuiDockNode* root = ImGui::DockBuilderGetNode(ParentLayer->DockspaceId); - if (!root) - return; - - std::error_code ec; - fs::create_directories(kLayoutsDir, ec); - - auto path = fmt::format("{}/{}.zlayout", kLayoutsDir, name); - if (FILE* f = fopen(path.c_str(), "w")) - { - WriteNodeToFile(f, root); - fclose(f); - } - ScanCustomLayouts(); - } - - void DockspaceUIComponent::DeleteCustomLayout(int index) - { - if (index < 0 || index >= m_custom_layout_count) - return; - std::error_code ec; - fs::remove(m_custom_layouts[index].Path, ec); - ScanCustomLayouts(); - } - - void DockspaceUIComponent::RenderLayoutMenu() - { - if (!ImGui::BeginMenu("Layout")) - return; - - for (auto& bl : kBuiltinLayouts) - { - bool active = (m_active_layout == bl.Id && m_custom_layout_count == 0); - if (ImGui::MenuItem(bl.Name, nullptr, active)) - { - m_pending_layout = bl.Id; - m_layout_dirty = true; - } - } - - if (m_custom_layout_count > 0) - { - ImGui::Separator(); - for (int i = 0; i < m_custom_layout_count; ++i) - { - if (ImGui::MenuItem(m_custom_layouts[i].Name)) - { - // Defer to next frame — DockBuilder must run before DockSpace, not inside a menu - ZEngine::Helpers::secure_strncpy(m_pending_layout_path, sizeof(m_pending_layout_path), m_custom_layouts[i].Path, sizeof(m_pending_layout_path) - 1); - } - } - } - - ImGui::Separator(); - if (ImGui::MenuItem("Save Current Layout...")) - { - m_save_layout_buf[0] = '\0'; - m_open_save_layout = true; - } - if (m_custom_layout_count > 0 && ImGui::MenuItem("Manage Layouts...")) - m_open_manage_layouts = true; - - ImGui::EndMenu(); - } - - void DockspaceUIComponent::RenderSaveLayoutModal() - { - if (m_open_save_layout) - { - ImGui::OpenPopup("Save Layout##modal"); - m_open_save_layout = false; - } - - ImVec2 center = ImGui::GetMainViewport()->GetCenter(); - ImGui::SetNextWindowPos(center, ImGuiCond_Always, {0.5f, 0.5f}); - ImGui::SetNextWindowSize({340.0f, 0.0f}, ImGuiCond_Always); - if (ImGui::BeginPopupModal("Save Layout##modal", nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove)) - { - ImGui::TextUnformatted("Layout name:"); - ImGui::SetNextItemWidth(-1.0f); - ImGui::InputText("##layout_name", m_save_layout_buf, sizeof(m_save_layout_buf)); - - bool empty = (m_save_layout_buf[0] == '\0'); - if (empty) - ImGui::BeginDisabled(); - if (ImGui::Button("Save", {100.0f, 0.0f})) - { - SaveCurrentLayout(m_save_layout_buf); - ImGui::CloseCurrentPopup(); - } - if (empty) - ImGui::EndDisabled(); - ImGui::SameLine(); - if (ImGui::Button("Cancel", {100.0f, 0.0f})) - ImGui::CloseCurrentPopup(); - - ImGui::EndPopup(); - } - } - - void DockspaceUIComponent::RenderManageLayoutsModal() - { - if (m_open_manage_layouts) - { - ImGui::OpenPopup("Manage Layouts##modal"); - m_open_manage_layouts = false; - } - - ImVec2 center = ImGui::GetMainViewport()->GetCenter(); - ImGui::SetNextWindowPos(center, ImGuiCond_Always, {0.5f, 0.5f}); - ImGui::SetNextWindowSize({360.0f, 0.0f}, ImGuiCond_Always); - if (ImGui::BeginPopupModal("Manage Layouts##modal", nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove)) - { - if (m_custom_layout_count == 0) - { - ImGui::TextDisabled("No custom layouts saved yet."); - } - else - { - int to_delete = -1; - for (int i = 0; i < m_custom_layout_count; ++i) - { - ImGui::TextUnformatted(m_custom_layouts[i].Name); - ImGui::SameLine(ImGui::GetContentRegionAvail().x - 56.0f); - ImGui::PushID(i); - if (ImGui::SmallButton("Delete")) - to_delete = i; - ImGui::PopID(); - } - if (to_delete >= 0) - DeleteCustomLayout(to_delete); - } - - ImGui::Separator(); - if (ImGui::Button("Close", {100.0f, 0.0f})) - ImGui::CloseCurrentPopup(); - - ImGui::EndPopup(); - } - } - - DockspaceUIComponent::DockspaceUIComponent() {} - - DockspaceUIComponent::~DockspaceUIComponent() {} - - void DockspaceUIComponent::Initialize(Layers::ImguiLayer* parent, const char* name, bool visibility, bool closed) - { - UIComponent::Initialize(parent, name, visibility, closed); - - parent->LocalArena.CreateSubArena(ZMega(32), &LocalArena); - - m_editor_serializer = ZPushStructCtor(parent->Arena, Serializers::EditorSceneSerializer); - - m_editor_serializer->Initialize(parent->Arena); - - m_dockspace_node_flag = ImGuiDockNodeFlags_NoWindowMenuButton | static_cast(ImGuiDockNodeFlags_PassthruCentralNode); - m_window_flags = ImGuiWindowFlags_MenuBar | ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoNavFocus; - - auto app = reinterpret_cast(ParentLayer->CurrentApp); - m_editor_serializer->Context = app; - - char editor_serializer_output_buf[MAX_FILE_PATH_COUNT] = {}; - VFSPath::Parse(app->Configuration->ScenePath.c_str()).Value().ResolveNative(app->Configuration->WorkingSpacePath.c_str(), editor_serializer_output_buf, sizeof(editor_serializer_output_buf)); - std::string editor_serializer_default_output = editor_serializer_output_buf; - - m_editor_serializer->SetDefaultOutput(editor_serializer_default_output); - m_editor_serializer->SetOnProgressCallback(OnEditorSceneSerializerProgress); - m_editor_serializer->SetOnCompleteCallback(OnEditorSceneSerializerComplete); - m_editor_serializer->SetOnDeserializeCompleteCallback(OnEditorSceneSerializerDeserializeComplete); - m_editor_serializer->SetOnLogCallback(OnEditorSceneSerializerLog); - m_editor_serializer->SetOnErrorCallback(OnEditorSceneSerializerError); - - ApplyTheme(m_active_theme); - ScanCustomLayouts(); - } - - void DockspaceUIComponent::Update(ZEngine::Core::TimeStep dt) {} - - void DockspaceUIComponent::Render(ZEngine::Rendering::Renderers::GraphicRenderer* const renderer, ZEngine::Hardwares::CommandBuffer* const command_buffer) - { - static constexpr float kStatusBarHeight = 28.0f; - const ImGuiViewport* viewport = ImGui::GetMainViewport(); - ImGui::SetNextWindowPos(viewport->Pos); - ImGui::SetNextWindowSize({viewport->Size.x, viewport->Size.y - kStatusBarHeight}); - ImGui::SetNextWindowViewport(viewport->ID); - ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); - ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); - m_window_flags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove; - m_window_flags |= ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoNavFocus; - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); - - ImGui::Begin(Name, (CanBeClosed ? &CanBeClosed : NULL), m_window_flags); - - ImGui::PopStyleVar(3); - - if (ImGui::GetIO().ConfigFlags & ImGuiConfigFlags_DockingEnable) - { - // Dock space - const auto window_id = ImGui::GetID(Name); - static ImGuiID s_console_dock_id = 0; - ParentLayer->DockspaceId = window_id; - ParentLayer->ConsoleDockId = s_console_dock_id; - - if (m_pending_layout_path[0] != '\0') - { - // Apply a saved .zlayout — runs before DockSpace so DockBuilder is in a clean state - if (FILE* lf = fopen(m_pending_layout_path, "r")) - { - ImGui::DockBuilderRemoveNode(window_id); - ImGui::DockBuilderAddNode(window_id, ImGuiDockNodeFlags_None); - ImGui::DockBuilderSetNodeSize(window_id, ImGui::GetMainViewport()->Size); - - // DockBuilderRemoveNode clears DockId for currently-active windows but - // NOT for inactive ones (e.g. Console/Project when their toggle is off). - // Those windows still hold the old stale DockId pointing to removed nodes. - // When they next appear via the status bar button, ImGui tries to attach - // them to those dead nodes and crashes in its internal table code. - // Fix: clear DockId for all managed windows — both live and persisted settings. - static constexpr cstring kManaged[] = {"Hierarchy", "Inspector", "Scene", "Project", "Console"}; - for (cstring wname : kManaged) - { - if (ImGuiWindow* w = ImGui::FindWindowByName(wname)) - w->DockId = 0; - if (ImGuiWindowSettings* ws = ImGui::FindWindowSettingsByID(ImHashStr(wname))) - ws->DockId = 0; - } - - LoadNodeFromFile(lf, window_id); // re-assigns via DockBuilderDockWindow for windows in layout - ImGui::DockBuilderFinish(window_id); - fclose(lf); - } - m_pending_layout_path[0] = '\0'; - } - else if (m_layout_dirty || !ImGui::DockBuilderGetNode(window_id)) - { - EditorLayout target = m_layout_dirty ? m_pending_layout : EditorLayout::Default; - s_console_dock_id = ApplyBuiltinLayout(window_id, target); - ParentLayer->ConsoleDockId = s_console_dock_id; - m_active_layout = target; - m_layout_dirty = false; - } - - ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); - ImGui::DockSpace(window_id, ImVec2(0.0f, 0.0f), m_dockspace_node_flag); - ImGui::PopStyleVar(); - } - - RenderMenuBar(); - - RenderLoadScene(); - RenderSaveScene(); - RenderSaveSceneAs(); - RenderSaveLayoutModal(); - RenderManageLayoutsModal(); - - RenderEngineSettingsWindow(); - RenderMemoryProfilerWindow(); - - RenderExitPopup(); - - ImGui::End(); - } - - void DockspaceUIComponent::RenderLoadScene() - { - if (!s_is_scene_loading) - { - return; - } - - const char* str_id = "Loading Scene"; - ImGui::OpenPopup(str_id); - ImVec2 center = ImGui::GetMainViewport()->GetCenter(); - ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); - ImGui::SetNextWindowSize(ImVec2(700, 100), ImGuiCond_Always); - - if (ImGui::BeginPopupModal(str_id, NULL, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize)) - { - // Calculate position for the progress bar - ImVec2 wind_size = ImGui::GetWindowSize(); - ImVec2 reg_available = ImGui::GetContentRegionAvail(); - ImVec2 progress_bar_pos = ImVec2((wind_size.x - reg_available.x) * 0.5f, (wind_size.y - reg_available.y)); - - // Display the progress bar - ImGui::SetCursorPos(progress_bar_pos); - ImGui::ProgressBar(s_editor_scene_serializer_progress, ImVec2(reg_available.x, 20.0f), " "); - - ImGui::PushFont(ImGui::GetIO().Fonts->Fonts[0]); - ImGui::SetCursorPos(ImVec2(10, wind_size.y - 30)); - ImGui::TextColored(s_scene_serializer_log_color, "%s", s_scene_serializer_log); - ImGui::PopFont(); - - ImGui::EndPopup(); - } - } - - void DockspaceUIComponent::RenderSaveScene() - { - if (!m_open_save_scene) - { - return; - } - - ImVec2 center = ImGui::GetMainViewport()->GetCenter(); - ImGui::SetNextWindowPos(center, ImGuiCond_Always, ImVec2(0.5f, 0.5f)); - ImGui::SetNextWindowSize(ImVec2(700, 70), ImGuiCond_Always); - - if (!ImGui::Begin("Saving Scene", NULL, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse)) - { - ImGui::End(); - return; - } - - // Calculate position for the progress bar - ImVec2 progress_bar_pos = ImVec2((ImGui::GetWindowSize().x - ImGui::GetContentRegionAvail().x) * 0.5f, (ImGui::GetWindowSize().y - ImGui::GetContentRegionAvail().y)); - - // Display the progress bar - ImGui::SetCursorPos(progress_bar_pos); - ImGui::ProgressBar(s_editor_scene_serializer_progress, ImVec2(ImGui::GetContentRegionAvail().x, 10.0f), " "); - ImGui::End(); - - if (m_editor_serializer->IsSerializing()) - { - return; - } - - if (m_pending_shutdown) - { - m_open_save_scene = false; - ZEngine::Core::MainThreadScheduler::Post(this, [](void* context) { reinterpret_cast(context)->OnExitAsync(); }); - } - else if (m_request_save_scene_ui_close) - { - m_open_save_scene = false; - m_request_save_scene_ui_close = false; - } - } - - void DockspaceUIComponent::RenderSaveSceneAs() - { - if (!m_open_save_scene_as) - { - std::string_view buffer_view = s_save_as_input_buffer; - if (!buffer_view.empty()) - { - ResetSaveAsBuffers(); - } - return; - } - - const char* str_id = "Scene name"; - ImGui::OpenPopup(str_id); - ImVec2 center = ImGui::GetMainViewport()->GetCenter(); - ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); - ImGui::SetNextWindowSize(ImVec2(500, 100), ImGuiCond_Always); - - bool is_save_button_enabled = !std::string_view(s_save_as_input_buffer).empty(); - - if (ImGui::BeginPopupModal(str_id, NULL, ImGuiWindowFlags_AlwaysAutoResize)) - { - ImGui::PushItemWidth(485); - ImGui::InputText("##SaveAsUI", s_save_as_input_buffer, IM_ARRAYSIZE(s_save_as_input_buffer)); - ImGui::PopItemWidth(); - - ImGui::Separator(); - - ImGui::SetCursorPosX(ImGui::GetWindowSize().x - 180); - ImGui::SetCursorPosY(ImGui::GetWindowSize().y - ImGui::GetFrameHeightWithSpacing() - 5); - - if (!is_save_button_enabled) - { - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.5f, 0.5f, 0.5f, 1.0f)); // Grayed out color - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.5f, 0.5f, 0.5f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.5f, 0.5f, 0.5f, 1.0f)); - } - - if (ImGui::Button("Save", ImVec2(80, 0)) && is_save_button_enabled) - { - auto app = reinterpret_cast(ParentLayer->CurrentApp); - auto editor_scene = reinterpret_cast(app->CurrentScene); - editor_scene->Name = s_save_as_input_buffer; - m_editor_serializer->Serialize(editor_scene); - - m_open_save_scene_as = false; - m_open_save_scene = true; - m_request_save_scene_ui_close = true; - ImGui::CloseCurrentPopup(); - } - - if (!is_save_button_enabled) - { - ImGui::PopStyleColor(3); // Pop the grayed out color - } - - ImGui::SameLine(); - if (ImGui::Button("Cancel", ImVec2(80, 0))) - { - m_open_save_scene_as = false; - ImGui::CloseCurrentPopup(); - } - - ImGui::EndPopup(); - } - } - - void DockspaceUIComponent::RenderExitPopup() - { - if (!m_open_exit) - { - return; - } - - m_pending_shutdown = true; - - auto app = reinterpret_cast(ParentLayer->CurrentApp); - auto current_scene = reinterpret_cast(app->CurrentScene); - - if (!current_scene->HasPendingChange()) - { - ZEngine::Core::MainThreadScheduler::Post(this, [](void* context) { reinterpret_cast(context)->OnExitAsync(); }); - } - - const char* str_id = "Saving changes to the current Scene ?"; - ImGui::OpenPopup(str_id); - // Always center this window when appearing - ImVec2 center = ImGui::GetMainViewport()->GetCenter(); - ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); - - if (ImGui::BeginPopupModal(str_id, NULL, ImGuiWindowFlags_AlwaysAutoResize)) - { - ImGui::Text("%s", fmt::format("You have unsaved changes for your current scene : {}", current_scene->Name).c_str()); - ImGui::Separator(); - - if (ImGui::Button("Save", ImVec2(120, 0))) - { - m_open_save_scene = true; - m_open_exit = false; - ImGui::CloseCurrentPopup(); - - m_editor_serializer->Serialize(current_scene); - } - ImGui::SetItemDefaultFocus(); - ImGui::SameLine(); - if (ImGui::Button("Don't save", ImVec2(120, 0))) - { - ImGui::CloseCurrentPopup(); - ZEngine::Core::MainThreadScheduler::Post(this, [](void* context) { reinterpret_cast(context)->OnExitAsync(); }); - } - ImGui::SetItemDefaultFocus(); - ImGui::SameLine(); - if (ImGui::Button("Cancel", ImVec2(120, 0))) - { - m_open_exit = false; - m_pending_shutdown = false; - ImGui::CloseCurrentPopup(); - } - ImGui::EndPopup(); - } - } - - void DockspaceUIComponent::DrawSettingsIcon(ImDrawList* dl, ImVec2 pos, SettingsPageId id, bool selected) - { - const float sz = 14.0f; - ImU32 col; - switch (id) - { - case SettingsPageId::Theme: - col = selected ? IM_COL32(170, 110, 255, 255) : IM_COL32(120, 70, 180, 160); - break; - case SettingsPageId::Grid: - col = selected ? IM_COL32(55, 210, 200, 255) : IM_COL32(35, 130, 125, 160); - break; - case SettingsPageId::Renderer: - col = selected ? IM_COL32(255, 165, 50, 255) : IM_COL32(170, 100, 30, 160); - break; - default: - col = selected ? IM_COL32(220, 220, 220, 255) : IM_COL32(140, 140, 140, 180); - break; - } - - switch (id) - { - case SettingsPageId::Grid: - for (int i = 1; i <= 3; ++i) - { - float tx = pos.x + sz * i / 4.0f; - float ty = pos.y + sz * i / 4.0f; - dl->AddLine({tx, pos.y}, {tx, pos.y + sz}, col, 1.2f); - dl->AddLine({pos.x, ty}, {pos.x + sz, ty}, col, 1.2f); - } - break; - case SettingsPageId::Renderer: - dl->AddRect(pos, {pos.x + sz, pos.y + sz}, col, 2.0f, 0, 1.5f); - dl->AddCircleFilled({pos.x + sz * 0.5f, pos.y + sz * 0.5f}, sz * 0.22f, col, 8); - break; - case SettingsPageId::Theme: - { - const float cx = pos.x + sz * 0.5f, cy = pos.y + sz * 0.5f; - dl->AddCircleFilled({cx, cy}, sz * 0.28f, col, 12); - for (int r = 0; r < 8; ++r) - { - float a = r * 3.14159f / 4.0f; - float r0 = sz * 0.38f, r1 = sz * 0.50f; - dl->AddLine({cx + cosf(a) * r0, cy + sinf(a) * r0}, {cx + cosf(a) * r1, cy + sinf(a) * r1}, col, 1.2f); - } - break; - } - default: - break; - } - } - - void DockspaceUIComponent::RenderEngineSettingsWindow() - { - if (!m_open_engine_settings) - return; - - static constexpr struct - { - cstring Label; - SettingsPageId Id; - } kPages[] = { - { "Theme", SettingsPageId::Theme}, - { "Grid", SettingsPageId::Grid}, - {"Renderer", SettingsPageId::Renderer}, - }; - static constexpr int kPageCount = static_cast(SettingsPageId::COUNT); - - ImGui::SetNextWindowSize({700, 500}, ImGuiCond_FirstUseEver); - if (!ImGui::Begin("Engine Settings", &m_open_engine_settings)) - { - ImGui::End(); - return; - } - - ImGui::BeginChild("##settings_sidebar", ImVec2(170, 0), true); - ImDrawList* dl = ImGui::GetWindowDrawList(); - for (int i = 0; i < kPageCount; ++i) - { - bool active = (m_active_settings_page == kPages[i].Id); - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 22.0f); - if (ImGui::Selectable(kPages[i].Label, active, ImGuiSelectableFlags_SpanAllColumns, ImVec2(0, 22))) - m_active_settings_page = kPages[i].Id; - // Draw icon after Selectable so it renders on top of the selection highlight - ImVec2 icon_pos = ImGui::GetItemRectMin() + ImVec2{4.0f, 4.0f}; - DrawSettingsIcon(dl, icon_pos, kPages[i].Id, active); - } - ImGui::EndChild(); - - ImGui::SameLine(); - - ImGui::BeginChild("##settings_content", ImVec2(0, 0), false); - switch (m_active_settings_page) - { - case SettingsPageId::Theme: - RenderSettingsContentTheme(); - break; - case SettingsPageId::Grid: - RenderSettingsContentGrid(); - break; - case SettingsPageId::Renderer: - RenderSettingsContentRenderer(); - break; - default: - break; - } - ImGui::EndChild(); - - ImGui::End(); - } - - void DockspaceUIComponent::RenderSettingsContentGrid() - { - if (!ParentLayer || !ParentLayer->CurrentApp) - return; - - auto app = reinterpret_cast(ParentLayer->CurrentApp); - auto* current_scene = reinterpret_cast(app->CurrentScene); - if (!current_scene) - return; - - ImGui::TextUnformatted("Grid"); - ImGui::Separator(); - ImGui::Spacing(); - - auto& cfg = current_scene->Grid; - bool changed = false; - - changed |= ImGui::Checkbox("Show Grid", &cfg.Enabled); - ImGui::Spacing(); - changed |= ImGui::SliderFloat("Cell Size", &cfg.CellSize, 0.001f, 1.0f, "%.4f", ImGuiSliderFlags_Logarithmic); - changed |= ImGui::SliderFloat("Fade Radius", &cfg.FadeRadius, 10.0f, 2000.0f, "%.1f"); - changed |= ImGui::SliderFloat("Fade Strength", &cfg.FadeStrength, 0.1f, 2.0f, "%.2f"); - changed |= ImGui::SliderFloat("Line Width", &cfg.LineWidth, 0.5f, 4.0f, "%.2f"); - changed |= ImGui::SliderInt("Max LOD", &cfg.MaxLOD, 1, 6); - changed |= ImGui::SliderFloat("Ground Y", &cfg.GroundY, -100.0f, 100.0f, "%.2f"); - ImGui::Spacing(); - ImGui::Separator(); - ImGui::Spacing(); - changed |= ImGui::ColorEdit4("Thin Lines", cfg.ColorThin); - changed |= ImGui::ColorEdit4("Thick Lines", cfg.ColorThick); - changed |= ImGui::ColorEdit4("X Axis", cfg.ColorXAxis); - changed |= ImGui::ColorEdit4("Z Axis", cfg.ColorZAxis); - - if (changed) - { - current_scene->GridDirty[0].value.store(true, std::memory_order_release); - current_scene->GridDirty[1].value.store(true, std::memory_order_release); - current_scene->GridDirty[2].value.store(true, std::memory_order_release); - } - } - - void DockspaceUIComponent::RenderSettingsContentRenderer() - { - ImGui::TextUnformatted("Renderer"); - ImGui::Separator(); - ImGui::Spacing(); - ImGui::TextDisabled("No renderer settings yet."); - } - - void DockspaceUIComponent::RenderMemoryProfilerWindow() - { - if (!m_open_memory_profiler) - return; - - ZEngine::Profiling::MemoryProfiler::Update(); - - ImGui::SetNextWindowSize({520, 500}, ImGuiCond_FirstUseEver); - if (!ImGui::Begin("Memory Profiler", &m_open_memory_profiler)) - { - ImGui::End(); - return; - } - - auto scratch = ZGetScratch(&LocalArena); - ZEngine::Core::Containers::Array stats; - stats.init(scratch.Arena, 32); - ZEngine::Profiling::MemoryProfiler::GetStats(stats); - - if (stats.size() == 0) - { - ImGui::TextDisabled("No arenas tracked — ensure ZENGINE_PROFILING=1."); - ZReleaseScratch(scratch); - ImGui::End(); - return; - } - - auto fmt_bytes = [](uint64_t b, char* buf, size_t n) { - if (b >= 1024u * 1024u) - snprintf(buf, n, "%.1f MB", b / (1024.0 * 1024.0)); - else if (b >= 1024u) - snprintf(buf, n, "%.1f KB", b / 1024.0); - else - snprintf(buf, n, "%u B", static_cast(b)); - }; - - // Resize history array if arena count changed - uint32_t n = static_cast(stats.size()); - if (n > kMaxArenas) - n = kMaxArenas; - if (m_arena_history_count != n) - { - for (uint32_t i = m_arena_history_count; i < n; ++i) - m_arena_history[i] = {}; - m_arena_history_count = n; - } - - // Append this frame's samples - for (uint32_t i = 0; i < n; ++i) - { - auto& h = m_arena_history[i]; - float val_mb = static_cast(stats[i].CurrentOffset) / (1024.0f * 1024.0f); - h.samples[h.head] = val_mb; - h.head = (h.head + 1) % kMemHistorySize; - if (h.count < kMemHistorySize) - ++h.count; - } - - // Header - uint64_t total_used = 0, total_cap = 0; - for (uint32_t i = 0; i < n; ++i) - { - total_used += stats[i].CurrentOffset; - total_cap += stats[i].Capacity; - } - char t_used[32], t_cap[32]; - fmt_bytes(total_used, t_used, sizeof(t_used)); - fmt_bytes(total_cap, t_cap, sizeof(t_cap)); - ImGui::Text("Total %s / %s", t_used, t_cap); - ImGui::SameLine(); - if (ImGui::SmallButton("Reset Peaks")) - ZEngine::Profiling::MemoryProfiler::ResetPeaks(); - ImGui::Separator(); - - const float graph_h = 45.0f; - const float avail_w = ImGui::GetContentRegionAvail().x; - - for (uint32_t i = 0; i < n; ++i) - { - const auto& s = stats[i]; - auto& h = m_arena_history[i]; - float frac = s.Capacity > 0 ? static_cast(s.CurrentOffset) / s.Capacity : 0.0f; - float cap_mb = static_cast(s.Capacity) / (1024.0f * 1024.0f); - - // Color by usage level - ImVec4 line_col = frac > 0.85f ? ImVec4{0.90f, 0.25f, 0.25f, 1.0f} : frac > 0.60f ? ImVec4{0.90f, 0.70f, 0.10f, 1.0f} : ImVec4{0.30f, 0.75f, 0.45f, 1.0f}; - - // Label row: name + numbers - char used_s[32], peak_s[32], cap_s[32]; - fmt_bytes(s.CurrentOffset, used_s, sizeof(used_s)); - fmt_bytes(s.PeakOffset, peak_s, sizeof(peak_s)); - fmt_bytes(s.Capacity, cap_s, sizeof(cap_s)); - - ImGui::PushStyleColor(ImGuiCol_Text, line_col); - ImGui::TextUnformatted(s.Name ? s.Name : "?"); - ImGui::PopStyleColor(); - ImGui::SameLine(); - ImGui::TextDisabled("%s / %s (peak %s)", used_s, cap_s, peak_s); - - // Graph - char graph_id[64]; - snprintf(graph_id, sizeof(graph_id), "##graph_%u", i); - char overlay[32]; - snprintf(overlay, sizeof(overlay), "%.1f%%", frac * 100.0f); - ImGui::PushStyleColor(ImGuiCol_PlotLines, line_col); - ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4{line_col.x, line_col.y, line_col.z, 0.08f}); - ImGui::PlotLines(graph_id, h.samples, kMemHistorySize, h.head, overlay, 0.0f, cap_mb > 0.0f ? cap_mb : 1.0f, ImVec2(avail_w, graph_h)); - ImGui::PopStyleColor(2); - - ImGui::Spacing(); - } - - ZReleaseScratch(scratch); - ImGui::End(); - } - - void DockspaceUIComponent::ApplyTheme(ThemeId theme) - { - // Propagate to EditorConfiguration so other components can read it. - if (ParentLayer && ParentLayer->CurrentApp) - { - auto app = reinterpret_cast(ParentLayer->CurrentApp); - if (app->Configuration) - app->Configuration->DarkTheme = (theme == ThemeId::Dark); - } - - auto& colors = ImGui::GetStyle().Colors; - if (theme == ThemeId::Dark) - { - ImGui::StyleColorsDark(); - colors[ImGuiCol_WindowBg] = {0.10f, 0.105f, 0.11f, 1.0f}; - colors[ImGuiCol_Header] = {0.20f, 0.205f, 0.21f, 1.0f}; - colors[ImGuiCol_HeaderHovered] = {0.30f, 0.305f, 0.31f, 1.0f}; - colors[ImGuiCol_HeaderActive] = {0.15f, 0.150f, 0.15f, 1.0f}; - colors[ImGuiCol_Button] = {0.20f, 0.205f, 0.21f, 1.0f}; - colors[ImGuiCol_ButtonHovered] = {0.30f, 0.305f, 0.31f, 1.0f}; - colors[ImGuiCol_ButtonActive] = {0.15f, 0.150f, 0.15f, 1.0f}; - colors[ImGuiCol_FrameBg] = {0.20f, 0.205f, 0.21f, 1.0f}; - colors[ImGuiCol_FrameBgHovered] = {0.30f, 0.305f, 0.31f, 1.0f}; - colors[ImGuiCol_FrameBgActive] = {0.15f, 0.150f, 0.15f, 1.0f}; - colors[ImGuiCol_Tab] = {0.15f, 0.150f, 0.15f, 1.0f}; - colors[ImGuiCol_TabHovered] = {0.38f, 0.380f, 0.38f, 1.0f}; - colors[ImGuiCol_TabActive] = {0.28f, 0.280f, 0.28f, 1.0f}; - colors[ImGuiCol_TabUnfocused] = {0.15f, 0.150f, 0.15f, 1.0f}; - colors[ImGuiCol_TabUnfocusedActive] = {0.20f, 0.205f, 0.21f, 1.0f}; - colors[ImGuiCol_TitleBg] = {0.15f, 0.150f, 0.15f, 1.0f}; - colors[ImGuiCol_TitleBgActive] = {0.15f, 0.150f, 0.15f, 1.0f}; - colors[ImGuiCol_TitleBgCollapsed] = {0.15f, 0.150f, 0.15f, 1.0f}; - colors[ImGuiCol_DockingPreview] = {0.20f, 0.205f, 0.21f, 0.5f}; - colors[ImGuiCol_SeparatorHovered] = {1.00f, 1.000f, 1.00f, 0.5f}; - colors[ImGuiCol_SeparatorActive] = {1.00f, 1.000f, 1.00f, 0.5f}; - colors[ImGuiCol_CheckMark] = {1.00f, 1.000f, 1.00f, 1.0f}; - colors[ImGuiCol_PlotHistogram] = {1.00f, 1.000f, 1.00f, 1.0f}; - } - else - { - // Option C: pure neutral charcoal — all grays have R=G=B so zero colour cast. - ImGui::StyleColorsLight(); - - // -- palette (all R=G=B) -- - static constexpr ImVec4 kText = {0.110f, 0.110f, 0.110f, 1.0f}; // #1C1C1C - static constexpr ImVec4 kInk = {0.110f, 0.110f, 0.110f, 1.0f}; // #1C1C1C title active - static constexpr ImVec4 kDark = {0.235f, 0.235f, 0.235f, 1.0f}; // #3C3C3C pressed / marks - static constexpr ImVec4 kMid = {0.361f, 0.361f, 0.361f, 1.0f}; // #5C5C5C hover grabs - static constexpr ImVec4 kBorder = {0.878f, 0.878f, 0.878f, 1.0f}; // #E0E0E0 - static constexpr ImVec4 kBgAlt = {0.941f, 0.941f, 0.941f, 1.0f}; // #F0F0F0 - static constexpr ImVec4 kBg = {0.973f, 0.973f, 0.973f, 1.0f}; // #F8F8F8 - static constexpr ImVec4 kSel = {0.863f, 0.863f, 0.863f, 1.0f}; // #DCDCDC selection fill - static constexpr ImVec4 kWhite = {1.000f, 1.000f, 1.000f, 1.0f}; - - // -- all colors that StyleColorsLight leaves blue-tinted -- - colors[ImGuiCol_Text] = kText; - colors[ImGuiCol_WindowBg] = kBg; - colors[ImGuiCol_ChildBg] = kWhite; - colors[ImGuiCol_PopupBg] = kWhite; - colors[ImGuiCol_Border] = kBorder; - colors[ImGuiCol_FrameBg] = {0.910f, 0.910f, 0.910f, 1.0f}; // #E8E8E8 visible trough - colors[ImGuiCol_FrameBgHovered] = {0.863f, 0.863f, 0.863f, 1.0f}; // slightly darker on hover - colors[ImGuiCol_FrameBgActive] = {0.780f, 0.780f, 0.780f, 1.0f}; - colors[ImGuiCol_TitleBg] = kBgAlt; - colors[ImGuiCol_TitleBgActive] = {0.780f, 0.780f, 0.780f, 1.0f}; // #C7C7C7 — medium gray - colors[ImGuiCol_TitleBgCollapsed] = kBgAlt; - colors[ImGuiCol_MenuBarBg] = kBg; - colors[ImGuiCol_ScrollbarBg] = kBg; - colors[ImGuiCol_ScrollbarGrab] = kBorder; - colors[ImGuiCol_ScrollbarGrabHovered] = kMid; - colors[ImGuiCol_ScrollbarGrabActive] = kDark; - colors[ImGuiCol_CheckMark] = kDark; - colors[ImGuiCol_SliderGrab] = kMid; - colors[ImGuiCol_SliderGrabActive] = kDark; - colors[ImGuiCol_Button] = kBorder; - colors[ImGuiCol_ButtonHovered] = kBgAlt; - colors[ImGuiCol_ButtonActive] = kDark; - colors[ImGuiCol_Header] = kSel; - colors[ImGuiCol_HeaderHovered] = kBgAlt; - colors[ImGuiCol_HeaderActive] = kBorder; - colors[ImGuiCol_ResizeGrip] = {kBorder.x, kBorder.y, kBorder.z, 0.5f}; - colors[ImGuiCol_ResizeGripHovered] = kMid; - colors[ImGuiCol_ResizeGripActive] = kDark; - colors[ImGuiCol_Separator] = kBorder; - colors[ImGuiCol_SeparatorHovered] = kMid; - colors[ImGuiCol_SeparatorActive] = kDark; - colors[ImGuiCol_Tab] = kBgAlt; - colors[ImGuiCol_TabHovered] = kSel; - colors[ImGuiCol_TabActive] = kWhite; - colors[ImGuiCol_TabUnfocused] = kBgAlt; - colors[ImGuiCol_TabUnfocusedActive] = kBg; - colors[ImGuiCol_NavHighlight] = {kDark.x, kDark.y, kDark.z, 0.7f}; - colors[ImGuiCol_NavWindowingHighlight] = {kDark.x, kDark.y, kDark.z, 0.7f}; - colors[ImGuiCol_NavWindowingDimBg] = {kDark.x, kDark.y, kDark.z, 0.2f}; - colors[ImGuiCol_DockingPreview] = {kDark.x, kDark.y, kDark.z, 0.4f}; - colors[ImGuiCol_TextSelectedBg] = {kSel.x, kSel.y, kSel.z, 0.6f}; - colors[ImGuiCol_PlotLines] = kMid; - colors[ImGuiCol_PlotLinesHovered] = kDark; - colors[ImGuiCol_PlotHistogram] = kMid; - colors[ImGuiCol_PlotHistogramHovered] = kDark; - } - } - - void DockspaceUIComponent::RenderSettingsContentTheme() - { - ImGui::TextUnformatted("Theme"); - ImGui::Separator(); - ImGui::Spacing(); - - auto render_theme_card = [](bool active, cstring label, cstring desc, ImVec4 preview_bg, ImVec4 preview_text) { - ImGui::PushStyleColor(ImGuiCol_ChildBg, preview_bg); - ImGui::PushStyleColor(ImGuiCol_Border, active ? ImVec4{0.30f, 0.55f, 1.0f, 1.0f} : ImVec4{0.50f, 0.50f, 0.50f, 0.40f}); - ImGui::PushStyleVar(ImGuiStyleVar_ChildBorderSize, active ? 2.0f : 1.0f); - ImGui::BeginChild(label, ImVec2(160, 70), true); - ImGui::PopStyleColor(2); - ImGui::PopStyleVar(); - ImGui::Spacing(); - ImGui::PushStyleColor(ImGuiCol_Text, preview_text); - ImGui::TextUnformatted(label); - ImGui::PopStyleColor(); - ImGui::TextDisabled("%s", desc); - ImGui::EndChild(); - }; - - { - bool active = (m_active_theme == ThemeId::Dark); - render_theme_card(active, "Dark", "Dark background", ImVec4{0.15f, 0.15f, 0.17f, 1.0f}, ImVec4{0.90f, 0.90f, 0.90f, 1.0f}); - if (ImGui::IsItemClicked() && !active) - { - m_active_theme = ThemeId::Dark; - ApplyTheme(ThemeId::Dark); - } - } - ImGui::SameLine(); - { - bool active = (m_active_theme == ThemeId::Light); - render_theme_card(active, "Light", "Light background", ImVec4{0.94f, 0.94f, 0.94f, 1.0f}, ImVec4{0.12f, 0.12f, 0.12f, 1.0f}); - if (ImGui::IsItemClicked() && !active) - { - m_active_theme = ThemeId::Light; - ApplyTheme(ThemeId::Light); - } - } - } - - void DockspaceUIComponent::ResetSaveAsBuffers() - { - ZEngine::Helpers::secure_memset(s_save_as_input_buffer, 0, IM_ARRAYSIZE(s_save_as_input_buffer), IM_ARRAYSIZE(s_save_as_input_buffer)); - } - - void DockspaceUIComponent::OnEditorSceneSerializerError(void* const, std::string_view msg) - { - ZENGINE_CORE_ERROR("{}", msg) - } - - void DockspaceUIComponent::OnEditorSceneSerializerLog(void* const, std::string_view msg) - { - ZEngine::Helpers::secure_strcpy(s_scene_serializer_log, DEFAULT_STR_BUFFER, msg.data()); - } - - void DockspaceUIComponent::RenderMenuBar() - { - if (ImGui::BeginMenuBar()) - { - if (ImGui::BeginMenu("File")) - { - if (ImGui::MenuItem("New Scene")) - { - ZEngine::Core::MainThreadScheduler::Post(this, [](void* context) { reinterpret_cast(context)->OnNewSceneAsync(); }); - } - - if (ImGui::MenuItem("Open Scene")) - { - ZEngine::Core::MainThreadScheduler::Post(this, [](void* context) { reinterpret_cast(context)->OnOpenSceneAsync(); }); - } - - if (ImGui::MenuItem("Import New Asset...")) - { - auto app_ptr = reinterpret_cast(ParentLayer->CurrentApp); - app_ptr->Configuration->ShowImporter = true; - app_ptr->Configuration->FocusImporter = true; - } - ImGui::Separator(); - - if (ImGui::MenuItem("Save")) - { - m_open_save_scene = true; - m_request_save_scene_ui_close = true; - if (ParentLayer->CurrentApp) - { - auto app = reinterpret_cast(ParentLayer->CurrentApp); - auto current_scene = reinterpret_cast(app->CurrentScene); - m_editor_serializer->Serialize(current_scene); - } - } - - ImGui::MenuItem("Save As...", NULL, &m_open_save_scene_as); - ImGui::Separator(); - - ImGui::MenuItem("Exit", NULL, &m_open_exit); - - ImGui::EndMenu(); - } - - if (ImGui::BeginMenu("Settings")) - { - ImGui::MenuItem("Engine", NULL, &m_open_engine_settings); - ImGui::EndMenu(); - } - - if (ImGui::BeginMenu("Performances")) - { - ImGui::MenuItem("Memory Profiler", NULL, &m_open_memory_profiler); - ImGui::EndMenu(); - } - - RenderLayoutMenu(); - - ImGui::EndMenuBar(); - } - } - - void DockspaceUIComponent::OnEditorSceneSerializerProgress(void* const, float value) - { - s_editor_scene_serializer_progress = value; - } - - void DockspaceUIComponent::OnEditorSceneSerializerComplete(void* const context) - { - auto app = reinterpret_cast(context); - auto current_scene = reinterpret_cast(app->CurrentScene); - current_scene->HasPendingChanges.value.store(false, std::memory_order_release); - } - - void DockspaceUIComponent::OnEditorSceneSerializerDeserializeComplete(void* const context, EditorScene&& scene) - { - auto app = reinterpret_cast(context); - auto current_scene = reinterpret_cast(app->CurrentScene); - - // Todo : Ensure no data race on CurrentScenePtr - app->Configuration->ActiveSceneName.clear(); - app->Configuration->ActiveSceneName.append(scene.Name); - - current_scene->MarkDirty(true); - current_scene->SelectedInstanceId.value.store(-1, std::memory_order_release); - current_scene->Reset(); - current_scene->ExtractAsync(scene); - - current_scene->Name = app->Configuration->ActiveSceneName.c_str(); - - // Copy sky config from deserialized scene; resolve env map filename to absolute path - current_scene->Sky.Mode.init(¤t_scene->LocalArena, scene.Sky.Mode.empty() ? "atmosphere" : scene.Sky.Mode.c_str()); - if (!scene.Sky.EnvironmentMap.empty() && !app->Configuration->EnvironmentMapImportPath.empty()) - { - auto abs_env = fmt::format("{}/{}", app->Configuration->EnvironmentMapImportPath.c_str(), scene.Sky.EnvironmentMap.c_str()); - current_scene->Sky.EnvironmentMap.init(¤t_scene->LocalArena, abs_env.c_str()); - } - current_scene->SkyDirty[0].value.store(true, std::memory_order_release); - current_scene->SkyDirty[1].value.store(true, std::memory_order_release); - current_scene->SkyDirty[2].value.store(true, std::memory_order_release); - - current_scene->MarkDirty(false); - - { - auto msg = fmt::format("Scene {} deserialized successfully", current_scene->Name); - ZEngine::Helpers::secure_strcpy(s_scene_serializer_log, DEFAULT_STR_BUFFER, msg.data()); - - ZENGINE_CORE_INFO("{}", msg.c_str()) - } - - s_is_scene_loading = false; - } - - std::future DockspaceUIComponent::OnNewSceneAsync() - { - co_return; - } - - std::future DockspaceUIComponent::OnOpenSceneAsync() - { - if (ParentLayer && ParentLayer->CurrentApp->CurrentWindow) - { - auto window = ParentLayer->CurrentApp->CurrentWindow; - std::vector filters = {".zescene"}; - std::string scene_filename = co_await window->OpenFileDialogAsync(filters); - - if (!scene_filename.empty()) - { - s_is_scene_loading = true; - m_editor_serializer->Deserialize(scene_filename.c_str()); - } - } - co_return; - } - - std::future DockspaceUIComponent::OnOpenSceneRequestAsync(const char* filename) - { - if (!ZEngine::Helpers::secure_strlen(filename)) - { - co_return; - } - - s_is_scene_loading = true; - m_editor_serializer->Deserialize(filename); - co_return; - } - - std::future DockspaceUIComponent::OnOpenMeshRequestAsync(const char* filename) - { - ZEngine::Importers::AssetCodec::AssetMeshFileHeader header; - if (!ZEngine::Importers::AssetCodec::ReadAssetMeshFileHeader(filename, header)) - co_return; - - // If the mesh isn't in memory (fresh session), ingest it from disk first - // so both the CPU asset registry and the RRM GPU buffers are populated. - if (!ZEngine::Managers::AssetManager::GetAsset(header.Id)) - { - // Phase 1: deserialize mesh. Copy material names to stack before releasing - // the scratch — material names live in scratch.Arena and are invalidated on release. - static constexpr size_t kMaxMaterials = 64; - char mat_names[kMaxMaterials][256] = {}; - size_t mat_count = 0; - - { - auto scratch = ZGetScratch(&LocalArena); - ZEngine::Importers::AssetMesh mesh{}; - ZEngine::Importers::AssetNodeHierarchy hier{}; - ZEngine::Importers::AssetCodec::DeserializeMeshAssetFile(scratch.Arena, filename, mesh, hier); - - mat_count = hier.MaterialNames.size() < kMaxMaterials ? hier.MaterialNames.size() : kMaxMaterials; - for (size_t i = 0; i < mat_count; ++i) - ZEngine::Helpers::secure_strncpy(mat_names[i], sizeof(mat_names[i]), hier.MaterialNames[i].c_str(), sizeof(mat_names[i]) - 1); - - ZEngine::Managers::AssetManager::IngestMesh(std::move(mesh), std::move(hier)); - ZReleaseScratch(scratch); - } - - // Phase 2: load associated .zematerial files in a fresh scratch so mesh and - // material deserializations never compete for the same 1 MB LocalArena. - auto* app_cfg = ParentLayer && ParentLayer->CurrentApp ? reinterpret_cast(ParentLayer->CurrentApp)->Configuration : nullptr; - if (app_cfg) - { - for (size_t i = 0; i < mat_count; ++i) - { - char mat_path[MAX_FILE_PATH_COUNT] = {}; - snprintf(mat_path, sizeof(mat_path), "%s/%s/%s.zematerial", app_cfg->WorkingSpacePath.c_str(), app_cfg->MaterialPath.c_str(), mat_names[i]); - - auto scratch = ZGetScratch(&LocalArena); - ZEngine::Importers::AssetMaterial mat{}; - ZEngine::Importers::AssetCodec::DeserializeMaterialAssetFile(scratch.Arena, mat_path, mat); - if (!mat.MaterialUUID.is_nil()) - ZEngine::Managers::AssetManager::IngestMaterial(std::move(mat)); - ZReleaseScratch(scratch); - } - } - } - - auto app = reinterpret_cast(ParentLayer->CurrentApp); - auto current_scene = reinterpret_cast(app->CurrentScene); - const char* name = strrchr(filename, '/'); - name = name ? name + 1 : filename; - current_scene->SpawnMeshActor(header.Id, name); - - co_return; - } - - std::future DockspaceUIComponent::OnExitAsync() - { - if (ParentLayer) - { - ZEngine::Windows::Events::WindowClosedEvent e{}; - ParentLayer->OnEvent(e); - } - ZENGINE_CORE_WARN("Editor stopped") - co_return; - } -} // namespace Tetragrama::Components diff --git a/Tetragrama/Components/DockspaceUIComponent.h b/Tetragrama/Components/DockspaceUIComponent.h deleted file mode 100644 index 78d33bf73..000000000 --- a/Tetragrama/Components/DockspaceUIComponent.h +++ /dev/null @@ -1,145 +0,0 @@ -#pragma once -#include -#include -#include -#include - -namespace Tetragrama::Components -{ - enum class ThemeId - { - Dark = 0, - Light = 1, - }; - - enum class SettingsPageId - { - Grid = 0, - Renderer = 1, - Theme = 2, - COUNT - }; - - enum class EditorLayout - { - Default = 0, - COUNT - }; - - class DockspaceUIComponent : public UIComponent - { - public: - DockspaceUIComponent(); - virtual ~DockspaceUIComponent(); - - ZEngine::Core::Memory::ArenaAllocator LocalArena = {}; - - void Initialize(Layers::ImguiLayer* parent = nullptr, const char* name = "Dockspace", bool visibility = true, bool closed = false) override; - - void Update(ZEngine::Core::TimeStep dt) override; - virtual void Render(ZEngine::Rendering::Renderers::GraphicRenderer* const renderer, ZEngine::Hardwares::CommandBuffer* const command_buffer) override; - - void RenderMenuBar(); - void ResetSaveAsBuffers(); - void RenderExitPopup(); - - /* - * Performances Menu Windows - */ - void RenderMemoryProfilerWindow(); - - /* - * Engine Settings Window - */ - void RenderEngineSettingsWindow(); - void RenderSettingsContentGrid(); - void RenderSettingsContentRenderer(); - void RenderSettingsContentTheme(); - static void DrawSettingsIcon(ImDrawList* dl, ImVec2 pos, SettingsPageId id, bool selected); - void ApplyTheme(ThemeId theme); - - /* - * Layout Management - */ - void ScanCustomLayouts(); - void SaveCurrentLayout(cstring name); - void DeleteCustomLayout(int index); - ImGuiID ApplyBuiltinLayout(ImGuiID root, EditorLayout layout); - void RenderLayoutMenu(); - void RenderSaveLayoutModal(); - void RenderManageLayoutsModal(); - - /* - * Editor Scene Funcs - */ - void RenderLoadScene(); - void RenderSaveScene(); - void RenderSaveSceneAs(); - static void OnEditorSceneSerializerProgress(void* const, float value); - static void OnEditorSceneSerializerComplete(void* const); - static void OnEditorSceneSerializerDeserializeComplete(void* const, EditorScene&&); - static void OnEditorSceneSerializerError(void* const, std::string_view); - static void OnEditorSceneSerializerLog(void* const, std::string_view); - - std::future OnNewSceneAsync(); - std::future OnOpenSceneAsync(); - std::future OnOpenSceneRequestAsync(const char* filename); - std::future OnOpenMeshRequestAsync(const char* filename); - std::future OnExitAsync(); - - private: - static char s_save_as_input_buffer[1024]; - static float s_editor_scene_serializer_progress; - - private: - bool m_open_engine_settings{false}; - bool m_open_memory_profiler{false}; - - static constexpr int kMemHistorySize = 128; - static constexpr int kMaxArenas = 32; - struct ArenaHistory - { - float samples[kMemHistorySize] = {}; - int head = 0; - uint32_t count = 0; - }; - ArenaHistory m_arena_history[kMaxArenas] = {}; - uint32_t m_arena_history_count = 0; - bool m_open_exit{false}; - ThemeId m_active_theme{ThemeId::Dark}; - bool m_pending_shutdown{false}; - bool m_open_save_scene{false}; - bool m_open_save_scene_as{false}; - bool m_request_save_scene_ui_close{false}; - SettingsPageId m_active_settings_page{SettingsPageId::Theme}; - ImGuiDockNodeFlags m_dockspace_node_flag; - ImGuiWindowFlags m_window_flags; - ZRawPtr(Serializers::EditorSceneSerializer) m_editor_serializer; - - struct BuiltinLayoutDef - { - cstring Name; - EditorLayout Id; - }; - static constexpr BuiltinLayoutDef kBuiltinLayouts[] = { - {"Default", EditorLayout::Default}, - }; - - struct CustomLayoutEntry - { - char Name[128] = {}; - char Path[512] = {}; - }; - static constexpr int kMaxCustomLayouts = 16; - CustomLayoutEntry m_custom_layouts[kMaxCustomLayouts] = {}; - int m_custom_layout_count = 0; - - EditorLayout m_active_layout = EditorLayout::Default; - EditorLayout m_pending_layout = EditorLayout::Default; - bool m_layout_dirty = false; - char m_pending_layout_path[512] = {}; // .zlayout to apply before next DockSpace - bool m_open_save_layout = false; - bool m_open_manage_layouts = false; - char m_save_layout_buf[128] = {}; - }; -} // namespace Tetragrama::Components diff --git a/Tetragrama/Components/HierarchyViewUIComponent.cpp b/Tetragrama/Components/HierarchyViewUIComponent.cpp deleted file mode 100644 index 7eb91d60f..000000000 --- a/Tetragrama/Components/HierarchyViewUIComponent.cpp +++ /dev/null @@ -1,664 +0,0 @@ -// clang-format off -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -// clang-format on - -using namespace ZEngine; -using namespace ZEngine::ECS; -using namespace ZEngine::ECS::Components; -using namespace ZEngine::Core::Maths; -using namespace ZEngine::Windows::Inputs; - -namespace Tetragrama::Components -{ - HierarchyViewUIComponent::HierarchyViewUIComponent() = default; - HierarchyViewUIComponent::~HierarchyViewUIComponent() = default; - - void HierarchyViewUIComponent::Initialize(Layers::ImguiLayer* parent, const char* name, bool visibility, bool closed) - { - UIComponent::Initialize(parent, name, visibility, closed); - parent->LocalArena.CreateSubArena(ZKilo(512), &m_outliner_arena); - m_collapsed.init(&m_outliner_arena, 64); - } - - void HierarchyViewUIComponent::Update(ZEngine::Core::TimeStep /*dt*/) {} - - bool HierarchyViewUIComponent::IsCollapsed(EntityID eid) const - { - for (uint32_t i = 0; i < m_collapsed.size(); ++i) - if (m_collapsed[i] == eid) - return true; - return false; - } - - void HierarchyViewUIComponent::ToggleCollapsed(EntityID eid) - { - for (uint32_t i = 0; i < m_collapsed.size(); ++i) - { - if (m_collapsed[i] == eid) - { - m_collapsed[i] = INVALID_ENTITY; // sentinel slot; IsCollapsed skips it (valid entities have Generation!=0) - return; - } - } - m_collapsed.push(eid); - } - - // Static helpers - - void HierarchyViewUIComponent::DrawArrow(ImDrawList* dl, ImVec2 pos, float row_h, bool expanded, ImU32 color) - { - float cx = pos.x + 8.f; - float cy = pos.y + row_h * 0.5f; - float sz = 5.f; - if (expanded) - dl->AddTriangleFilled({cx - sz, cy - sz * 0.5f}, {cx + sz, cy - sz * 0.5f}, {cx, cy + sz * 0.65f}, color); - else - dl->AddTriangleFilled({cx - sz * 0.5f, cy - sz}, {cx + sz * 0.65f, cy}, {cx - sz * 0.5f, cy + sz}, color); - } - - void HierarchyViewUIComponent::DrawTypeIcon(ImDrawList* dl, ImVec2 pos, float sz, const char* type, bool is_collection) - { - float cx = pos.x + sz * 0.5f; - float cy = pos.y + sz * 0.5f; - - if (is_collection) - { - ImU32 col = ImGui::ColorConvertFloat4ToU32({0.85f, 0.65f, 0.15f, 0.95f}); - float fw = sz * 0.80f; - float fh = sz * 0.65f; - float ix = pos.x + (sz - fw) * 0.5f; - float iy = pos.y + sz - fh; - dl->AddRectFilled({ix, iy + fh * 0.28f}, {ix + fw, iy + fh}, col, 1.5f); - dl->AddRectFilled({ix, iy}, {ix + fw * 0.44f, iy + fh * 0.32f}, col, 1.5f, ImDrawFlags_RoundCornersTopLeft | ImDrawFlags_RoundCornersTopRight); - return; - } - - if (Helpers::secure_strcmp(type, "World") == 0) - { - ImU32 col = ImGui::ColorConvertFloat4ToU32({0.35f, 0.80f, 0.45f, 1.0f}); - float r = sz * 0.40f; - dl->AddCircle({cx, cy}, r, col, 16, 1.2f); - dl->AddLine({cx - r, cy}, {cx + r, cy}, col, 1.0f); - dl->AddLine({cx, cy - r}, {cx, cy + r}, col, 1.0f); - return; - } - - if (Helpers::secure_strcmp(type, "Light") == 0) - { - ImU32 col = ImGui::ColorConvertFloat4ToU32({1.0f, 0.85f, 0.20f, 1.0f}); - float r = sz * 0.20f; - float ray = sz * 0.40f; - float diag = ray * 0.70f; - dl->AddCircleFilled({cx, cy}, r, col, 8); - dl->AddLine({cx, cy - ray}, {cx, cy - r}, col, 1.2f); - dl->AddLine({cx, cy + r}, {cx, cy + ray}, col, 1.2f); - dl->AddLine({cx - ray, cy}, {cx - r, cy}, col, 1.2f); - dl->AddLine({cx + r, cy}, {cx + ray, cy}, col, 1.2f); - dl->AddLine({cx - diag, cy - diag}, {cx - r * 0.7f, cy - r * 0.7f}, col, 1.0f); - dl->AddLine({cx + r * 0.7f, cy - r * 0.7f}, {cx + diag, cy - diag}, col, 1.0f); - dl->AddLine({cx - diag, cy + diag}, {cx - r * 0.7f, cy + r * 0.7f}, col, 1.0f); - dl->AddLine({cx + r * 0.7f, cy + r * 0.7f}, {cx + diag, cy + diag}, col, 1.0f); - return; - } - - if (Helpers::secure_strcmp(type, "Static Mesh") == 0) - { - ImU32 col = ImGui::ColorConvertFloat4ToU32({0.55f, 0.75f, 0.90f, 1.0f}); - float hw = sz * 0.28f, hh = sz * 0.28f, d = sz * 0.16f; - float bx = cx - hw, by = cy; - // Front face - dl->AddRect({bx, by}, {bx + hw * 2, by + hh * 2}, col, 0.f, 0, 1.0f); - // Back face - dl->AddRect({bx + d, by - d}, {bx + hw * 2 + d, by + hh * 2 - d}, col, 0.f, 0, 0.6f); - dl->AddLine({bx, by}, {bx + d, by - d}, col, 0.6f); - dl->AddLine({bx + hw * 2, by}, {bx + hw * 2 + d, by - d}, col, 0.6f); - dl->AddLine({bx, by + hh * 2}, {bx + d, by + hh * 2 - d}, col, 0.6f); - return; - } - - if (Helpers::secure_strcmp(type, "Camera") == 0) - { - ImU32 col = ImGui::ColorConvertFloat4ToU32({0.45f, 0.85f, 0.55f, 1.0f}); - float bw = sz * 0.55f, bh = sz * 0.40f; - float bx = pos.x + sz * 0.05f, by = cy - bh * 0.5f; - dl->AddRectFilled({bx, by}, {bx + bw, by + bh}, col, 1.5f); - dl->AddTriangleFilled({bx + bw, by + bh * 0.1f}, {bx + bw + sz * 0.25f, cy}, {bx + bw, by + bh * 0.9f}, col); - return; - } - - // Default — grey diamond - { - ImU32 col = ImGui::ColorConvertFloat4ToU32({0.55f, 0.55f, 0.60f, 1.0f}); - float r = sz * 0.35f; - dl->AddQuadFilled({cx, cy - r}, {cx + r, cy}, {cx, cy + r}, {cx - r, cy}, col); - } - } - - // Render - - void HierarchyViewUIComponent::Render(ZEngine::Rendering::Renderers::GraphicRenderer* const /*renderer*/, ZEngine::Hardwares::CommandBuffer* const /*command_buffer*/) - { - if (!ParentLayer || !ParentLayer->CurrentApp) - return; - - auto* app = reinterpret_cast(ParentLayer->CurrentApp); - auto* current_scene = reinterpret_cast(app->CurrentScene); - auto* ctx = Engine::GetContext(); - if (!current_scene || !ctx || !ctx->ActorManager) - return; - - ImGui::Begin(Name, CanBeClosed ? &CanBeClosed : nullptr, ImGuiWindowFlags_NoCollapse); - - // Search + Buttons - { - const float btn_sz = 22.f; - const float spacing = ImGui::GetStyle().ItemSpacing.x; - const float avail = ImGui::GetContentRegionAvail().x; - const float box_w = avail - btn_sz * 2.f - spacing * 2.f; - - ImGui::SetNextItemWidth(box_w); - ImGui::InputText("##filter", m_filter_buf, sizeof(m_filter_buf)); - ImGui::SameLine(); - - // Folder+ — New Collection - ImVec2 btn_pos = ImGui::GetCursorScreenPos(); - bool clicked_add = ImGui::InvisibleButton("##new_collection", {btn_sz, btn_sz}); - bool col_hov = ImGui::IsItemHovered(); - if (col_hov) - ImGui::SetTooltip("New Collection"); - { - ImDrawList* fdl = ImGui::GetWindowDrawList(); - ImU32 col = ImGui::ColorConvertFloat4ToU32(col_hov ? ImVec4(0.85f, 0.85f, 0.90f, 1.f) : ImVec4(0.55f, 0.58f, 0.65f, 1.f)); - float x = btn_pos.x, y = btn_pos.y, s = btn_sz; - float m = s * 0.12f; - float bx0 = x + m, by0 = y + s * 0.32f; - float bx1 = x + s - m, by1 = y + s - m; - fdl->AddRectFilled({bx0, by0}, {bx1, by1}, col, 2.f); - float tx0 = bx0, ty0 = by0 - s * 0.14f; - float tx1 = bx0 + (bx1 - bx0) * 0.45f, ty1 = by0 + 1.f; - fdl->AddRectFilled({tx0, ty0}, {tx1, ty1}, col, 2.f, ImDrawFlags_RoundCornersTopLeft | ImDrawFlags_RoundCornersTopRight); - float cx = (bx0 + bx1) * 0.5f, cy = (by0 + by1) * 0.5f; - float hl = s * 0.18f, thk = 1.5f; - ImU32 bg = ImGui::ColorConvertFloat4ToU32(ImVec4(0.14f, 0.15f, 0.18f, 1.f)); - fdl->AddLine({cx - hl, cy}, {cx + hl, cy}, bg, thk + 1.5f); - fdl->AddLine({cx, cy - hl}, {cx, cy + hl}, bg, thk + 1.5f); - fdl->AddLine({cx - hl, cy}, {cx + hl, cy}, col, thk); - fdl->AddLine({cx, cy - hl}, {cx, cy + hl}, col, thk); - } - ImGui::SameLine(); - - // Settings gear placeholder - ImGui::Button("##gear", {btn_sz, btn_sz}); - { - ImDrawList* gdl = ImGui::GetWindowDrawList(); - ImVec2 gp = ImGui::GetItemRectMin(); - float gs = btn_sz * 0.45f; - float gcx = gp.x + btn_sz * 0.5f, gcy = gp.y + btn_sz * 0.5f; - ImU32 gc = ImGui::ColorConvertFloat4ToU32({0.65f, 0.65f, 0.70f, 1.f}); - gdl->AddCircle({gcx, gcy}, gs * 0.5f, gc, 8, 1.5f); - gdl->AddCircleFilled({gcx, gcy}, gs * 0.2f, gc); - } - - if (clicked_add) - { - ActorHandle coll_h = ctx->ActorManager->Create(); - Actor* coll_a = ctx->ActorManager->Access(coll_h); - if (coll_a) - { - NameComponent nc = {}; - Helpers::secure_strncpy(nc.Value, sizeof(nc.Value), "Collection", 10); - coll_a->AddComponent(nc); - coll_a->AddComponent({}); - current_scene->SelectedActorHandle = coll_h; - } - } - } - ImGui::Spacing(); - - // O(n) Build - auto scratch = ZGetScratch(&ParentLayer->LocalArena); - uint32_t n = ctx->ActorManager->Count(); - - struct OutlinerNode - { - ActorHandle Handle; - EntityID EID; - EntityID Parent; - }; - - OutlinerNode* nodes = static_cast(scratch.Arena->Allocate(n * sizeof(OutlinerNode), alignof(OutlinerNode))); - uint32_t* first_child = static_cast(scratch.Arena->Allocate(n * sizeof(uint32_t), alignof(uint32_t))); - uint32_t* next_sib = static_cast(scratch.Arena->Allocate(n * sizeof(uint32_t), alignof(uint32_t))); - uint32_t nc = 0; - - for (uint32_t i = 0; i < n; ++i) - { - first_child[i] = UINT32_MAX; - next_sib[i] = UINT32_MAX; - } - - ctx->ActorManager->ForEach([&](ActorHandle h, Actor* actor) { - auto* pc = actor->GetComponent(); - nodes[nc++] = {h, actor->GetEntityID(), (pc && pc->Parent != INVALID_ENTITY) ? pc->Parent : INVALID_ENTITY}; - }); - - ZEngine::Core::Containers::UnorderedHashMap eid_to_idx; - eid_to_idx.init(scratch.Arena, nc * 2); - for (uint32_t i = 0; i < nc; ++i) - eid_to_idx.insert(nodes[i].EID, i); - - for (uint32_t i = 0; i < nc; ++i) - { - if (nodes[i].Parent == INVALID_ENTITY) - continue; - auto* pidx = eid_to_idx.find(nodes[i].Parent); - if (!pidx) - continue; - next_sib[i] = first_child[*pidx]; - first_child[*pidx] = i; - } - - struct DFSEntry - { - uint32_t idx; - int depth; - }; - DFSEntry* stk = static_cast(scratch.Arena->Allocate(nc * 2 * sizeof(DFSEntry), alignof(DFSEntry))); - int32_t sp = 0; - for (int32_t i = (int32_t) nc - 1; i >= 0; --i) - if (nodes[i].Parent == INVALID_ENTITY) - stk[sp++] = {(uint32_t) i, 0}; - - // Table - ActorHandle pending_delete = {}; - ActorHandle pending_reparent_child = {}; - EntityID pending_reparent_parent = INVALID_ENTITY; - ActorHandle pending_remove_parent_handle = {}; - uint32_t total_actors = nc; - uint32_t selected_count = 0; - - constexpr float ROW_H = 22.f; - constexpr float ICON_SZ = 13.f; - constexpr float INDENT_W = 16.f; - constexpr float ARROW_W = 16.f; - const ImU32 COL_ARROW = ImGui::ColorConvertFloat4ToU32({0.65f, 0.65f, 0.70f, 1.f}); - const ImU32 COL_SEL = ImGui::ColorConvertFloat4ToU32({0.26f, 0.44f, 0.70f, 0.60f}); - const ImU32 COL_HOVER = ImGui::ColorConvertFloat4ToU32({0.26f, 0.44f, 0.70f, 0.25f}); - const ImU32 COL_TEXT = ImGui::ColorConvertFloat4ToU32(ImGui::GetStyleColorVec4(ImGuiCol_Text)); - const ImU32 COL_DIMTEXT = ImGui::ColorConvertFloat4ToU32({0.55f, 0.55f, 0.60f, 1.f}); - - static ImGuiTableFlags tbl_flags = ImGuiTableFlags_ScrollY | ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV; - float tbl_h = ImGui::GetContentRegionAvail().y - ROW_H - ImGui::GetStyle().ItemSpacing.y * 2.f; - ImVec2 tbl_sz = {0.f, tbl_h}; - - if (ImGui::BeginTable("##outliner", 3, tbl_flags, tbl_sz)) - { - ImGui::TableSetupScrollFreeze(0, 1); - ImGui::TableSetupColumn("Item Label", ImGuiTableColumnFlags_WidthStretch, 0.60f); - ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthStretch, 0.25f); - ImGui::TableSetupColumn("Level", ImGuiTableColumnFlags_WidthStretch, 0.15f); - - // Column headers - ImGui::TableNextRow(ImGuiTableRowFlags_Headers); - ImGui::TableSetColumnIndex(0); - { - // Eye icon in header - ImDrawList* hdl = ImGui::GetWindowDrawList(); - ImVec2 hp = ImGui::GetCursorScreenPos(); - float hs = ImGui::GetTextLineHeight(); - ImU32 hc = COL_DIMTEXT; - float ex = hp.x + 7.f, ey = hp.y + hs * 0.5f; - // Eye icon: two arcs (top + bottom) with a pupil dot - hdl->AddCircle({ex, ey}, 3.5f, hc, 8, 1.2f); - hdl->AddCircleFilled({ex, ey}, 1.5f, hc); - ImGui::Dummy({14.f, hs}); - ImGui::SameLine(0.f, 2.f); - } - ImGui::TableHeader("Item Label"); - ImGui::TableSetColumnIndex(1); - ImGui::TableHeader("Type"); - ImGui::TableSetColumnIndex(2); - ImGui::TableHeader("Level"); - - ImDrawList* dl = ImGui::GetWindowDrawList(); - - // Scene Root row - { - ImGui::TableNextRow(ImGuiTableRowFlags_None, ROW_H); - ImGui::TableSetColumnIndex(0); - ImVec2 row_p = ImGui::GetCursorScreenPos(); - - bool root_collapsed = m_scene_root_collapsed; - bool root_selected = false; // scene root can't be selected - - // Hover / bg - ImVec2 row_end = {row_p.x + ImGui::GetContentRegionAvail().x + ImGui::GetScrollX() + 800.f, row_p.y + ROW_H}; - bool hov = ImGui::IsMouseHoveringRect(row_p, row_end); - if (hov) - dl->AddRectFilled(row_p, row_end, COL_HOVER); - - // Arrow - float ax = row_p.x + 2.f; - ImGui::SetCursorScreenPos({ax, row_p.y}); - ImGui::PushID(0xFFFF0000); - ImGui::InvisibleButton("##root_arrow", {ARROW_W, ROW_H}); - if (ImGui::IsItemClicked()) - m_scene_root_collapsed = !m_scene_root_collapsed; - ImGui::PopID(); - DrawArrow(dl, {ax, row_p.y}, ROW_H, !root_collapsed, COL_ARROW); - - // Icon - float ix = ax + ARROW_W + 2.f; - DrawTypeIcon(dl, {ix, row_p.y + (ROW_H - ICON_SZ) * 0.5f}, ICON_SZ, "World", false); - - // Label - float lx = ix + ICON_SZ + 5.f; - ImGui::SetCursorScreenPos({lx, row_p.y + (ROW_H - ImGui::GetTextLineHeight()) * 0.5f}); - const char* scene_name = (current_scene->Name && current_scene->Name[0]) ? current_scene->Name : "DefaultScene"; - ImGui::TextUnformatted(scene_name); - - ImGui::TableSetColumnIndex(1); - ImGui::SetCursorPosY(ImGui::GetCursorPosY() + (ROW_H - ImGui::GetTextLineHeight()) * 0.5f); - ImGui::TextDisabled("World"); - } - - // Actor rows (DFS) - bool scene_root_collapsed = m_scene_root_collapsed; - if (!scene_root_collapsed) - { - while (sp > 0) - { - DFSEntry e = stk[--sp]; - uint32_t ni = e.idx; - Actor* actor = ctx->ActorManager->Access(nodes[ni].Handle); - if (!actor) - continue; - - auto* nc_comp = actor->GetComponent(); - const char* label = (nc_comp && nc_comp->Value[0]) ? nc_comp->Value : "Actor"; - - if (m_filter_buf[0] && !strstr(label, m_filter_buf)) - continue; - - bool is_collection = !actor->HasComponent() && !actor->HasComponent() && !actor->HasComponent(); - const char* type_str = is_collection ? "Collection" : actor->HasComponent() ? "Light" : actor->HasComponent() ? "Camera" : actor->HasComponent() ? "Static Mesh" : "Actor"; - - bool has_children = (first_child[ni] != UINT32_MAX); - bool collapsed = has_children && IsCollapsed(nodes[ni].EID); - bool selected = (current_scene->SelectedActorHandle.Index == nodes[ni].Handle.Index && current_scene->SelectedActorHandle.Generation == nodes[ni].Handle.Generation); - if (selected) - ++selected_count; - - bool renaming = (m_renaming_handle.Index == nodes[ni].Handle.Index && m_renaming_handle.Generation == nodes[ni].Handle.Generation); - - ImGui::TableNextRow(ImGuiTableRowFlags_None, ROW_H); - ImGui::TableSetColumnIndex(0); - ImVec2 row_p = ImGui::GetCursorScreenPos(); - ImVec2 row_end = {row_p.x + ImGui::GetContentRegionAvail().x + ImGui::GetScrollX() + 800.f, row_p.y + ROW_H}; - - // Row background - bool hov = ImGui::IsMouseHoveringRect(row_p, row_end); - if (selected) - dl->AddRectFilled(row_p, row_end, COL_SEL); - else if (hov) - dl->AddRectFilled(row_p, row_end, COL_HOVER); - - // Row click - if (hov && ImGui::IsMouseClicked(0)) - current_scene->SelectedActorHandle = nodes[ni].Handle; - - // Double-click rename - if (hov && ImGui::IsMouseDoubleClicked(0) && !renaming) - { - m_renaming_handle = nodes[ni].Handle; - Helpers::secure_strncpy(m_rename_buf, sizeof(m_rename_buf), label, sizeof(m_rename_buf) - 1); - } - - // Indent - float indent = (e.depth + 1) * INDENT_W; // +1 because everything is under scene root - - // Arrow area - float ax = row_p.x + indent; - ImGui::SetCursorScreenPos({ax, row_p.y}); - ImGui::PushID((int) nodes[ni].Handle.Index); - ImGui::InvisibleButton("##arrow", {ARROW_W, ROW_H}); - if (ImGui::IsItemClicked() && has_children) - ToggleCollapsed(nodes[ni].EID); - ImGui::PopID(); - if (has_children) - DrawArrow(dl, {ax, row_p.y}, ROW_H, !collapsed, COL_ARROW); - - // Icon - float ix = ax + ARROW_W + 2.f; - DrawTypeIcon(dl, {ix, row_p.y + (ROW_H - ICON_SZ) * 0.5f}, ICON_SZ, type_str, is_collection); - - // Label / rename - float lx = ix + ICON_SZ + 5.f; - ImGui::SetCursorScreenPos({lx, row_p.y + (ROW_H - ImGui::GetFrameHeight()) * 0.5f}); - ImGui::PushID((int) nodes[ni].Handle.Index + 1000); - - if (renaming) - { - ImGui::SetNextItemWidth(120.f); - if (ImGui::InputText("##ren", m_rename_buf, sizeof(m_rename_buf), ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AutoSelectAll)) - { - if (nc_comp && m_rename_buf[0]) - Helpers::secure_strncpy(nc_comp->Value, sizeof(nc_comp->Value), m_rename_buf, sizeof(m_rename_buf) - 1); - m_renaming_handle = {}; - } - else if (!ImGui::IsItemActive() && ImGui::IsItemDeactivated()) - { - if (nc_comp && m_rename_buf[0]) - Helpers::secure_strncpy(nc_comp->Value, sizeof(nc_comp->Value), m_rename_buf, sizeof(m_rename_buf) - 1); - m_renaming_handle = {}; - } - else - ImGui::SetKeyboardFocusHere(-1); - } - else - { - ImGui::SetCursorScreenPos({lx, row_p.y + (ROW_H - ImGui::GetTextLineHeight()) * 0.5f}); - dl->AddText(ImGui::GetCursorScreenPos(), selected ? 0xFFFFFFFF : COL_TEXT, label); - ImGui::Dummy({ImGui::CalcTextSize(label).x, ImGui::GetTextLineHeight()}); - } - - // Drag source - if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceAllowNullID)) - { - ImGui::SetDragDropPayload("ACTOR_REPARENT", &nodes[ni].Handle, sizeof(ActorHandle)); - ImGui::Text("Move: %s", label); - ImGui::EndDragDropSource(); - } - - // Drop target - if (ImGui::BeginDragDropTarget()) - { - if (const ImGuiPayload* p = ImGui::AcceptDragDropPayload("ACTOR_REPARENT")) - { - ActorHandle dragged = *reinterpret_cast(p->Data); - if (dragged.Index != nodes[ni].Handle.Index) - { - pending_reparent_child = dragged; - pending_reparent_parent = nodes[ni].EID; - } - } - ImGui::EndDragDropTarget(); - } - - // Context menu - if (ImGui::BeginPopupContextItem("##ctx")) - { - if (nodes[ni].Parent != INVALID_ENTITY && ImGui::MenuItem("Remove from Parent")) - pending_remove_parent_handle = nodes[ni].Handle; - if (ImGui::MenuItem("Delete")) - { - auto* mc = actor->GetComponent(); - if (mc && mc->RenderInstanceId != UINT32_MAX) - current_scene->RemoveMeshInstance(mc->RenderInstanceId, ctx->RenderResourceManager); - if (selected) - current_scene->SelectedActorHandle = {}; - pending_delete = nodes[ni].Handle; - } - ImGui::EndPopup(); - } - ImGui::PopID(); - - // Other columns - ImGui::TableSetColumnIndex(1); - ImGui::SetCursorPosY(ImGui::GetCursorPosY() + (ROW_H - ImGui::GetTextLineHeight()) * 0.5f); - ImGui::TextDisabled("%s", type_str); - - ImGui::TableSetColumnIndex(2); - ImGui::SetCursorPosY(ImGui::GetCursorPosY() + (ROW_H - ImGui::GetTextLineHeight()) * 0.5f); - ImGui::TextDisabled("Default"); - - // Push children - if (has_children && !collapsed) - { - uint32_t tmp[256]; - int tc = 0; - uint32_t c = first_child[ni]; - while (c != UINT32_MAX && tc < 256) - { - tmp[tc++] = c; - c = next_sib[c]; - } - for (int ci = tc - 1; ci >= 0; --ci) - stk[sp++] = {tmp[ci], e.depth + 1}; - } - } - } - - // Root-level drop zone - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::InvisibleButton("##root_drop_zone", {ImGui::GetContentRegionAvail().x, 8.f}); - if (ImGui::BeginDragDropTarget()) - { - if (const ImGuiPayload* p = ImGui::AcceptDragDropPayload("ACTOR_REPARENT")) - { - pending_reparent_child = *reinterpret_cast(p->Data); - pending_reparent_parent = INVALID_ENTITY; - } - ImGui::EndDragDropTarget(); - } - - ImGui::EndTable(); - } - - ZReleaseScratch(scratch); - - // Status bar - ImGui::Separator(); - ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 2.f); - if (selected_count > 0) - ImGui::TextDisabled("%u actor%s (%u selected)", total_actors, total_actors == 1 ? "" : "s", selected_count); - else - ImGui::TextDisabled("%u actor%s", total_actors, total_actors == 1 ? "" : "s"); - - // Deferred mutations - if (pending_reparent_child.Valid()) - { - Actor* child = ctx->ActorManager->Access(pending_reparent_child); - if (child) - { - if (pending_reparent_parent == INVALID_ENTITY) - child->RemoveComponent(); - else - { - ParentComponent pc_new = {pending_reparent_parent}; - if (child->HasComponent()) - child->GetComponent()->Parent = pending_reparent_parent; - else - child->AddComponent(pc_new); - } - } - } - if (pending_remove_parent_handle.Valid()) - { - Actor* a = ctx->ActorManager->Access(pending_remove_parent_handle); - if (a) - a->RemoveComponent(); - } - if (pending_delete.Valid()) - ctx->ActorManager->Destroy(pending_delete); - - RenderGuizmo(app, current_scene); - ImGui::End(); - } - - // Gizmo - - void HierarchyViewUIComponent::RenderGuizmo(EditorPtr app, EditorScenePtr scene) - { - if (!app || !app->CameraController || !scene) - return; - - auto* ctx = Engine::GetContext(); - if (!ctx || !ctx->ActorManager) - return; - - ActorHandle h = scene->SelectedActorHandle; - Actor* actor = ctx->ActorManager->Access(h); - if (!actor) - return; - - auto* tc = actor->GetComponent(); - if (!tc) - return; - - auto camera = app->CameraController->GetCamera(); - if (!camera) - return; - - const auto view = camera->GetView(); - auto projection = camera->GetProjection(); - projection[1][1] = -projection[1][1]; - - Mat4f transform = tc->WorldTransform; - - int gizmo_op = app->Configuration->GizmoOperation; - float snap_val = 0.5f; - bool snapping = app->CurrentWindow && IDevice::As() && IDevice::As()->IsKeyPressed(ZENGINE_KEY_LEFT_CONTROL, app->CurrentWindow); - if (snapping && static_cast(gizmo_op) == ImGuizmo::ROTATE) - snap_val = 45.0f; - float snap_arr[3] = {snap_val, snap_val, snap_val}; - - if (gizmo_op > 0) - ImGuizmo::Manipulate(value_ptr(view), value_ptr(projection), static_cast(gizmo_op), ImGuizmo::MODE::WORLD, value_ptr(transform), nullptr, snapping ? snap_arr : nullptr); - - if (ImGuizmo::IsUsing()) - { - Mat4f local_mat = transform; - auto* pc = actor->GetComponent(); - if (pc && pc->Parent != INVALID_ENTITY) - { - auto* parent_tc = ctx->Scene->GetComponent(pc->Parent); - if (parent_tc) - local_mat = parent_tc->WorldTransform.Inverse() * transform; - } - - Vec3f new_pos, new_rot, new_scale; - if (DecomposeTransformComponent(local_mat, new_pos, new_rot, new_scale)) - { - tc->Position = new_pos; - tc->Rotation = new_rot; - tc->Scale = new_scale; - tc->PreviousPosition = new_pos; - } - } - } -} // namespace Tetragrama::Components diff --git a/Tetragrama/Components/HierarchyViewUIComponent.h b/Tetragrama/Components/HierarchyViewUIComponent.h deleted file mode 100644 index 4d06e6fd6..000000000 --- a/Tetragrama/Components/HierarchyViewUIComponent.h +++ /dev/null @@ -1,42 +0,0 @@ -#pragma once -#include -#include -#include -#include -#include -#include -#include - -namespace Tetragrama::Components -{ - class HierarchyViewUIComponent : public UIComponent - { - public: - HierarchyViewUIComponent(); - virtual ~HierarchyViewUIComponent(); - - void Initialize(Layers::ImguiLayer* parent = nullptr, const char* name = "Hierarchy", bool visibility = true, bool closed = false) override; - void Update(ZEngine::Core::TimeStep dt) override; - virtual void Render(ZEngine::Rendering::Renderers::GraphicRenderer* const renderer, ZEngine::Hardwares::CommandBuffer* const command_buffer) override; - - private: - void RenderGuizmo(EditorPtr app, EditorScenePtr scene); - - static void DrawArrow(ImDrawList* dl, ImVec2 pos, float row_h, bool expanded, ImU32 color); - static void DrawTypeIcon(ImDrawList* dl, ImVec2 pos, float sz, const char* type, bool is_collection); - - int m_gizmo_operation{-1}; - char m_filter_buf[128] = {}; - - ZEngine::ECS::ActorHandle m_renaming_handle = {}; - char m_rename_buf[128] = {}; - - // Collapsed entity IDs — everything NOT in this list is expanded by default. - ZEngine::Core::Memory::ArenaAllocator m_outliner_arena = {}; - ZEngine::Core::Containers::Array m_collapsed = {}; - bool m_scene_root_collapsed = false; - - bool IsCollapsed(ZEngine::ECS::EntityID eid) const; - void ToggleCollapsed(ZEngine::ECS::EntityID eid); - }; -} // namespace Tetragrama::Components diff --git a/Tetragrama/Components/InspectorViewUIComponent.cpp b/Tetragrama/Components/InspectorViewUIComponent.cpp deleted file mode 100644 index 33cf9516c..000000000 --- a/Tetragrama/Components/InspectorViewUIComponent.cpp +++ /dev/null @@ -1,341 +0,0 @@ -// clang-format off -#include -#include -#include -#include -#include -#include -#include -// clang-format on - -using namespace ZEngine::ECS; -using namespace ZEngine::ECS::Components; -using namespace ZEngine::Core::Maths; - -namespace -{ - using namespace ZEngine::ECS; - using ZEngine::Core::Maths::Vec3f; - - bool SectionHeader(const char* id, const char* label, bool* open) - { - ImGui::PushID(id); - ImVec2 pos = ImGui::GetCursorScreenPos(); - float w = ImGui::GetContentRegionAvail().x; - float h = 22.f; - ImDrawList* dl = ImGui::GetWindowDrawList(); - bool hov = ImGui::IsMouseHoveringRect(pos, {pos.x + w, pos.y + h}); - dl->AddRectFilled(pos, {pos.x + w, pos.y + h}, ImGui::GetColorU32(hov ? ImGuiCol_HeaderHovered : ImGuiCol_Header)); - ImU32 tri = ImGui::GetColorU32(ImGuiCol_Text); - float tx = pos.x + 8.f, ty = pos.y + h * 0.5f; - if (*open) - { - dl->AddTriangleFilled({tx, ty - 4.f}, {tx + 7.f, ty - 4.f}, {tx + 3.5f, ty + 4.f}, tri); - } - else - { - dl->AddTriangleFilled({tx, ty - 4.f}, {tx, ty + 4.f}, {tx + 7.f, ty}, tri); - } - dl->AddText({tx + 14.f, pos.y + (h - ImGui::GetTextLineHeight()) * 0.5f}, ImGui::GetColorU32(ImGuiCol_Text), label); - ImGui::InvisibleButton("##hdr", {w, h}); - if (ImGui::IsItemClicked()) - { - *open = !*open; - } - ImGui::PopID(); - return *open; - } - - int64_t ReadEnum(const void* ptr, uint32_t size) - { - switch (size) - { - case 1: - return *static_cast(ptr); - case 2: - return *static_cast(ptr); - case 4: - return *static_cast(ptr); - case 8: - return *static_cast(ptr); - default: - return 0; - } - } - - void WriteEnum(void* ptr, uint32_t size, int64_t value) - { - switch (size) - { - case 1: - *static_cast(ptr) = static_cast(value); - break; - case 2: - *static_cast(ptr) = static_cast(value); - break; - case 4: - *static_cast(ptr) = static_cast(value); - break; - case 8: - *static_cast(ptr) = value; - break; - default: - break; - } - } - - // Draws the three components of a Vec3f with the R/G/B axis bars. - void Vec3Row(float* v) - { - constexpr float kGap = 4.f; - float avail = ImGui::GetContentRegionAvail().x; - float fw = (avail - kGap * 2.f) / 3.f; - ImU32 border[3] = { - IM_COL32(215, 90, 80, 255), - IM_COL32(100, 200, 110, 255), - IM_COL32(90, 140, 230, 255), - }; - const char* ids[3] = {"##ax0", "##ax1", "##ax2"}; - ImDrawList* dl = ImGui::GetWindowDrawList(); - - for (int i = 0; i < 3; ++i) - { - if (i > 0) - { - ImGui::SameLine(0.f, kGap); - } - ImGui::SetNextItemWidth(fw); - ImGui::DragFloat(ids[i], &v[i], 0.05f, 0.f, 0.f, "%.3f"); - ImVec2 p0 = ImGui::GetItemRectMin(); - dl->AddRectFilled(p0, {p0.x + 3.f, ImGui::GetItemRectMax().y}, border[i]); - } - } - - void DrawField(const FieldDescriptor& field, void* ptr) - { - ImGui::TableSetColumnIndex(0); - ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled)); - ImGui::TextUnformatted(field.Name); - ImGui::PopStyleColor(); - if (field.Tooltip && ImGui::IsItemHovered()) - { - ImGui::SetTooltip("%s", field.Tooltip); - } - - ImGui::TableSetColumnIndex(1); - ImGui::PushID(field.Name); - ImGui::SetNextItemWidth(-1.f); - - switch (field.Type) - { - case FieldType::Bool: - ImGui::Checkbox("##v", static_cast(ptr)); - break; - case FieldType::Int32: - ImGui::DragInt("##v", static_cast(ptr)); - break; - case FieldType::UInt32: - ImGui::DragScalar("##v", ImGuiDataType_U32, ptr); - break; - case FieldType::Float: - ImGui::DragFloat("##v", static_cast(ptr), 0.05f, field.Min, field.Max); - break; - case FieldType::Vec3f: - Vec3Row(static_cast(ptr)); - break; - case FieldType::String: - ImGui::InputText("##v", static_cast(ptr), field.StringCap); - break; - case FieldType::AssetUUID: - { - char text[37] = {}; - uuids::to_string(*static_cast(ptr), text); - ImGui::InputText("##v", text, sizeof(text), ImGuiInputTextFlags_ReadOnly); - break; - } - case FieldType::Enum: - { - int64_t value = ReadEnum(ptr, field.Size); - const char* current = "unknown"; - for (uint32_t i = 0; i < field.EnumCount; ++i) - { - if (field.EnumValues[i].Value == value) - { - current = field.EnumValues[i].Name; - } - } - if (ImGui::BeginCombo("##v", current)) - { - for (uint32_t i = 0; i < field.EnumCount; ++i) - { - bool selected = (field.EnumValues[i].Value == value); - if (ImGui::Selectable(field.EnumValues[i].Name, selected)) - { - WriteEnum(ptr, field.Size, field.EnumValues[i].Value); - } - } - ImGui::EndCombo(); - } - break; - } - default: - ImGui::TextDisabled("(unsupported type)"); - break; - } - - ImGui::PopID(); - } - - void DrawEntityComponents(Scene& scene, EntityID id) - { - static constexpr ImGuiTableFlags kTableFlags = ImGuiTableFlags_NoPadInnerX | ImGuiTableFlags_NoPadOuterX; - - const ArchetypeMask mask = scene.GetMask(id); - - ComponentReflectionRegistry::Get().ForEach([&](const ComponentMeta& meta) { - if (!MaskHas(mask, meta.TypeID)) - { - return; - } - - void* raw = scene.GetComponentRaw(id, meta.TypeID); - if (!raw) - { - return; - } - - // Section open state is per component type, not per entity. - static bool s_open[ARCHETYPE_MASK_CAPACITY] = {}; - static bool s_init = [] { - for (bool& b : s_open) - { - b = true; - } - return true; - }(); - (void) s_init; - - if (!SectionHeader(meta.TypeName, meta.TypeName, &s_open[meta.TypeID])) - return; - - ImGui::PushID(static_cast(meta.TypeID)); - if (ImGui::BeginTable("##fields", 2, kTableFlags)) - { - ImGui::TableSetupColumn("##lbl", ImGuiTableColumnFlags_WidthFixed, 96.f); - ImGui::TableSetupColumn("##val", ImGuiTableColumnFlags_WidthStretch); - ImGui::TableNextRow(); - - for (uint32_t i = 0; i < meta.FieldCount; ++i) - { - const FieldDescriptor& field = meta.Fields[i]; - if (field.Hidden) - { - continue; - } - - ImGui::TableNextRow(); - ImGui::BeginDisabled(field.ReadOnly); - DrawField(field, static_cast(raw) + field.Offset); - ImGui::EndDisabled(); - } - - ImGui::TableNextRow(); - ImGui::EndTable(); - } - ImGui::PopID(); - ImGui::Spacing(); - }); - } -} // namespace - -namespace Tetragrama::Components -{ - InspectorViewUIComponent::InspectorViewUIComponent() = default; - InspectorViewUIComponent::~InspectorViewUIComponent() = default; - - void InspectorViewUIComponent::Initialize(Layers::ImguiLayer* parent, const char* name, bool visibility, bool closed) - { - UIComponent::Initialize(parent, name, visibility, closed); - } - - void InspectorViewUIComponent::Update(ZEngine::Core::TimeStep /*dt*/) {} - - void InspectorViewUIComponent::Render(ZEngine::Rendering::Renderers::GraphicRenderer* const /*renderer*/, ZEngine::Hardwares::CommandBuffer* const /*command_buffer*/) - { - ImGui::Begin(Name, CanBeClosed ? &CanBeClosed : nullptr, ImGuiWindowFlags_NoCollapse); - - auto* ctx = ZEngine::Engine::GetContext(); - if (!ctx || !ctx->ActorManager || !ParentLayer || !ParentLayer->CurrentApp) - { - ImGui::End(); - return; - } - - auto* app = reinterpret_cast(ParentLayer->CurrentApp); - auto* current_scene = reinterpret_cast(app->CurrentScene); - if (!current_scene) - { - ImGui::End(); - return; - } - - { - static char s_filter[128] = {}; - ImGui::SetNextItemWidth(-1.f); - ImGui::InputTextWithHint("##details_search", "Search Details...", s_filter, sizeof(s_filter)); - } - - ImGui::Spacing(); - - ActorHandle h = current_scene->SelectedActorHandle; - Actor* actor = ctx->ActorManager->Access(h); - if (!actor) - { - ImGui::SetCursorPosX((ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize("No actor selected").x) * 0.5f); - ImGui::TextDisabled("No actor selected"); - ImGui::End(); - return; - } - - { - constexpr float kBtnW = 112.f; - constexpr float kMargin = 10.f; - - ImVec4 hdr = ImGui::GetStyleColorVec4(ImGuiCol_Header); - ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(hdr.x, hdr.y, hdr.z, 0.4f)); - ImGui::BeginChild("##actor_header", {-1.f, 42.f}, false, ImGuiWindowFlags_NoScrollbar); - - float card_w = ImGui::GetContentRegionAvail().x; - - ImGui::SetCursorPos({kMargin, 6.f}); - - ImGui::SetCursorPos({kMargin, 24.f}); - ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled)); - ImGui::TextUnformatted("Actor"); - ImGui::PopStyleColor(); - - constexpr float kBtnH = 22.f; - ImGui::SetCursorPos({card_w - kBtnW - 4.f, (42.f - kBtnH) * 0.5f}); - bool add_clicked = ImGui::InvisibleButton("##add_comp", {kBtnW, kBtnH}); - ImVec2 btn_scr = ImGui::GetItemRectMin(); - ImDrawList* cdl = ImGui::GetWindowDrawList(); - ImVec4 add_bg = ImGui::IsItemHovered() ? ImGui::GetStyleColorVec4(ImGuiCol_HeaderHovered) : ImGui::GetStyleColorVec4(ImGuiCol_Header); - cdl->AddRectFilled(btn_scr, {btn_scr.x + kBtnW, btn_scr.y + kBtnH}, ImGui::ColorConvertFloat4ToU32(add_bg), 3.f); - float th = ImGui::GetTextLineHeight(); - float ty = btn_scr.y + (kBtnH - th) * 0.5f; - float tx = btn_scr.x + 8.f; - cdl->AddText({tx, ty}, IM_COL32(90, 210, 120, 255), "+"); - cdl->AddText({tx + ImGui::CalcTextSize("+").x + 5.f, ty}, ImGui::GetColorU32(ImGuiCol_Text), "Add"); - (void) add_clicked; - - ImGui::EndChild(); - ImGui::PopStyleColor(); - } - - ImGui::Spacing(); - - DrawEntityComponents(*ctx->Scene, actor->GetEntityID()); - - ImGui::End(); - } -} // namespace Tetragrama::Components diff --git a/Tetragrama/Components/InspectorViewUIComponent.h b/Tetragrama/Components/InspectorViewUIComponent.h deleted file mode 100644 index fd4a04a0e..000000000 --- a/Tetragrama/Components/InspectorViewUIComponent.h +++ /dev/null @@ -1,24 +0,0 @@ -#pragma once -#include -#include -#include -#include - -namespace Tetragrama::Components -{ - class InspectorViewUIComponent : public UIComponent - { - public: - InspectorViewUIComponent(); - virtual ~InspectorViewUIComponent(); - - void Initialize(Layers::ImguiLayer* parent = nullptr, const char* name = "Inspector", bool visibility = true, bool closed = false) override; - - void Update(ZEngine::Core::TimeStep dt) override; - - virtual void Render(ZEngine::Rendering::Renderers::GraphicRenderer* const renderer, ZEngine::Hardwares::CommandBuffer* const command_buffer) override; - - private: - ImGuiTreeNodeFlags m_node_flag; - }; -} // namespace Tetragrama::Components diff --git a/Tetragrama/Components/LogUIComponent.cpp b/Tetragrama/Components/LogUIComponent.cpp deleted file mode 100644 index 68918c367..000000000 --- a/Tetragrama/Components/LogUIComponent.cpp +++ /dev/null @@ -1,147 +0,0 @@ -#include -#include -#include -#include -#include - -namespace Tetragrama::Components -{ - LogUIComponent::~LogUIComponent() - { - if (m_cookie) - ZEngine::Logging::Logger::RemoveEventHandler(m_cookie); - } - - void LogUIComponent::Initialize(Layers::ImguiLayer* parent, cstring name, bool visibility, bool closed) - { - UIComponent::Initialize(parent, name, visibility, closed); - m_filter_level = 5; - m_cookie = ZEngine::Logging::Logger::AddEventHandler({OnLogEntry, this}); - } - - void LogUIComponent::Update(ZEngine::Core::TimeStep /*dt*/) {} - - void LogUIComponent::OnLogEntry(void* ctx, const ZEngine::Logging::LogMessage& msg) - { - auto* self = static_cast(ctx); - LogEntry e; - ZEngine::Helpers::secure_strncpy(e.Text, sizeof(e.Text), msg.Message ? msg.Message : "", sizeof(e.Text) - 1); - e.Color[0] = msg.Color[0]; - e.Color[1] = msg.Color[1]; - e.Color[2] = msg.Color[2]; - e.Color[3] = msg.Color[3]; - e.Level = static_cast(msg.Level); - self->PushEntry(e); - } - - void LogUIComponent::PushEntry(const LogEntry& e) - { - std::lock_guard lock(m_mutex); - m_ring[m_head] = e; - m_head = (m_head + 1) % kMaxEntries; - if (m_count < kMaxEntries) - ++m_count; - m_scroll_to_bottom = true; - } - - void LogUIComponent::Render(ZEngine::Rendering::Renderers::GraphicRenderer* const, ZEngine::Hardwares::CommandBuffer* const) - { - if (!ParentLayer || !ParentLayer->CurrentApp) - return; - - auto* app = reinterpret_cast(ParentLayer->CurrentApp); - if (!app || !app->Configuration->ShowConsole) - return; - - if (app->Configuration->FocusConsole) - { - ImGui::SetNextWindowFocus(); - app->Configuration->FocusConsole = false; - } - - const bool dark = ImGui::GetStyle().Colors[ImGuiCol_WindowBg].x < 0.5f; - ImGui::PushStyleColor(ImGuiCol_WindowBg, dark ? ImVec4{0.10f, 0.10f, 0.11f, 1.0f} : ImVec4{0.96f, 0.96f, 0.96f, 1.0f}); - bool open = ImGui::Begin(Name, &app->Configuration->ShowConsole, ImGuiWindowFlags_NoCollapse); - ImGui::PopStyleColor(); - - if (open) - { - static constexpr cstring kLevelItems[] = {"Trace", "Info", "Warn", "Error", "Critical", "All"}; - ImGui::SetNextItemWidth(180.0f); - ImGui::InputTextWithHint("##search", "Search logs...", m_search_buf, sizeof(m_search_buf)); - ImGui::SameLine(); - ImGui::SetNextItemWidth(80.0f); - ImGui::Combo("##level", &m_filter_level, kLevelItems, 6); - ImGui::SameLine(); - if (ImGui::SmallButton("Copy")) - m_copy_requested = true; - ImGui::SameLine(); - if (ImGui::SmallButton("Clear")) - { - std::lock_guard lock(m_mutex); - m_count = 0; - m_head = 0; - } - ImGui::Separator(); - - ImGui::BeginChild("##log_scroll", ImVec2(0, 0), false, ImGuiWindowFlags_HorizontalScrollbar); - { - std::lock_guard lock(m_mutex); - int count = m_count < kMaxEntries ? m_count : kMaxEntries; - int start = (m_count >= kMaxEntries) ? m_head : 0; - - static char clip_buf[kMaxEntries * 260]; - int clip_pos = 0; - if (m_copy_requested) - clip_buf[0] = '\0'; - - for (int i = 0; i < count; ++i) - { - const auto& e = m_ring[(start + i) % kMaxEntries]; - - if (m_filter_level < 5 && e.Level != static_cast(m_filter_level)) - continue; - - if (m_search_buf[0] != '\0') - { - bool found = false; - for (int si = 0; e.Text[si] && !found; ++si) - { - int j = 0; - for (; m_search_buf[j] && e.Text[si + j]; ++j) - if (::tolower(e.Text[si + j]) != ::tolower(m_search_buf[j])) - break; - if (!m_search_buf[j]) - found = true; - } - if (!found) - continue; - } - - ImGui::TextColored({e.Color[0], e.Color[1], e.Color[2], e.Color[3]}, "%s", e.Text); - - if (m_copy_requested && clip_pos < (int) sizeof(clip_buf) - 2) - { - int n = snprintf(clip_buf + clip_pos, sizeof(clip_buf) - clip_pos, "%s\n", e.Text); - if (n > 0) - clip_pos += n; - } - } - - if (m_copy_requested) - { - ImGui::SetClipboardText(clip_buf); - m_copy_requested = false; - } - - if (m_scroll_to_bottom) - { - ImGui::SetScrollHereY(1.0f); - m_scroll_to_bottom = false; - } - } - ImGui::EndChild(); - } - ImGui::End(); - } -} // namespace Tetragrama::Components diff --git a/Tetragrama/Components/LogUIComponent.h b/Tetragrama/Components/LogUIComponent.h deleted file mode 100644 index 0f93f8576..000000000 --- a/Tetragrama/Components/LogUIComponent.h +++ /dev/null @@ -1,41 +0,0 @@ -#pragma once -#include -#include -#include - -namespace Tetragrama::Components -{ - class LogUIComponent : public UIComponent - { - public: - LogUIComponent() = default; - ~LogUIComponent() override; - - void Initialize(Layers::ImguiLayer* parent = nullptr, cstring name = "Console", bool visibility = true, bool closed = false) override; - void Update(ZEngine::Core::TimeStep dt) override; - virtual void Render(ZEngine::Rendering::Renderers::GraphicRenderer* const renderer, ZEngine::Hardwares::CommandBuffer* const command_buffer) override; - - private: - struct LogEntry - { - char Text[256] = {}; - float Color[4] = {0.8f, 0.8f, 0.8f, 1.0f}; - uint8_t Level = 0; - }; - - static constexpr int kMaxEntries = 512; - LogEntry m_ring[kMaxEntries] = {}; - int m_head = 0; - int m_count = 0; - std::mutex m_mutex; - uint32_t m_cookie = 0; - - bool m_scroll_to_bottom = false; - char m_search_buf[256] = {}; - bool m_copy_requested = false; - int m_filter_level = 5; - - void PushEntry(const LogEntry& e); - static void OnLogEntry(void* ctx, const ZEngine::Logging::LogMessage& msg); - }; -} // namespace Tetragrama::Components diff --git a/Tetragrama/Components/ProjectViewUIComponent.cpp b/Tetragrama/Components/ProjectViewUIComponent.cpp deleted file mode 100644 index 65ca8d388..000000000 --- a/Tetragrama/Components/ProjectViewUIComponent.cpp +++ /dev/null @@ -1,994 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -using namespace ZEngine::Helpers; -using namespace ZEngine::Core::VFS; - -namespace Tetragrama::Components -{ - ProjectViewUIComponent::ProjectViewUIComponent() {} - - ProjectViewUIComponent::~ProjectViewUIComponent() {} - - void ProjectViewUIComponent::Initialize(Layers::ImguiLayer* parent, const char* name, bool visibility, bool closed) - { - UIComponent::Initialize(parent, name, visibility, closed); - parent->LocalArena.CreateSubArena(ZMega(4), &m_local_arena); - - m_vfs_context = ZEngine::Engine::GetContext()->VFS; - m_directory_cache = &ParentLayer->Cache; - m_scanner = &ParentLayer->Scanner; - - m_assets_vfs_root = VFSPath::Root(); - m_current_vfs_dir = m_assets_vfs_root; - - { - cstring ws = ParentLayer->CurrentApp->WorkingSpacePath; - secure_strcpy(m_workspace_root, sizeof(m_workspace_root), ws ? ws : ""); - const char* slash = strrchr(ws, '/'); - const char* label = (slash && slash[1] != '\0') ? slash + 1 : ws; - secure_strcpy(m_root_label, sizeof(m_root_label), label); - } - - // Reset popup input buffers to their defaults. - secure_strcpy(m_popup_new_file_name, sizeof(m_popup_new_file_name), "NewFile.txt"); - secure_strcpy(m_popup_new_folder_name, sizeof(m_popup_new_folder_name), "New Folder"); - m_popup_rename_name[0] = '\0'; - m_popup_rename_initialized = false; - - TriggerScan(); - } - - void ProjectViewUIComponent::TriggerScan() - { - if (m_scanner && m_vfs_context && m_directory_cache) - { - m_scanner->Scan(m_vfs_context, m_assets_vfs_root, m_directory_cache); - } - } - - void ProjectViewUIComponent::Update(ZEngine::Core::TimeStep dt) {} - - void ProjectViewUIComponent::Render(ZEngine::Rendering::Renderers::GraphicRenderer* const renderer, ZEngine::Hardwares::CommandBuffer* const command_buffer) - { - auto* app = reinterpret_cast(ParentLayer->CurrentApp); - if (!app || !app->Configuration->ShowContentBrowser) - return; - - if (app->Configuration->FocusContentBrowser) - { - ImGui::SetNextWindowFocus(); - app->Configuration->FocusContentBrowser = false; - } - - if (!ImGui::Begin(Name, (CanBeClosed ? &CanBeClosed : NULL), ImGuiWindowFlags_NoCollapse)) - { - ImGui::End(); - return; - } - - // Bail out if the content area is degenerate (first docking frame, zero-size, etc.) - ImVec2 avail = ImGui::GetContentRegionAvail(); - if (avail.x < 10.0f || avail.y < 10.0f) - { - ImGui::End(); - return; - } - - // Top bar — breadcrumb, rendered inline (fixed height reserved via cursor advance) - float top_h = ImGui::GetFrameHeight() + 4.0f; - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4.0f, 0.0f)); - RenderTopBar(renderer); - ImGui::PopStyleVar(); - float used = ImGui::GetCursorPosY(); - if (used < top_h) - ImGui::SetCursorPosY(top_h); - - ImGui::Separator(); - - // Main split — left sidecar (fixed width) + right content (fills rest) - float remaining_h = ImGui::GetContentRegionAvail().y; - float sidecar_w = 180.0f; - float content_w = ImGui::GetContentRegionAvail().x - sidecar_w - ImGui::GetStyle().ItemSpacing.x; - - ImGui::BeginChild("##cb_sidecar", ImVec2(sidecar_w, remaining_h), false); - RenderTreeBrowser(); - ImGui::EndChild(); - - ImGui::SameLine(); - - if (content_w > 10.0f) - { - ImGui::BeginChild("##cb_content", ImVec2(content_w, remaining_h), false); - { - ImVec2 mn = ImGui::GetCursorScreenPos(); - ImVec2 av = ImGui::GetContentRegionAvail(); - m_right_pane_hovered = ImGui::IsMouseHoveringRect(mn, {mn.x + av.x, mn.y + av.y}, false); - } - if (m_right_pane_hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) - ImGui::OpenPopup("ContextMenu"); - if (ImGui::BeginPopup("ContextMenu")) - { - char native[MAX_FILE_PATH_COUNT]; - m_current_vfs_dir.ResolveNative(m_workspace_root, native, sizeof(native)); - RenderContextMenu(ContextMenuType::RightPane, native); - ImGui::EndPopup(); - } - ImGui::SetNextItemWidth(-1.0f); - ImGui::InputTextWithHint("##cb_search", "Search ...", m_search_buffer, IM_ARRAYSIZE(m_search_buffer)); - ImGui::Separator(); - RenderContentBrowser(renderer); - ImGui::EndChild(); - } - - // Modals must be in the root window context (not inside a child or table cell) - RenderPopUpMenu(); - - ImGui::End(); - } - - void ProjectViewUIComponent::RenderTopBar(ZEngine::Rendering::Renderers::GraphicRenderer* const /*renderer*/) - { - using namespace ZEngine::Core::VFS; - - bool can_go_back = (m_current_vfs_dir != m_assets_vfs_root); - if (!can_go_back) - ImGui::BeginDisabled(); - if (ImGui::ArrowButton("##cb_back", ImGuiDir_Left)) - { - m_current_vfs_dir = m_current_vfs_dir.Parent(); - m_search_buffer[0] = '\0'; - } - if (!can_go_back) - ImGui::EndDisabled(); - - ImGui::SameLine(0.0f, 6.0f); - - uint32_t depth = m_current_vfs_dir.ComponentCount(); - bool at_root = (depth == 0); - - // Root segment - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2.0f, 1.0f)); - if (at_root) - { - ImGui::TextUnformatted(m_root_label); - } - else - { - if (ImGui::SmallButton(m_root_label)) - { - m_current_vfs_dir = m_assets_vfs_root; - m_search_buffer[0] = '\0'; - } - } - - // Sub-path segments - // VFSPathComponent::Data is a pointer into the path buffer and is NOT - // null-terminated at the component boundary — copy to a local buffer first. - VFSPath accum = m_assets_vfs_root; - for (uint32_t i = 0; i < depth; ++i) - { - VFSPathComponent comp = m_current_vfs_dir.ComponentAt(i); - char label[256]; - snprintf(label, sizeof(label), "%.*s", static_cast(comp.Length), comp.Data); - - auto next = accum.Append(label); - if (!next.Succeeded()) - break; - accum = next.Value(); - - ImGui::SameLine(0.0f, 3.0f); - ImGui::TextDisabled("›"); - ImGui::SameLine(0.0f, 3.0f); - - bool is_last = (i == depth - 1); - if (is_last) - { - ImGui::TextUnformatted(label); - } - else - { - ImGui::PushID(static_cast(i)); - if (ImGui::SmallButton(label)) - { - m_current_vfs_dir = accum; - m_search_buffer[0] = '\0'; - } - ImGui::PopID(); - } - } - ImGui::PopStyleVar(); - } - - void ProjectViewUIComponent::RenderContentBrowser(ZEngine::Rendering::Renderers::GraphicRenderer* const renderer) - { - const float padding = 16.0f; - const float cellSize = m_thumbnail_size + padding; - const float panelWidth = ImGui::GetContentRegionAvail().x; - const int columnCount = std::max(1, static_cast(panelWidth / cellSize)); - - if (ImGui::BeginTable("GridTable", columnCount)) - { - if (auto len = secure_strlen(m_search_buffer)) - { - // Rebuild cached results only when the query changes. - if (strcmp(m_search_buffer, m_last_search) != 0) - { - secure_strcpy(m_last_search, sizeof(m_last_search), m_search_buffer); - m_search_results.clear(); - - char search_term_lower[MAX_FILE_PATH_COUNT] = {}; - for (size_t i = 0; i < len && i < sizeof(search_term_lower) - 1; ++i) - search_term_lower[i] = static_cast(::tolower(static_cast(m_search_buffer[i]))); - - auto scratch = ZGetScratch(&m_local_arena); - m_directory_cache->ForEachDir([&](const VFSPath& /*dir*/, ZEngine::Core::Containers::ArrayView entries) { - for (size_t i = 0; i < entries.size(); ++i) - { - char name_lower[MAX_FILE_PATH_COUNT] = {}; - char raw[MAX_FILE_PATH_COUNT]; - entries[i].Path.CopyFilename(raw, sizeof(raw)); - size_t rlen = secure_strlen(raw); - for (size_t j = 0; j < rlen && j < sizeof(name_lower) - 1; ++j) - name_lower[j] = static_cast(::tolower(static_cast(raw[j]))); - - if (!entries[i].Path.Extension().Equals(".meta") && Helpers::KMPSearch(scratch.Arena, name_lower, search_term_lower)) - m_search_results.push_back(entries[i]); - } - }); - ZReleaseScratch(scratch); - } - - for (const auto& entry : m_search_results) - { - ImGui::TableNextColumn(); - RenderContentTile(renderer, entry); - } - } - else - { - if (m_last_search[0] != '\0') - { - m_last_search[0] = '\0'; - m_search_results.clear(); - } - auto listing = m_directory_cache->GetListing(m_current_vfs_dir); - for (size_t i = 0; i < listing.size(); ++i) - { - if (listing[i].Path.Extension().Equals(".meta")) - continue; - ImGui::TableNextColumn(); - RenderContentTile(renderer, listing[i]); - } - } - ImGui::EndTable(); - } - } - - void ProjectViewUIComponent::RenderContentTile(ZEngine::Rendering::Renderers::GraphicRenderer* const renderer, const VFSDirEntry& entry) - { - char name[MAX_FILE_PATH_COUNT]; - entry.Path.CopyFilename(name, sizeof(name)); - - // Icon fills the top portion, name overlaid on a semi-transparent footer strip. - const float sz = m_thumbnail_size; - const float pad = 6.0f; - const float line_h = ImGui::GetTextLineHeightWithSpacing(); - const float footer_h = line_h * 2.0f + pad * 2.0f; // fixed 2-line footer - const float card_w = sz + pad * 2.0f; - const float card_h = sz + footer_h; - const float rounding = 4.0f; - - ImGui::PushID(entry.Path.CStr()); - - ImVec2 origin = ImGui::GetCursorScreenPos(); - ImVec2 card_end = {origin.x + card_w, origin.y + card_h}; - const bool hov = ImGui::IsMouseHoveringRect(origin, card_end); - - // Invisible button for interaction (hover, click, drag-drop) - ImGui::InvisibleButton("##card", {card_w, card_h}); - - // Drag-and-drop (files only) - if (!entry.IsDirectory && ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceAllowNullID)) - { - char native[MAX_FILE_PATH_COUNT]; - entry.Path.ResolveNative(m_workspace_root, native, sizeof(native)); - ImGui::SetDragDropPayload("CONTENT_BROWSER_FILE_DRAG_OP", native, secure_strlen(native) + 1); - ImGui::EndDragDropSource(); - } - - // Navigate into directory on double-click - if (hov && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left) && entry.IsDirectory) - { - m_current_vfs_dir = entry.Path; - secure_memset(m_search_buffer, 0, sizeof(m_search_buffer), sizeof(m_search_buffer)); - } - - // Context menu - if (hov && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) - ImGui::OpenPopup("ItemContextMenu"); - if (ImGui::BeginPopup("ItemContextMenu")) - { - char native[MAX_FILE_PATH_COUNT]; - entry.Path.ResolveNative(m_workspace_root, native, sizeof(native)); - entry.IsDirectory ? RenderContextMenu(ContextMenuType::Folder, native) : RenderContextMenu(ContextMenuType::File, native); - ImGui::EndPopup(); - } - - ImDrawList* dl = ImGui::GetWindowDrawList(); - ImVec2 icon_end = {origin.x + card_w, origin.y + sz}; - - // Resolve current theme - bool dark_theme = true; - if (ParentLayer && ParentLayer->CurrentApp) - { - auto* app = reinterpret_cast(ParentLayer->CurrentApp); - if (app->Configuration) - dark_theme = app->Configuration->DarkTheme; - } - - // Card body background - const ImU32 card_bg = dark_theme ? (hov ? IM_COL32(70, 70, 70, 200) : IM_COL32(42, 42, 42, 140)) : (hov ? IM_COL32(213, 217, 224, 240) : IM_COL32(237, 239, 243, 210)); - const ImU32 card_border = dark_theme ? IM_COL32(0, 0, 0, 0) : IM_COL32(200, 204, 212, 180); - dl->AddRectFilled(origin, card_end, card_bg, rounding); - if (!dark_theme) - dl->AddRect(origin, card_end, card_border, rounding, 0, 1.0f); - - // When a per-asset thumbnail is ready, call - // dl->AddImage((ImTextureID)(intptr_t)thumb.Index, ixo, {ixo.x + ic, ixo.y + ic * 0.92f}) - // instead of DrawContentIcon(). - const float ic = sz * 0.85f; - const float ofx = (sz - ic) * 0.5f + pad; - const float ofy = (sz - ic * 0.92f) * 0.5f; - ImVec2 ixo = {origin.x + ofx, origin.y + ofy}; - DrawContentIcon(dl, ixo, ic, GetContentIconType(entry.IsDirectory, entry.Path.Extension()), dark_theme); - - // Semi-transparent footer strip - ImVec2 footer_min = {origin.x, origin.y + sz}; - const ImU32 footer_col = dark_theme ? IM_COL32(0, 0, 0, 140) : IM_COL32(200, 204, 212, 180); - dl->AddRectFilled(footer_min, card_end, footer_col, rounding, ImDrawFlags_RoundCornersBottom); - - // Name text inside footer - { - const float text_x = footer_min.x + pad; - const float text_y = footer_min.y + pad; - const float max_w = card_w - pad * 2.0f; - const ImVec4 clip = {text_x, text_y, text_x + max_w, text_y + line_h * 2.0f}; - const ImU32 text_col = dark_theme ? IM_COL32(230, 230, 230, 255) : IM_COL32(31, 41, 55, 255); - dl->AddText(nullptr, 0.0f, {text_x, text_y}, text_col, name, nullptr, max_w, &clip); - } - - ImGui::PopID(); - } - - void ProjectViewUIComponent::RenderFilteredContent(ZEngine::Rendering::Renderers::GraphicRenderer* const renderer, const char* searchTerm) - { - auto scratch = ZGetScratch(&m_local_arena); - char name_lower[MAX_FILE_PATH_COUNT]; - - m_directory_cache->ForEachDir([&](const VFSPath& /*dir*/, ZEngine::Core::Containers::ArrayView entries) { - for (size_t i = 0; i < entries.size(); ++i) - { - const VFSDirEntry& entry = entries[i]; - - char raw[MAX_FILE_PATH_COUNT]; - entry.Path.CopyFilename(raw, sizeof(raw)); - - size_t len = secure_strlen(raw); - size_t copy = (len < sizeof(name_lower) - 1) ? len : sizeof(name_lower) - 1; - for (size_t j = 0; j < copy; ++j) - { - name_lower[j] = static_cast(::tolower(static_cast(raw[j]))); - } - name_lower[copy] = '\0'; - - if (!entry.Path.Extension().Equals(".meta") && Helpers::KMPSearch(scratch.Arena, name_lower, searchTerm)) - { - ImGui::TableNextColumn(); - RenderContentTile(renderer, entry); - } - } - }); - - ZReleaseScratch(scratch); - } - - // Draws a small folder icon at `pos` with text-line height, using the - // same golden palette as the content browser tiles. - static void DrawTreeFolderIcon(ImDrawList* dl, ImVec2 pos, float line_h) - { - const float sz = line_h * 0.80f; - const float off_y = (line_h - sz) * 0.50f; - const float tab_w = sz * 0.42f; - const float tab_h = sz * 0.18f; - const float body_top = pos.y + off_y + tab_h; - const ImU32 tab_col = IM_COL32(220, 195, 120, 255); - const ImU32 body_col = IM_COL32(200, 175, 100, 255); - dl->AddRectFilled({pos.x, pos.y + off_y}, {pos.x + tab_w, body_top + 1.0f}, tab_col, 1.0f); - dl->AddRectFilled({pos.x, body_top}, {pos.x + sz, pos.y + off_y + sz * 0.88f}, body_col, 1.0f); - } - - void ProjectViewUIComponent::RenderTreeBrowser() - { - ImGui::PushID("##root"); - bool nodeOpen = ImGui::TreeNodeEx("##", ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_DefaultOpen); - bool clicked = ImGui::IsItemClicked(); - bool rightClicked = ImGui::IsItemClicked(ImGuiMouseButton_Right); - - // Folder icon + root label inline - ImGui::SameLine(); - { - float lh = ImGui::GetTextLineHeight(); - ImVec2 pos = ImGui::GetCursorScreenPos(); - DrawTreeFolderIcon(ImGui::GetWindowDrawList(), pos, lh); - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + lh * 0.80f + 3.0f); - } - ImGui::TextUnformatted(m_root_label); - clicked |= ImGui::IsItemClicked(); - rightClicked |= ImGui::IsItemClicked(ImGuiMouseButton_Right); - ImGui::PopID(); - - if (clicked) - { - m_current_vfs_dir = m_assets_vfs_root; - secure_memset(m_search_buffer, 0, sizeof(m_search_buffer), sizeof(m_search_buffer)); - } - if (rightClicked) - ImGui::OpenPopup("RootContextMenu"); - - if (ImGui::BeginPopup("RootContextMenu")) - { - char native[MAX_FILE_PATH_COUNT]; - m_assets_vfs_root.ResolveNative(m_workspace_root, native, sizeof(native)); - RenderContextMenu(ContextMenuType::LeftPane, native); - ImGui::EndPopup(); - } - - if (nodeOpen) - { - RenderDirectoryNode(m_assets_vfs_root); - ImGui::TreePop(); - } - } - - void ProjectViewUIComponent::RenderDirectoryNode(const VFSPath& directory) - { - auto listing = m_directory_cache->GetListing(directory); - for (size_t i = 0; i < listing.size(); ++i) - { - const VFSDirEntry& entry = listing[i]; - if (!entry.IsDirectory) - continue; - - ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow; - if (entry.Path == m_current_vfs_dir) - flags |= ImGuiTreeNodeFlags_Selected; - - char label[MAX_FILE_PATH_COUNT]; - entry.Path.CopyFilename(label, sizeof(label)); - - char popup_id[MAX_FILE_PATH_COUNT + 4 + 1]; - std::snprintf(popup_id, sizeof(popup_id), "Dir_%s", entry.Path.CStr()); - - ImGui::PushID(entry.Path.CStr()); - bool nodeOpen = ImGui::TreeNodeEx("##", flags); - bool clicked = ImGui::IsItemClicked(); - bool rightClicked = ImGui::IsItemClicked(ImGuiMouseButton_Right); - - // Folder icon + label inline - ImGui::SameLine(); - { - float lh = ImGui::GetTextLineHeight(); - ImVec2 pos = ImGui::GetCursorScreenPos(); - DrawTreeFolderIcon(ImGui::GetWindowDrawList(), pos, lh); - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + lh * 0.80f + 3.0f); - } - ImGui::TextUnformatted(label); - clicked |= ImGui::IsItemClicked(); - rightClicked |= ImGui::IsItemClicked(ImGuiMouseButton_Right); - ImGui::PopID(); - - if (clicked) - { - m_current_vfs_dir = entry.Path; - secure_memset(m_search_buffer, 0, sizeof(m_search_buffer), sizeof(m_search_buffer)); - } - if (rightClicked) - ImGui::OpenPopup(popup_id); - - if (ImGui::BeginPopup(popup_id)) - { - char native[MAX_FILE_PATH_COUNT]; - entry.Path.ResolveNative(m_workspace_root, native, sizeof(native)); - RenderContextMenu(ContextMenuType::LeftPane, native); - ImGui::EndPopup(); - } - - if (nodeOpen) - { - RenderDirectoryNode(entry.Path); - ImGui::TreePop(); - } - } - } - - void ProjectViewUIComponent::HandleCreateFilePopup(const char* path) - { - ImVec2 center = ImGui::GetMainViewport()->GetCenter(); - ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); - - if (ImGui::BeginPopupModal("Create New File", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) - { - ImGui::Text("Enter file name (with extension):"); - ImGui::InputText("##create", m_popup_new_file_name, sizeof(m_popup_new_file_name)); - - if (ImGui::Button("Create", ImVec2(120, 0))) - { - if (secure_strlen(m_popup_new_file_name) > 0) - { - char new_native[MAX_FILE_PATH_COUNT]; - snprintf(new_native, sizeof(new_native), "%s/%s", path, m_popup_new_file_name); - - size_t ws_len = secure_strlen(m_workspace_root); - const char* rel = (ws_len > 0 && std::strncmp(new_native, m_workspace_root, ws_len) == 0) ? new_native + ws_len : new_native; - auto vfs_res = VFSPath::Parse(rel); - - if (vfs_res.Succeeded()) - { - auto exists = m_vfs_context->Exists(vfs_res.Value()); - if (!exists.Succeeded() || !exists.Value()) - { - auto file_res = m_vfs_context->Open(vfs_res.Value(), VFSOpenFlags::Write | VFSOpenFlags::Create); - if (file_res.Succeeded()) - { - m_vfs_context->Close(file_res.Value()); - m_active_popup = PopupType::None; - TriggerScan(); - ImGui::CloseCurrentPopup(); - } - else - { - ZENGINE_CORE_ERROR("Failed to create file: {}", m_popup_new_file_name); - } - } - else - { - ZENGINE_CORE_ERROR("A file with the name {} already exists!", m_popup_new_file_name); - } - } - } - else - { - ZENGINE_CORE_ERROR("File name cannot be empty."); - } - } - - ImGui::SameLine(); - - if (ImGui::Button("Cancel", ImVec2(120, 0))) - { - m_active_popup = PopupType::None; - ImGui::CloseCurrentPopup(); - } - - ImGui::EndPopup(); - } - } - - void ProjectViewUIComponent::HandleCreateFolderPopup(const char* path) - { - ImVec2 center = ImGui::GetMainViewport()->GetCenter(); - ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); - - if (ImGui::BeginPopupModal("Create New Folder", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) - { - ImGui::Text("Enter folder name:"); - ImGui::InputText("##create", m_popup_new_folder_name, sizeof(m_popup_new_folder_name)); - - if (ImGui::Button("Create", ImVec2(120, 0))) - { - if (secure_strlen(m_popup_new_folder_name) > 0) - { - char new_native[MAX_FILE_PATH_COUNT]; - snprintf(new_native, sizeof(new_native), "%s/%s", path, m_popup_new_folder_name); - - size_t ws_len = secure_strlen(m_workspace_root); - const char* rel = (ws_len > 0 && std::strncmp(new_native, m_workspace_root, ws_len) == 0) ? new_native + ws_len : new_native; - auto vfs_res = VFSPath::Parse(rel); - - if (vfs_res.Succeeded()) - { - auto exists = m_vfs_context->Exists(vfs_res.Value()); - if (!exists.Succeeded() || !exists.Value()) - { - m_vfs_context->CreateDir(vfs_res.Value()); - m_active_popup = PopupType::None; - TriggerScan(); - ImGui::CloseCurrentPopup(); - } - else - { - ZENGINE_CORE_ERROR("A folder with the name {} already exists!", m_popup_new_folder_name); - } - } - } - else - { - ZENGINE_CORE_ERROR("Folder name cannot be empty."); - } - } - ImGui::SameLine(); - if (ImGui::Button("Cancel", ImVec2(120, 0))) - { - m_active_popup = PopupType::None; - ImGui::CloseCurrentPopup(); - } - ImGui::EndPopup(); - } - } - - void ProjectViewUIComponent::HandleRenameFolderPopup(const char* path) - { - char root_native[MAX_FILE_PATH_COUNT]; - m_assets_vfs_root.ResolveNative(m_workspace_root, root_native, sizeof(root_native)); - if (strcmp(m_popup_target_path, root_native) == 0) - { - ZENGINE_CORE_ERROR("Cannot rename root folder"); - m_active_popup = PopupType::None; - return; - } - - ImVec2 center = ImGui::GetMainViewport()->GetCenter(); - ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); - - if (ImGui::BeginPopupModal("Rename Folder", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) - { - if (!m_popup_rename_initialized) - { - const char* slash = strrchr(path, '/'); - secure_strcpy(m_popup_rename_name, sizeof(m_popup_rename_name), slash ? slash + 1 : path); - m_popup_rename_initialized = true; - } - - ImGui::Text("Enter new folder name:"); - ImGui::InputText("##rename", m_popup_rename_name, sizeof(m_popup_rename_name)); - - if (ImGui::Button("Rename", ImVec2(120, 0))) - { - if (secure_strlen(m_popup_rename_name) > 0) - { - const char* last_slash = strrchr(path, '/'); - char parent[MAX_FILE_PATH_COUNT] = {}; - if (last_slash && last_slash > path) - secure_strncpy(parent, sizeof(parent), path, (size_t) (last_slash - path)); - - char new_native[MAX_FILE_PATH_COUNT]; - snprintf(new_native, sizeof(new_native), "%s/%s", parent, m_popup_rename_name); - - size_t ws_len = secure_strlen(m_workspace_root); - auto to_vfs = [&](const char* n) { - const char* rel = (ws_len > 0 && std::strncmp(n, m_workspace_root, ws_len) == 0) ? n + ws_len : n; - return VFSPath::Parse(rel); - }; - auto src_res = to_vfs(path); - auto dst_res = to_vfs(new_native); - - if (src_res.Succeeded() && dst_res.Succeeded()) - { - auto exists = m_vfs_context->Exists(dst_res.Value()); - if (!exists.Succeeded() || !exists.Value()) - { - m_vfs_context->Rename(src_res.Value(), dst_res.Value()); - m_current_vfs_dir = m_assets_vfs_root; - m_active_popup = PopupType::None; - m_popup_rename_initialized = false; - TriggerScan(); - ImGui::CloseCurrentPopup(); - } - else - { - ZENGINE_CORE_ERROR("A folder with the name {} already exists!", m_popup_rename_name); - } - } - } - else - { - ZENGINE_CORE_ERROR("Folder name cannot be empty."); - } - } - - ImGui::SameLine(); - - if (ImGui::Button("Cancel", ImVec2(120, 0))) - { - m_active_popup = PopupType::None; - m_popup_rename_initialized = false; - ImGui::CloseCurrentPopup(); - } - - ImGui::EndPopup(); - } - } - - void ProjectViewUIComponent::HandleDeleteFilePopup(const char* path) - { - ImVec2 center = ImGui::GetMainViewport()->GetCenter(); - ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); - - if (ImGui::BeginPopupModal("Delete File", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) - { - const char* filename = strrchr(path, '/'); - filename = filename ? filename + 1 : path; - - ImGui::Text("Are you sure you want to delete this file?"); - ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "%s", filename); - - ImGui::Spacing(); - ImGui::Separator(); - ImGui::Spacing(); - - if (ImGui::Button("Delete", ImVec2(120, 0))) - { - size_t ws_len = secure_strlen(m_workspace_root); - const char* rel = (ws_len > 0 && std::strncmp(path, m_workspace_root, ws_len) == 0) ? path + ws_len : path; - auto vfs_res = VFSPath::Parse(rel); - - if (vfs_res.Succeeded()) - { - auto exists = m_vfs_context->Exists(vfs_res.Value()); - if (exists.Succeeded() && exists.Value()) - m_vfs_context->Remove(vfs_res.Value()); - } - - m_active_popup = PopupType::None; - TriggerScan(); - ImGui::CloseCurrentPopup(); - } - - ImGui::SameLine(); - - if (ImGui::Button("Cancel", ImVec2(120, 0))) - { - m_active_popup = PopupType::None; - ImGui::CloseCurrentPopup(); - } - - ImGui::EndPopup(); - } - } - - void ProjectViewUIComponent::HandleDeleteFolderPopup(const char* path) - { - char root_native[MAX_FILE_PATH_COUNT]; - m_assets_vfs_root.ResolveNative(m_workspace_root, root_native, sizeof(root_native)); - if (strcmp(m_popup_target_path, root_native) == 0) - { - ZENGINE_CORE_ERROR("Cannot delete root folder"); - m_active_popup = PopupType::None; - return; - } - - ImVec2 center = ImGui::GetMainViewport()->GetCenter(); - ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); - - if (ImGui::BeginPopupModal("Delete Folder", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) - { - const char* dirname = strrchr(path, '/'); - dirname = dirname ? dirname + 1 : path; - - ImGui::Text("Are you sure you want to delete this folder?"); - ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "%s", dirname); - - { - size_t ws_len = secure_strlen(m_workspace_root); - const char* rel = (ws_len > 0 && std::strncmp(path, m_workspace_root, ws_len) == 0) ? path + ws_len : path; - auto vp = VFSPath::Parse(rel); - if (vp.Succeeded()) - { - // Use a scratch arena so this per-frame listing doesn't leak into m_local_arena. - auto scratch = ZGetScratch(&m_local_arena); - auto listing = m_vfs_context->List(vp.Value(), scratch.Arena); - if (listing.Succeeded() && !listing.Value().empty()) - { - ImGui::Spacing(); - ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "Warning: This folder is not empty!"); - ImGui::Text("All contents will be permanently deleted."); - } - ZReleaseScratch(scratch); - } - } - - ImGui::Spacing(); - ImGui::Separator(); - ImGui::Spacing(); - - if (ImGui::Button("Delete", ImVec2(120, 0))) - { - size_t ws_len = secure_strlen(m_workspace_root); - const char* rel = (ws_len > 0 && std::strncmp(path, m_workspace_root, ws_len) == 0) ? path + ws_len : path; - auto vp = VFSPath::Parse(rel); - if (vp.Succeeded()) - m_vfs_context->RemoveAll(vp.Value()); - m_current_vfs_dir = m_assets_vfs_root; - m_active_popup = PopupType::None; - TriggerScan(); - ImGui::CloseCurrentPopup(); - } - - ImGui::SameLine(); - - if (ImGui::Button("Cancel", ImVec2(120, 0))) - { - m_active_popup = PopupType::None; - ImGui::CloseCurrentPopup(); - } - - ImGui::EndPopup(); - } - } - - void ProjectViewUIComponent::HandleRenameFilePopup(const char* path) - { - ImVec2 center = ImGui::GetMainViewport()->GetCenter(); - ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); - - if (ImGui::BeginPopupModal("Rename File", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) - { - if (!m_popup_rename_initialized) - { - const char* slash = strrchr(path, '/'); - secure_strcpy(m_popup_rename_name, sizeof(m_popup_rename_name), slash ? slash + 1 : path); - m_popup_rename_initialized = true; - } - - ImGui::Text("Enter new file name (with extension):"); - ImGui::InputText("##rename", m_popup_rename_name, sizeof(m_popup_rename_name)); - - if (ImGui::Button("Rename", ImVec2(120, 0))) - { - if (secure_strlen(m_popup_rename_name) > 0) - { - const char* last_slash = strrchr(path, '/'); - char parent[MAX_FILE_PATH_COUNT] = {}; - if (last_slash && last_slash > path) - secure_strncpy(parent, sizeof(parent), path, (size_t) (last_slash - path)); - - char new_native[MAX_FILE_PATH_COUNT]; - snprintf(new_native, sizeof(new_native), "%s/%s", parent, m_popup_rename_name); - - size_t ws_len = secure_strlen(m_workspace_root); - auto to_vfs = [&](const char* n) { - const char* rel = (ws_len > 0 && std::strncmp(n, m_workspace_root, ws_len) == 0) ? n + ws_len : n; - return VFSPath::Parse(rel); - }; - auto src_res = to_vfs(path); - auto dst_res = to_vfs(new_native); - - if (src_res.Succeeded() && dst_res.Succeeded()) - { - auto exists = m_vfs_context->Exists(dst_res.Value()); - if (!exists.Succeeded() || !exists.Value()) - { - m_vfs_context->Rename(src_res.Value(), dst_res.Value()); - m_active_popup = PopupType::None; - m_popup_rename_initialized = false; - TriggerScan(); - ImGui::CloseCurrentPopup(); - } - else - { - ZENGINE_CORE_ERROR("A file with the name {} already exists!", m_popup_rename_name); - } - } - } - else - { - ZENGINE_CORE_ERROR("File name cannot be empty."); - } - } - - ImGui::SameLine(); - - if (ImGui::Button("Cancel", ImVec2(120, 0))) - { - m_active_popup = PopupType::None; - m_popup_rename_initialized = false; - ImGui::CloseCurrentPopup(); - } - - ImGui::EndPopup(); - } - } - - void ProjectViewUIComponent::RenderContextMenu(ContextMenuType type, const char* targetPath) - { - // Helper: set the target path, open the named popup, and reset the relevant - // input buffer so each opening starts fresh. - auto open_popup = [&](PopupType popup, const char* popup_name) { - secure_strcpy(m_popup_target_path, sizeof(m_popup_target_path), targetPath ? targetPath : ""); - m_active_popup = popup; - if (popup == PopupType::NewFile) - secure_strcpy(m_popup_new_file_name, sizeof(m_popup_new_file_name), "NewFile.txt"); - else if (popup == PopupType::CreateFolder) - secure_strcpy(m_popup_new_folder_name, sizeof(m_popup_new_folder_name), "New Folder"); - else if (popup == PopupType::RenameFolder || popup == PopupType::RenameFile) - { - m_popup_rename_name[0] = '\0'; - m_popup_rename_initialized = false; - } - ImGui::OpenPopup(popup_name); - }; - - switch (type) - { - case ContextMenuType::RightPane: - if (ImGui::MenuItem("Create New File")) - open_popup(PopupType::NewFile, "Create New File"); - if (ImGui::MenuItem("Create New Folder")) - open_popup(PopupType::CreateFolder, "Create New Folder"); - break; - - case ContextMenuType::LeftPane: - if (ImGui::MenuItem("Create New Folder")) - open_popup(PopupType::CreateFolder, "Create New Folder"); - if (ImGui::MenuItem("Create New File")) - open_popup(PopupType::NewFile, "Create New File"); - if (ImGui::MenuItem("Delete Folder")) - open_popup(PopupType::DeleteFolder, "Delete Folder"); - if (ImGui::MenuItem("Rename Folder")) - open_popup(PopupType::RenameFolder, "Rename Folder"); - break; - - case ContextMenuType::File: - if (ImGui::MenuItem("Rename File")) - open_popup(PopupType::RenameFile, "Rename File"); - if (ImGui::MenuItem("Delete File")) - open_popup(PopupType::RemoveFile, "Delete File"); - break; - - case ContextMenuType::Folder: - if (ImGui::MenuItem("Rename Folder")) - open_popup(PopupType::RenameFolder, "Rename Folder"); - if (ImGui::MenuItem("Delete Folder")) - open_popup(PopupType::DeleteFolder, "Delete Folder"); - break; - } - } - - void ProjectViewUIComponent::RenderPopUpMenu() - { - switch (m_active_popup) - { - case PopupType::CreateFolder: - HandleCreateFolderPopup(m_popup_target_path); - break; - case PopupType::RenameFolder: - HandleRenameFolderPopup(m_popup_target_path); - break; - case PopupType::DeleteFolder: - HandleDeleteFolderPopup(m_popup_target_path); - break; - case PopupType::NewFile: - HandleCreateFilePopup(m_popup_target_path); - break; - case PopupType::RemoveFile: - HandleDeleteFilePopup(m_popup_target_path); - break; - case PopupType::RenameFile: - HandleRenameFilePopup(m_popup_target_path); - break; - case PopupType::None: - default: - break; - } - } - -} // namespace Tetragrama::Components diff --git a/Tetragrama/Components/ProjectViewUIComponent.h b/Tetragrama/Components/ProjectViewUIComponent.h deleted file mode 100644 index 90d9c4744..000000000 --- a/Tetragrama/Components/ProjectViewUIComponent.h +++ /dev/null @@ -1,88 +0,0 @@ -#pragma once -#include -#include -#include -#include -namespace Tetragrama::Components -{ - enum class ContextMenuType - { - RightPane, - LeftPane, - File, - Folder - }; - - enum class PopupType - { - None, - CreateFolder, - NewFile, - RenameFolder, - RenameFile, - DeleteFolder, - RemoveFile, - }; - - class ProjectViewUIComponent : public UIComponent - { - public: - ProjectViewUIComponent(); - virtual ~ProjectViewUIComponent(); - - void Initialize(Layers::ImguiLayer* parent = nullptr, const char* name = "Project", bool visibility = true, bool closed = false) override; - - void Update(ZEngine::Core::TimeStep dt) override; - - virtual void Render(ZEngine::Rendering::Renderers::GraphicRenderer* const renderer, ZEngine::Hardwares::CommandBuffer* const command_buffer) override; - - // Render Panes - void RenderTopBar(ZEngine::Rendering::Renderers::GraphicRenderer* const renderer); - void RenderContentBrowser(ZEngine::Rendering::Renderers::GraphicRenderer* const renderer); - void RenderFilteredContent(ZEngine::Rendering::Renderers::GraphicRenderer* const renderer, const char* searchTerm); - void RenderDirectoryNode(const ZEngine::Core::VFS::VFSPath& directory); - void RenderContentTile(ZEngine::Rendering::Renderers::GraphicRenderer* const renderer, const ZEngine::Core::VFS::VFSDirEntry& entry); - void RenderTreeBrowser(); - - // Popup helpers — all paths are native absolute C strings - void RenderContextMenu(ContextMenuType type, const char* targetPath); - void RenderPopUpMenu(); - void HandleCreateFolderPopup(const char* path); - void HandleCreateFilePopup(const char* path); - void HandleRenameFolderPopup(const char* path); - void HandleDeleteFolderPopup(const char* path); - void HandleRenameFilePopup(const char* path); - void HandleDeleteFilePopup(const char* path); - - void TriggerScan(); - - private: - ZEngine::Core::Memory::ArenaAllocator m_local_arena = {}; - - ZEngine::Core::VFS::IVFSContext* m_vfs_context = nullptr; - ZEngine::Core::VFS::VFSDirectoryCache* m_directory_cache = nullptr; - ZEngine::Core::VFS::VFSScanner* m_scanner = nullptr; - - ZEngine::Core::VFS::VFSPath m_assets_vfs_root = {}; - ZEngine::Core::VFS::VFSPath m_current_vfs_dir = {}; - char m_root_label[MAX_FILE_PATH_COUNT] = ""; - char m_workspace_root[MAX_FILE_PATH_COUNT] = ""; - - PopupType m_active_popup = PopupType::None; - char m_popup_target_path[MAX_FILE_PATH_COUNT] = {}; - // Input buffers for popup modals — promoted from static locals so each - // instance has its own state and opening a new item resets the buffer. - char m_popup_new_file_name[MAX_FILE_PATH_COUNT] = "NewFile.txt"; - char m_popup_new_folder_name[MAX_FILE_PATH_COUNT] = "New Folder"; - char m_popup_rename_name[MAX_FILE_PATH_COUNT] = {}; - bool m_popup_rename_initialized = false; - - // Search result cache — rebuilt only when m_search_buffer changes. - char m_last_search[MAX_FILE_PATH_COUNT] = {}; - std::vector m_search_results; - - bool m_right_pane_hovered = false; - static constexpr float m_thumbnail_size = 80.0f; - char m_search_buffer[MAX_FILE_PATH_COUNT] = ""; - }; -} // namespace Tetragrama::Components diff --git a/Tetragrama/Components/SceneViewportUIComponent.cpp b/Tetragrama/Components/SceneViewportUIComponent.cpp deleted file mode 100644 index c8064f795..000000000 --- a/Tetragrama/Components/SceneViewportUIComponent.cpp +++ /dev/null @@ -1,312 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -/**/ -#include -#include -#include -#include - -using namespace Tetragrama::Components::Event; -using namespace ZEngine::Rendering::Renderers; -using namespace ZEngine::Hardwares; -using namespace ZEngine::Rendering; -using namespace ZEngine; - -namespace Tetragrama::Components -{ - SceneViewportUIComponent::SceneViewportUIComponent() {} - - SceneViewportUIComponent::~SceneViewportUIComponent() {} - - void SceneViewportUIComponent::Initialize(Layers::ImguiLayer* parent, const char* name, bool visibility, bool closed) - { - UIComponent::Initialize(parent, name, visibility, closed); - - // ImGuizmo configuration - ImGuizmo::AllowAxisFlip(false); - ImGuizmo::SetOrthographic(false); - } - - void SceneViewportUIComponent::Update(ZEngine::Core::TimeStep dt) - { - auto app = reinterpret_cast(ParentLayer->CurrentApp); - - if ((m_viewport_size.x != m_content_region_available_size.x) || (m_viewport_size.y != m_content_region_available_size.y)) - { - if (!m_is_resizing) - { - m_is_resizing = true; - } - - m_viewport_size = m_content_region_available_size; - m_idle_frame_count = 0; - } - else if (m_is_resizing) - { - m_idle_frame_count++; - if (m_idle_frame_count >= app->RenderPipeline->Device->SwapchainPtr->IdleFrameThreshold) - { - m_is_resizing = false; - m_request_renderer_resize = true; - } - } - - auto* camera_controller = app->CameraController; - - camera_controller->SetViewportOrigin(m_viewport_bounds[0].x, m_viewport_bounds[0].y); - - if (m_request_renderer_resize) - { - camera_controller->SetViewport(m_viewport_size.x, m_viewport_size.y); - } - - if (m_is_window_hovered && m_is_window_focused) - { - camera_controller->ResumeEventProcessing(); - } - else - { - camera_controller->PauseEventProcessing(); - } - - if (m_is_window_clicked && m_is_window_hovered && m_is_window_focused) - { - auto mouse_position = ImGui::GetMousePos(); - mouse_position.x -= m_viewport_bounds[0].x; - mouse_position.y -= m_viewport_bounds[0].y; - - auto mouse_bounded_x = static_cast(mouse_position.x); - auto mouse_bounded_y = static_cast(mouse_position.y); - // Todo : We should store mouse position... - } - } - - void SceneViewportUIComponent::Render(ZEngine::Rendering::Renderers::GraphicRenderer* const renderer, ZEngine::Hardwares::CommandBuffer* const command_buffer) - { - auto app = reinterpret_cast(ParentLayer->CurrentApp); - - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); - ImGui::Begin(Name, (CanBeClosed ? &CanBeClosed : NULL), ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoMove); - - auto viewport_offset = ImGui::GetCursorPos(); - m_content_region_available_size = ImGui::GetContentRegionAvail(); - m_is_window_focused = ImGui::IsWindowFocused(); - m_is_window_hovered = ImGui::IsWindowHovered(); - m_is_window_clicked = ImGui::IsMouseClicked(static_cast(ZENGINE_KEY_MOUSE_LEFT)); - - // Scene texture representation - if (!m_scene_texture || m_refresh_texture_handle) - { - m_scene_texture = app->RenderPipeline->SceneRenderer->GetFrameOutput(); - m_refresh_texture_handle = false; - } - - if (m_scene_texture.Valid()) - { - ImGui::Image((ImTextureID) m_scene_texture.Index, m_viewport_size, ImVec2(0, 0), ImVec2(1, 1)); - } - - // ViewPort bound computation - ImVec2 viewport_windows_size = ImGui::GetWindowSize(); - ImVec2 minimum_bound = ImGui::GetWindowPos(); - minimum_bound.x += viewport_offset.x; - minimum_bound.y += viewport_offset.y; - - ImVec2 maximum_bound = {minimum_bound.x + viewport_windows_size.x, minimum_bound.y + viewport_windows_size.y}; - - m_viewport_bounds[0] = minimum_bound; - m_viewport_bounds[1] = maximum_bound; - - // ImGuizmo configuration - ImGuizmo::SetRect(minimum_bound.x, minimum_bound.y, m_viewport_size.x, m_viewport_size.y); - - ImGuizmo::SetDrawlist(); - - if (ImGui::BeginDragDropTarget()) - { - char buf[DEFAULT_STR_BUFFER] = {0}; - if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("CONTENT_BROWSER_FILE_DRAG_OP")) - { - ZEngine::Helpers::secure_memcpy(buf, DEFAULT_STR_BUFFER, payload->Data, payload->DataSize); - if (ZEngine::Helpers::secure_strlen(buf) > 0) - { - auto file_ext = std::filesystem::path(buf).extension().string(); - if (file_ext == ".zescene") - { - Messengers::IMessenger::SendAsync>(EDITOR_COMPONENT_DOCKSPACE_REQUEST_OPENSCENE, Messengers::GenericMessage(buf)); - } - else if (file_ext == ".zemesh") - { - Messengers::IMessenger::SendAsync>(EDITOR_COMPONENT_DOCKSPACE_REQUEST_OPENMESH, Messengers::GenericMessage(buf)); - } - else if (file_ext == ".glb" || file_ext == ".gltf" || file_ext == ".fbx" || file_ext == ".obj") - { - // Route through AssetImporterUIComponent so the import produces - // cooked artifacts (.zemesh / .zetextures / .zematerial) on disk. - // The importer will call AddMeshInstance after cooking completes. - auto* app = reinterpret_cast(ParentLayer->CurrentApp); - if (app && app->Configuration) - { - ZEngine::Helpers::secure_strncpy(app->Configuration->PendingImportPath, sizeof(app->Configuration->PendingImportPath), buf, sizeof(app->Configuration->PendingImportPath) - 1); - - const char* p = buf; - const char* name = strrchr(p, '/'); - name = name ? name + 1 : p; - ZEngine::Helpers::secure_strncpy(app->Configuration->PendingImportName, sizeof(app->Configuration->PendingImportName), name, sizeof(app->Configuration->PendingImportName) - 1); - - app->Configuration->ShowImporter = true; - app->Configuration->FocusImporter = true; - - ZENGINE_CORE_INFO("SceneViewport: queued '{}' for import via panel", buf) - } - } - } - } - ImGui::EndDragDropTarget(); - } - - // Viewport overlay toolbar — after drag-drop so Image stays the last item for drop target. - { - typedef void (*DrawIconFn)(ImDrawList*, ImVec2, float, ImU32); - - auto overlay_btn = [](cstring id, ImVec2 pos, float btn_sz, bool active, ImVec4 active_col, cstring tip, ImDrawList* dl, DrawIconFn icon_fn) -> bool { - ImVec4 bg = active ? ImVec4{active_col.x * .25f, active_col.y * .25f, active_col.z * .25f, .92f} : ImVec4{.12f, .12f, .12f, .70f}; - ImGui::SetCursorScreenPos(pos); - ImGui::PushStyleColor(ImGuiCol_Button, bg); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4{.30f, .30f, .30f, .90f}); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, {active_col.x * .4f, active_col.y * .4f, active_col.z * .4f, 1.f}); - bool hit = ImGui::Button(id, {btn_sz, btn_sz}); - ImGui::PopStyleColor(3); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", tip); - ImVec4 ic = active ? active_col : ImVec4{active_col.x * .5f, active_col.y * .5f, active_col.z * .5f, 1.f}; - icon_fn(dl, pos, btn_sz, ImGui::ColorConvertFloat4ToU32(ic)); - return hit; - }; - - static DrawIconFn icon_grid = [](ImDrawList* d, ImVec2 p, float sz, ImU32 c) { - float m = 5.f; - ImVec2 p0 = {p.x + m, p.y + m}; - float s = sz - m * 2.f; - for (int i = 1; i <= 3; ++i) - { - float tx = p0.x + s * i / 4.f, ty = p0.y + s * i / 4.f; - d->AddLine({tx, p0.y}, {tx, p0.y + s}, c, 1.2f); - d->AddLine({p0.x, ty}, {p0.x + s, ty}, c, 1.2f); - } - }; - static DrawIconFn icon_translate = [](ImDrawList* d, ImVec2 p, float sz, ImU32 c) { - float cx = p.x + sz * .5f, cy = p.y + sz * .5f, r = sz * .28f, al = r * .9f, aw = r * .35f; - d->AddLine({cx, cy - al}, {cx, cy + al}, c, 1.5f); - d->AddLine({cx - al, cy}, {cx + al, cy}, c, 1.5f); - d->AddTriangleFilled({cx, cy - al - aw}, {cx - aw * .6f, cy - al + aw * .5f}, {cx + aw * .6f, cy - al + aw * .5f}, c); - d->AddTriangleFilled({cx, cy + al + aw}, {cx - aw * .6f, cy + al - aw * .5f}, {cx + aw * .6f, cy + al - aw * .5f}, c); - d->AddTriangleFilled({cx - al - aw, cy}, {cx - al + aw * .5f, cy - aw * .6f}, {cx - al + aw * .5f, cy + aw * .6f}, c); - d->AddTriangleFilled({cx + al + aw, cy}, {cx + al - aw * .5f, cy - aw * .6f}, {cx + al - aw * .5f, cy + aw * .6f}, c); - }; - static DrawIconFn icon_rotate = [](ImDrawList* d, ImVec2 p, float sz, ImU32 c) { - float cx = p.x + sz * .5f, cy = p.y + sz * .5f, r = sz * .28f, aw = r * .45f; - d->AddCircle({cx, cy}, r, c, 24, 1.8f); - d->AddTriangleFilled({cx + r, cy}, {cx + r - aw, cy - aw * .6f}, {cx + r - aw, cy + aw * .6f}, c); - }; - static DrawIconFn icon_scale = [](ImDrawList* d, ImVec2 p, float sz, ImU32 c) { - float cx = p.x + sz * .5f, cy = p.y + sz * .5f, r = sz * .28f, h = r * .75f, dot = r * .22f; - d->AddRect({cx - h, cy - h}, {cx + h, cy + h}, c, 0, 0, 1.5f); - for (int dx = -1; dx <= 1; dx += 2) - for (int dy = -1; dy <= 1; dy += 2) - d->AddCircleFilled({cx + dx * h, cy + dy * h}, dot, c, 6); - }; - - auto* scene = app->CurrentScene ? reinterpret_cast(app->CurrentScene) : nullptr; - int& gizmo = app->Configuration->GizmoOperation; - - const float btn_sz = 28.0f, pad = 8.0f, gap = 4.0f; - ImVec2 base = ImGui::GetWindowPos(); - base.x += viewport_offset.x + pad; - base.y += viewport_offset.y + pad; - ImDrawList* dl = ImGui::GetWindowDrawList(); - - ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 4.0f); - - if (scene) - { - bool grid_on = scene->Grid.Enabled; - if (overlay_btn("##grid", base, btn_sz, grid_on, {0.30f, 0.80f, 0.90f, 1.0f}, grid_on ? "Hide Grid" : "Show Grid", dl, icon_grid)) - { - scene->Grid.Enabled = !grid_on; - scene->GridDirty[0].value.store(true, std::memory_order_release); - scene->GridDirty[1].value.store(true, std::memory_order_release); - scene->GridDirty[2].value.store(true, std::memory_order_release); - } - } - - dl->AddLine({base.x + 4.f, base.y + btn_sz + gap}, {base.x + btn_sz - 4.f, base.y + btn_sz + gap}, IM_COL32(255, 255, 255, 40), 1.f); - - struct - { - cstring id; - int op; - ImVec4 col; - cstring tip; - DrawIconFn fn; - } kBtns[] = { - {"##gt", ImGuizmo::OPERATION::TRANSLATE, {.33f, .60f, 1.f, 1.f}, "Translate (T)", icon_translate}, - {"##gr", ImGuizmo::OPERATION::ROTATE, {1.f, .60f, .20f, 1.f}, "Rotate (R)", icon_rotate}, - {"##gs", ImGuizmo::OPERATION::SCALE, {.30f, .85f, .40f, 1.f}, "Scale (S)", icon_scale}, - }; - float gy = base.y + btn_sz + gap * 3.f; - for (int i = 0; i < 3; ++i) - { - bool on = (gizmo == kBtns[i].op); - if (overlay_btn(kBtns[i].id, {base.x, gy + i * (btn_sz + gap)}, btn_sz, on, kBtns[i].col, kBtns[i].tip, dl, kBtns[i].fn)) - gizmo = on ? -1 : kBtns[i].op; - } - - ImGui::PopStyleVar(); - } - - ImGui::End(); - - ImGui::PopStyleVar(); - - if (m_request_renderer_resize) - { - app->State->RenderTargetResizeRequests.Emplace({.Width = (uint32_t) m_viewport_size.x, .Height = (uint32_t) m_viewport_size.y}); - m_refresh_texture_handle = true; - m_request_renderer_resize = false; - } - } - - // std::future - // SceneViewportUIComponent::SceneViewportClickedMessageHandlerAsync(Messengers::ArrayValueMessage& e) - //{ - // // Messengers::IMessenger::Send>>( - // // EDITOR_RENDER_LAYER_SCENE_REQUEST_SELECT_ENTITY_FROM_PIXEL, Messengers::GenericMessage>{e}); co_return; - // } - - // std::future - // SceneViewportUIComponent::SceneViewportFocusedMessageHandlerAsync(Messengers::GenericMessage& e) - //{ - // co_return; - // //co_await Messengers::IMessenger::SendAsync>(EDITOR_RENDER_LAYER_SCENE_REQUEST_FOCUS, - // Messengers::GenericMessage{e}); - // } - - // std::future - // SceneViewportUIComponent::SceneViewportUnfocusedMessageHandlerAsync(Messengers::GenericMessage& e) - //{ - // co_return; - // //co_await Messengers::IMessenger::SendAsync>(EDITOR_RENDER_LAYER_SCENE_REQUEST_UNFOCUS, - // Messengers::GenericMessage{e}); - // } -} // namespace Tetragrama::Components diff --git a/Tetragrama/Components/SceneViewportUIComponent.h b/Tetragrama/Components/SceneViewportUIComponent.h deleted file mode 100644 index 439f4da2a..000000000 --- a/Tetragrama/Components/SceneViewportUIComponent.h +++ /dev/null @@ -1,41 +0,0 @@ -#pragma once -#include -#include -#include -#include -#include -#include -#include - -namespace Tetragrama::Components -{ - class SceneViewportUIComponent : public UIComponent - { - public: - SceneViewportUIComponent(); - virtual ~SceneViewportUIComponent(); - - void Initialize(Layers::ImguiLayer* parent = nullptr, const char* name = "Scene", bool visibility = true, bool closed = false) override; - - void Update(ZEngine::Core::TimeStep dt) override; - virtual void Render(ZEngine::Rendering::Renderers::GraphicRenderer* const renderer, ZEngine::Hardwares::CommandBuffer* const command_buffer) override; - - public: - // std::future SceneViewportClickedMessageHandlerAsync(Messengers::ArrayValueMessage&); - // std::future SceneViewportFocusedMessageHandlerAsync(Messengers::GenericMessage&); - // std::future SceneViewportUnfocusedMessageHandlerAsync(Messengers::GenericMessage&); - - private: - bool m_is_window_focused{false}; - bool m_is_window_hovered{false}; - bool m_is_window_clicked{false}; - bool m_refresh_texture_handle{false}; - bool m_request_renderer_resize{false}; - bool m_is_resizing{false}; - int m_idle_frame_count = 0; - ImVec2 m_viewport_size{0.f, 0.f}; - ImVec2 m_content_region_available_size{0.f, 0.f}; - std::array m_viewport_bounds; - ZEngine::Rendering::Textures::TextureHandle m_scene_texture{}; - }; -} // namespace Tetragrama::Components diff --git a/Tetragrama/Components/StatusBarUIComponent.cpp b/Tetragrama/Components/StatusBarUIComponent.cpp deleted file mode 100644 index 4ae944483..000000000 --- a/Tetragrama/Components/StatusBarUIComponent.cpp +++ /dev/null @@ -1,204 +0,0 @@ -#include -#include -#include -#include - -namespace Tetragrama::Components -{ - static constexpr float kStatusBarHeight = 28.0f; - - void StatusBarUIComponent::Initialize(Layers::ImguiLayer* parent, cstring name, bool visibility, bool closed) - { - UIComponent::Initialize(parent, name, visibility, closed); - } - - void StatusBarUIComponent::Update(ZEngine::Core::TimeStep /*dt*/) {} - - void StatusBarUIComponent::Render(ZEngine::Rendering::Renderers::GraphicRenderer* const, ZEngine::Hardwares::CommandBuffer* const) - { - if (!ParentLayer || !ParentLayer->CurrentApp) - return; - - float raw_dt = ImGui::GetIO().DeltaTime; - m_frame_times[m_ft_head] = raw_dt; - m_ft_head = (m_ft_head + 1) % kFtSamples; - float sum = 0.0f; - for (int i = 0; i < kFtSamples; ++i) - sum += m_frame_times[i]; - m_smoothed_dt = sum / static_cast(kFtSamples); - - const ImGuiViewport* vp = ImGui::GetMainViewport(); - const bool dark = ImGui::GetStyle().Colors[ImGuiCol_WindowBg].x < 0.5f; - - ImGui::SetNextWindowPos({vp->Pos.x, vp->Pos.y + vp->Size.y - kStatusBarHeight}); - ImGui::SetNextWindowSize({vp->Size.x, kStatusBarHeight}); - ImGui::SetNextWindowViewport(vp->ID); - ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); - ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8.0f, 4.0f)); - ImGui::PushStyleColor(ImGuiCol_WindowBg, dark ? ImVec4{0.13f, 0.13f, 0.14f, 1.0f} : ImVec4{0.83f, 0.83f, 0.83f, 1.0f}); - - ImGui::Begin(Name, nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoBringToFrontOnFocus); - ImGui::PopStyleColor(); - ImGui::PopStyleVar(3); - - auto* app = reinterpret_cast(ParentLayer->CurrentApp); - - // Neutral button backgrounds (theme-aware) - ImVec4 btn_off = dark ? ImVec4{0.22f, 0.22f, 0.24f, 0.85f} : ImVec4{0.72f, 0.72f, 0.72f, 1.0f}; - ImVec4 btn_on = dark ? ImVec4{0.28f, 0.28f, 0.32f, 1.0f} : ImVec4{0.60f, 0.60f, 0.62f, 1.0f}; - - // Console — teal-green signature - static constexpr ImU32 kConsoleOn = IM_COL32(55, 210, 150, 255); - static constexpr ImU32 kConsoleOff = IM_COL32(55, 110, 85, 180); - - // Content Browser — amber signature - static constexpr ImU32 kBrowserOn = IM_COL32(255, 185, 50, 255); - static constexpr ImU32 kBrowserOff = IM_COL32(140, 100, 30, 180); - - // Console button - { - bool on = app->Configuration->ShowConsole; - ImGui::PushStyleColor(ImGuiCol_Button, on ? btn_on : btn_off); - ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f); - if (ImGui::SmallButton(" Console ")) - { - app->Configuration->ShowConsole = !on; - app->Configuration->FocusConsole = !on; - } - ImGui::PopStyleVar(); - ImGui::PopStyleColor(); - ImVec2 bmin = ImGui::GetItemRectMin(); - ImVec2 bmax = ImGui::GetItemRectMax(); - float cy = (bmin.y + bmax.y) * 0.5f; - const float isz = 10.0f; - ImVec2 ip = {bmin.x + 4.0f, cy - isz * 0.5f}; - ImDrawList* dl = ImGui::GetWindowDrawList(); - ImU32 ic = on ? kConsoleOn : kConsoleOff; - dl->AddRect(ip, {ip.x + isz, ip.y + isz}, ic, 1.0f, 0, 1.2f); - for (int li = 0; li < 3; ++li) - dl->AddLine({ip.x + 1.5f, ip.y + 2.0f + li * 2.5f}, {ip.x + isz - 1.5f, ip.y + 2.0f + li * 2.5f}, ic, 1.0f); - } - - ImGui::SameLine(0.0f, 6.0f); - - // Content Browser button - { - bool on = app->Configuration->ShowContentBrowser; - ImGui::PushStyleColor(ImGuiCol_Button, on ? btn_on : btn_off); - ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f); - if (ImGui::SmallButton(" Content Browser ")) - { - app->Configuration->ShowContentBrowser = !on; - app->Configuration->FocusContentBrowser = !on; - } - ImGui::PopStyleVar(); - ImGui::PopStyleColor(); - ImVec2 bmin = ImGui::GetItemRectMin(); - ImVec2 bmax = ImGui::GetItemRectMax(); - float cy = (bmin.y + bmax.y) * 0.5f; - const float isz = 10.0f; - ImVec2 ip = {bmin.x + 4.0f, cy - isz * 0.5f}; - ImDrawList* dl = ImGui::GetWindowDrawList(); - ImU32 ic = on ? kBrowserOn : kBrowserOff; - float tw = isz * 0.45f, th = isz * 0.28f; - dl->AddRectFilled({ip.x, ip.y + th}, {ip.x + isz, ip.y + isz}, ic, 1.0f); - dl->AddRectFilled({ip.x, ip.y + th - 1.5f}, {ip.x + tw, ip.y + th + 1.5f}, ic); - } - - ImGui::SameLine(0.0f, 6.0f); - - // Importer button — indigo import-arrow icon - { - static constexpr ImU32 kImporterOn = IM_COL32(170, 130, 255, 255); - static constexpr ImU32 kImporterOff = IM_COL32(95, 70, 150, 180); - bool on = app->Configuration->ShowImporter; - ImGui::PushStyleColor(ImGuiCol_Button, on ? btn_on : btn_off); - ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f); - if (ImGui::SmallButton(" Importer ")) - { - app->Configuration->ShowImporter = !on; - app->Configuration->FocusImporter = !on; - } - ImGui::PopStyleVar(); - ImGui::PopStyleColor(); - ImVec2 bmin = ImGui::GetItemRectMin(); - ImVec2 bmax = ImGui::GetItemRectMax(); - float cy = (bmin.y + bmax.y) * 0.5f; - const float isz2 = 10.0f; - ImVec2 ip2 = {bmin.x + 4.0f, cy - isz2 * 0.5f}; - ImDrawList* dl2 = ImGui::GetWindowDrawList(); - ImU32 ic2 = on ? kImporterOn : kImporterOff; - // Import arrow icon: box at bottom + down-arrow above - dl2->AddRect({ip2.x, ip2.y + isz2 * 0.45f}, {ip2.x + isz2, ip2.y + isz2}, ic2, 1.0f, 0, 1.0f); - float ax = ip2.x + isz2 * 0.5f, ay = ip2.y + isz2 * 0.4f; - dl2->AddLine({ax, ip2.y}, {ax, ay}, ic2, 1.5f); - dl2->AddTriangleFilled({ax, ay + isz2 * 0.18f}, {ax - isz2 * 0.22f, ay}, {ax + isz2 * 0.22f, ay}, ic2); - } - - ImGui::SameLine(0.0f, 10.0f); - ImGui::TextDisabled("|"); - ImGui::SameLine(0.0f, 10.0f); - - auto* scene = app->CurrentScene ? reinterpret_cast(app->CurrentScene) : nullptr; - cstring scene_name = (app->Configuration && app->Configuration->ActiveSceneName.c_str()) ? app->Configuration->ActiveSceneName.c_str() : "-"; - - ImGui::TextDisabled("Scene:"); - ImGui::SameLine(0.0f, 4.0f); - ImGui::TextUnformatted(scene_name); - ImGui::SameLine(0.0f, 10.0f); - ImGui::TextDisabled("|"); - ImGui::SameLine(0.0f, 10.0f); - - if (scene) - { - int32_t sel_id = scene->SelectedInstanceId.value.load(std::memory_order_acquire); - if (sel_id > 0) - { - cstring sel_name = nullptr; - for (uint32_t i = 0; i < scene->Instances.size(); ++i) - if ((int32_t) scene->Instances[i].Id == sel_id) - { - sel_name = scene->Instances[i].Name; - break; - } - ImGui::TextUnformatted((sel_name && sel_name[0]) ? sel_name : "Unnamed"); - } - else - { - ImGui::TextDisabled("Nothing selected"); - } - ImGui::SameLine(0.0f, 10.0f); - ImGui::TextDisabled("|"); - ImGui::SameLine(0.0f, 10.0f); - char ibuf[32]; - snprintf(ibuf, sizeof(ibuf), "Instances: %u", (uint32_t) scene->Instances.size()); - ImGui::TextUnformatted(ibuf); - } - else - { - ImGui::TextDisabled("No scene"); - } - - float fps = m_smoothed_dt > 0.0f ? 1.0f / m_smoothed_dt : 0.0f; - float dt_ms = m_smoothed_dt * 1000.0f; - char cam_buf[64] = "-"; - char fps_buf[48]; - if (app->CameraController) - { - auto pos = app->CameraController->GetPosition(); - snprintf(cam_buf, sizeof(cam_buf), "X: %.1f Y: %.1f Z: %.1f", pos.x, pos.y, pos.z); - } - snprintf(fps_buf, sizeof(fps_buf), "FPS: %.0f %.2f ms", fps, dt_ms); - - float right_w = ImGui::CalcTextSize(cam_buf).x + ImGui::CalcTextSize(" | ").x + ImGui::CalcTextSize(fps_buf).x + 24.0f; - ImGui::SameLine(ImGui::GetContentRegionAvail().x + ImGui::GetCursorPosX() - right_w); - ImGui::TextUnformatted(cam_buf); - ImGui::SameLine(0.0f, 10.0f); - ImGui::TextDisabled("|"); - ImGui::SameLine(0.0f, 10.0f); - ImGui::TextUnformatted(fps_buf); - - ImGui::End(); - } -} // namespace Tetragrama::Components diff --git a/Tetragrama/Components/StatusBarUIComponent.h b/Tetragrama/Components/StatusBarUIComponent.h deleted file mode 100644 index ac2e617f5..000000000 --- a/Tetragrama/Components/StatusBarUIComponent.h +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once -#include - -namespace Tetragrama::Components -{ - class StatusBarUIComponent : public UIComponent - { - public: - StatusBarUIComponent() = default; - ~StatusBarUIComponent() override = default; - - void Initialize(Layers::ImguiLayer* parent = nullptr, cstring name = "##StatusBar", bool visibility = true, bool closed = false) override; - void Update(ZEngine::Core::TimeStep dt) override; - virtual void Render(ZEngine::Rendering::Renderers::GraphicRenderer* const renderer, ZEngine::Hardwares::CommandBuffer* const command_buffer) override; - - private: - static constexpr int kFtSamples = 30; - float m_frame_times[kFtSamples] = {}; - int m_ft_head = 0; - float m_smoothed_dt = 0.016f; - }; -} // namespace Tetragrama::Components diff --git a/Tetragrama/Components/UIComponent.h b/Tetragrama/Components/UIComponent.h deleted file mode 100644 index 796682a06..000000000 --- a/Tetragrama/Components/UIComponent.h +++ /dev/null @@ -1,35 +0,0 @@ -#pragma once -#include -#include -#include -#include - -namespace Tetragrama::Layers -{ - class ImguiLayer; -} - -namespace Tetragrama::Components -{ - struct UIComponent : public ZEngine::Core::IRenderable, public ZEngine::Core::IUpdatable - { - UIComponent() = default; - virtual ~UIComponent() = default; - - virtual void Initialize(Layers::ImguiLayer* parent, cstring name, bool visibility, bool closed) - { - ParentLayer = parent; - Name = name; - IsVisible = visibility; - CanBeClosed = closed; - } - - bool IsVisible = true; - bool CanBeClosed = false; - cstring Name = ""; - uint32_t ChildrenCount = 0; - Tetragrama::Layers::ImguiLayer* ParentLayer = nullptr; - ZEngine::Core::Containers::Array Children = {}; - }; - ZDEFINE_PTR(UIComponent); -} // namespace Tetragrama::Components diff --git a/Tetragrama/Components/ZUI/ZUIComponent.h b/Tetragrama/Components/ZUI/ZUIComponent.h new file mode 100644 index 000000000..b89efb17d --- /dev/null +++ b/Tetragrama/Components/ZUI/ZUIComponent.h @@ -0,0 +1,43 @@ +#pragma once +#include +#include +#include +#include + +namespace Tetragrama::Layers +{ + struct ZUILayer; +} + +namespace Tetragrama::Components +{ + struct ZUIComponent : public ZEngine::Core::IRenderable, public ZEngine::Core::IUpdatable + { + virtual ~ZUIComponent() = default; + + virtual void Initialize(Tetragrama::Layers::ZUILayer* parent, cstring name = "", bool visibility = true) {} + + void Update(ZEngine::Core::TimeStep) override {} + + void Render(ZEngine::Rendering::Renderers::GraphicRenderer* const, ZEngine::Hardwares::CommandBuffer* const) override {} + + // Called each frame inside ZUILayer::BuildUI — build the box tree for this component + virtual void BuildUI(ZEngine::UI::ZUIContext* ctx) {} + + cstring Name = nullptr; + bool Visible = true; + Tetragrama::Layers::ZUILayer* ParentLayer = nullptr; + + // Layout region — set by ZUIDockspaceComponent before BuildUI is called. + // When RegionW > 0 the component uses these values for its panel position/size. + // When RegionW == 0 the component falls back to its own hardcoded defaults. + float RegionX = 0.f; + float RegionY = 0.f; + float RegionW = 0.f; + float RegionH = 0.f; + // When true the dockspace skips assigning this panel's region — the panel + // controls its own position via ZUIPanelDragHeader. Double-click to snap back. + bool Detached = false; + }; + ZDEFINE_PTR(ZUIComponent); +} // namespace Tetragrama::Components diff --git a/Tetragrama/Components/ZUI/ZUIDockspaceComponent.cpp b/Tetragrama/Components/ZUI/ZUIDockspaceComponent.cpp new file mode 100644 index 000000000..4f447812d --- /dev/null +++ b/Tetragrama/Components/ZUI/ZUIDockspaceComponent.cpp @@ -0,0 +1,337 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace ZEngine::UI; + +namespace Tetragrama::Components +{ + static constexpr float k_dim[4] = {0.55f, 0.55f, 0.60f, 1.f}; + static constexpr float kMenuH = 28.f; + static constexpr float kStatusH = 28.f; + static constexpr float kLeftW = 0.18f; + static constexpr float kRightW = 0.22f; + static constexpr float kBottomH = 0.25f; + + static void AssignRegion(ZUIComponent* cmp, float x, float y, float w, float h) + { + if (!cmp || cmp->Detached) + { + return; + } + cmp->RegionX = x; + cmp->RegionY = y; + cmp->RegionW = w; + cmp->RegionH = h; + } + + void ZUIDockspaceComponent::Initialize(Tetragrama::Layers::ZUILayer* parent, cstring name, bool visibility) + { + ParentLayer = parent; + Name = name; + Visible = visibility; + + // Build the dockspace split tree once at startup + auto* arena = ParentLayer ? &ParentLayer->LocalArena : nullptr; + if (arena) + { + m_dock_tree = ZUIDockTreeCreate(arena); + + // Root: H split → left 18% (Hierarchy) | right 82% + ZUIDockSplitH(m_dock_tree, m_dock_tree->Root, kLeftW, ZUIDockHashName("Hierarchy"), 0); + + // Right child of root: H split → left 78% (center) | right 22% (Inspector) + ZUIDockNode* right_node = m_dock_tree->Root->Last; + ZUIDockSplitH(m_dock_tree, right_node, 1.f - kRightW, 0, ZUIDockHashName("Inspector")); + + // Center child: V split → top 75% (Viewport) | bottom 25% + ZUIDockNode* center_node = right_node->First; + ZUIDockSplitV(m_dock_tree, center_node, 1.f - kBottomH, ZUIDockHashName("Viewport"), 0); + + // Bottom child: H split → left 40% (Log) | right 60% (Project) + ZUIDockNode* bottom_node = center_node->Last; + ZUIDockSplitH(m_dock_tree, bottom_node, 0.40f, ZUIDockHashName("Log"), ZUIDockHashName("Project")); + } + } + + void ZUIDockspaceComponent::BuildUI(ZUIContext* ctx) + { + if (!Visible) + { + return; + } + + float sw = (float) ctx->ScreenW; + float sh = (float) ctx->ScreenH; + float menu_h = kMenuH * ctx->UIScale; + float status_h = kStatusH * ctx->UIScale; + + // Recompute dock rects from the current window size + if (m_dock_tree) + { + const float root_rect[4] = {0.f, menu_h, sw, sh - status_h}; + ZUIDockLayout(m_dock_tree, root_rect); + + auto AssignFromDock = [&](ZUIComponent* cmp, const char* panel_name) { + if (!cmp || cmp->Detached) + { + return; + } + float r[4]; + if (ZUIDockRectForKey(m_dock_tree, ZUIDockHashName(panel_name), r)) + AssignRegion(cmp, r[0], r[1], r[2] - r[0], r[3] - r[1]); + }; + + AssignFromDock(Hierarchy, "Hierarchy"); + AssignFromDock(Inspector, "Inspector"); + AssignFromDock(Viewport, "Viewport"); + AssignFromDock(Log, "Log"); + AssignFromDock(Project, "Project"); + } + AssignRegion(StatusBar, 0.f, sh - status_h, sw, status_h); + + // --- Full-screen background --- + ZUIBox* bg = ZUIBeginColumn(ctx, "##dockspace_bg", ZPx(sw), ZPx(sh)); + bg->Flags = bg->Flags | ZUI_DrawBackground | ZUI_FloatX | ZUI_FloatY; + bg->FloatPos[0] = 0.f; + bg->FloatPos[1] = 0.f; + ZUIBoxSetColorArr(bg, ctx->Theme.WindowBg); + ZUIBoxSetCornerRadius(bg, 0.f); + bg->EdgeSoftness = 0.f; + + // Platform-aware shortcut display strings +#if defined(__APPLE__) + static constexpr const char* kMod = "Cmd+"; + static constexpr const char* kModShift = "Cmd+Shift+"; + static constexpr const char* kQuitShortcut = "Cmd+Q"; +#else + static constexpr const char* kMod = "Ctrl+"; + static constexpr const char* kModShift = "Ctrl+Shift+"; + static constexpr const char* kQuitShortcut = "Alt+F4"; +#endif + char sc_new[24], sc_open[24], sc_save[24], sc_save_as[24], sc_undo[24], sc_redo[24], sc_all[24]; + snprintf(sc_new, sizeof(sc_new), "%sN", kMod); + snprintf(sc_open, sizeof(sc_open), "%sO", kMod); + snprintf(sc_save, sizeof(sc_save), "%sS", kMod); + snprintf(sc_save_as, sizeof(sc_save_as), "%sS", kModShift); + snprintf(sc_undo, sizeof(sc_undo), "%sZ", kMod); + snprintf(sc_redo, sizeof(sc_redo), "%sY", kMod); + snprintf(sc_all, sizeof(sc_all), "%sA", kMod); + + // --- Menu bar --- + if (ZUIBeginMenuBar(ctx)) + { + ZUISpacer(ctx, 6.f); + + // "File" menu + if (ZUIBeginMenu(ctx, "File")) + { + auto* file_app = (ParentLayer && ParentLayer->CurrentApp) ? reinterpret_cast(ParentLayer->CurrentApp) : nullptr; + + if (ZUIMenuItemEx(ctx, "New Scene", sc_new)) + { + if (file_app && file_app->CurrentScene) + { + auto* scene = reinterpret_cast(file_app->CurrentScene); + scene->SelectedActorHandle = {}; + } + } + if (ZUIMenuItemEx(ctx, "Open Scene...", sc_open)) + { + ZENGINE_CORE_INFO("[Editor] Use the Project panel to locate and drop a .zescene into the viewport") + } + ZUISeparator(ctx); + if (ZUIMenuItemEx(ctx, "Save Scene", sc_save)) + { + if (file_app && file_app->CurrentScene && file_app->Configuration) + { + auto* scene = reinterpret_cast(file_app->CurrentScene); + Serializers::EditorSceneSerializer serializer; + serializer.Serialize(scene); + ZENGINE_CORE_INFO("[Editor] Scene saved") + } + } + if (ZUIMenuItemEx(ctx, "Save Scene As...", sc_save_as)) + { + if (file_app && file_app->CurrentScene) + { + auto* scene = reinterpret_cast(file_app->CurrentScene); + Serializers::EditorSceneSerializer serializer; + serializer.Serialize(scene); + } + } + ZUISeparator(ctx); + if (ZUIMenuItemEx(ctx, "Quit", kQuitShortcut)) + { + if (file_app && file_app->CurrentWindow) + { + auto* glfw_win = static_cast(file_app->CurrentWindow->GetNativeWindow()); + if (glfw_win) + glfwSetWindowShouldClose(glfw_win, GLFW_TRUE); + } + } + ZUIEndMenu(ctx); + } + ZUISpacer(ctx, 4.f); + + // "Edit" menu + if (ZUIBeginMenu(ctx, "Edit")) + { + if (ZUIMenuItemEx(ctx, "Undo", sc_undo, false, false)) + { + } + if (ZUIMenuItemEx(ctx, "Redo", sc_redo, false, false)) + { + } + ZUISeparator(ctx); + if (ZUIMenuItemEx(ctx, "Select All", sc_all)) + { + if (ParentLayer && ParentLayer->CurrentApp) + { + auto* edit_app = reinterpret_cast(ParentLayer->CurrentApp); + auto* edit_scene = reinterpret_cast(edit_app->CurrentScene); + auto* eng = ZEngine::Engine::GetContext(); + if (edit_scene && eng && eng->ActorManager && eng->ActorManager->Count() > 0) + { + bool found = false; + eng->ActorManager->ForEach([&](ZEngine::ECS::ActorHandle h, ZEngine::ECS::Actor*) { + if (!found) + { + edit_scene->SelectedActorHandle = h; + found = true; + } + }); + } + } + } + ZUIEndMenu(ctx); + } + ZUISpacer(ctx, 4.f); + + // "View" menu — checkable toggles using ZUIMenuItemEx selected state + if (ZUIBeginMenu(ctx, "View")) + { + auto vis_item = [&](const char* label, ZUIComponent* cmp) { + if (!cmp) + return; + if (ZUIMenuItemEx(ctx, label, nullptr, cmp->Visible)) + cmp->Visible = !cmp->Visible; + }; + vis_item("Scene", Viewport); + vis_item("Hierarchy", Hierarchy); + vis_item("Inspector", Inspector); + vis_item("Console", Log); + vis_item("Project", Project); + ZUIEndMenu(ctx); + } + + ZUILabel(ctx, " | ", k_dim); + + // Scene name + if (ParentLayer && ParentLayer->CurrentApp) + { + auto* app = reinterpret_cast(ParentLayer->CurrentApp); + if (app->Configuration) + { + const char* sname = app->Configuration->ActiveSceneName.empty() ? "-" : app->Configuration->ActiveSceneName.c_str(); + char scene_buf[128]; + snprintf(scene_buf, sizeof(scene_buf), "Scene: %s", sname); + ZUILabel(ctx, scene_buf, k_dim); + } + } + + ZUIEndMenuBar(ctx); + } + + // --- Workspace resize dividers (RAD Debugger style) --- + // Direct mouse-bound tracking — bypasses z-ordered hit-test so dividers + // always have priority over panel content, regardless of render order. + if (m_dock_tree) + { + static constexpr float kDivW = 6.f; // grab width in logical px + + for (int di = 0; di < 4; ++di) + { + Divider& div = m_dividers[di]; + float lr[4] = {}; + if (!ZUIDockRectForKey(m_dock_tree, ZUIDockHashName(div.leaf_name), lr)) + continue; + + // Divider rect: at the right/bottom (or left/top if use_near) edge of the leaf + float dx0, dy0, dx1, dy1; + if (!div.horizontal) + { // vertical divider (left|right split) + float edge_x = div.use_near ? lr[0] : lr[2]; + dx0 = edge_x - kDivW * 0.5f; + dy0 = lr[1]; + dx1 = edge_x + kDivW * 0.5f; + dy1 = lr[3]; + } + else + { // horizontal divider (top|bottom split) + float edge_y = div.use_near ? lr[1] : lr[3]; + dx0 = lr[0]; + dy0 = edge_y - kDivW * 0.5f; + dx1 = lr[2]; + dy1 = edge_y + kDivW * 0.5f; + } + + float mx = ctx->MousePos[0], my = ctx->MousePos[1]; + bool in_rect = (mx >= dx0 && mx <= dx1 && my >= dy0 && my <= dy1); + + // Start drag when mouse pressed in divider area + if (ctx->MousePressed[0] && in_rect) + div.dragging = true; + if (ctx->MouseReleased[0]) + div.dragging = false; + + // Apply resize while dragging + if (div.dragging && ctx->MouseDown[0]) + { + float delta = div.horizontal ? (ctx->MousePos[1] - ctx->PrevMousePos[1]) : (ctx->MousePos[0] - ctx->PrevMousePos[0]); + if (delta != 0.f) + { + uint64_t key = ZUIDockHashName(div.leaf_name); + ZUIDockNode* leaf = ZUIDockFindLeaf(m_dock_tree, key); + if (leaf) + ZUIDockResize(m_dock_tree, leaf, delta); + } + } + + // Visual indicator: thin colored line at the divider, brighter on hover/drag + bool highlight = in_rect || div.dragging; + float vis_col[4] = {highlight ? 0.45f : 0.22f, highlight ? 0.55f : 0.28f, highlight ? 0.70f : 0.35f, 1.f}; + char vis_key[32]; + snprintf(vis_key, sizeof(vis_key), "##divvis_%d", di); + ZUIBox* vis = ZUIPushBox(ctx, vis_key, (uint32_t) strlen(vis_key), ZUI_DrawBackground | ZUI_FloatX | ZUI_FloatY); + if (!div.horizontal) + { + vis->Size[0] = ZPx(1.f); + vis->Size[1] = ZPx(dy1 - dy0); + vis->FloatPos[0] = (dx0 + dx1) * 0.5f; + vis->FloatPos[1] = dy0; + } + else + { + vis->Size[0] = ZPx(dx1 - dx0); + vis->Size[1] = ZPx(1.f); + vis->FloatPos[0] = dx0; + vis->FloatPos[1] = (dy0 + dy1) * 0.5f; + } + vis->EdgeSoftness = 0.f; + ZUIBoxSetColorArr(vis, vis_col); + ZUIPopBox(ctx); + } + } + + ZUIEndColumn(ctx); + } +} // namespace Tetragrama::Components diff --git a/Tetragrama/Components/ZUI/ZUIDockspaceComponent.h b/Tetragrama/Components/ZUI/ZUIDockspaceComponent.h new file mode 100644 index 000000000..05d1b1f2a --- /dev/null +++ b/Tetragrama/Components/ZUI/ZUIDockspaceComponent.h @@ -0,0 +1,50 @@ +#pragma once +#include +#include + +namespace Tetragrama::Components +{ + // Layout coordinator — must be registered FIRST with ZUILayer so it runs before + // the panels it manages. Sets each panel's RegionX/Y/W/H based on ScreenW/H then + // renders the menu bar. The panels render themselves in subsequent BuildUI calls. + class ZUIDockspaceComponent : public ZUIComponent + { + public: + ZUIDockspaceComponent() = default; + ~ZUIDockspaceComponent() override = default; + + void Initialize(Tetragrama::Layers::ZUILayer* parent, cstring name = "Dockspace", bool visibility = true) override; + + void BuildUI(ZEngine::UI::ZUIContext* ctx) override; + + // Panels registered here get their regions assigned each frame + ZUIComponent* Viewport = nullptr; + ZUIComponent* Hierarchy = nullptr; + ZUIComponent* Inspector = nullptr; + ZUIComponent* Log = nullptr; + ZUIComponent* Project = nullptr; + ZUIComponent* StatusBar = nullptr; + + private: + ZEngine::UI::ZUIDockTree* m_dock_tree = nullptr; + + // Workspace resize state — separate from the normal hit-test pass. + // RAD Debugger style: dividers check mouse bounds directly, not through + // the z-ordered box tree. This gives dividers priority over panel content. + struct Divider + { + const char* leaf_name; // which dock leaf to resize on drag + bool horizontal; // true = drag Y (top/bottom), false = drag X (left/right) + bool use_near; // true = divider on left/top edge; false = right/bottom edge + bool dragging = false; + }; + // leaf_name, horizontal, use_near(left/top vs right/bottom edge) + Divider m_dividers[4] = { + {"Hierarchy", false, false}, // vertical at hierarchy's RIGHT edge + {"Inspector", false, true}, // vertical at inspector's LEFT edge + { "Viewport", true, false}, // horizontal at viewport's BOTTOM edge + { "Log", false, false}, // vertical at log's RIGHT edge + }; + }; + ZDEFINE_PTR(ZUIDockspaceComponent); +} // namespace Tetragrama::Components diff --git a/Tetragrama/Components/ZUI/ZUIHierarchyViewComponent.cpp b/Tetragrama/Components/ZUI/ZUIHierarchyViewComponent.cpp new file mode 100644 index 000000000..3de3bfd97 --- /dev/null +++ b/Tetragrama/Components/ZUI/ZUIHierarchyViewComponent.cpp @@ -0,0 +1,605 @@ +// clang-format off +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +// clang-format on + +using namespace ZEngine; +using namespace ZEngine::ECS; +using namespace ZEngine::ECS::Components; +using namespace ZEngine::UI; + +namespace Tetragrama::Components +{ + void ZUIHierarchyViewComponent::Initialize(Tetragrama::Layers::ZUILayer* parent, cstring name, bool visibility) + { + ParentLayer = parent; + Name = name; + Visible = visibility; + parent->LocalArena.CreateSubArena(ZKilo(512), &m_arena); + m_collapsed.init(&m_arena, 64); + } + + bool ZUIHierarchyViewComponent::IsCollapsed(EntityID eid) const + { + for (uint32_t i = 0; i < m_collapsed.size(); ++i) + if (m_collapsed[i] == eid) + return true; + return false; + } + + void ZUIHierarchyViewComponent::ToggleCollapsed(EntityID eid) + { + for (uint32_t i = 0; i < m_collapsed.size(); ++i) + { + if (m_collapsed[i] == eid) + { + m_collapsed[i] = INVALID_ENTITY; + return; + } + } + m_collapsed.push(eid); + } + + void ZUIHierarchyViewComponent::BuildUI(ZEngine::UI::ZUIContext* ctx) + { + if (!Visible || !ParentLayer || !ParentLayer->CurrentApp) + { + return; + } + + auto* app = reinterpret_cast(ParentLayer->CurrentApp); + auto* current_scene = reinterpret_cast(app->CurrentScene); + auto* eng = Engine::GetContext(); + if (!current_scene || !eng || !eng->ActorManager) + { + return; + } + + if (RegionW == 0) + { + RegionX = 480.f; + RegionY = 80.f; + RegionW = 280.f; + RegionH = 700.f; + } + + ZUIBox* panel = ZUIBeginColumn(ctx, "##zui_hier_panel", ZPx(RegionW), ZPx(RegionH)); + panel->Flags = panel->Flags | ZUI_DrawBackground | ZUI_DrawBorder | ZUI_FloatX | ZUI_FloatY; + panel->FloatPos[0] = RegionX; + panel->FloatPos[1] = RegionY; + ZUIBoxSetColorArr(panel, ctx->Theme.PanelBg); + panel->BorderColor[0] = ctx->Theme.PanelBorder[0]; + panel->BorderColor[1] = ctx->Theme.PanelBorder[1]; + panel->BorderColor[2] = ctx->Theme.PanelBorder[2]; + panel->BorderColor[3] = ctx->Theme.PanelBorder[3]; + panel->BorderColor[3] = 1.0f; + panel->BorderThickness = 1.f; + panel->EdgeSoftness = 0.f; + + // --- Header — draggable --- + ZUIBox* hdr = ZUIBeginRow(ctx, "##hier_hdr", ZFill(), ZSPx(ctx, 28.f)); + hdr->Flags = hdr->Flags | ZUI_DrawBackground | ZUI_Clickable; + ZUIBoxSetColorArr(hdr, ctx->Theme.TitleBarBg); + ZUISpacer(ctx, 6.f); + ZUILabel(ctx, Name ? Name : "Hierarchy", ctx->Theme.TextDefault); + ZUISpacer(ctx, 8.f); + ZUISignal add_sig = ZUIButton(ctx, "Add##hier"); + ZUISpacer(ctx, 4.f); + ZUISignal del_sig = ZUIButton(ctx, "Del##hier"); + ZUISignal drag_sig = ZUISignalFromBox(ctx, hdr); + ZUIEndRow(ctx); + if ((drag_sig.Flags & ZUI_SignalHeld) && (drag_sig.DragDelta[0] != 0.f || drag_sig.DragDelta[1] != 0.f)) + { + RegionX += drag_sig.DragDelta[0]; + RegionY += drag_sig.DragDelta[1]; + Detached = true; + panel->FloatPos[0] = RegionX; + panel->FloatPos[1] = RegionY; + } + if (drag_sig.Flags & ZUI_SignalDoubleClicked) + { + Detached = false; + } + ZUISeparator(ctx); + + // --- Scrollable actor list --- + ZUIBeginScrollRegion(ctx, "##hier_scroll", ZFill(), ZFill()); + + // --- DFS tree build (no ImGui dependency) --- + auto scratch = ZGetScratch(&m_arena); + uint32_t n = eng->ActorManager->Count(); + + struct OutlinerNode + { + ActorHandle Handle; + EntityID EID; + EntityID Parent; + }; + OutlinerNode* nodes = ZPushArray(scratch.Arena, OutlinerNode, n + 1); + uint32_t* first_child = ZPushArray(scratch.Arena, uint32_t, n + 1); + uint32_t* next_sib = ZPushArray(scratch.Arena, uint32_t, n + 1); + uint32_t nc = 0; + + for (uint32_t i = 0; i < n; ++i) + { + first_child[i] = UINT32_MAX; + next_sib[i] = UINT32_MAX; + } + + eng->ActorManager->ForEach([&](ActorHandle h, Actor* actor) { + auto* pc = actor->GetComponent(); + nodes[nc++] = {h, actor->GetEntityID(), (pc && pc->Parent != INVALID_ENTITY) ? pc->Parent : INVALID_ENTITY}; + }); + + ZEngine::Core::Containers::UnorderedHashMap eid_to_idx; + eid_to_idx.init(scratch.Arena, nc * 2 + 1); + for (uint32_t i = 0; i < nc; ++i) + eid_to_idx.insert(nodes[i].EID, i); + for (uint32_t i = 0; i < nc; ++i) + { + if (nodes[i].Parent == INVALID_ENTITY) + continue; + auto* pidx = eid_to_idx.find(nodes[i].Parent); + if (!pidx) + continue; + next_sib[i] = first_child[*pidx]; + first_child[*pidx] = i; + } + + struct DFSEntry + { + uint32_t idx; + int depth; + }; + DFSEntry* stk = ZPushArray(scratch.Arena, DFSEntry, nc * 2 + 1); + int32_t sp = 0; + for (int32_t i = (int32_t) nc - 1; i >= 0; --i) + if (nodes[i].Parent == INVALID_ENTITY) + stk[sp++] = {(uint32_t) i, 0}; + + // --- Type icon color tables --- + static const float k_icon_light[4] = {1.0f, 0.85f, 0.20f, 1.f}; + static const float k_icon_camera[4] = {0.45f, 0.85f, 0.55f, 1.f}; + static const float k_icon_mesh[4] = {0.55f, 0.75f, 0.90f, 1.f}; + static const float k_icon_coll[4] = {0.85f, 0.65f, 0.15f, 1.f}; + static const float k_icon_default[4] = {0.55f, 0.55f, 0.60f, 1.f}; + (void) k_icon_default; + + // --- Scene root row --- + { + static const float k_dim[4] = {0.55f, 0.55f, 0.60f, 1.f}; + + ZUIBox* root_row = ZUIBeginRow(ctx, "##sc_root", ZFill(), ZSPx(ctx, 26.f)); + root_row->Flags = root_row->Flags | ZUI_DrawBackground | ZUI_Clickable; + ZUIBoxSetColor(root_row, 0.30f, 0.30f, 0.36f, 0.18f); + + // Disclosure + const char* root_ind = m_root_open ? "v##scr" : ">##scr"; + ZUIBox* root_arrow = ZUIPushBox(ctx, root_ind, (uint32_t) Helpers::secure_strlen(root_ind), ZUI_DrawText | ZUI_Clickable); + root_arrow->Size[0] = ZPx(14.f); + root_arrow->Size[1] = ZPx(22.f); + root_arrow->TextColor[0] = k_dim[0]; + root_arrow->TextColor[1] = k_dim[1]; + root_arrow->TextColor[2] = k_dim[2]; + root_arrow->TextColor[3] = k_dim[3]; + ZUISignal rarrow_sig = ZUISignalFromBox(ctx, root_arrow); + ZUIPopBox(ctx); + + // World type icon + { + static const float k_icon_world[4] = {0.35f, 0.80f, 0.45f, 1.f}; + ZUIBox* icon = ZUIPushBox(ctx, "W##ti_root", 10, ZUI_DrawBackground | ZUI_DrawText); + icon->Size[0] = ZPx(14.f); + icon->Size[1] = ZPx(14.f); + ZUIBoxSetColorArr(icon, k_icon_world); + icon->TextColor[0] = 1.f; + icon->TextColor[1] = 1.f; + icon->TextColor[2] = 1.f; + icon->TextColor[3] = 1.f; + ZUIPopBox(ctx); + } + ZUISpacer(ctx, 4.f); + + const char* scene_name = (current_scene->Name && current_scene->Name[0]) ? current_scene->Name : "Scene"; + ZUILabel(ctx, scene_name); + + ZUISignal root_sig = ZUISignalFromBox(ctx, root_row); + ZUIEndRow(ctx); + + if (rarrow_sig.Flags & ZUI_SignalClicked) + { + m_root_open = !m_root_open; + } + (void) root_sig; // scene root not selectable + } + + // --- Actor rows (DFS) --- + if (m_root_open) + { + constexpr float INDENT_W = 14.f; + static const float k_dim[4] = {0.55f, 0.55f, 0.60f, 1.f}; + static const float k_sel[4] = {0.26f, 0.44f, 0.70f, 0.50f}; + + ActorHandle pending_delete = {}; + ActorHandle pending_duplicate = {}; + ActorHandle pending_reparent_child = {}; + ActorHandle pending_reparent_parent = {}; + + while (sp > 0) + { + DFSEntry e = stk[--sp]; + uint32_t ni = e.idx; + Actor* actor = eng->ActorManager->Access(nodes[ni].Handle); + if (!actor) + continue; + + auto* nc_comp = actor->GetComponent(); + const char* label = (nc_comp && nc_comp->Value[0]) ? nc_comp->Value : "Actor"; + + // Determine type icon character and color + char type_char; + const float* type_bg; + if (actor->HasComponent()) + { + type_char = 'L'; + type_bg = k_icon_light; + } + else if (actor->HasComponent()) + { + type_char = 'C'; + type_bg = k_icon_camera; + } + else if (actor->HasComponent()) + { + type_char = 'M'; + type_bg = k_icon_mesh; + } + else + { + type_char = '+'; + type_bg = k_icon_coll; + } + + bool has_ch = (first_child[ni] != UINT32_MAX); + bool is_open = has_ch && !IsCollapsed(nodes[ni].EID); + bool selected = (current_scene->SelectedActorHandle.Index == nodes[ni].Handle.Index && current_scene->SelectedActorHandle.Generation == nodes[ni].Handle.Generation); + + float indent = (float) (e.depth + 1) * INDENT_W; + + bool renaming_this = (m_renaming_handle.Valid() && m_renaming_handle.Index == nodes[ni].Handle.Index && m_renaming_handle.Generation == nodes[ni].Handle.Generation); + + // Build row key + char row_key[64]; + snprintf(row_key, sizeof(row_key), "##hr_%u_%u", nodes[ni].Handle.Index, nodes[ni].Handle.Generation); + + ZUIBox* row = ZUIBeginRow(ctx, row_key, ZFill(), ZSPx(ctx, 24.f)); + row->Flags = row->Flags | ZUI_DrawBackground | ZUI_Clickable; + if (selected) + { + ZUIBoxSetColorArr(row, k_sel); + } + else + { + // Neutral base — transparent but non-zero RGB so hover blending + // produces a visible highlight rather than dark gray. + ZUIBoxSetColor(row, 0.42f, 0.42f, 0.48f, 0.f); + } + + // Indent spacer + ZUISpacer(ctx, indent); + + // Disclosure arrow or alignment spacer + if (has_ch) + { + char arrow_key[64]; + const char* ind = is_open ? "v" : ">"; + snprintf(arrow_key, sizeof(arrow_key), "%s##ar_%u_%u", ind, nodes[ni].Handle.Index, nodes[ni].Handle.Generation); + ZUIBox* arrow = ZUIPushBox(ctx, arrow_key, (uint32_t) Helpers::secure_strlen(arrow_key), ZUI_DrawText | ZUI_Clickable); + arrow->Size[0] = ZPx(14.f); + arrow->Size[1] = ZPx(22.f); + arrow->TextColor[0] = k_dim[0]; + arrow->TextColor[1] = k_dim[1]; + arrow->TextColor[2] = k_dim[2]; + arrow->TextColor[3] = k_dim[3]; + ZUISignal asig = ZUISignalFromBox(ctx, arrow); + ZUIPopBox(ctx); + if (asig.Flags & ZUI_SignalClicked) + { + ToggleCollapsed(nodes[ni].EID); + } + } + else + { + ZUISpacer(ctx, 14.f); + } + + // Type icon — 14x14 colored box with a 1-char label + { + char icon_key[32]; + snprintf(icon_key, sizeof(icon_key), "%c##ti_%u_%u", type_char, nodes[ni].Handle.Index, nodes[ni].Handle.Generation); + ZUIBox* icon = ZUIPushBox(ctx, icon_key, (uint32_t) Helpers::secure_strlen(icon_key), ZUI_DrawBackground | ZUI_DrawText); + icon->Size[0] = ZPx(14.f); + icon->Size[1] = ZPx(14.f); + ZUIBoxSetColorArr(icon, type_bg); + icon->TextColor[0] = 1.f; + icon->TextColor[1] = 1.f; + icon->TextColor[2] = 1.f; + icon->TextColor[3] = 1.f; + ZUIPopBox(ctx); + } + ZUISpacer(ctx, 4.f); + + // Actor name label or inline rename TextField + if (renaming_this) + { + char tf_key[64]; + snprintf(tf_key, sizeof(tf_key), "##ren_%u_%u", nodes[ni].Handle.Index, nodes[ni].Handle.Generation); + uint64_t focus_before = ctx->FocusKey; + ZUITextField(ctx, tf_key, m_rename_buf, sizeof(m_rename_buf), 150.f); + uint64_t focus_after = ctx->FocusKey; + + if (m_rename_started) + { + // First frame after rename triggered: suppress commit check. + // User must click the field to focus it. + m_rename_started = false; + m_rename_focus_key = 0; + } + else if (m_rename_focus_key != 0 && focus_after != m_rename_focus_key) + { + // TextField had focus and FocusKey changed — commit rename + auto* nc_ren = actor->GetComponent(); + if (nc_ren && m_rename_buf[0]) + { + Helpers::secure_strncpy(nc_ren->Value, sizeof(nc_ren->Value), m_rename_buf, sizeof(m_rename_buf) - 1); + } + m_renaming_handle = {}; + m_rename_focus_key = 0; + } + // Detect when the TextField first receives focus (user clicks it) + if (focus_before != focus_after && focus_after != 0) + { + m_rename_focus_key = focus_after; + } + } + else + { + ZUILabel(ctx, label); + } + + ZUISignal row_sig = ZUISignalFromBox(ctx, row); + ZUIEndRow(ctx); + + // Single-click: select actor + if (row_sig.Flags & ZUI_SignalClicked) + { + current_scene->SelectedActorHandle = nodes[ni].Handle; + } + + // Double-click: begin inline rename + if (!renaming_this && (row_sig.Flags & ZUI_SignalDoubleClicked)) + { + m_renaming_handle = nodes[ni].Handle; + m_rename_started = true; + m_rename_focus_key = 0; + if (nc_comp && nc_comp->Value[0]) + { + Helpers::secure_strncpy(m_rename_buf, sizeof(m_rename_buf), nc_comp->Value, sizeof(nc_comp->Value)); + } + else + { + m_rename_buf[0] = '\0'; + } + } + + // Right-click context menu + if (ZUIBeginPopupContextItem(ctx, "##actor_ctx", row_sig)) + { + if (ZUIMenuItem(ctx, "Rename")) + { + m_renaming_handle = nodes[ni].Handle; + m_rename_started = true; + m_rename_focus_key = 0; + if (nc_comp && nc_comp->Value[0]) + { + Helpers::secure_strncpy(m_rename_buf, sizeof(m_rename_buf), nc_comp->Value, sizeof(nc_comp->Value)); + } + else + { + m_rename_buf[0] = '\0'; + } + } + if (ZUIMenuItem(ctx, "Delete")) + { + pending_delete = nodes[ni].Handle; + } + if (ZUIMenuItem(ctx, "Duplicate Actor")) + { + pending_duplicate = nodes[ni].Handle; + } + ZUIEndPopup(ctx); + } + + // Drag source: broadcast this actor's handle as the drag payload + ZUIBeginDragSource(ctx, row, (const char*) &nodes[ni].Handle, sizeof(ActorHandle)); + + // Drop target: accept a dragged actor handle for reparenting + char drop_buf[sizeof(ActorHandle)] = {}; + if (ZUIAcceptDrop(ctx, row, drop_buf, sizeof(drop_buf))) + { + ActorHandle dragged = {}; + Helpers::secure_memcpy(&dragged, sizeof(dragged), drop_buf, sizeof(drop_buf)); + if (dragged.Valid() && (dragged.Index != nodes[ni].Handle.Index || dragged.Generation != nodes[ni].Handle.Generation)) + { + pending_reparent_child = dragged; + pending_reparent_parent = nodes[ni].Handle; + } + } + + // Push expanded children (reverse order for correct DFS) + if (has_ch && is_open) + { + uint32_t tmp[256]; + int tc = 0; + uint32_t c = first_child[ni]; + while (c != UINT32_MAX && tc < 256) + { + tmp[tc++] = c; + c = next_sib[c]; + } + for (int ci = tc - 1; ci >= 0; --ci) + stk[sp++] = {tmp[ci], e.depth + 1}; + } + } + + // --- Deferred mutations (applied after DFS to avoid tree invalidation) --- + + // Reparent: assign new ParentComponent to the dragged actor + if (pending_reparent_child.Valid() && pending_reparent_parent.Valid()) + { + Actor* child_actor = eng->ActorManager->Access(pending_reparent_child); + Actor* parent_actor = eng->ActorManager->Access(pending_reparent_parent); + if (child_actor && parent_actor) + { + EntityID new_parent_eid = parent_actor->GetEntityID(); + auto* pc = child_actor->GetComponent(); + if (pc) + { + pc->Parent = new_parent_eid; + } + else + { + ParentComponent new_pc = {}; + new_pc.Parent = new_parent_eid; + child_actor->AddComponent(new_pc); + } + } + } + + // Duplicate: create a new actor copying Name and Transform from the source + if (pending_duplicate.Valid()) + { + Actor* src = eng->ActorManager->Access(pending_duplicate); + if (src) + { + ActorHandle dup_h = eng->ActorManager->Create(); + Actor* dup_a = eng->ActorManager->Access(dup_h); + if (dup_a) + { + auto* nc_src = src->GetComponent(); + NameComponent nc_dup = {}; + if (nc_src && nc_src->Value[0]) + { + Helpers::secure_strncpy(nc_dup.Value, sizeof(nc_dup.Value), nc_src->Value, sizeof(nc_src->Value)); + uint32_t name_len = (uint32_t) Helpers::secure_strlen(nc_dup.Value); + if (name_len + 5 < sizeof(nc_dup.Value)) + { + Helpers::secure_strncpy(nc_dup.Value + name_len, sizeof(nc_dup.Value) - name_len, " Copy", 5); + } + } + else + { + Helpers::secure_strncpy(nc_dup.Value, sizeof(nc_dup.Value), "Actor Copy", 10); + } + dup_a->AddComponent(nc_dup); + auto* tc_src = src->GetComponent(); + if (tc_src) + { + dup_a->AddComponent(*tc_src); + } + else + { + dup_a->AddComponent({}); + } + current_scene->SelectedActorHandle = dup_h; + } + } + } + + // Context-menu delete + if (pending_delete.Valid()) + { + Actor* del_actor = eng->ActorManager->Access(pending_delete); + if (del_actor) + { + auto* mc = del_actor->GetComponent(); + if (mc && mc->RenderInstanceId != UINT32_MAX) + current_scene->RemoveMeshInstance(mc->RenderInstanceId, eng->RenderResourceManager); + if (current_scene->SelectedActorHandle.Index == pending_delete.Index && current_scene->SelectedActorHandle.Generation == pending_delete.Generation) + current_scene->SelectedActorHandle = {}; + if (m_renaming_handle.Index == pending_delete.Index && m_renaming_handle.Generation == pending_delete.Generation) + { + m_renaming_handle = {}; + m_rename_focus_key = 0; + } + eng->ActorManager->Destroy(pending_delete); + } + } + + // Header Del button: delete the currently selected actor + if (del_sig.Flags & ZUI_SignalClicked) + { + ActorHandle h = current_scene->SelectedActorHandle; + Actor* actor = h.Valid() ? eng->ActorManager->Access(h) : nullptr; + if (actor) + { + auto* mc = actor->GetComponent(); + if (mc && mc->RenderInstanceId != UINT32_MAX) + current_scene->RemoveMeshInstance(mc->RenderInstanceId, eng->RenderResourceManager); + if (m_renaming_handle.Index == h.Index && m_renaming_handle.Generation == h.Generation) + { + m_renaming_handle = {}; + m_rename_focus_key = 0; + } + current_scene->SelectedActorHandle = {}; + eng->ActorManager->Destroy(h); + } + } + + // Add actor + if (add_sig.Flags & ZUI_SignalClicked) + { + ActorHandle new_h = eng->ActorManager->Create(); + Actor* new_a = eng->ActorManager->Access(new_h); + if (new_a) + { + NameComponent nc_new = {}; + Helpers::secure_strncpy(nc_new.Value, sizeof(nc_new.Value), "Actor", 5); + new_a->AddComponent(nc_new); + new_a->AddComponent({}); + current_scene->SelectedActorHandle = new_h; + } + } + } + + ZUIEndScrollRegion(ctx); // end scrollable actor list + ZReleaseScratch(scratch); + + // Status bar — always visible outside the scroll region + ZUISeparator(ctx); + { + char status[64]; + int total = 0; + if (eng->ActorManager) + { + total = (int) eng->ActorManager->Count(); + } + snprintf(status, sizeof(status), "%d actor%s", total, total == 1 ? "" : "s"); + ZUILabel(ctx, status, ctx->Theme.TextDim); + } + + ZUIEndColumn(ctx); // end panel + } +} // namespace Tetragrama::Components diff --git a/Tetragrama/Components/ZUI/ZUIHierarchyViewComponent.h b/Tetragrama/Components/ZUI/ZUIHierarchyViewComponent.h new file mode 100644 index 000000000..a0fe69d4f --- /dev/null +++ b/Tetragrama/Components/ZUI/ZUIHierarchyViewComponent.h @@ -0,0 +1,36 @@ +#pragma once +#include +#include +#include +#include +#include + +namespace Tetragrama::Components +{ + class ZUIHierarchyViewComponent : public ZUIComponent + { + public: + ZUIHierarchyViewComponent() = default; + ~ZUIHierarchyViewComponent() override = default; + + void Initialize(Tetragrama::Layers::ZUILayer* parent, cstring name = "Hierarchy", bool visibility = true) override; + + void BuildUI(ZEngine::UI::ZUIContext* ctx) override; + + private: + // Arena for collapsed-set and scratch DFS allocations + ZEngine::Core::Memory::ArenaAllocator m_arena = {}; + ZEngine::Core::Containers::Array m_collapsed = {}; + bool m_root_open = true; + + // Inline rename state + ZEngine::ECS::ActorHandle m_renaming_handle = {}; + char m_rename_buf[128] = {}; + bool m_rename_started = false; + uint64_t m_rename_focus_key = 0; + + bool IsCollapsed(ZEngine::ECS::EntityID eid) const; + void ToggleCollapsed(ZEngine::ECS::EntityID eid); + }; + ZDEFINE_PTR(ZUIHierarchyViewComponent); +} // namespace Tetragrama::Components diff --git a/Tetragrama/Components/ZUI/ZUIInspectorViewComponent.cpp b/Tetragrama/Components/ZUI/ZUIInspectorViewComponent.cpp new file mode 100644 index 000000000..d6cc812ca --- /dev/null +++ b/Tetragrama/Components/ZUI/ZUIInspectorViewComponent.cpp @@ -0,0 +1,241 @@ +// clang-format off +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +// clang-format on + +using namespace ZEngine::ECS; +using namespace ZEngine::ECS::Components; +using namespace ZEngine::UI; + +namespace Tetragrama::Components +{ + static constexpr float kLabelW = 72.f; + static constexpr float kRowH = 21.f; + + // Label + read-only value row + static void PropRow(ZUIContext* ctx, const char* row_key, const char* label, const char* value) + { + ZUIBeginRow(ctx, row_key, ZFill(), ZSPx(ctx, kRowH)); + ZUIBox* lbl = ZUIPushBox(ctx, label, (uint32_t) ZEngine::Helpers::secure_strlen(label), ZUI_DrawText); + lbl->Size[0] = ZPx(kLabelW); + lbl->Size[1] = ZSPx(ctx, kRowH); + lbl->TextColor[0] = ctx->Theme.TextDim[0]; + lbl->TextColor[1] = ctx->Theme.TextDim[1]; + lbl->TextColor[2] = ctx->Theme.TextDim[2]; + lbl->TextColor[3] = ctx->Theme.TextDim[3]; + ZUIPopBox(ctx); + ZUILabel(ctx, value, ctx->Theme.TextDefault); + ZUIEndRow(ctx); + } + + // Label + three DragFloat fields on one row — editable XYZ + static bool XYZDragRow(ZUIContext* ctx, const char* row_key, const char* label, float* x, float* y, float* z, float speed) + { + ZUIBeginRow(ctx, row_key, ZFill(), ZSPx(ctx, kRowH)); + ZUIBox* lbl = ZUIPushBox(ctx, label, (uint32_t) ZEngine::Helpers::secure_strlen(label), ZUI_DrawText); + lbl->Size[0] = ZPx(kLabelW); + lbl->Size[1] = ZSPx(ctx, kRowH); + lbl->TextColor[0] = ctx->Theme.TextDim[0]; + lbl->TextColor[1] = ctx->Theme.TextDim[1]; + lbl->TextColor[2] = ctx->Theme.TextDim[2]; + lbl->TextColor[3] = ctx->Theme.TextDim[3]; + ZUIPopBox(ctx); + + // Build unique per-axis keys from the row key + char kx[40], ky[40], kz[40]; + snprintf(kx, sizeof(kx), "##x_%s", row_key + 2); + snprintf(ky, sizeof(ky), "##y_%s", row_key + 2); + snprintf(kz, sizeof(kz), "##z_%s", row_key + 2); + bool cx = ZUIDragFloat(ctx, kx, x, speed, 54.f); + bool cy = ZUIDragFloat(ctx, ky, y, speed, 54.f); + bool cz = ZUIDragFloat(ctx, kz, z, speed, 54.f); + ZUIEndRow(ctx); + return cx || cy || cz; + } + + void ZUIInspectorViewComponent::Initialize(Tetragrama::Layers::ZUILayer* parent, cstring name, bool visibility) + { + ParentLayer = parent; + Name = name; + Visible = visibility; + } + + void ZUIInspectorViewComponent::BuildUI(ZUIContext* ctx) + { + if (!Visible || !ParentLayer || !ParentLayer->CurrentApp) + { + return; + } + + auto* app = reinterpret_cast(ParentLayer->CurrentApp); + auto* current_scene = reinterpret_cast(app->CurrentScene); + auto* eng = ZEngine::Engine::GetContext(); + if (!current_scene || !eng || !eng->ActorManager) + { + return; + } + + if (RegionW == 0) + { + RegionX = 760.f; + RegionY = 80.f; + RegionW = 280.f; + RegionH = 600.f; + } + + ZUIBox* panel = ZUIBeginColumn(ctx, "##zui_insp_panel", ZPx(RegionW), ZPx(RegionH)); + panel->Flags = panel->Flags | ZUI_DrawBackground | ZUI_DrawBorder | ZUI_FloatX | ZUI_FloatY; + panel->FloatPos[0] = RegionX; + panel->FloatPos[1] = RegionY; + ZUIBoxSetColorArr(panel, ctx->Theme.PanelBg); + panel->BorderColor[0] = ctx->Theme.PanelBorder[0]; + panel->BorderColor[1] = ctx->Theme.PanelBorder[1]; + panel->BorderColor[2] = ctx->Theme.PanelBorder[2]; + panel->BorderColor[3] = ctx->Theme.PanelBorder[3]; + panel->BorderColor[3] = 1.0f; + panel->BorderThickness = 1.f; + panel->EdgeSoftness = 0.f; + ZUIBoxSetCornerRadius(panel, 0.f); + + // Title bar — draggable (Gap 4) + ZUIBox* hdr = ZUIBeginRow(ctx, "##insp_hdr", ZFill(), ZSPx(ctx, 26.f)); + hdr->Flags = hdr->Flags | ZUI_DrawBackground | ZUI_Clickable; + ZUIBoxSetColorArr(hdr, ctx->Theme.TitleBarBg); + hdr->EdgeSoftness = 0.f; + ZUISpacer(ctx, 6.f); + ZUILabel(ctx, Name ? Name : "Inspector"); + ZUISignal drag_sig = ZUISignalFromBox(ctx, hdr); + ZUIEndRow(ctx); + if ((drag_sig.Flags & ZUI_SignalHeld) && (drag_sig.DragDelta[0] != 0.f || drag_sig.DragDelta[1] != 0.f)) + { + RegionX += drag_sig.DragDelta[0]; + RegionY += drag_sig.DragDelta[1]; + Detached = true; + panel->FloatPos[0] = RegionX; + panel->FloatPos[1] = RegionY; + } + if (drag_sig.Flags & ZUI_SignalDoubleClicked) + { + Detached = false; + } + ZUISeparator(ctx); + + // Scroll region wrapping all actor content (fills remaining panel height) + ZUIBeginScrollRegion(ctx, "##insp_scroll", ZFill(), ZFill()); + + // No selection guard + ActorHandle h = current_scene->SelectedActorHandle; + Actor* actor = eng->ActorManager->Access(h); + if (!actor) + { + ZUISpacer(ctx, 8.f); + ZUILabel(ctx, "No actor selected", ctx->Theme.TextDim); + ZUIEndScrollRegion(ctx); + ZUIEndColumn(ctx); + return; + } + + // --- Actor header --- + auto* nc = actor->GetComponent(); + { + ZUIBox* hdr = ZUIBeginColumn(ctx, "##actor_hdr_card", ZFill(), ZSPx(ctx, 42.f)); + hdr->Flags = hdr->Flags | ZUI_DrawBackground; + ZUIBoxSetColor(hdr, 0.18f, 0.18f, 0.22f, 1.f); + + if (nc) + { + ZUITextField(ctx, "##actor_name_field", nc->Value, sizeof(nc->Value), 200.f); + } + else + { + ZUILabel(ctx, "Actor"); + } + ZUILabel(ctx, "Actor", ctx->Theme.TextDim); + + ZUIEndColumn(ctx); + } + + ZUISpacer(ctx, 4.f); + + // --- Transform section --- + auto* tc = actor->GetComponent(); + if (tc) + { + ZUICollapsingHeader(ctx, "Transform", &m_transform_open); + if (m_transform_open) + { + float widths[2] = {80.f, 0.f}; + ZUIBeginTable(ctx, "##transform_tbl", 2, widths); + + ZUITableNextRow(ctx); + ZUITableSetColumn(ctx, 0); + ZUITableSetColumn(ctx, 1); + XYZDragRow(ctx, "##loc", "Location", &tc->Position.x, &tc->Position.y, &tc->Position.z, 0.05f); + + ZUITableNextRow(ctx); + ZUITableSetColumn(ctx, 0); + ZUITableSetColumn(ctx, 1); + { + float deg[3] = {tc->Rotation.x * 57.2957f, tc->Rotation.y * 57.2957f, tc->Rotation.z * 57.2957f}; + XYZDragRow(ctx, "##rot", "Rotation", °[0], °[1], °[2], 1.0f); + tc->Rotation.x = deg[0] / 57.2957f; + tc->Rotation.y = deg[1] / 57.2957f; + tc->Rotation.z = deg[2] / 57.2957f; + } + + ZUITableNextRow(ctx); + ZUITableSetColumn(ctx, 0); + ZUITableSetColumn(ctx, 1); + XYZDragRow(ctx, "##scl", "Scale", &tc->Scale.x, &tc->Scale.y, &tc->Scale.z, 0.01f); + + ZUIEndTable(ctx); + ZUISpacer(ctx, 4.f); + } + } + + // --- Mesh section --- + auto* mc = actor->GetComponent(); + if (mc) + { + ZUICollapsingHeader(ctx, "Mesh", &m_mesh_open); + if (m_mesh_open) + { + std::string uuid_str = uuids::to_string(mc->MeshUUID); + PropRow(ctx, "##mesh_uuid", "UUID", uuid_str.c_str()); + ZUISpacer(ctx, 4.f); + } + } + + // --- Light section --- + auto* lc = actor->GetComponent(); + if (lc) + { + ZUICollapsingHeader(ctx, "Light", &m_light_open); + if (m_light_open) + { + ZUIBeginRow(ctx, "##light_intensity_row", ZFill(), ZSPx(ctx, kRowH)); + ZUILabel(ctx, "Intensity", ctx->Theme.TextDim); + ZUIDragFloat(ctx, "##light_intensity", &lc->Intensity, 0.1f, 120.f); + ZUIEndRow(ctx); + + char type_buf[32]; + snprintf(type_buf, sizeof(type_buf), "%d", (int) lc->LightType); + PropRow(ctx, "##light_type", "Type", type_buf); + ZUISpacer(ctx, 4.f); + } + } + + ZUIEndScrollRegion(ctx); + ZUIEndColumn(ctx); // end panel + } +} // namespace Tetragrama::Components diff --git a/Tetragrama/Components/ZUI/ZUIInspectorViewComponent.h b/Tetragrama/Components/ZUI/ZUIInspectorViewComponent.h new file mode 100644 index 000000000..6309cf6d9 --- /dev/null +++ b/Tetragrama/Components/ZUI/ZUIInspectorViewComponent.h @@ -0,0 +1,22 @@ +#pragma once +#include + +namespace Tetragrama::Components +{ + class ZUIInspectorViewComponent : public ZUIComponent + { + public: + ZUIInspectorViewComponent() = default; + ~ZUIInspectorViewComponent() override = default; + + void Initialize(Tetragrama::Layers::ZUILayer* parent, cstring name = "Inspector", bool visibility = true) override; + + void BuildUI(ZEngine::UI::ZUIContext* ctx) override; + + private: + bool m_transform_open = true; + bool m_mesh_open = true; + bool m_light_open = true; + }; + ZDEFINE_PTR(ZUIInspectorViewComponent); +} // namespace Tetragrama::Components diff --git a/Tetragrama/Components/ZUI/ZUILogComponent.cpp b/Tetragrama/Components/ZUI/ZUILogComponent.cpp new file mode 100644 index 000000000..bfbc9e168 --- /dev/null +++ b/Tetragrama/Components/ZUI/ZUILogComponent.cpp @@ -0,0 +1,167 @@ +#include +#include +#include + +using namespace ZEngine::UI; +using namespace ZEngine::Helpers; + +namespace Tetragrama::Components +{ + ZUILogComponent::~ZUILogComponent() + { + if (m_cookie) + ZEngine::Logging::Logger::RemoveEventHandler(m_cookie); + } + + void ZUILogComponent::Initialize(Tetragrama::Layers::ZUILayer* parent, cstring name, bool visibility) + { + ParentLayer = parent; + Name = name; + Visible = visibility; + m_cookie = ZEngine::Logging::Logger::AddEventHandler({OnLogEntry, this}); + } + + void ZUILogComponent::OnLogEntry(void* user, const ZEngine::Logging::LogMessage& msg) + { + auto* self = static_cast(user); + LogEntry e; + secure_strncpy(e.Text, sizeof(e.Text), msg.Message ? msg.Message : "", sizeof(e.Text) - 1); + e.Color[0] = msg.Color[0]; + e.Color[1] = msg.Color[1]; + e.Color[2] = msg.Color[2]; + e.Color[3] = msg.Color[3]; + e.Level = static_cast(msg.Level); + self->PushEntry(e); + } + + void ZUILogComponent::PushEntry(const LogEntry& e) + { + std::lock_guard lock(m_mutex); + m_ring[m_head] = e; + m_head = (m_head + 1) % kMaxEntries; + if (m_count < kMaxEntries) + ++m_count; + m_scroll_to_bottom = true; + } + + void ZUILogComponent::BuildUI(ZEngine::UI::ZUIContext* ctx) + { + if (!Visible) + { + return; + } + + float sx = RegionW > 0 ? RegionX : 20.f; + float sy = RegionW > 0 ? RegionY : 500.f; + float sw = RegionW > 0 ? RegionW : kPanelW; + float sh = RegionW > 0 ? RegionH : kPanelH; + + if (RegionW == 0) + { + RegionX = sx; + RegionY = sy; + RegionW = sw; + RegionH = sh; + } + ZUIBox* panel = ZUIBeginColumn(ctx, "##zui_log_panel", ZPx(RegionW), ZPx(RegionH)); + panel->Flags = panel->Flags | ZUI_DrawBackground | ZUI_DrawBorder | ZUI_FloatX | ZUI_FloatY; + panel->FloatPos[0] = RegionX; + panel->FloatPos[1] = RegionY; + ZUIBoxSetColorArr(panel, ctx->Theme.PanelBg); + panel->BorderColor[0] = ctx->Theme.PanelBorder[0]; + panel->BorderColor[1] = ctx->Theme.PanelBorder[1]; + panel->BorderColor[2] = ctx->Theme.PanelBorder[2]; + panel->BorderColor[3] = ctx->Theme.PanelBorder[3]; + panel->BorderColor[3] = 1.0f; + panel->BorderThickness = 1.f; + panel->EdgeSoftness = 0.f; + + // --- Header row — draggable --- + ZUIBox* hdr = ZUIBeginRow(ctx, "##log_header", ZFill(), ZSPx(ctx, 28.f)); + hdr->Flags = hdr->Flags | ZUI_DrawBackground | ZUI_Clickable; + ZUIBoxSetColorArr(hdr, ctx->Theme.TitleBarBg); + ZUISpacer(ctx, 6.f); + ZUILabel(ctx, Name ? Name : "Console", ctx->Theme.TextDefault); + ZUISpacer(ctx, 8.f); + ZUISignal clear_sig = ZUIButton(ctx, "Clear##log"); + ZUISignal drag_sig = ZUISignalFromBox(ctx, hdr); + ZUIEndRow(ctx); + if ((drag_sig.Flags & ZUI_SignalHeld) && (drag_sig.DragDelta[0] != 0.f || drag_sig.DragDelta[1] != 0.f)) + { + RegionX += drag_sig.DragDelta[0]; + RegionY += drag_sig.DragDelta[1]; + Detached = true; + panel->FloatPos[0] = RegionX; + panel->FloatPos[1] = RegionY; + } + if (drag_sig.Flags & ZUI_SignalDoubleClicked) + { + Detached = false; + } + + ZUISeparator(ctx); + + // --- Search + level filter toolbar --- + static const char* kLevelLabels[6] = {"Trace", "Info", "Warn", "Error", "Critical", "All"}; + ZUIBeginRow(ctx, "##log_toolbar", ZFill(), ZSPx(ctx, 24.f)); + ZUILabel(ctx, "Search:", ctx->Theme.TextDim); + ZUISpacer(ctx, 4.f); + ZUITextField(ctx, "##log_search", m_search_buf, sizeof(m_search_buf), 160.f); + ZUISpacer(ctx, 8.f); + ZUILabel(ctx, "Level:", ctx->Theme.TextDim); + ZUISpacer(ctx, 4.f); + if (ZUIBeginCombo(ctx, "##log_lvl", kLevelLabels[m_filter_level], ZEngine::UI::ZPx(90.f))) + { + for (int lvl = 0; lvl < 6; ++lvl) + { + bool sel = (m_filter_level == lvl); + if (ZUIComboItem(ctx, kLevelLabels[lvl], sel)) + m_filter_level = lvl; + } + ZUIEndCombo(ctx); + } + ZUIEndRow(ctx); + + // --- All log entries inside a scroll region --- + bool do_scroll; + ZUIBox* scroll = ZUIBeginScrollRegion(ctx, "##log_scroll", ZFill(), ZFill()); + ZUIPaddingXY(scroll, 4.f, 2.f); + { + std::lock_guard lock(m_mutex); + + do_scroll = m_scroll_to_bottom; + m_scroll_to_bottom = false; + + int total = m_count < kMaxEntries ? m_count : kMaxEntries; + int start = (m_count >= kMaxEntries) ? m_head : 0; + + for (int i = 0; i < total; ++i) + { + const LogEntry& e = m_ring[(start + i) % kMaxEntries]; + + // Level filter: skip entries below the selected minimum level. + // m_filter_level 5 == "All" — nothing skipped. + if (m_filter_level < 5 && e.Level < (uint8_t) m_filter_level) + continue; + + // Search filter (case-sensitive strstr) + if (m_search_buf[0] && !strstr(e.Text, m_search_buf)) + continue; + + ZUILabel(ctx, e.Text, e.Color); + } + + if (clear_sig.Flags & ZUI_SignalClicked) + { + m_count = 0; + m_head = 0; + } + } + + if (do_scroll) + ZUIScrollToBottom(ctx, "##log_scroll"); + + ZUIEndScrollRegion(ctx); + ZUIEndColumn(ctx); // end panel + } +} // namespace Tetragrama::Components diff --git a/Tetragrama/Components/ZUI/ZUILogComponent.h b/Tetragrama/Components/ZUI/ZUILogComponent.h new file mode 100644 index 000000000..a1191f396 --- /dev/null +++ b/Tetragrama/Components/ZUI/ZUILogComponent.h @@ -0,0 +1,45 @@ +#pragma once +#include +#include +#include + +namespace Tetragrama::Components +{ + class ZUILogComponent : public ZUIComponent + { + public: + ZUILogComponent() = default; + ~ZUILogComponent() override; + + void Initialize(Tetragrama::Layers::ZUILayer* parent, cstring name = "Console", bool visibility = true) override; + + void BuildUI(ZEngine::UI::ZUIContext* ctx) override; + + private: + struct LogEntry + { + char Text[256] = {}; + float Color[4] = {0.90f, 0.90f, 0.90f, 1.f}; + uint8_t Level = 0; + }; + + static constexpr int kMaxEntries = 512; + static constexpr int kVisibleLines = 12; // entries shown without scroll + static constexpr float kPanelW = 420.f; + static constexpr float kPanelH = 310.f; + static constexpr float kEntryH = 22.f; + + LogEntry m_ring[kMaxEntries] = {}; + int m_head = 0; + int m_count = 0; + std::mutex m_mutex; + uint32_t m_cookie = 0; + char m_search_buf[128] = {}; + int m_filter_level = 5; // 5 = All + bool m_scroll_to_bottom = false; + + void PushEntry(const LogEntry& e); + static void OnLogEntry(void* ctx, const ZEngine::Logging::LogMessage& msg); + }; + ZDEFINE_PTR(ZUILogComponent); +} // namespace Tetragrama::Components diff --git a/Tetragrama/Components/ZUI/ZUIProjectViewComponent.cpp b/Tetragrama/Components/ZUI/ZUIProjectViewComponent.cpp new file mode 100644 index 000000000..4d23f95a2 --- /dev/null +++ b/Tetragrama/Components/ZUI/ZUIProjectViewComponent.cpp @@ -0,0 +1,276 @@ +#include +#include +#include +#include +#include +#include +#include + +using namespace ZEngine::UI; +using namespace ZEngine::Core::VFS; + +namespace Tetragrama::Components +{ + + void ZUIProjectViewComponent::Initialize(Tetragrama::Layers::ZUILayer* parent, cstring name, bool visibility) + { + ParentLayer = parent; + Name = name; + Visible = visibility; + parent->LocalArena.CreateSubArena(ZKilo(256), &m_arena); + } + + void ZUIProjectViewComponent::RefreshIfNeeded() + { + if (m_initialized && m_current_path == m_listed_path) + { + return; + } + + // Reset cache and re-list + m_arena.Clear(); + m_entries = ZPushArray(&m_arena, CachedEntry, kMaxEntries); + m_entry_count = 0; + + auto* vfs = ZEngine::Engine::GetContext()->VFS; + if (!vfs) + { + m_listed_path = m_current_path; + m_initialized = true; + return; + } + + auto scratch = ZGetScratch(&m_arena); + auto list_res = vfs->List(m_current_path, scratch.Arena); + if (list_res.Succeeded()) + { + auto& entries = list_res.Value(); + uint32_t n = entries.size() < kMaxEntries ? (uint32_t) entries.size() : kMaxEntries; + for (uint32_t i = 0; i < n; ++i) + { + const VFSDirEntry& e = entries[i]; + CachedEntry& c = m_entries[m_entry_count++]; + e.Path.CopyFilename(c.name, sizeof(c.name)); + c.is_dir = e.IsDirectory; + if (e.Path.CStr()) + ZEngine::Helpers::secure_strncpy(c.full_path, sizeof(c.full_path), e.Path.CStr(), sizeof(c.full_path) - 1); + } + } + ZReleaseScratch(scratch); + + m_listed_path = m_current_path; + m_initialized = true; + } + + void ZUIProjectViewComponent::BuildUI(ZUIContext* ctx) + { + if (!Visible) + { + return; + } + + if (!m_initialized) + { + m_current_path = VFSPath::Root(); + } + RefreshIfNeeded(); // no-op unless path changed + + if (RegionW == 0) + { + RegionW = (float) ctx->ScreenW * 0.48f; + RegionH = 200.f; + RegionX = (float) ctx->ScreenW * 0.19f; + RegionY = (float) ctx->ScreenH - RegionH - 28.f; + } + + ZUIBox* panel = ZUIBeginColumn(ctx, "##zui_proj_panel", ZPx(RegionW), ZPx(RegionH)); + panel->Flags = panel->Flags | ZUI_DrawBackground | ZUI_DrawBorder | ZUI_FloatX | ZUI_FloatY; + panel->FloatPos[0] = RegionX; + panel->FloatPos[1] = RegionY; + ZUIBoxSetColorArr(panel, ctx->Theme.PanelBg); + panel->BorderColor[0] = ctx->Theme.PanelBorder[0]; + panel->BorderColor[1] = ctx->Theme.PanelBorder[1]; + panel->BorderColor[2] = ctx->Theme.PanelBorder[2]; + panel->BorderColor[3] = ctx->Theme.PanelBorder[3]; + panel->BorderColor[3] = 1.0f; + panel->BorderThickness = 1.f; + panel->EdgeSoftness = 0.f; + + // --- Header: draggable title + path + up button --- + ZUIBox* hdr = ZUIBeginRow(ctx, "##proj_hdr", ZFill(), ZSPx(ctx, 24.f)); + hdr->Flags = hdr->Flags | ZUI_DrawBackground | ZUI_Clickable; + ZUIBoxSetColorArr(hdr, ctx->Theme.TitleBarBg); + hdr->EdgeSoftness = 0.f; + ZUILabel(ctx, Name ? Name : "Project", ctx->Theme.TextDim); + ZUISpacer(ctx, 4.f); + const char* path_str = m_current_path.CStr() ? m_current_path.CStr() : "/"; + ZUILabel(ctx, path_str, ctx->Theme.TextDim); + ZUISpacer(ctx, 8.f); + ZUISignal up_sig = ZUIButton(ctx, "Up##proj"); + ZUISignal drag_sig = ZUISignalFromBox(ctx, hdr); + ZUIEndRow(ctx); + if ((drag_sig.Flags & ZUI_SignalHeld) && (drag_sig.DragDelta[0] != 0.f || drag_sig.DragDelta[1] != 0.f)) + { + RegionX += drag_sig.DragDelta[0]; + RegionY += drag_sig.DragDelta[1]; + Detached = true; + panel->FloatPos[0] = RegionX; + panel->FloatPos[1] = RegionY; + } + if (drag_sig.Flags & ZUI_SignalDoubleClicked) + { + Detached = false; + } + + // --- Search row --- + ZUIBeginRow(ctx, "##proj_search_row", ZFill(), ZSPx(ctx, 24.f)); + ZUILabel(ctx, "Search:", ctx->Theme.TextDim); + ZUISpacer(ctx, 4.f); + ZUITextField(ctx, "##proj_search", m_search_buf, sizeof(m_search_buf), 160.f); + ZUIEndRow(ctx); + + ZUISeparator(ctx); + + // --- Cached directory entries (scrollable) --- + // Extension color palette + static const float kColDir[4] = {1.f, 0.9f, 0.2f, 1.f}; // yellow — directories + static const float kColMesh[4] = {0.4f, 0.8f, 1.f, 1.f}; // blue — .glb/gltf/fbx/obj + static const float kColScene[4] = {0.8f, 0.5f, 1.f, 1.f}; // purple — .zescene + static const float kColAsset[4] = {0.5f, 1.f, 0.5f, 1.f}; // green — .zemesh + static const float kColTex[4] = {1.f, 0.7f, 0.4f, 1.f}; // orange — image files + + auto ExtColor = [&](const char* name) -> const float* { + const char* dot = strrchr(name, '.'); + if (!dot) + { + return ctx->Theme.TextDefault; + } + if (strcmp(dot, ".glb") == 0 || strcmp(dot, ".gltf") == 0 || strcmp(dot, ".fbx") == 0 || strcmp(dot, ".obj") == 0) + { + return kColMesh; + } + if (strcmp(dot, ".zescene") == 0) + { + return kColScene; + } + if (strcmp(dot, ".zemesh") == 0) + { + return kColAsset; + } + if (strcmp(dot, ".png") == 0 || strcmp(dot, ".jpg") == 0 || strcmp(dot, ".jpeg") == 0) + { + return kColTex; + } + return ctx->Theme.TextDefault; + }; + + ZUIBeginScrollRegion(ctx, "##proj_scroll", ZFill(), ZFill()); + for (uint32_t i = 0; i < m_entry_count; ++i) + { + const CachedEntry& e = m_entries[i]; + if (!e.name[0]) + { + continue; + } + + // Search filter + if (m_search_buf[0] && !strstr(e.name, m_search_buf)) + { + continue; + } + + char row_key[32]; + snprintf(row_key, sizeof(row_key), "##prow_%u", i); + + // --- Row: icon square + name + ext tag --- + ZUIBox* row = ZUIBeginRow(ctx, row_key, ZFill(), ZSPx(ctx, 28.f)); + row->Flags = row->Flags | ZUI_DrawBackground | ZUI_Clickable; + ZUIBoxSetColor(row, 0.45f, 0.45f, 0.50f, 0.f); // transparent, hover fades in + + ZUISpacer(ctx, 4.f); + // Type icon — 14×14 colored square + { + const float* icon_col = e.is_dir ? kColDir : ExtColor(e.name); + char icon_key[40]; + snprintf(icon_key, sizeof(icon_key), "##picon_%u", i); + ZUIBox* icon = ZUIPushBox(ctx, icon_key, (uint32_t) ZEngine::Helpers::secure_strlen(icon_key), ZUI_DrawBackground); + icon->Size[0] = ZSPx(ctx, 16.f); + icon->Size[1] = ZSPx(ctx, 16.f); + ZUIBoxSetColorArr(icon, icon_col); + icon->EdgeSoftness = 0.5f; + ZUIBoxSetCornerRadius(icon, 3.f); + ZUIPopBox(ctx); + } + ZUISpacer(ctx, 5.f); + // Name + { + const float* name_col = e.is_dir ? kColDir : ExtColor(e.name); + ZUILabel(ctx, e.name, name_col); + } + ZUISpacer(ctx, 4.f); + + ZUISignal row_sig = ZUISignalFromBox(ctx, row); + + // Drag source for file assets → drop on scene viewport + if (!e.is_dir && e.full_path[0]) + ZUIBeginDragSource(ctx, row, e.full_path, (uint32_t) ZEngine::Helpers::secure_strlen(e.full_path)); + + ZUIEndRow(ctx); + + // Double-click on directory: navigate into it + if ((row_sig.Flags & ZUI_SignalDoubleClicked) && e.is_dir) + { + auto next = m_current_path.Append(e.name); + if (next.Succeeded()) + { + m_current_path = next.Value(); + } + } + + // Context menu + char ctx_key[40]; + snprintf(ctx_key, sizeof(ctx_key), "##proj_ctx_%u", i); + if (ZUIBeginPopupContextItem(ctx, ctx_key, row_sig)) + { + if (e.is_dir) + { + if (ZUIMenuItem(ctx, "Open##proj")) + { + auto next = m_current_path.Append(e.name); + if (next.Succeeded()) + { + m_current_path = next.Value(); + } + } + } + else + { + if (ZUIMenuItem(ctx, "Import##proj") && e.full_path[0]) + { + ZEngine::Helpers::secure_strncpy(PendingImportPath, sizeof(PendingImportPath), e.full_path, sizeof(PendingImportPath) - 1); + ShowImporter = true; + } + } + ZUIEndContextMenu(ctx); + } + + // Single-click on directory also navigates (kept for discoverability) + if ((row_sig.Flags & ZUI_SignalClicked) && e.is_dir) + { + auto next = m_current_path.Append(e.name); + if (next.Succeeded()) + { + m_current_path = next.Value(); + } + } + } + + ZUIEndScrollRegion(ctx); + + // Up navigation (applied after rendering so signal is from prev frame) + if ((up_sig.Flags & ZUI_SignalClicked) && !m_current_path.IsRoot()) + m_current_path = m_current_path.Parent(); + + ZUIEndColumn(ctx); + } +} // namespace Tetragrama::Components diff --git a/Tetragrama/Components/ZUI/ZUIProjectViewComponent.h b/Tetragrama/Components/ZUI/ZUIProjectViewComponent.h new file mode 100644 index 000000000..aa8975f71 --- /dev/null +++ b/Tetragrama/Components/ZUI/ZUIProjectViewComponent.h @@ -0,0 +1,44 @@ +#pragma once +#include +#include +#include + +namespace Tetragrama::Components +{ + class ZUIProjectViewComponent : public ZUIComponent + { + public: + ZUIProjectViewComponent() = default; + ~ZUIProjectViewComponent() override = default; + + void Initialize(Tetragrama::Layers::ZUILayer* parent, cstring name = "Project", bool visibility = true) override; + + void BuildUI(ZEngine::UI::ZUIContext* ctx) override; + + private: + struct CachedEntry + { + char name[256] = {}; + char full_path[512] = {}; + bool is_dir = false; + }; + + static constexpr uint32_t kMaxEntries = 256; + + ZEngine::Core::Memory::ArenaAllocator m_arena = {}; + ZEngine::Core::VFS::VFSPath m_current_path = {}; + ZEngine::Core::VFS::VFSPath m_listed_path = {}; + CachedEntry* m_entries = nullptr; + uint32_t m_entry_count = 0; + bool m_initialized = false; + char m_search_buf[256] = {}; + + // Re-lists only when m_current_path != m_listed_path — NOT every frame + void RefreshIfNeeded(); + + public: + char PendingImportPath[512] = {}; + bool ShowImporter = false; + }; + ZDEFINE_PTR(ZUIProjectViewComponent); +} // namespace Tetragrama::Components diff --git a/Tetragrama/Components/ZUI/ZUISceneViewportComponent.cpp b/Tetragrama/Components/ZUI/ZUISceneViewportComponent.cpp new file mode 100644 index 000000000..727460331 --- /dev/null +++ b/Tetragrama/Components/ZUI/ZUISceneViewportComponent.cpp @@ -0,0 +1,144 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace ZEngine::UI; + +namespace Tetragrama::Components +{ + void ZUISceneViewportComponent::Initialize(Tetragrama::Layers::ZUILayer* parent, cstring name, bool visibility) + { + ParentLayer = parent; + Name = name; + Visible = visibility; + } + + void ZUISceneViewportComponent::BuildUI(ZUIContext* ctx) + { + if (!Visible || !ParentLayer || !ParentLayer->CurrentApp) + { + return; + } + + auto* app = reinterpret_cast(ParentLayer->CurrentApp); + if (!app->RenderPipeline || !app->RenderPipeline->SceneRenderer) + { + return; + } + + // Fetch latest scene render output — updated every render frame + m_scene_texture = app->RenderPipeline->SceneRenderer->GetFrameOutput(); + if (!m_scene_texture.Valid()) + { + return; + } + + float sw = RegionW > 0 ? RegionW : (float) ctx->ScreenW * 0.60f; + float sh = RegionW > 0 ? RegionH : (float) ctx->ScreenH * 0.72f; + float sx = RegionW > 0 ? RegionX : (float) ctx->ScreenW * 0.19f; + float sy = RegionW > 0 ? RegionY : 28.f; // below menu bar + + ZUIBox* panel = ZUIBeginColumn(ctx, "##zui_vp_panel", ZPx(sw), ZPx(sh)); + panel->Flags = panel->Flags | ZUI_DrawBorder | ZUI_FloatX | ZUI_FloatY; + panel->FloatPos[0] = sx; + panel->FloatPos[1] = sy; + panel->BorderColor[0] = ctx->Theme.PanelBorder[0]; + panel->BorderColor[1] = ctx->Theme.PanelBorder[1]; + panel->BorderColor[2] = ctx->Theme.PanelBorder[2]; + panel->BorderColor[3] = 1.0f; + panel->BorderThickness = 1.f; + panel->EdgeSoftness = 0.f; + + // Scene image fills the full panel — drag-drop target and viewport-hover source + ZUIBox* img_box = ZUIPushBox(ctx, "##scene_img", 11, ZUI_DrawBackground | ZUI_Clickable); + img_box->Size[0] = ZFill(); + img_box->Size[1] = ZFill(); + img_box->TextureIndex = m_scene_texture.Index; + ZUIBoxSetColor(img_box, 1.f, 1.f, 1.f, 1.f); + + ZUISignal vp_sig = ZUISignalFromBox(ctx, img_box); + ZUIPopBox(ctx); + + // Gap 3: viewport-hover → gate camera controller + ctx->ViewportHovered = (vp_sig.Flags & ZUI_SignalHovered) != 0; + + // Gap 2: accept file drops + char drop_buf[512] = {}; + if (ZUIAcceptDrop(ctx, img_box, drop_buf, sizeof(drop_buf)) && ZEngine::Helpers::secure_strlen(drop_buf) > 0) + { + auto* app = reinterpret_cast(ParentLayer->CurrentApp); + const char* dot = strrchr(drop_buf, '.'); + if (dot && strcmp(dot, ".zescene") == 0) + { + Messengers::IMessenger::SendAsync>(Tetragrama::EDITOR_COMPONENT_DOCKSPACE_REQUEST_OPENSCENE, Messengers::GenericMessage(drop_buf)); + } + else if (dot && strcmp(dot, ".zemesh") == 0) + { + Messengers::IMessenger::SendAsync>(Tetragrama::EDITOR_COMPONENT_DOCKSPACE_REQUEST_OPENMESH, Messengers::GenericMessage(drop_buf)); + } + else if (app && app->Configuration && dot && (strcmp(dot, ".glb") == 0 || strcmp(dot, ".gltf") == 0 || strcmp(dot, ".fbx") == 0 || strcmp(dot, ".obj") == 0)) + { + ZEngine::Helpers::secure_strncpy(app->Configuration->PendingImportPath, sizeof(app->Configuration->PendingImportPath), drop_buf, sizeof(app->Configuration->PendingImportPath) - 1); + const char* name = strrchr(drop_buf, '/'); + name = name ? name + 1 : drop_buf; + ZEngine::Helpers::secure_strncpy(app->Configuration->PendingImportName, sizeof(app->Configuration->PendingImportName), name, sizeof(app->Configuration->PendingImportName) - 1); + app->Configuration->ShowImporter = true; + app->Configuration->FocusImporter = true; + } + } + + // --- TRS toolbar overlay (top-left of viewport, floats over the scene image) --- + { + static const float kTransparent[4] = {0.f, 0.f, 0.f, 0.f}; + ZUIBox* trs = ZUIBeginRow(ctx, "##vp_trs_overlay", ZSPx(ctx, 108.f), ZSPx(ctx, 28.f)); + trs->Flags = trs->Flags | ZUI_FloatX | ZUI_FloatY; + trs->FloatPos[0] = sx + 8.f; + trs->FloatPos[1] = sy + 8.f; + ZUIBoxSetColorArr(trs, kTransparent); + ZUISmallButton(ctx, "T##gizmo"); // Translate + ZUISameLine(ctx); + ZUISmallButton(ctx, "R##gizmo"); // Rotate + ZUISameLine(ctx); + ZUISmallButton(ctx, "S##gizmo"); // Scale + ZUIEndRow(ctx); + } + + // --- FPS overlay (top-right of viewport) --- + { + static const float kTransparent[4] = {0.f, 0.f, 0.f, 0.f}; + static float s_fps = 0.f; + if (ctx->DeltaTime > 0.f) + s_fps = s_fps * 0.95f + (1.f / ctx->DeltaTime) * 0.05f; + char fps_buf[32]; + snprintf(fps_buf, sizeof(fps_buf), "%.0f fps", (double) s_fps); + ZUIBox* fps_box = ZUIBeginRow(ctx, "##vp_fps_overlay", ZSPx(ctx, 120.f), ZSPx(ctx, 22.f)); + fps_box->Flags = fps_box->Flags | ZUI_FloatX | ZUI_FloatY; + fps_box->FloatPos[0] = sx + sw - 80.f; + fps_box->FloatPos[1] = sy + 8.f; + ZUIBoxSetColorArr(fps_box, kTransparent); + ZUILabel(ctx, fps_buf, ctx->Theme.TextDefault); + ZUIEndRow(ctx); + } + + // Request resize if dimensions changed + if ((uint32_t) sw != m_last_w || (uint32_t) sh != m_last_h) + { + m_last_w = (uint32_t) sw; + m_last_h = (uint32_t) sh; + if (app->State) + { + app->State->RenderTargetResizeRequests.Emplace({.Width = m_last_w, .Height = m_last_h}); + } + } + + ZUIEndColumn(ctx); + } +} // namespace Tetragrama::Components diff --git a/Tetragrama/Components/ZUI/ZUISceneViewportComponent.h b/Tetragrama/Components/ZUI/ZUISceneViewportComponent.h new file mode 100644 index 000000000..2de6d29d4 --- /dev/null +++ b/Tetragrama/Components/ZUI/ZUISceneViewportComponent.h @@ -0,0 +1,23 @@ +#pragma once +#include +#include + +namespace Tetragrama::Components +{ + class ZUISceneViewportComponent : public ZUIComponent + { + public: + ZUISceneViewportComponent() = default; + ~ZUISceneViewportComponent() override = default; + + void Initialize(Tetragrama::Layers::ZUILayer* parent, cstring name = "Scene", bool visibility = true) override; + + void BuildUI(ZEngine::UI::ZUIContext* ctx) override; + + private: + ZEngine::Rendering::Textures::TextureHandle m_scene_texture = {}; + uint32_t m_last_w = 0; + uint32_t m_last_h = 0; + }; + ZDEFINE_PTR(ZUISceneViewportComponent); +} // namespace Tetragrama::Components diff --git a/Tetragrama/Components/ZUI/ZUIStatusBarComponent.cpp b/Tetragrama/Components/ZUI/ZUIStatusBarComponent.cpp new file mode 100644 index 000000000..cc004b256 --- /dev/null +++ b/Tetragrama/Components/ZUI/ZUIStatusBarComponent.cpp @@ -0,0 +1,134 @@ +#include +#include +#include +#include + +using namespace ZEngine::UI; + +namespace Tetragrama::Components +{ + + void ZUIStatusBarComponent::Initialize(Tetragrama::Layers::ZUILayer* parent, cstring name, bool visibility) + { + ParentLayer = parent; + Name = name; + Visible = visibility; + } + + void ZUIStatusBarComponent::BuildUI(ZUIContext* ctx) + { + if (!Visible || !ParentLayer || !ParentLayer->CurrentApp) + { + return; + } + + auto* app = reinterpret_cast(ParentLayer->CurrentApp); + + // Update smoothed frame time + m_frame_times[m_ft_head] = ctx->DeltaTime; + m_ft_head = (m_ft_head + 1) % kFtSamples; + float sum = 0.f; + for (int i = 0; i < kFtSamples; ++i) + { + sum += m_frame_times[i]; + } + m_smoothed_dt = sum / (float) kFtSamples; + + float sw = RegionW > 0 ? RegionW : (float) ctx->ScreenW; + float sy = RegionW > 0 ? RegionY : (float) ctx->ScreenH - kBarH; + + // Bar: full-width, bottom-anchored + ZUIBox* bar = ZUIBeginRow(ctx, "##status_bar", ZPx(sw), ZPx(kBarH)); + bar->Flags = bar->Flags | ZUI_DrawBackground | ZUI_DrawBorder | ZUI_FloatX | ZUI_FloatY; + bar->FloatPos[0] = RegionW > 0 ? RegionX : 0.f; + bar->FloatPos[1] = sy; + ZUIBoxSetColorArr(bar, ctx->Theme.StatusBarBg); + bar->BorderColor[0] = ctx->Theme.PanelBorder[0]; + bar->BorderColor[1] = ctx->Theme.PanelBorder[1]; + bar->BorderColor[2] = ctx->Theme.PanelBorder[2]; + bar->BorderColor[3] = ctx->Theme.PanelBorder[3]; + bar->BorderThickness = 1.f; + bar->EdgeSoftness = 0.f; + + ZUISpacer(ctx, 6.f); + + // Console toggle + { + bool on = app->Configuration->ShowConsole; + ZUISignal s = ZUIButton(ctx, on ? "Console##on" : "Console##off"); + if (on) + { + ZUIBox* btn = ctx->Current ? ctx->Current->LastChild : nullptr; + (void) btn; // future: tint button with k_on color + } + if (s.Flags & ZUI_SignalClicked) + { + app->Configuration->ShowConsole = !on; + app->Configuration->FocusConsole = !on; + } + } + + ZUISpacer(ctx, 4.f); + + // Browser toggle + { + bool on = app->Configuration->ShowContentBrowser; + ZUISignal s = ZUIButton(ctx, on ? "Browser##on" : "Browser##off"); + if (s.Flags & ZUI_SignalClicked) + { + app->Configuration->ShowContentBrowser = !on; + app->Configuration->FocusContentBrowser = !on; + } + } + + ZUISpacer(ctx, 4.f); + + // Importer toggle + { + bool on = app->Configuration->ShowImporter; + ZUISignal s = ZUIButton(ctx, on ? "Importer##on" : "Importer##off"); + if (s.Flags & ZUI_SignalClicked) + { + app->Configuration->ShowImporter = !on; + app->Configuration->FocusImporter = !on; + } + } + + ZUISpacer(ctx, 8.f); + ZUILabel(ctx, "|", ctx->Theme.TextDim); + ZUISpacer(ctx, 8.f); + + // Scene name + ZUILabel(ctx, "Scene:", ctx->Theme.TextDim); + ZUISpacer(ctx, 4.f); + const char* scene_name = (app->Configuration && !app->Configuration->ActiveSceneName.empty()) ? app->Configuration->ActiveSceneName.c_str() : "-"; + ZUILabel(ctx, scene_name, ctx->Theme.TextDefault); + + ZUISpacer(ctx, 10.f); + ZUILabel(ctx, "|", ctx->Theme.TextDim); + ZUISpacer(ctx, 10.f); + + // Camera position + if (app->CameraController) + { + auto pos = app->CameraController->GetPosition(); + char cam_buf[64]; + snprintf(cam_buf, sizeof(cam_buf), "X:%.1f Y:%.1f Z:%.1f", (double) pos.x, (double) pos.y, (double) pos.z); + ZUILabel(ctx, cam_buf, ctx->Theme.TextDim); + } + + ZUISpacer(ctx, 10.f); + ZUILabel(ctx, "|", ctx->Theme.TextDim); + ZUISpacer(ctx, 10.f); + + // FPS + { + float fps = m_smoothed_dt > 0.f ? 1.f / m_smoothed_dt : 0.f; + char fps_buf[32]; + snprintf(fps_buf, sizeof(fps_buf), "FPS: %.0f %.2f ms", (double) fps, (double) (m_smoothed_dt * 1000.f)); + ZUILabel(ctx, fps_buf, ctx->Theme.TextDim); + } + + ZUIEndRow(ctx); + } +} // namespace Tetragrama::Components diff --git a/Tetragrama/Components/ZUI/ZUIStatusBarComponent.h b/Tetragrama/Components/ZUI/ZUIStatusBarComponent.h new file mode 100644 index 000000000..3f7cd06c5 --- /dev/null +++ b/Tetragrama/Components/ZUI/ZUIStatusBarComponent.h @@ -0,0 +1,25 @@ +#pragma once +#include + +namespace Tetragrama::Components +{ + class ZUIStatusBarComponent : public ZUIComponent + { + public: + ZUIStatusBarComponent() = default; + ~ZUIStatusBarComponent() override = default; + + void Initialize(Tetragrama::Layers::ZUILayer* parent, cstring name = "StatusBar", bool visibility = true) override; + + void BuildUI(ZEngine::UI::ZUIContext* ctx) override; + + private: + static constexpr int kFtSamples = 32; + static constexpr float kBarH = 28.f; + + float m_frame_times[kFtSamples] = {}; + int m_ft_head = 0; + float m_smoothed_dt = 0.f; + }; + ZDEFINE_PTR(ZUIStatusBarComponent); +} // namespace Tetragrama::Components diff --git a/Tetragrama/Editor.cpp b/Tetragrama/Editor.cpp index 8fa053e7b..66a25ed6e 100644 --- a/Tetragrama/Editor.cpp +++ b/Tetragrama/Editor.cpp @@ -1,10 +1,16 @@ +// New panel-manager-based UI (replaces old per-component system) +#include #include #include #include #include +#include +#include #include #include #include +#include +#include #include #include #include @@ -54,31 +60,109 @@ namespace Tetragrama { auto editor_scene = ZPushStructCtor(&Memory->MainArena, EditorScene); auto editor_cam_controller = ZPushStructCtor(&Memory->MainArena, Controllers::EditorCameraController); - UILayer = ZPushStructCtor(&Memory->MainArena, ImguiLayer); + ZUIUILayer = ZPushStructCtor(&Memory->MainArena, ZUILayer); - UILayer->Initialize(&Memory->MainArena, this); + ZUIUILayer->Initialize(&Memory->MainArena, this); + + // Single panel-manager component replaces all old per-panel components. + // It owns the dock tree, tab bars, and all panel views. + auto* pm = ZPushStructCtor(&Memory->MainArena, Tetragrama::Panels::ZUIPanelManagerComponent); + pm->Initialize(ZUIUILayer, "PanelManager"); + ZUIUILayer->AddComponent(pm); editor_cam_controller->Initialize(&Memory->MainArena, CurrentWindow, ZEngine::Engine::GetContext()->InputManager, this); editor_scene->Initialize(&Memory->MainArena, Configuration->ActiveSceneName.c_str()); CameraController = editor_cam_controller; CurrentScene = editor_scene; + // Bake ZUI font atlases. Font sizes are chosen to be readable on high-resolution + // displays where glfwGetWindowSize returns physical pixel counts (~3024px wide). + // ImGui approach: fonts are baked at physical pixel density and all draw + // coordinates are also in physical pixels — the NDC transform handles the rest. + if (RenderPipeline && RenderPipeline->ZUICtx && RenderPipeline->ZUIRenderer) + { + constexpr const char* kFontPath = "/ZodiacEngine/Settings/Fonts/OpenSans/OpenSans-Regular.ttf"; + constexpr const char* kHeaderFontPath = "/ZodiacEngine/Settings/Fonts/OpenSans/OpenSans-SemiBold.ttf"; + auto* ctx = RenderPipeline->ZUICtx; + + // Font atlas is always baked at 2× the logical base size so that + // every display gets a 2× oversampled atlas — the same sharpness + // advantage Retina screens had before, now available everywhere. + // + // kBase = logical display size (matches ZUIStyle.FontSize) + // kBake = atlas physical size (kBase * kOversample) + // FontScale = 1/kOversample (maps atlas px → logical px) + // + // UIScale (fb/win ratio) is set each frame by BeginOverlayFrame and + // handles the physical pixel density independently of the font atlas. + constexpr float kBase = 13.f; // logical body size in px + constexpr float kOversample = 2.f; // always 2× — sharp on every display + const float kBake = kBase * kOversample; // 26 px atlas + const float kSmall = kBase * 0.80f * kOversample; + const float kHeader = kBase * 1.30f * kOversample; + const float kFontScale = 1.f / kOversample; // 0.5 + + ZENGINE_CORE_INFO("[ZUI] FontBake body={:.0f} small={:.0f} header={:.0f} FontScale={:.2f}", kBake, kSmall, kHeader, kFontScale); + + auto scratch = ZGetScratch(&Memory->MainArena); + ctx->Atlas = ZEngine::UI::ZUIFontAtlasBake(&ctx->PersistentArena, scratch.Arena, RenderPipeline->Device, kFontPath, kSmall, kBake, kHeader, 32, 96, kHeaderFontPath); + ZReleaseScratch(scratch); + + if (ctx->Atlas) + { + if (ctx->Atlas->Small) + ctx->Atlas->Small->FontScale = kFontScale; + if (ctx->Atlas->Body) + ctx->Atlas->Body->FontScale = kFontScale; + if (ctx->Atlas->Header) + ctx->Atlas->Header->FontScale = kFontScale; + + // Style.FontSize = logical body size → FrameHeight = 13 + 3*2 = 19 px + ctx->Style.FontSize = kBase; + ZUIStyleUpdate(&ctx->Style); + ZENGINE_CORE_INFO("[ZUI] Style.FontSize={:.0f} FrameHeight={:.0f}", ctx->Style.FontSize, ctx->Style.FrameHeight); + } + } + // Scene instance creation is handled directly in SceneViewportUIComponent::OnDrop // via ImportCoordinator::Enqueue's returned UUID — no callback needed here. } - void Editor::OnUpdate(float dt) + void Editor::ProcessEvent(ZEngine::Core::CoreEvent& e) { - CHECK_AND_ESCAPE_NULL(UILayer) + // Always route events to the window and ZUI layer + if (CurrentWindow) + { + CurrentWindow->OnEvent(e); + } + if (ZUIUILayer) + { + ZUIUILayer->OnEvent(e); + } + + // Gate camera-controller mouse routing on viewport focus (Gap 3) + bool is_mouse_event = (e.GetType() == ZEngine::Core::EventType::MouseButtonPressed || e.GetType() == ZEngine::Core::EventType::MouseButtonReleased || e.GetType() == ZEngine::Core::EventType::MouseMoved || e.GetType() == ZEngine::Core::EventType::MouseWheel); - UILayer->Update(dt); + bool viewport_active = RenderPipeline && RenderPipeline->ZUICtx && RenderPipeline->ZUICtx->ViewportHovered; + + if (CameraController && (!is_mouse_event || viewport_active)) + { + CameraController->OnEvent(e); + } + + OnEvent(e); } - void Editor::OnEvent(Core::CoreEvent& e) + void Editor::OnUpdate(float dt) { - CHECK_AND_ESCAPE_NULL(UILayer) + CHECK_AND_ESCAPE_NULL(ZUIUILayer) + ZUIUILayer->Update(dt); + } - UILayer->OnEvent(e); + void Editor::OnEvent(Core::CoreEvent& /*e*/) + { + // Event routing is handled in ProcessEvent (which also gates camera input + // on viewport focus). Nothing extra needed here. } void Editor::OnPreRender() {} @@ -87,7 +171,10 @@ namespace Tetragrama void Editor::OnRenderUI() { - UILayer->Render(nullptr, nullptr); + if (ZUIUILayer) + { + ZUIUILayer->Render(nullptr, nullptr); + } } void Editor::OnClosing() {} diff --git a/Tetragrama/Editor.h b/Tetragrama/Editor.h index 74eb4f623..769c453e6 100644 --- a/Tetragrama/Editor.h +++ b/Tetragrama/Editor.h @@ -1,6 +1,6 @@ #pragma once #include -#include +#include #include #include #include @@ -49,7 +49,7 @@ namespace Tetragrama virtual ~Editor() {} - ZRawPtr(Layers::ImguiLayer) UILayer = nullptr; + ZRawPtr(Layers::ZUILayer) ZUIUILayer = nullptr; ZEngine::Core::VFS::VFSDiskBackend WorkingSpaceBackend = {}; @@ -60,6 +60,9 @@ namespace Tetragrama virtual void OnUpdate(float dt) override; virtual void OnEvent(ZEngine::Core::CoreEvent&) override; + // Gates camera-controller routing on viewport hover (Gap 3) + void ProcessEvent(ZEngine::Core::CoreEvent&) override; + virtual void OnPreRender() override; virtual void OnPostRender() override; virtual void OnRenderUI() override; diff --git a/Tetragrama/Helpers/UIComponentDrawerHelper.cpp b/Tetragrama/Helpers/UIComponentDrawerHelper.cpp deleted file mode 100644 index 9e4364558..000000000 --- a/Tetragrama/Helpers/UIComponentDrawerHelper.cpp +++ /dev/null @@ -1,509 +0,0 @@ -#include -#include - -namespace Tetragrama::Helpers -{ - - void DrawVec4Control(std::string_view label, ZEngine::Core::Maths::Vec4f& values, const std::function& callback, float default_value, float column_width) - { - ImGuiIO& io = ImGui::GetIO(); - auto default_bold_font = io.Fonts->Fonts[0]; - - ImGui::PushID(label.data(), (label.data() + label.size())); - - ImGui::Columns(2); - - ImGui::SetColumnWidth(0, column_width); - ImGui::Text(label.data()); - ImGui::NextColumn(); - - ImGui::PushMultiItemsWidths(4, ImGui::CalcItemWidth()); - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2{0.5f, 0}); - - float line_height = GImGui->Font->LegacySize + GImGui->Style.FramePadding.y * 2.0f; - ImVec2 button_size = {line_height + 3.0f, line_height}; - - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4{0.8f, 0.1f, 0.15f, 1.0f}); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4{0.9f, 0.2f, 0.2f, 1.0f}); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4{0.8f, 0.1f, 0.15f, 1.0f}); - ImGui::PushFont(default_bold_font); - if (ImGui::Button("X", button_size)) - { - values.x = default_value; - if (callback) - { - callback(values); - } - } - ImGui::PopFont(); - ImGui::PopStyleColor(3); - - ImGui::SameLine(); - if (ImGui::DragFloat("##X", &values.x, 0.1f, 0.0f, 0.0f, "%.2f")) - { - if (callback) - { - callback(values); - } - } - ImGui::PopItemWidth(); - ImGui::SameLine(); - - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4{0.1f, 0.8f, 0.15f, 1.0f}); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4{0.2f, 0.9f, 0.2f, 1.0f}); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4{0.1f, 0.8f, 0.15f, 1.0f}); - ImGui::PushFont(default_bold_font); - if (ImGui::Button("Y", button_size)) - { - values.y = default_value; - if (callback) - { - callback(values); - } - } - ImGui::PopFont(); - ImGui::PopStyleColor(3); - - ImGui::SameLine(); - if (ImGui::DragFloat("##Y", &values.y, 0.1f, 0.0f, 0.0f, "%.2f")) - { - if (callback) - { - callback(values); - } - } - ImGui::PopItemWidth(); - ImGui::SameLine(); - - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4{0.1f, 0.15f, 0.8f, 1.0f}); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4{0.2f, 0.2f, 0.9f, 1.0f}); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4{0.1f, 0.15f, 0.8f, 1.0f}); - ImGui::PushFont(default_bold_font); - if (ImGui::Button("Z", button_size)) - { - values.z = default_value; - if (callback) - { - callback(values); - } - } - ImGui::PopFont(); - ImGui::PopStyleColor(3); - - ImGui::SameLine(); - if (ImGui::DragFloat("##Z", &values.z, 0.1f, 0.0f, 0.0f, "%.2f")) - { - if (callback) - { - callback(values); - } - } - ImGui::PopItemWidth(); - ImGui::SameLine(); - - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4{1.0f, 1.0f, 1.0f, 0.5f}); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4{1.f, 1.0f, 1.0f, 0.8f}); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4{1.0f, 1.0f, 1.0f, 0.5f}); - ImGui::PushFont(default_bold_font); - if (ImGui::Button("W", button_size)) - { - values.w = default_value; - if (callback) - { - callback(values); - } - } - ImGui::PopFont(); - ImGui::PopStyleColor(3); - - ImGui::SameLine(); - if (ImGui::DragFloat("##W", &values.w, 0.1f, 0.0f, 0.0f, "%.2f")) - { - if (callback) - { - callback(values); - } - } - - ImGui::PopItemWidth(); - ImGui::PopStyleVar(); - ImGui::Columns(1); - ImGui::PopID(); - } - - void DrawVec3Control(std::string_view label, ZEngine::Core::Maths::Vec3f& values, const std::function& callback, float default_value, float column_width) - { - ImGuiIO& io = ImGui::GetIO(); - auto default_bold_font = io.Fonts->Fonts[0]; - - ImGui::PushID(label.data(), (label.data() + label.size())); - - ImGui::Columns(2); - - ImGui::SetColumnWidth(0, column_width); - ImGui::Text(label.data()); - ImGui::NextColumn(); - - ImGui::PushMultiItemsWidths(5, ImGui::CalcItemWidth()); - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2{1.0f, 0}); - - float line_height = GImGui->Font->LegacySize + GImGui->Style.FramePadding.y * 2.0f; - ImVec2 button_size = {line_height + 3.0f, line_height}; - - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4{0.8f, 0.1f, 0.15f, 1.0f}); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4{0.9f, 0.2f, 0.2f, 1.0f}); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4{0.8f, 0.1f, 0.15f, 1.0f}); - ImGui::PushFont(default_bold_font); - if (ImGui::Button("X", button_size)) - { - values.x = default_value; - if (callback) - { - callback(values); - } - } - ImGui::PopFont(); - ImGui::PopStyleColor(3); - - ImGui::SameLine(); - if (ImGui::DragFloat("##X", &values.x, 0.1f, 0.0f, 0.0f, "%.2f")) - { - if (callback) - { - callback(values); - } - } - ImGui::PopItemWidth(); - ImGui::SameLine(); - - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4{0.1f, 0.8f, 0.15f, 1.0f}); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4{0.2f, 0.9f, 0.2f, 1.0f}); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4{0.1f, 0.8f, 0.15f, 1.0f}); - ImGui::PushFont(default_bold_font); - if (ImGui::Button("Y", button_size)) - { - values.y = default_value; - if (callback) - { - callback(values); - } - } - ImGui::PopFont(); - ImGui::PopStyleColor(3); - - ImGui::SameLine(); - if (ImGui::DragFloat("##Y", &values.y, 0.1f, 0.0f, 0.0f, "%.2f")) - { - if (callback) - { - callback(values); - } - } - ImGui::PopItemWidth(); - ImGui::SameLine(); - - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4{0.1f, 0.15f, 0.8f, 1.0f}); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4{0.2f, 0.2f, 0.9f, 1.0f}); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4{0.1f, 0.15f, 0.8f, 1.0f}); - ImGui::PushFont(default_bold_font); - if (ImGui::Button("Z", button_size)) - { - values.z = default_value; - if (callback) - { - callback(values); - } - } - ImGui::PopFont(); - ImGui::PopStyleColor(3); - - ImGui::SameLine(); - if (ImGui::DragFloat("##Z", &values.z, 0.1f, 0.0f, 0.0f, "%.2f")) - { - if (callback) - { - callback(values); - } - } - ImGui::PopItemWidth(); - - ImGui::PopStyleVar(); - - ImGui::Columns(1); - - ImGui::PopID(); - } - - void DrawVec2Control(std::string_view label, ZEngine::Core::Maths::Vec2f& values, const std::function& callback, float default_value, float column_width) - { - ImGuiIO& io = ImGui::GetIO(); - auto default_bold_font = io.Fonts->Fonts[0]; - - ImGui::PushID(label.data(), (label.data() + label.size())); - - ImGui::Columns(2); - - ImGui::SetColumnWidth(0, column_width); - ImGui::Text(label.data()); - ImGui::NextColumn(); - - ImGui::PushMultiItemsWidths(2, ImGui::CalcItemWidth()); - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2{0.5f, 0}); - - float line_height = GImGui->Font->LegacySize + GImGui->Style.FramePadding.y * 2.0f; - ImVec2 button_size = {line_height + 3.0f, line_height}; - - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4{0.8f, 0.1f, 0.15f, 1.0f}); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4{0.9f, 0.2f, 0.2f, 1.0f}); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4{0.8f, 0.1f, 0.15f, 1.0f}); - ImGui::PushFont(default_bold_font); - if (ImGui::Button("X", button_size)) - { - values.x = default_value; - if (callback) - { - callback(values); - } - } - ImGui::PopFont(); - ImGui::PopStyleColor(3); - - ImGui::SameLine(); - if (ImGui::DragFloat("##X", &values.x, 0.1f, 0.0f, 0.0f, "%.2f")) - { - if (callback) - { - callback(values); - } - } - ImGui::PopItemWidth(); - ImGui::SameLine(); - - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4{0.1f, 0.8f, 0.15f, 1.0f}); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4{0.2f, 0.9f, 0.2f, 1.0f}); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4{0.1f, 0.8f, 0.15f, 1.0f}); - ImGui::PushFont(default_bold_font); - if (ImGui::Button("Y", button_size)) - { - values.y = default_value; - if (callback) - { - callback(values); - } - } - ImGui::PopFont(); - ImGui::PopStyleColor(3); - - ImGui::SameLine(); - if (ImGui::DragFloat("##Y", &values.y, 0.1f, 0.0f, 0.0f, "%.2f")) - { - if (callback) - { - callback(values); - } - } - - ImGui::PopItemWidth(); - ImGui::PopStyleVar(); - ImGui::Columns(1); - ImGui::PopID(); - } - - void DrawInputTextControl(std::string_view label, std::string_view content, const std::function& callback, bool read_only_mode, float column_width) - { - ImGui::PushID(label.data(), (label.data() + label.size())); - ImGui::Columns(2); - - ImGui::SetColumnWidth(0, column_width); - ImGui::Text(label.data()); - ImGui::NextColumn(); - - ImGui::PushMultiItemsWidths(1, ImGui::CalcItemWidth() + 60.f); - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2{0.5f, 0}); - - char buffer[1024]; - memset(buffer, 0, sizeof(buffer)); - auto raw_entity_name = content.data(); - memcpy(buffer, raw_entity_name, strlen(raw_entity_name)); - - auto flag = 0; - if (read_only_mode) - { - flag |= ImGuiInputTextFlags_ReadOnly; - } - if (ImGui::InputTextEx("##Input", NULL, buffer, sizeof(buffer), ImVec2(0, 0), flag)) - { - if (callback) - { - callback(std::string_view{buffer}); - } - } - ImGui::PopItemWidth(); - ImGui::PopStyleVar(); - ImGui::Columns(1); - ImGui::PopID(); - } - - void DrawDragFloatControl(std::string_view label, float value, float increment_speed, float min_value, float max_value, std::string_view fmt, const std::function& callback, float column_width) - { - ImGui::PushID(label.data(), (label.data() + label.size())); - ImGui::Columns(2); - - ImGui::SetColumnWidth(0, column_width); - ImGui::Text(label.data()); - ImGui::NextColumn(); - - ImGui::PushMultiItemsWidths(1, ImGui::CalcItemWidth() + 60.f); - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2{0.5f, 0}); - - if (ImGui::DragFloat("##DragFloat", &value, increment_speed, min_value, max_value, fmt.data())) - { - if (callback) - { - callback(value); - } - } - ImGui::PopItemWidth(); - ImGui::PopStyleVar(); - ImGui::Columns(1); - ImGui::PopID(); - } - - void DrawCenteredButtonControl(std::string_view label, const std::function& callback) - { - - ImGui::PushID(label.data(), (label.data() + label.size())); - - ImGui::BeginTable("##table", 3); - - ImGui::TableNextColumn(); - - ImGui::TableNextColumn(); - auto table_colum_width = ImGui::GetColumnWidth(); - float line_height = GImGui->Font->LegacySize + GImGui->Style.FramePadding.y * 2.0f; - ImVec2 button_size = {table_colum_width, line_height}; - - if (ImGui::Button(label.data(), button_size)) - { - if (callback) - { - callback(); - } - } - - ImGui::TableNextColumn(); - ImGui::EndTable(); - ImGui::PopID(); - } - - void DrawColorEdit4Control(std::string_view label, ZEngine::Core::Maths::Vec4f& values, const std::function& callback, float default_value, float column_width) - { - ImGui::PushID(label.data(), (label.data() + label.size())); - - ImGui::Columns(2); - - ImGui::SetColumnWidth(0, column_width); - ImGui::Text(label.data()); - ImGui::NextColumn(); - - ImGui::PushMultiItemsWidths(1, ImGui::CalcItemWidth() + 60.f); - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2{0.5f, 0}); - - if (ImGui::ColorEdit4("##TintColor", ZEngine::Core::Maths::value_ptr(values))) - { - if (callback) - { - callback(values); - } - } - - ImGui::PopItemWidth(); - ImGui::PopStyleVar(); - ImGui::Columns(1); - ImGui::PopID(); - } - - void DrawColorEdit3Control(std::string_view label, ZEngine::Core::Maths::Vec3f& values, const std::function& callback, float default_value, float column_width) - { - ImGui::PushID(label.data(), (label.data() + label.size())); - - ImGui::Columns(2); - - ImGui::SetColumnWidth(0, column_width); - ImGui::Text(label.data()); - ImGui::NextColumn(); - - ImGui::PushMultiItemsWidths(1, ImGui::CalcItemWidth() + 60.f); - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2{0.5f, 0}); - - if (ImGui::ColorEdit3("##TintColor", ZEngine::Core::Maths::value_ptr(values))) - { - if (callback) - { - callback(values); - } - } - - ImGui::PopItemWidth(); - ImGui::PopStyleVar(); - ImGui::Columns(1); - ImGui::PopID(); - } - - void DrawTextureColorControl(std::string_view label, ImTextureID texture_id, ZEngine::Core::Maths::Vec4f& tint_color, bool enable_zoom, const std::function& image_click_callback, const std::function& tint_color_change_callback, float column_width) - { - ImGui::PushID(label.data(), (label.data() + label.size())); - ImGui::Columns(2); - - ImGui::SetColumnWidth(0, column_width); - ImGui::Text(label.data()); - ImGui::NextColumn(); - - ImGui::PushMultiItemsWidths(1, ImGui::CalcItemWidth() + 60.f); - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2{0.5f, 0}); - - float line_height = GImGui->Font->LegacySize + GImGui->Style.FramePadding.y * 2.0f; - ImVec2 button_size = {line_height + 3.0f, line_height}; - - if (ImGui::ImageButton("##imgbtn", texture_id, button_size, ImVec2(0, 0), ImVec2(1, 1), ImVec4(0, 0, 0, 0), ImVec4{tint_color.x, tint_color.y, tint_color.z, tint_color.w})) - { - if (image_click_callback) - { - image_click_callback(); - } - } - if (enable_zoom) - { - if (ImGui::IsItemHovered()) - { - ImGui::BeginTooltip(); - ImGui::Image(texture_id, ImVec2(200, 200)); - ImGui::EndTooltip(); - } - } - ImGui::PopItemWidth(); - - ImGui::SameLine(); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(1, 4)); - if (ImGui::ColorEdit4("##TintColor", ZEngine::Core::Maths::value_ptr(tint_color))) - { - if (tint_color_change_callback) - { - tint_color_change_callback(tint_color); - } - } - ImGui::PopItemWidth(); - - ImGui::PopStyleVar(2); - ImGui::Columns(1); - ImGui::PopID(); - } - - void DrawColoredTextLine(const char* start, const char* end, const ImVec4& color) - { - ImGui::PushStyleColor(ImGuiCol_Text, color); - ImGui::TextUnformatted(start, end); - ImGui::PopStyleColor(); - } - -} // namespace Tetragrama::Helpers diff --git a/Tetragrama/Helpers/UIComponentDrawerHelper.h b/Tetragrama/Helpers/UIComponentDrawerHelper.h deleted file mode 100644 index 70777c4c2..000000000 --- a/Tetragrama/Helpers/UIComponentDrawerHelper.h +++ /dev/null @@ -1,24 +0,0 @@ -#pragma once -#include -#include - -namespace Tetragrama::Helpers -{ - void DrawVec4Control(std::string_view label, ZEngine::Core::Maths::Vec4f& values, const std::function& callback = nullptr, float default_value = 0.0f, float column_width = 100.0f); - void DrawVec3Control(std::string_view label, ZEngine::Core::Maths::Vec3f& values, const std::function& callback = nullptr, float default_value = 0.0f, float column_width = 100.0f); - void DrawVec2Control(std::string_view label, ZEngine::Core::Maths::Vec2f& values, const std::function& callback = nullptr, float default_value = 0.0f, float column_width = 100.0f); - - void DrawInputTextControl(std::string_view label, std::string_view content, const std::function& callback = nullptr, bool read_only_mode = false, float column_width = 50.f); - - void DrawDragFloatControl(std::string_view label, float value, float increment_speed = 1.0f, float min_value = 0.0f, float max_value = 0.0f, std::string_view fmt = "%.2f", const std::function& callback = nullptr, float column_width = 100.0f); - - void DrawCenteredButtonControl(std::string_view label, const std::function& callback = nullptr); - - void DrawColorEdit4Control(std::string_view label, ZEngine::Core::Maths::Vec4f& values, const std::function& callback = nullptr, float default_value = 0.0f, float column_width = 100.0f); - - void DrawColorEdit3Control(std::string_view label, ZEngine::Core::Maths::Vec3f& values, const std::function& callback, float default_value = 0.0f, float column_width = 100.0f); - - void DrawTextureColorControl(std::string_view label, ImTextureID texture_id, ZEngine::Core::Maths::Vec4f& texture_tint_color, bool enable_zoom = true, const std::function& image_click_callback = nullptr, const std::function& tint_color_change_callback = nullptr, float column_width = 100.0f); - - void DrawColoredTextLine(const char* start, const char* end, const ImVec4& color); -} // namespace Tetragrama::Helpers diff --git a/Tetragrama/Layers/ImguiLayer.cpp b/Tetragrama/Layers/ImguiLayer.cpp deleted file mode 100644 index da5d0e5d9..000000000 --- a/Tetragrama/Layers/ImguiLayer.cpp +++ /dev/null @@ -1,383 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -using namespace ZEngine; -using namespace ZEngine::Rendering::Renderers; -using namespace ZEngine::Windows::Events; -using namespace ZEngine::Core::Containers; -using namespace ZEngine::Helpers; -using namespace Tetragrama::Messengers; - -namespace Tetragrama::Layers -{ - ImguiLayer::~ImguiLayer() {} - - void ImguiLayer::Initialize(ZEngine::Core::Memory::ArenaAllocator* arena, ZEngine::Applications::GameApplicationPtr app) - { - Arena = arena; - CurrentApp = app; - arena->CreateSubArena(ZMega(64), &LocalArena); - - Scanner.Initialize(arena); - Scanner.SetAssetRegistry(ZEngine::Managers::AssetManager::Instance()->Registry); - Scanner.SetOnScanComplete(this, [](void* ctx, ZEngine::Core::VFS::ScanStats) { - ZEngine::Core::MainThreadScheduler::Post(ctx, [](void* p) { - auto* layer = static_cast(p); - auto scratch = ZGetScratch(&layer->LocalArena); - ZEngine::Managers::AssetManager::ReloadFromDisk(scratch.Arena); - ZReleaseScratch(scratch); - }); - }); - Cache.Initialize(arena); - - NodeHierarchies.init(arena, 10, 0); - NodeUIComponents.init(arena); - NodeToRender.init(arena, 10); - KeyEntries.init(arena, 32); - - KeyEntries[ZENGINE_KEY_SPACE] = ImGuiKey_Space; - KeyEntries[ZENGINE_KEY_BACKSPACE] = ImGuiKey_Backspace; - KeyEntries[ZENGINE_KEY_RIGHT] = ImGuiKey_RightArrow; - KeyEntries[ZENGINE_KEY_LEFT] = ImGuiKey_LeftArrow; - KeyEntries[ZENGINE_KEY_DOWN] = ImGuiKey_DownArrow; - KeyEntries[ZENGINE_KEY_UP] = ImGuiKey_UpArrow; - KeyEntries[ZENGINE_KEY_DELETE] = ImGuiKey_Delete; - KeyEntries[ZENGINE_KEY_ENTER] = ImGuiKey_Enter; - KeyEntries[ZENGINE_KEY_MOUSE_LEFT] = ImGuiKey_MouseLeft; - KeyEntries[ZENGINE_KEY_MOUSE_RIGHT] = ImGuiKey_MouseRight; - KeyEntries[ZENGINE_KEY_MOUSE_MIDDLE] = ImGuiKey_MouseMiddle; - - auto dockspace_cmp = ZPushStructCtor(arena, Components::DockspaceUIComponent); - auto scene_cmp = ZPushStructCtor(arena, Components::SceneViewportUIComponent); - auto project_view_cmp = ZPushStructCtor(arena, Components::ProjectViewUIComponent); - auto inspector_view_cmp = ZPushStructCtor(arena, Components::InspectorViewUIComponent); - auto hierarchy_view_cmp = ZPushStructCtor(arena, Components::HierarchyViewUIComponent); - auto log_cmp = ZPushStructCtor(arena, Components::LogUIComponent); - auto importer_cmp = ZPushStructCtor(arena, Components::AssetImporterUIComponent); - auto status_bar_cmp = ZPushStructCtor(arena, Components::StatusBarUIComponent); - - dockspace_cmp->Initialize(this); - scene_cmp->Initialize(this); - project_view_cmp->Initialize(this); - inspector_view_cmp->Initialize(this); - hierarchy_view_cmp->Initialize(this); - log_cmp->Initialize(this); - importer_cmp->Initialize(this); - status_bar_cmp->Initialize(this); - - dockspace_cmp->Children.init(arena, 7); - dockspace_cmp->Children.push(scene_cmp); - dockspace_cmp->Children.push(project_view_cmp); - dockspace_cmp->Children.push(inspector_view_cmp); - dockspace_cmp->Children.push(hierarchy_view_cmp); - dockspace_cmp->Children.push(log_cmp); - dockspace_cmp->Children.push(importer_cmp); - dockspace_cmp->Children.push(status_bar_cmp); - - dockspace_cmp->ChildrenCount = dockspace_cmp->Children.size(); - - AddUIComponent(dockspace_cmp, -1, 0); - IMessenger::Register>(dockspace_cmp, EDITOR_COMPONENT_DOCKSPACE_REQUEST_OPENSCENE, [=](void* const message) -> std::future { - auto message_ptr = reinterpret_cast*>(message); - const auto& value = message_ptr->GetValue(); - return dockspace_cmp->OnOpenSceneRequestAsync(value.c_str()); - }); - - IMessenger::Register>(dockspace_cmp, EDITOR_COMPONENT_DOCKSPACE_REQUEST_OPENMESH, [=](void* const message) -> std::future { - auto message_ptr = reinterpret_cast*>(message); - const auto& value = message_ptr->GetValue(); - return dockspace_cmp->OnOpenMeshRequestAsync(value.c_str()); - }); - - ZEngine::Core::MainThreadScheduler::Post(importer_cmp, [](void* ctx) { reinterpret_cast(ctx)->TriggerScan(); }); - } - - void ImguiLayer::Deinitialize() - { - NodeHierarchies.clear(); - NodeUIComponents.clear(); - } - - bool ImguiLayer::OnEvent(Core::CoreEvent& event) - { - Core::EventDispatcher event_dispatcher(event); - - event_dispatcher.Dispatch(std::bind(&ImguiLayer::OnKeyPressed, this, std::placeholders::_1)); - event_dispatcher.Dispatch(std::bind(&ImguiLayer::OnKeyReleased, this, std::placeholders::_1)); - - event_dispatcher.Dispatch(std::bind(&ImguiLayer::OnMouseButtonPressed, this, std::placeholders::_1)); - event_dispatcher.Dispatch(std::bind(&ImguiLayer::OnMouseButtonReleased, this, std::placeholders::_1)); - event_dispatcher.Dispatch(std::bind(&ImguiLayer::OnMouseButtonMoved, this, std::placeholders::_1)); - event_dispatcher.Dispatch(std::bind(&ImguiLayer::OnMouseButtonWheelMoved, this, std::placeholders::_1)); - event_dispatcher.Dispatch(std::bind(&ImguiLayer::OnTextInputRaised, this, std::placeholders::_1)); - - event_dispatcher.Dispatch(std::bind(&ImguiLayer::OnWindowClosed, this, std::placeholders::_1)); - - return false; - } - - void ImguiLayer::Update(Core::TimeStep dt) - { - ImGuiIO& io = ImGui::GetIO(); - if (dt > 0.0f) - { - io.DeltaTime = dt; - } - - if (!NodeToRender.empty()) - { - NodeToRender.clear(); - } - - auto temp_arena = ZGetScratch(&LocalArena); - - Array roots = {}; - Array children = {}; - Array siblings = {}; - - roots.init(temp_arena.Arena, 1); - children.init(temp_arena.Arena, 1); - siblings.init(temp_arena.Arena, 1); - - uint32_t i = 0; - for (auto& node : NodeHierarchies) - { - if (node.Parent == -1) - { - auto& cmp = NodeUIComponents.at(i); - if (cmp->IsVisible) - { - roots.push(i); - if (node.FirstChild != -1) - { - auto& fc = NodeUIComponents[node.FirstChild]; - if (fc->IsVisible) - { - children.push(node.FirstChild); - } - } - } - } - ++i; - } - - for (auto ch : children) - { - for (auto sibling = NodeHierarchies[ch].RightSibling; sibling != -1; sibling = NodeHierarchies[sibling].RightSibling) - { - siblings.push(sibling); - } - } - - int size = roots.size() + children.size() + siblings.size(); - if (NodeToRender.capacity() < size) - { - } - - for (auto r : roots) - { - NodeToRender.push(r); - } - - for (auto c : children) - { - NodeToRender.push(c); - } - - for (auto s : siblings) - { - NodeToRender.push(s); - } - - for (auto node : NodeToRender) - { - - auto& cmp = NodeUIComponents[node]; - cmp->Update(dt); - } - - ZReleaseScratch(temp_arena); - } - - int ImguiLayer::AddNode(Components::UIComponent* cmp, int parent, int depth) - { - if ((!cmp) || (depth < 0)) - { - return -1; - } - - auto node = NodeHierarchies.size(); - NodeHierarchies.push(Helpers::NodeHierarchy{.Parent = parent}); - - ArrayView nodes_view(NodeHierarchies.data(), NodeHierarchies.size()); - if (parent > -1) - { - int first = NodeHierarchies[parent].FirstChild; - if (first == -1) - { - nodes_view[parent].FirstChild = node; - } - else - { - int sibling = NodeHierarchies[first].RightSibling; - if (sibling == -1) - { - nodes_view[first].RightSibling = node; - } - else - { - for (sibling = first; NodeHierarchies[sibling].RightSibling != -1; sibling = NodeHierarchies[sibling].RightSibling) - { - } - nodes_view[sibling].RightSibling = node; - } - } - } - nodes_view[node].DepthLevel = depth; - return node; - } - - void ImguiLayer::AddUIComponent(Components::UIComponent* cmp, int parent, int depth) - { - if (!cmp) - { - return; - } - - auto node_id = AddNode(cmp, parent, depth); - if (!cmp->ParentLayer) - { - cmp->ParentLayer = this; - } - NodeUIComponents[node_id] = cmp; - for (int i = 0; i < cmp->ChildrenCount; ++i) - { - AddUIComponent(cmp->Children[i], node_id, (depth + 1)); - } - } - - bool ImguiLayer::OnKeyPressed(KeyPressedEvent& e) - { - ImGuiIO& io = ImGui::GetIO(); - if (KeyEntries.contains(e.GetKeyCode())) - { - io.AddKeyEvent((ImGuiKey) KeyEntries.at(e.GetKeyCode()), true); - } - return false; - } - - bool ImguiLayer::OnKeyReleased(KeyReleasedEvent& e) - { - ImGuiIO& io = ImGui::GetIO(); - if (KeyEntries.contains(e.GetKeyCode())) - { - io.AddKeyEvent((ImGuiKey) KeyEntries.at(e.GetKeyCode()), false); - } - return false; - } - - bool ImguiLayer::OnMouseButtonPressed(MouseButtonPressedEvent& e) - { - ImGuiIO& io = ImGui::GetIO(); - io.AddMouseButtonEvent((int) e.GetButton(), true); - return false; - } - - bool ImguiLayer::OnMouseButtonReleased(MouseButtonReleasedEvent& e) - { - ImGuiIO& io = ImGui::GetIO(); - io.AddMouseButtonEvent((int) e.GetButton(), false); - return false; - } - - bool ImguiLayer::OnMouseButtonMoved(MouseButtonMovedEvent& e) - { - ImGuiIO& io = ImGui::GetIO(); - io.MousePos = ImVec2(float(e.GetPosX()), float(e.GetPosY())); - return false; - } - - bool ImguiLayer::OnMouseButtonWheelMoved(MouseButtonWheelEvent& e) - { - ImGuiIO& io = ImGui::GetIO(); - if (e.GetOffetX() > 0) - { - io.MouseWheelH += 1; - } - else if (e.GetOffetX() < 0) - { - io.MouseWheelH -= 1; - } - else if (e.GetOffetY() > 0) - { - io.MouseWheel += 1; - } - else if (e.GetOffetY() < 0) - { - io.MouseWheel -= 1; - } - return false; - } - - bool ImguiLayer::OnTextInputRaised(TextInputEvent& event) - { - ImGuiIO& io = ImGui::GetIO(); - for (unsigned char c : event.GetText()) - { - io.AddInputCharacter(c); - } - return false; - } - - bool ImguiLayer::OnWindowClosed(WindowClosedEvent& event) - { - Core::EventDispatcher event_dispatcher(event); - event_dispatcher.ForwardTo(std::bind(&ZEngine::Windows::CoreWindow::OnWindowClosed, CurrentApp->CurrentWindow, std::placeholders::_1)); - return true; - } - - bool ImguiLayer::OnWindowResized(WindowResizedEvent&) - { - return false; - } - - bool ImguiLayer::OnWindowMinimized(WindowMinimizedEvent&) - { - return false; - } - - bool ImguiLayer::OnWindowMaximized(WindowMaximizedEvent&) - { - return false; - } - - bool ImguiLayer::OnWindowRestored(WindowRestoredEvent&) - { - return false; - } - - void ImguiLayer::Render(Rendering::Renderers::GraphicRenderer* const renderer, ZEngine::Hardwares::CommandBuffer* const command_buffer) - { - for (auto& id : NodeToRender) - { - auto& cmp = NodeUIComponents[id]; - cmp->Render(renderer, command_buffer); - } - } -} // namespace Tetragrama::Layers diff --git a/Tetragrama/Layers/ImguiLayer.h b/Tetragrama/Layers/ImguiLayer.h deleted file mode 100644 index 49f0ae5d1..000000000 --- a/Tetragrama/Layers/ImguiLayer.h +++ /dev/null @@ -1,62 +0,0 @@ -#pragma once -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace Tetragrama::Components -{ - struct UIComponent; -} - -namespace Tetragrama::Layers -{ - struct ImguiLayer : public ZEngine::Applications::Layer, public ZEngine::Windows::Inputs::IKeyboardEventCallback, public ZEngine::Windows::Inputs::IMouseEventCallback, public ZEngine::Windows::Inputs::ITextInputEventCallback, public ZEngine::Windows::Inputs::IWindowEventCallback - { - ImguiLayer(cstring name = "ImGUI Layer") : ZEngine::Applications::Layer(name) {} - virtual ~ImguiLayer(); - - ZEngine::Core::Containers::Array NodeHierarchies = {}; - ZEngine::Core::Containers::Array NodeToRender = {}; - ZEngine::Core::Containers::UnorderedHashMap NodeUIComponents = {}; - ZEngine::Core::Containers::UnorderedHashMap KeyEntries = {}; - - ZEngine::Core::VFS::VFSScanner Scanner = {}; - ZEngine::Core::VFS::VFSDirectoryCache Cache = {}; - unsigned int DockspaceId = 0; - unsigned int ConsoleDockId = 0; - - virtual void Initialize(ZEngine::Core::Memory::ArenaAllocator* arena, ZEngine::Applications::GameApplicationPtr app) override; - virtual void Deinitialize() override; - - bool OnEvent(ZEngine::Core::CoreEvent& event) override; - - void Update(ZEngine::Core::TimeStep dt) override; - - void Render(ZEngine::Rendering::Renderers::GraphicRenderer* const renderer, ZEngine::Hardwares::CommandBuffer* const command_buffer) override; - - int AddNode(Components::UIComponent* cmp, int parent, int depth); - virtual void AddUIComponent(Components::UIComponent* cmp, int parent, int depth); - - bool OnKeyPressed(ZEngine::Windows::Events::KeyPressedEvent&) override; - bool OnKeyReleased(ZEngine::Windows::Events::KeyReleasedEvent&) override; - - bool OnMouseButtonPressed(ZEngine::Windows::Events::MouseButtonPressedEvent&) override; - bool OnMouseButtonReleased(ZEngine::Windows::Events::MouseButtonReleasedEvent&) override; - bool OnMouseButtonMoved(ZEngine::Windows::Events::MouseButtonMovedEvent&) override; - bool OnMouseButtonWheelMoved(ZEngine::Windows::Events::MouseButtonWheelEvent&) override; - bool OnTextInputRaised(ZEngine::Windows::Events::TextInputEvent&) override; - - bool OnWindowClosed(ZEngine::Windows::Events::WindowClosedEvent&) override; - bool OnWindowResized(ZEngine::Windows::Events::WindowResizedEvent&) override; - bool OnWindowMinimized(ZEngine::Windows::Events::WindowMinimizedEvent&) override; - bool OnWindowMaximized(ZEngine::Windows::Events::WindowMaximizedEvent&) override; - bool OnWindowRestored(ZEngine::Windows::Events::WindowRestoredEvent&) override; - }; - ZDEFINE_PTR(ImguiLayer); -} // namespace Tetragrama::Layers diff --git a/Tetragrama/Layers/ZUILayer.cpp b/Tetragrama/Layers/ZUILayer.cpp new file mode 100644 index 000000000..3f4014309 --- /dev/null +++ b/Tetragrama/Layers/ZUILayer.cpp @@ -0,0 +1,330 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace ZEngine::Windows::Events; + +namespace Tetragrama::Layers +{ + // File-static ZUI context pointer — used by the chained GLFW scroll callback below. + // Single ZUILayer instance per process, so a static is safe. + static ZEngine::UI::ZUIContext* s_zui_ctx_for_scroll = nullptr; + + void ZUILayer::Initialize(ZEngine::Core::Memory::ArenaAllocator* arena, ZEngine::Applications::GameApplicationPtr app) + { + Arena = arena; + CurrentApp = app; + m_ctx = app->RenderPipeline ? app->RenderPipeline->ZUICtx : nullptr; + arena->CreateSubArena(ZMega(4), &LocalArena); + + // Engine::Initialize registers a GLFW scroll callback that overrides GameWindow's, + // so MouseButtonWheelEvent never fires for ZUI. Register here (AFTER Engine::Initialize) + // so our callback wins. We chain both consumers: InputManager (FlyCamera) + ZUIContext. + s_zui_ctx_for_scroll = m_ctx; + if (app->CurrentWindow) + { + auto* native = static_cast(app->CurrentWindow->GetNativeWindow()); + glfwSetScrollCallback(native, [](GLFWwindow*, double /*x*/, double y) { + auto* eng = ZEngine::Engine::GetContext(); + if (eng && eng->InputManager) + eng->InputManager->AccumulateScroll(y); + // Clamp to ±1 per event so trackpad momentum doesn't overshoot + if (s_zui_ctx_for_scroll) + s_zui_ctx_for_scroll->ScrollDelta += (float) std::clamp(y, -1.0, 1.0); + }); + } + } + + bool ZUILayer::OnEvent(ZEngine::Core::CoreEvent& event) + { + ZEngine::Core::EventDispatcher event_dispatcher(event); + event_dispatcher.Dispatch(std::bind(&ZUILayer::OnKeyPressed, this, std::placeholders::_1)); + event_dispatcher.Dispatch(std::bind(&ZUILayer::OnKeyReleased, this, std::placeholders::_1)); + event_dispatcher.Dispatch(std::bind(&ZUILayer::OnMouseButtonPressed, this, std::placeholders::_1)); + event_dispatcher.Dispatch(std::bind(&ZUILayer::OnMouseButtonReleased, this, std::placeholders::_1)); + event_dispatcher.Dispatch(std::bind(&ZUILayer::OnMouseButtonMoved, this, std::placeholders::_1)); + event_dispatcher.Dispatch(std::bind(&ZUILayer::OnTextInputRaised, this, std::placeholders::_1)); + return false; + } + + void ZUILayer::Render(ZEngine::Rendering::Renderers::GraphicRenderer* const, ZEngine::Hardwares::CommandBuffer* const) + { + if (!m_ctx || m_component_count == 0) + { + return; + } + + // Root box — fully opaque background covering the full swapchain surface. + // This is critical: the scene render pass leaves bloom/HDR data in the + // swapchain; without an opaque root the scene bleeds through semi-transparent + // panel backgrounds. ImGui solves this the same way with its main DockSpace + // window background. + ZEngine::UI::ZUIBox* root = ZEngine::UI::ZUIBeginColumn(m_ctx, "##zui_root", ZEngine::UI::ZPx((float) m_ctx->ScreenW), ZEngine::UI::ZPx((float) m_ctx->ScreenH)); + root->Flags = root->Flags | ZEngine::UI::ZUI_DrawBackground; + root->EdgeSoftness = 0.f; + ZUIBoxSetColorArr(root, m_ctx->Theme.WindowBg); // WindowBg is now fully opaque + + for (uint32_t i = 0; i < m_component_count; ++i) + { + m_components[i]->BuildUI(m_ctx); + } + + ZEngine::UI::ZUIEndColumn(m_ctx); + + // Apply resize cursor set by panel dividers this frame. + if (CurrentApp && CurrentApp->CurrentWindow) + { + static GLFWcursor* s_cur_ew = nullptr; + static GLFWcursor* s_cur_ns = nullptr; + if (!s_cur_ew) + s_cur_ew = glfwCreateStandardCursor(GLFW_HRESIZE_CURSOR); + if (!s_cur_ns) + s_cur_ns = glfwCreateStandardCursor(GLFW_VRESIZE_CURSOR); + + auto* native = static_cast(CurrentApp->CurrentWindow->GetNativeWindow()); + switch (m_ctx->ResizeCursor) + { + case 1: + glfwSetCursor(native, s_cur_ew); + break; + case 2: + glfwSetCursor(native, s_cur_ns); + break; + default: + glfwSetCursor(native, nullptr); + break; + } + } + } + + void ZUILayer::AddComponent(Components::ZUIComponent* cmp) + { + if (m_component_count < kMaxComponents) + { + m_components[m_component_count++] = cmp; + } + } + + bool ZUILayer::OnKeyPressed(KeyPressedEvent& e) + { + if (!m_ctx) + { + return false; + } + auto key = e.GetKeyCode(); + + // Modifier tracking + if (key == ZENGINE_KEY_LEFT_CONTROL || key == ZENGINE_KEY_RIGHT_CONTROL) + m_ctx->CtrlDown = true; + if (key == ZENGINE_KEY_LEFT_SHIFT || key == ZENGINE_KEY_RIGHT_SHIFT) + m_ctx->ShiftDown = true; + if (key == ZENGINE_KEY_LEFT_ALT || key == ZENGINE_KEY_RIGHT_ALT) + m_ctx->AltDown = true; + + // Backspace (also starts key-repeat timer) + if (key == ZENGINE_KEY_BACKSPACE) + { + m_ctx->BackspacePressed = true; + m_ctx->BackspaceHeld = true; + m_ctx->KeyRepeatTimer = 0.f; + } + + // Tab / Shift+Tab: cycle focus between interactive widgets + if (key == ZENGINE_KEY_TAB) + { + if (m_ctx->ShiftDown) + m_ctx->ShiftTabPressed = true; + else + m_ctx->TabPressed = true; + } + // Escape: drop keyboard focus; Enter: confirm and drop focus + if (key == ZENGINE_KEY_ESCAPE) + m_ctx->EscapePressed = true; + if (key == ZENGINE_KEY_ENTER || key == ZENGINE_KEY_KP_ENTER) + m_ctx->EnterPressed = true; + // Space: activate focused button (not Enter, which clears focus for text fields) + if (key == ZENGINE_KEY_SPACE && !m_ctx->CtrlDown && !m_ctx->AltDown) + m_ctx->SpacePressed = true; + + // Arrow keys for drag-float nudge / text cursor / combo navigation + if (key == ZENGINE_KEY_UP) + m_ctx->ArrowUpPressed = true; + if (key == ZENGINE_KEY_DOWN) + m_ctx->ArrowDownPressed = true; + if (key == ZENGINE_KEY_LEFT) + m_ctx->ArrowLeftPressed = true; + if (key == ZENGINE_KEY_RIGHT) + m_ctx->ArrowRightPressed = true; + if (key == ZENGINE_KEY_HOME) + m_ctx->HomePressed = true; + if (key == ZENGINE_KEY_END) + m_ctx->EndPressed = true; + if (key == ZENGINE_KEY_DELETE) + { + m_ctx->DeletePressed = true; + m_ctx->DeleteHeld = true; + m_ctx->ArrowRepeatTimer = 0.f; + } + if (key == ZENGINE_KEY_LEFT) + { + m_ctx->ArrowLeftHeld = true; + m_ctx->ArrowRepeatTimer = 0.f; + } + if (key == ZENGINE_KEY_RIGHT) + { + m_ctx->ArrowRightHeld = true; + m_ctx->ArrowRepeatTimer = 0.f; + } + if (key == ZENGINE_KEY_UP) + { + m_ctx->ArrowUpHeld = true; + m_ctx->ArrowRepeatTimer = 0.f; + } + if (key == ZENGINE_KEY_DOWN) + { + m_ctx->ArrowDownHeld = true; + m_ctx->ArrowRepeatTimer = 0.f; + } + + if (m_ctx->CtrlDown && key == ZEngine::Windows::Inputs::GlfwKey::KEY_C) + m_ctx->CtrlCPressed = true; + if (m_ctx->CtrlDown && key == ZEngine::Windows::Inputs::GlfwKey::KEY_X) + m_ctx->CtrlXPressed = true; + if (m_ctx->CtrlDown && key == ZEngine::Windows::Inputs::GlfwKey::KEY_A) + m_ctx->CtrlAPressed = true; + if (m_ctx->CtrlDown && !m_ctx->ShiftDown && key == ZEngine::Windows::Inputs::GlfwKey::KEY_Z) + m_ctx->CtrlZPressed = true; + if (m_ctx->CtrlDown && (key == ZEngine::Windows::Inputs::GlfwKey::KEY_Y || (m_ctx->ShiftDown && key == ZEngine::Windows::Inputs::GlfwKey::KEY_Z))) + m_ctx->CtrlYPressed = true; + + // Ctrl+Backspace → delete word before cursor + if (m_ctx->CtrlDown && key == ZENGINE_KEY_BACKSPACE) + m_ctx->CtrlBackspacePressed = true; + + // Clipboard paste: Ctrl+V → inject clipboard text into TextInput + if (m_ctx->CtrlDown && key == ZEngine::Windows::Inputs::GlfwKey::KEY_V) + { + if (CurrentApp && CurrentApp->CurrentWindow) + { + auto* native = static_cast(CurrentApp->CurrentWindow->GetNativeWindow()); + const char* clip = glfwGetClipboardString(native); + if (clip) + { + for (uint32_t i = 0; clip[i] && m_ctx->TextInputLen < 31; ++i) + m_ctx->TextInput[m_ctx->TextInputLen++] = clip[i]; + m_ctx->TextInput[m_ctx->TextInputLen] = '\0'; + } + } + } + + return false; + } + + bool ZUILayer::OnKeyReleased(KeyReleasedEvent& e) + { + if (!m_ctx) + { + return false; + } + auto key = e.GetKeyCode(); + if (key == ZENGINE_KEY_LEFT_CONTROL || key == ZENGINE_KEY_RIGHT_CONTROL) + m_ctx->CtrlDown = false; + if (key == ZENGINE_KEY_LEFT_SHIFT || key == ZENGINE_KEY_RIGHT_SHIFT) + m_ctx->ShiftDown = false; + if (key == ZENGINE_KEY_LEFT_ALT || key == ZENGINE_KEY_RIGHT_ALT) + m_ctx->AltDown = false; + if (key == ZENGINE_KEY_BACKSPACE) + { + m_ctx->BackspaceHeld = false; + m_ctx->KeyRepeatTimer = 0.f; + } + if (key == ZENGINE_KEY_LEFT) + m_ctx->ArrowLeftHeld = false; + if (key == ZENGINE_KEY_RIGHT) + m_ctx->ArrowRightHeld = false; + if (key == ZENGINE_KEY_UP) + m_ctx->ArrowUpHeld = false; + if (key == ZENGINE_KEY_DOWN) + m_ctx->ArrowDownHeld = false; + if (key == ZENGINE_KEY_DELETE) + m_ctx->DeleteHeld = false; + return false; + } + + bool ZUILayer::OnMouseButtonPressed(MouseButtonPressedEvent& e) + { + if (!m_ctx) + { + return false; + } + int btn = (int) e.GetButton(); + if (btn >= 0 && btn < 3) + { + m_ctx->MouseDown[btn] = true; + m_ctx->MousePressed[btn] = true; + } + return false; + } + + bool ZUILayer::OnMouseButtonReleased(MouseButtonReleasedEvent& e) + { + if (!m_ctx) + { + return false; + } + int btn = (int) e.GetButton(); + if (btn >= 0 && btn < 3) + { + m_ctx->MouseDown[btn] = false; + m_ctx->MouseReleased[btn] = true; + } + return false; + } + + bool ZUILayer::OnMouseButtonMoved(MouseButtonMovedEvent& e) + { + if (!m_ctx) + { + return false; + } + // GLFW cursor callback reports in logical screen coords (same space as + // glfwGetWindowSize / ScreenW). No division needed on any platform. + m_ctx->MousePos[0] = (float) e.GetPosX(); + m_ctx->MousePos[1] = (float) e.GetPosY(); + return false; + } + + bool ZUILayer::OnMouseButtonWheelMoved(MouseButtonWheelEvent& /*e*/) + { + // Scroll is handled by the GLFW callback registered in Initialize. + return false; + } + + bool ZUILayer::OnTextInputRaised(TextInputEvent& e) + { + if (!m_ctx) + { + return false; + } + for (unsigned char c : e.GetText()) + { + if (m_ctx->TextInputLen < 31) + { + m_ctx->TextInput[m_ctx->TextInputLen++] = (char) c; + } + } + m_ctx->TextInput[m_ctx->TextInputLen] = '\0'; + return false; + } +} // namespace Tetragrama::Layers diff --git a/Tetragrama/Layers/ZUILayer.h b/Tetragrama/Layers/ZUILayer.h new file mode 100644 index 000000000..1a40c4f7a --- /dev/null +++ b/Tetragrama/Layers/ZUILayer.h @@ -0,0 +1,46 @@ +#pragma once +#include +#include + +namespace ZEngine::UI +{ + struct ZUIContext; +} +namespace Tetragrama::Components +{ + struct ZUIComponent; +} + +namespace Tetragrama::Layers +{ + struct ZUILayer : public ZEngine::Applications::Layer, public ZEngine::Windows::Inputs::IKeyboardEventCallback, public ZEngine::Windows::Inputs::IMouseEventCallback, public ZEngine::Windows::Inputs::ITextInputEventCallback + { + ZUILayer(cstring name = "ZUI Layer") : ZEngine::Applications::Layer(name) {} + + void Initialize(ZEngine::Core::Memory::ArenaAllocator* arena, ZEngine::Applications::GameApplicationPtr app) override; + void Deinitialize() override {} + + bool OnEvent(ZEngine::Core::CoreEvent& event) override; + void Update(ZEngine::Core::TimeStep dt) override {} + void Render(ZEngine::Rendering::Renderers::GraphicRenderer* const, ZEngine::Hardwares::CommandBuffer* const) override; + + bool OnKeyPressed(ZEngine::Windows::Events::KeyPressedEvent&) override; + bool OnKeyReleased(ZEngine::Windows::Events::KeyReleasedEvent&) override; + + bool OnMouseButtonPressed(ZEngine::Windows::Events::MouseButtonPressedEvent&) override; + bool OnMouseButtonReleased(ZEngine::Windows::Events::MouseButtonReleasedEvent&) override; + bool OnMouseButtonMoved(ZEngine::Windows::Events::MouseButtonMovedEvent&) override; + bool OnMouseButtonWheelMoved(ZEngine::Windows::Events::MouseButtonWheelEvent&) override; + + bool OnTextInputRaised(ZEngine::Windows::Events::TextInputEvent&) override; + + void AddComponent(Components::ZUIComponent* cmp); + + private: + static constexpr uint32_t kMaxComponents = 32; + ZEngine::UI::ZUIContext* m_ctx = nullptr; + Components::ZUIComponent* m_components[kMaxComponents] = {}; + uint32_t m_component_count = 0; + }; + ZDEFINE_PTR(ZUILayer); +} // namespace Tetragrama::Layers diff --git a/Tetragrama/Messengers/Messenger.h b/Tetragrama/Messengers/Messenger.h index a4e58e022..4e9ca8db5 100644 --- a/Tetragrama/Messengers/Messenger.h +++ b/Tetragrama/Messengers/Messenger.h @@ -1,5 +1,4 @@ #pragma once -#include #include #include #include diff --git a/Tetragrama/Panels/EditorPanels.h b/Tetragrama/Panels/EditorPanels.h new file mode 100644 index 000000000..d395a2011 --- /dev/null +++ b/Tetragrama/Panels/EditorPanels.h @@ -0,0 +1,688 @@ +#pragma once +#include +#include + +namespace Tetragrama::Panels +{ + using namespace ZEngine::UI; + + // ── Shared helpers ──────────────────────────────────────────────────────── + + static void EmptyPanelBg(ZUIContext* ctx, const char* key, const float col[4], const char* msg) + { + ZUIBox* bg = ZUIBeginColumn(ctx, key, ZFill(), ZFill()); + bg->Flags = bg->Flags | ZUI_DrawBackground; + ZUIBoxSetColorArr(bg, col); + bg->EdgeSoftness = 0.f; + if (msg && msg[0]) + { + { + char fk[48]; + snprintf(fk, sizeof(fk), "##ept_%s", key); + ZUIBox* f = ZUIPushBox(ctx, fk, (uint32_t) strlen(fk), ZUI_None); + f->Size[0] = ZFill(); + f->Size[1] = ZFill(); + ZUIPopBox(ctx); + } + { + char lk[48]; + snprintf(lk, sizeof(lk), "##epl_%s", key); + uint32_t mlen = (uint32_t) strlen(msg); + ZUIBox* lbl = ZUIPushBox(ctx, lk, (uint32_t) strlen(lk), ZUI_DrawText); + lbl->Size[0] = ZFill(); + lbl->Size[1] = ZText(); + lbl->TextAlign = ZUITextAlign::Center; + lbl->Label = ZUIPushStr(&ctx->FrameArena, msg, mlen); + lbl->TextColor[0] = ctx->Theme.TextDim[0]; + lbl->TextColor[1] = ctx->Theme.TextDim[1]; + lbl->TextColor[2] = ctx->Theme.TextDim[2]; + lbl->TextColor[3] = ctx->Theme.TextDim[3]; + ZUIPopBox(ctx); + } + { + char fk[48]; + snprintf(fk, sizeof(fk), "##epb_%s", key); + ZUIBox* f = ZUIPushBox(ctx, fk, (uint32_t) strlen(fk), ZUI_None); + f->Size[0] = ZFill(); + f->Size[1] = ZFill(); + ZUIPopBox(ctx); + } + } + ZUIEndColumn(ctx); + } + + // ── Hierarchy panel ─────────────────────────────────────────────────────── + // + // Tree view of scene entities — up to 9 levels of nesting. + // ZUITreeNode uses the VS Code chevron (∨/›) built-in. + // Indentation via wrapper column Padding[0] = depth * IndentSpacing. + // + struct HierarchyPanel : ZUIPanelView + { + struct Node + { + const char* name; + int parent; // -1 = root + }; + + static constexpr int kN = 22; + + // Sample scene — deepest path: World → Player → Armature → Hips → Spine + // → Chest → Shoulder R → UpperArm R + // → LowerArm R → Hand R (depth 9) + static constexpr Node kNodes[kN] = { + { "World", -1}, // 0 depth 0 + { "Camera", 0}, // 1 depth 1 + {"Main Camera", 1}, // 2 depth 2 + { "Lighting", 0}, // 3 depth 1 + {"Directional", 3}, // 4 depth 2 + {"Point Light", 3}, // 5 depth 2 + { "Player", 0}, // 6 depth 1 + { "Armature", 6}, // 7 depth 2 + { "Hips", 7}, // 8 depth 3 + { "Spine", 8}, // 9 depth 4 + { "Chest", 9}, // 10 depth 5 + { "Shoulder R", 10}, // 11 depth 6 + { "UpperArm R", 11}, // 12 depth 7 + { "LowerArm R", 12}, // 13 depth 8 + { "Hand R", 13}, // 14 depth 9 ← max depth + { "Shoulder L", 10}, // 15 depth 6 + { "UpperArm L", 15}, // 16 depth 7 + { "LowerArm L", 16}, // 17 depth 8 + { "Hand L", 17}, // 18 depth 9 + {"Environment", 0}, // 19 depth 1 + { "Ground", 19}, // 20 depth 2 + { "Trees", 19}, // 21 depth 2 + }; + + bool m_open[kN] = {true, true, false, true, false, false, true, true, true, true, true, true, true, true, false, true, true, true, false, true, false, false}; + int m_selected = -1; + + HierarchyPanel() + { + Title = "Hierarchy"; + } + + int Depth(int i) const + { + int d = 0, p = kNodes[i].parent; + while (p >= 0) + { + d++; + p = kNodes[p].parent; + } + return d; + } + + bool HasChildren(int i) const + { + for (int j = 0; j < kN; j++) + if (kNodes[j].parent == i) + return true; + return false; + } + + bool AncestorCollapsed(int i) const + { + int p = kNodes[i].parent; + while (p >= 0) + { + if (!m_open[p]) + return true; + p = kNodes[p].parent; + } + return false; + } + + void BuildContent(ZUIContext* ctx, float rect[4]) override + { + using namespace ZEngine::UI; + (void) rect; + + const float hdrH = ZUIGetFrameHeight(ctx); + + ZUIBox* bg = ZUIBeginScrollRegion(ctx, "##hier_sr", ZFill(), ZFill()); + bg->Flags = bg->Flags | ZUI_DrawBackground; + ZUIBoxSetColorArr(bg, ctx->Theme.PanelBg); + bg->EdgeSoftness = 0.f; + + ZUISpacer(ctx, 4.f); + + for (int i = 0; i < kN; i++) + { + if (AncestorCollapsed(i)) + continue; + + float indent = (float) Depth(i) * ctx->Style.IndentSpacing; + bool has_chld = HasChildren(i); + bool is_sel = (m_selected == i); + char ck[32]; + snprintf(ck, sizeof(ck), "##hn%d", i); + + // Wrapper column — provides indentation via left padding + ZUIBox* col = ZUIBeginColumn(ctx, ck, ZFill(), ZPx(hdrH)); + col->Padding[0] = indent; + col->EdgeSoftness = 0.f; + if (is_sel) + { + col->Flags = col->Flags | ZUI_DrawBackground; + ZUIBoxSetColorArr(col, ctx->Theme.RowSelectedBg); + } + + if (has_chld) + { + ZUISignal sig = ZUITreeNode(ctx, kNodes[i].name, &m_open[i]); + if (sig.Flags & ZUI_SignalClicked) + m_selected = i; + } + else + { + // Leaf — indent to align label with parent tree-node text + bool sel = is_sel; + if (ZUISelectable(ctx, kNodes[i].name, &sel, ZPx(hdrH))) + m_selected = i; + } + + ZUIEndColumn(ctx); + } + + ZUIEndScrollRegion(ctx); + } + }; + + struct ViewportPanel : ZUIPanelView + { + ViewportPanel() + { + Title = "Viewport"; + } + void BuildContent(ZUIContext* ctx, float rect[4]) override + { + (void) rect; + const float c[4] = {0.09f, 0.09f, 0.095f, 1.f}; + EmptyPanelBg(ctx, "##vp_bg", c, "Viewport"); + } + }; + + // ── Inspector panel ─────────────────────────────────────────────────────── + // + // Six mini-panel sections in a vertical stack. + // Each section is a ZUICollapsingHeader (click = collapse/expand, no close). + // Drag header to reorder using panel-docking visual helpers. + // Sash (resize) appears only below expanded sections. + // + struct InspectorPanel : ZUIPanelView + { + // ── Config ───────────────────────────────────────────────────────── + static constexpr int N = 6; + static constexpr float kMinH = 80.f; // minimum expanded content height + static constexpr float kSashH = 6.f; // resize grip height + + // ── Section display state (indexed by display position) ──────────── + int m_order[N] = {0, 1, 2, 3, 4, 5}; // section type at each display slot + bool m_open[N] = {true, true, true, false, false, false}; + float m_h[N] = {200.f, 80.f, 100.f, 80.f, 80.f, 80.f}; + + // ── Drag-reorder state ───────────────────────────────────────────── + int m_drag_di = -1; + bool m_drag_active = false; + float m_drag_acc_y = 0.f; + int m_drop_slot = 0; + bool m_drop_is_bot = false; + + // ── Component data (by section type) ────────────────────────────── + float m_position[3] = {0.f, 0.f, 0.f}; + float m_rotation[3] = {0.f, 0.f, 0.f}; + float m_scale[3] = {1.f, 1.f, 1.f}; + bool m_cast_shadows = true, m_receive_shadows = true; + float m_mass = 1.f; + bool m_use_gravity = true, m_is_kinematic = false; + float m_light_intensity = 1.f, m_light_range = 10.f; + float m_light_color[3] = {1.f, 1.f, 1.f}; + float m_audio_volume = 1.f; + bool m_audio_loop = false, m_audio_play_awake = true; + + static constexpr const char* kLabels[N] = {"Transform", "Mesh Renderer", "Rigid Body", "Lighting", "Audio Source", "Script"}; + + InspectorPanel() + { + Title = "Inspector"; + } + + // Content height for section at display position di, clamped to kMinH + float ContentH(int di) const + { + return fmaxf(m_h[di], kMinH); + } + + // Total scroll-content height for section di (placeholder height when source) + float SectionRunH(int di, float hdrH) const + { + if (di == m_drag_di) + return hdrH; + float h = hdrH + (m_open[di] ? ContentH(di) : 0.f); + float sash = (m_open[di] && di < N - 1) ? kSashH : 0.f; + return h + sash; + } + + void BuildContent(ZUIContext* ctx, float rect[4]) override + { + using namespace ZEngine::UI; + (void) rect; + + const float hdrH = ZUIGetFrameHeight(ctx); + const float sectW = rect[2] - rect[0]; + const float fw = sectW - 100.f - 16.f; + + // ── Open scroll region ──────────────────────────────────────── + ZUIBox* sr = ZUIBeginScrollRegion(ctx, "##insp_sr", ZFill(), ZFill()); + sr->Flags = sr->Flags | ZUI_DrawBackground; + ZUIBoxSetColorArr(sr, ctx->Theme.PanelBg); + sr->EdgeSoftness = 0.f; + + ZUIPersistentState* srps = ZUIStateGetOrInsert(&ctx->StateStore, sr->Key); + const float scy = srps ? srps->ScrollY : 0.f; + + // ── Drop slot from cursor ───────────────────────────────────── + if (m_drag_active) + { + float crel = ctx->MousePos[1] - rect[1] + scy; + float run = 0.f; + m_drop_slot = 0; + m_drop_is_bot = false; + for (int di = 0; di < N; di++) + { + float sh = SectionRunH(di, hdrH); + float top = run, bot = run + sh; + if (crel >= top && crel < bot) + { + if (di != m_drag_di && m_open[di]) + { + bool bot_half = crel >= top + sh * 0.5f; + m_drop_slot = bot_half ? di + 1 : di; + m_drop_is_bot = bot_half; + } + else + { + m_drop_slot = di; + } + break; + } + m_drop_slot = (crel < top) ? di : di + 1; + if (crel < top) + break; + run += sh; + } + m_drop_slot = (m_drop_slot < 0) ? 0 : (m_drop_slot > N ? N : m_drop_slot); + } + + // Cancel drag if cursor exits the inspector panel rect — sections are vertical-only + if (m_drag_active) + { + bool inside = ctx->MousePos[0] >= rect[0] && ctx->MousePos[0] <= rect[2] && ctx->MousePos[1] >= rect[1] && ctx->MousePos[1] <= rect[3]; + if (!inside) + { + m_drag_di = -1; + m_drag_active = false; + m_drag_acc_y = 0.f; + m_drop_is_bot = false; + } + } + + // ── Commit reorder ──────────────────────────────────────────── + if (ctx->MouseReleased[0] && m_drag_di >= 0) + { + if (m_drag_active && m_drop_slot != m_drag_di && m_drop_slot != m_drag_di + 1) + { + int os = m_order[m_drag_di]; + float oh = m_h[m_drag_di]; + bool oo = m_open[m_drag_di]; + for (int i = m_drag_di; i < N - 1; i++) + { + m_order[i] = m_order[i + 1]; + m_h[i] = m_h[i + 1]; + m_open[i] = m_open[i + 1]; + } + int ins = (m_drop_slot > m_drag_di) ? m_drop_slot - 1 : m_drop_slot; + ins = (ins < 0) ? 0 : (ins >= N ? N - 1 : ins); + for (int i = N - 1; i > ins; i--) + { + m_order[i] = m_order[i - 1]; + m_h[i] = m_h[i - 1]; + m_open[i] = m_open[i - 1]; + } + m_order[ins] = os; + m_h[ins] = oh; + m_open[ins] = oo; + } + m_drag_di = -1; + m_drag_active = false; + m_drag_acc_y = 0.f; + m_drop_is_bot = false; + } + + bool drop_chg = m_drag_active && m_drop_slot != m_drag_di && m_drop_slot != m_drag_di + 1; + const float* tb = ctx->Theme.TabActiveBorder; + + // ── Section loop ────────────────────────────────────────────── + const char* ghost_lbl = nullptr; + constexpr float kTopPad = 6.f; + float run_y = kTopPad; + ZUISpacer(ctx, kTopPad); + + auto Row = [&](const char* rk) { + ZUIBeginRow(ctx, rk, ZFill(), ZPx(hdrH)); + ZUISpacer(ctx, 8.f); + }; + auto EndRow = [&]() { ZUIEndRow(ctx); }; + + for (int di = 0; di < N; di++) + { + int s = m_order[di]; + bool is_src = m_drag_active && (di == m_drag_di); + char ck[32]; + + // Source slot: border placeholder + if (is_src) + { + ghost_lbl = kLabels[s]; + ZUIBox* ph = ZUIPushBox(ctx, "##ph", 4, ZUI_DrawBorder); + ph->Size[0] = ZFill(); + ph->Size[1] = ZPx(hdrH); + ph->EdgeSoftness = 0.f; + ph->BorderColor[0] = tb[0]; + ph->BorderColor[1] = tb[1]; + ph->BorderColor[2] = tb[2]; + ph->BorderColor[3] = 0.30f; + ph->BorderThickness = 1.f; + ZUIPopBox(ctx); + run_y += hdrH; + continue; + } + + // Drop zone visual — before this section + bool top_tgt = drop_chg && (m_drop_slot == di) && m_open[di] && !m_drop_is_bot; + bool bot_tgt = drop_chg && (m_drop_slot == di + 1) && m_open[di] && m_drop_is_bot; + bool col_tgt = drop_chg && (m_drop_slot == di) && !m_open[di] && !m_drop_is_bot; + + float sec_top = run_y - scy; + + if (top_tgt) + ZUIDockDividerH(ctx, "##dtop"); + + // Header — collapsed target gets teal highlight + { + float hi[4] = {tb[0], tb[1], tb[2], 0.22f}; + const float* hcol = col_tgt ? hi : nullptr; + ZUISignal sig = ZUICollapsingHeader(ctx, kLabels[s], &m_open[di], hcol); + + // Drag detection via signal — suppressed naturally when the divider hit zone + // owns ctx->ActiveKey, since ZUI_SignalHeld requires ActiveKey == header->Key. + if (!m_drag_active && (sig.Flags & ZUI_SignalHeld)) + { + m_drag_acc_y += sig.DragDelta[1]; + if (fabsf(m_drag_acc_y) > 8.f) + { + m_drag_active = true; + m_drag_di = di; + m_drag_acc_y = 0.f; + } + } + } + + // Content + if (m_open[di]) + { + float cH = ContentH(di); + float half_h = (hdrH + cH) * 0.5f; + + snprintf(ck, sizeof(ck), "##cc%d", di); + ZUIBox* c = ZUIBeginColumn(ctx, ck, ZFill(), ZPx(cH)); + c->Flags = c->Flags | ZUI_ClipChildren; + c->EdgeSoftness = 0.f; + ZUISpacer(ctx, 2.f); + + switch (s) + { + case 0: // Transform + Row("##r_pos"); + ZUILabel(ctx, "Position", ctx->Theme.TextDim); + ZUISpacer(ctx, 4.f); + ZUIDragFloat3(ctx, "##pos", m_position, 0.1f, fw / 3.f); + ZUISpacer(ctx, 8.f); + EndRow(); + Row("##r_rot"); + ZUILabel(ctx, "Rotation", ctx->Theme.TextDim); + ZUISpacer(ctx, 4.f); + ZUIDragFloat3(ctx, "##rot", m_rotation, 0.5f, fw / 3.f); + ZUISpacer(ctx, 8.f); + EndRow(); + Row("##r_scl"); + ZUILabel(ctx, "Scale", ctx->Theme.TextDim); + ZUISpacer(ctx, 4.f); + ZUIDragFloat3(ctx, "##scl", m_scale, 0.05f, fw / 3.f); + ZUISpacer(ctx, 8.f); + EndRow(); + break; + case 1: // Mesh Renderer + Row("##r_cs"); + ZUILabel(ctx, "Cast Shadows", ctx->Theme.TextDim); + ZUISpacer(ctx, 4.f); + ZUICheckbox(ctx, "##cs", &m_cast_shadows); + ZUISpacer(ctx, 8.f); + EndRow(); + Row("##r_rs"); + ZUILabel(ctx, "Recv Shadows", ctx->Theme.TextDim); + ZUISpacer(ctx, 4.f); + ZUICheckbox(ctx, "##rs", &m_receive_shadows); + ZUISpacer(ctx, 8.f); + EndRow(); + break; + case 2: // Rigid Body + Row("##r_ms"); + ZUILabel(ctx, "Mass", ctx->Theme.TextDim); + ZUISpacer(ctx, 4.f); + ZUIDragFloat(ctx, "##ms", &m_mass, 0.1f, fw); + ZUISpacer(ctx, 8.f); + EndRow(); + Row("##r_ug"); + ZUILabel(ctx, "Use Gravity", ctx->Theme.TextDim); + ZUISpacer(ctx, 4.f); + ZUICheckbox(ctx, "##ug", &m_use_gravity); + ZUISpacer(ctx, 8.f); + EndRow(); + Row("##r_km"); + ZUILabel(ctx, "Kinematic", ctx->Theme.TextDim); + ZUISpacer(ctx, 4.f); + ZUICheckbox(ctx, "##km", &m_is_kinematic); + ZUISpacer(ctx, 8.f); + EndRow(); + break; + case 3: // Lighting + Row("##r_li"); + ZUILabel(ctx, "Intensity", ctx->Theme.TextDim); + ZUISpacer(ctx, 4.f); + ZUIDragFloat(ctx, "##li", &m_light_intensity, 0.05f, fw); + ZUISpacer(ctx, 8.f); + EndRow(); + Row("##r_lr"); + ZUILabel(ctx, "Range", ctx->Theme.TextDim); + ZUISpacer(ctx, 4.f); + ZUIDragFloat(ctx, "##lr", &m_light_range, 0.5f, fw); + ZUISpacer(ctx, 8.f); + EndRow(); + Row("##r_lc"); + ZUILabel(ctx, "Color", ctx->Theme.TextDim); + ZUISpacer(ctx, 4.f); + ZUIDragFloat3(ctx, "##lc", m_light_color, 0.01f, fw / 3.f); + ZUISpacer(ctx, 8.f); + EndRow(); + break; + case 4: // Audio Source + Row("##r_av"); + ZUILabel(ctx, "Volume", ctx->Theme.TextDim); + ZUISpacer(ctx, 4.f); + ZUIDragFloat(ctx, "##av", &m_audio_volume, 0.02f, fw); + ZUISpacer(ctx, 8.f); + EndRow(); + Row("##r_al"); + ZUILabel(ctx, "Loop", ctx->Theme.TextDim); + ZUISpacer(ctx, 4.f); + ZUICheckbox(ctx, "##al", &m_audio_loop); + ZUISpacer(ctx, 8.f); + EndRow(); + Row("##r_pa"); + ZUILabel(ctx, "Play Awake", ctx->Theme.TextDim); + ZUISpacer(ctx, 4.f); + ZUICheckbox(ctx, "##pa", &m_audio_play_awake); + ZUISpacer(ctx, 8.f); + EndRow(); + break; + case 5: // Script + ZUILabel(ctx, "No script attached", ctx->Theme.TextDim); + break; + } + + ZUIEndColumn(ctx); + + // Sash — after expanded sections only + if (di < N - 1) + { + char sk[16], vk[16]; + snprintf(sk, sizeof(sk), "##sk%d", di); + snprintf(vk, sizeof(vk), "##sv%d", di); + + ZUIBox* sash = ZUIPushBox(ctx, sk, (uint32_t) strlen(sk), ZUI_Clickable); + sash->Size[0] = ZFill(); + sash->Size[1] = ZPx(kSashH); + sash->EdgeSoftness = 0.f; + + bool shot = (ctx->HotKey == sash->Key) || (ctx->ActiveKey == sash->Key); + if (shot) + ctx->ResizeCursor = 2; + + ZUIBox* vis = ZUIPushBox(ctx, vk, (uint32_t) strlen(vk), ZUI_DrawBackground | ZUI_FloatX | ZUI_FloatY); + vis->Size[0] = ZFill(); + vis->Size[1] = ZPx(2.f); + vis->FloatPos[0] = 0.f; + vis->FloatPos[1] = (kSashH - 2.f) * 0.5f; + vis->EdgeSoftness = shot ? (ctx->ActiveKey == sash->Key ? 3.f : 2.f) : 0.f; + if (shot) + ZUIBoxSetColor(vis, tb[0], tb[1], tb[2], ctx->ActiveKey == sash->Key ? 0.70f : 0.50f); + else + ZUIBoxSetColor(vis, ctx->Theme.Separator[0], ctx->Theme.Separator[1], ctx->Theme.Separator[2], ctx->Theme.Separator[3]); + ZUIPopBox(ctx); + + ZUISignal ssig = ZUISignalFromBox(ctx, sash); + ZUIPopBox(ctx); + + if ((ssig.Flags & ZUI_SignalHeld) && fabsf(ssig.DragDelta[1]) > 0.05f) + m_h[di] = fmaxf(m_h[di] + ssig.DragDelta[1], kMinH); + } + + // Drop zone teal fill — floated sibling after section wrapper + if (top_tgt || bot_tgt) + { + float fill_y = sec_top + (bot_tgt ? half_h : 0.f); + char dzk[16]; + snprintf(dzk, sizeof(dzk), "##dz%d", di); + ZUIDropZoneFill(ctx, dzk, 0.f, fill_y, sectW, half_h); + } + + // 2px divider after section (bottom-half target) + if (bot_tgt) + ZUIDockDividerH(ctx, "##dbot"); + } + + run_y += SectionRunH(di, hdrH); + } + + // Divider at end of list + if (drop_chg && m_drop_slot == N) + ZUIDockDividerH(ctx, "##dend"); + + ZUIEndScrollRegion(ctx); + + // Ghost — floated after scroll region, panel-content coords. + // cx is centered in the inspector width so the ghost never renders over other panels. + // cy is clamped to stay within the panel height. + if (ghost_lbl) + { + float panel_h = rect[3] - rect[1]; + float ghost_h = hdrH + ctx->Style.TabGhostContentH; + float cx = sectW * 0.5f; // centered — ghost stays inside inspector + float cy = ctx->MousePos[1] - rect[1]; + cy = fmaxf(ghost_h * 0.5f, fminf(cy, panel_h - ghost_h * 0.5f)); + ZUIDockGhostHeader(ctx, "##ghost", ghost_lbl, cx, cy); + } + } + }; + + // ── Console panel ───────────────────────────────────────────────────────── + + struct ConsolePanel : ZUIPanelView + { + ConsolePanel() + { + Title = "Console"; + } + + char m_search[256] = {}; + char m_filter[256] = "ZEngine"; + int m_level = 1; + static constexpr const char* kLevels[] = {"Info", "Warning", "Error"}; + + void BuildContent(ZUIContext* ctx, float rect[4]) override + { + using namespace ZEngine::UI; + (void) rect; + + ZUIBox* bg = ZUIBeginColumn(ctx, "##con_bg", ZFill(), ZFill()); + bg->Flags = bg->Flags | ZUI_DrawBackground; + ZUIBoxSetColorArr(bg, ctx->Theme.PanelBg); + bg->EdgeSoftness = 0.f; + + ZUISpacer(ctx, 8.f); + + ZUIBeginRow(ctx, "##con_r1", ZFill(), ZPx(ZUIGetFrameHeight(ctx))); + ZUISpacer(ctx, 8.f); + ZUILabel(ctx, "Search", ctx->Theme.TextDim); + ZUISpacer(ctx, 8.f); + ZUITextField(ctx, "##con_search", m_search, sizeof(m_search), rect[2] - rect[0] - 24.f); + ZUISpacer(ctx, 8.f); + ZUIEndRow(ctx); + + ZUISpacer(ctx, 6.f); + + ZUIBeginRow(ctx, "##con_r2", ZFill(), ZPx(ZUIGetFrameHeight(ctx))); + ZUISpacer(ctx, 8.f); + ZUILabel(ctx, "Filter", ctx->Theme.TextDim); + ZUISpacer(ctx, 8.f); + ZUITextField(ctx, "##con_filter", m_filter, sizeof(m_filter), rect[2] - rect[0] - 24.f); + ZUISpacer(ctx, 8.f); + ZUIEndRow(ctx); + + ZUISpacer(ctx, 6.f); + + ZUIBeginRow(ctx, "##con_r3", ZFill(), ZPx(ZUIGetFrameHeight(ctx))); + ZUISpacer(ctx, 8.f); + ZUILabel(ctx, "Level ", ctx->Theme.TextDim); + ZUISpacer(ctx, 4.f); + if (ZUIBeginCombo(ctx, "##con_level", kLevels[m_level], ZPx(120.f))) + { + for (int i = 0; i < 3; ++i) + if (ZUIComboItem(ctx, kLevels[i], m_level == i)) + m_level = i; + ZUIEndCombo(ctx); + } + ZUIEndRow(ctx); + + ZUISpacer(ctx, 10.f); + ZUISeparator(ctx); + ZUISpacer(ctx, 6.f); + + ZUIEndColumn(ctx); + } + }; + +} // namespace Tetragrama::Panels diff --git a/Tetragrama/Panels/ZUIPanelManagerComponent.h b/Tetragrama/Panels/ZUIPanelManagerComponent.h new file mode 100644 index 000000000..4f4a04255 --- /dev/null +++ b/Tetragrama/Panels/ZUIPanelManagerComponent.h @@ -0,0 +1,97 @@ +#pragma once +#include +#include +#include +#include +#include + +namespace Tetragrama::Panels +{ + // Default editor layout — matches a standard game engine editor: + // + // ┌─────────┬──────────────────────┬──────────┐ + // │ │ │ │ + // │Hierarchy│ Viewport │Inspector │ + // │ (18%) │ (60%) │ (22%) │ + // │ ├──────────────────────┤ │ + // │ │ Output │ │ + // │ │ (25%) │ │ + // └─────────┴──────────────────────┴──────────┘ + + struct ZUIPanelManagerComponent : public Tetragrama::Components::ZUIComponent + { + ZEngine::UI::ZUIPanelManager Manager; + + HierarchyPanel hierarchy; + ViewportPanel viewport; + InspectorPanel inspector; + ConsolePanel output; + + void Initialize(Tetragrama::Layers::ZUILayer* parent, cstring name = "PanelManager", bool visibility = true) override + { + ParentLayer = parent; + Name = name; + Visible = visibility; + + auto* arena = parent ? &parent->LocalArena : nullptr; + if (!arena) + { + return; + } + + Manager.Init(arena); + + using namespace ZEngine::UI; + + constexpr float kLeft = 0.18f; // Hierarchy width + constexpr float kRight = 0.22f; // Inspector width + constexpr float kBottom = 0.25f; // Output height + + // Root H-split: Hierarchy | rest + ZUIDockSplitH(Manager.DockTree, Manager.DockTree->Root, kLeft, ZUIDockHashName("Hierarchy"), 0); + + ZUIDockNode* mid_right = Manager.DockTree->Root->Last; + + // mid_right H-split: center | Inspector + ZUIDockSplitH(Manager.DockTree, mid_right, 1.f - kRight, 0, ZUIDockHashName("Inspector")); + + ZUIDockNode* center = mid_right->First; + + // center V-split: Viewport | Output + ZUIDockSplitV(Manager.DockTree, center, 1.f - kBottom, ZUIDockHashName("Viewport"), ZUIDockHashName("Console")); + + // Register panels + auto* p_hier = Manager.AddPanel(ZUIDockHashName("Hierarchy")); + Manager.AddView(p_hier, &hierarchy); + + auto* p_vp = Manager.AddPanel(ZUIDockHashName("Viewport")); + Manager.AddView(p_vp, &viewport); + + auto* p_insp = Manager.AddPanel(ZUIDockHashName("Inspector")); + Manager.AddView(p_insp, &inspector); + + auto* p_out = Manager.AddPanel(ZUIDockHashName("Console")); + Manager.AddView(p_out, &output); + + // Ini persistence — v3 format (AutoHideTabBar support) + // Old v2 files are intentionally incompatible — delete zui_layout.ini to start fresh. + ZUIPanelView* all_views[] = {&hierarchy, &viewport, &inspector, &output}; + Manager.SetLayoutPath("zui_layout.ini"); + ZUIDockLoad(&Manager, "zui_layout.ini", all_views, 4); // no-op if file not found or v2 + } + + void BuildUI(ZEngine::UI::ZUIContext* ctx) override + { + if (!Visible) + { + return; + } + // Menu bar and status bar heights from style (not hardcoded). + // Both use GetFrameHeight() = FontSize + FramePadding.y*2 = 19px default. + float menu_h = ZEngine::UI::ZUIGetFrameHeight(ctx); + float status_h = ZEngine::UI::ZUIGetFrameHeight(ctx); + Manager.BuildUI(ctx, menu_h, status_h); + } + }; + +} // namespace Tetragrama::Panels diff --git a/ZEngine/ZEngine/Applications/AppRenderPipeline.cpp b/ZEngine/ZEngine/Applications/AppRenderPipeline.cpp index ce718a904..eba265ead 100644 --- a/ZEngine/ZEngine/Applications/AppRenderPipeline.cpp +++ b/ZEngine/ZEngine/Applications/AppRenderPipeline.cpp @@ -1,10 +1,16 @@ +#include #include #include #include #include +#include +#include +#include #include #include #include +#include +#include using namespace ZEngine::Core::Containers; using namespace ZEngine::Core::Maths; @@ -65,30 +71,41 @@ namespace ZEngine::Applications { Device = device; RenderWorkerThreadCount = Device->CommandBufferMgr->TotalThreadCount > 0u ? Device->CommandBufferMgr->TotalThreadCount - 1u : 0u; - UICommandBufferIndex = RenderMainThreadIndex + 1u; - Device->Arena->CreateSubArena(ZMega(30), &LocalArena); - - SceneRenderer = ZPushStructCtor(Device->Arena, Rendering::Renderers::GraphicRenderer); - ImguiRenderer = ZPushStructCtor(Device->Arena, Rendering::Renderers::ImGUIRenderer); + SceneRenderer = ZPushStructCtor(Device->Arena, Rendering::Renderers::GraphicRenderer); + ZUIRenderer = ZPushStructCtor(Device->Arena, Rendering::Renderers::ZUIRenderer); SceneRenderer->Initialize(Device); - ImguiRenderer->Initialize(Device); + ZUIRenderer->Initialize(Device); + + // UIContext arena: created by Engine::Initialize via MemoryBudgetConfig::Editor().UIContext + // (128 MB budgeted, ~60 MB committed: FrameArena 32 MB · PersistentArena 1 MB · ZUIPayloadArenas 9 MB × 3) + auto* ui_arena = &Engine::GetContext()->UIContextArena; + ZUICtx = ZPushStructCtor(ui_arena, ZEngine::UI::ZUIContext); + ZEngine::UI::ZUIContextInit(ZUICtx, ui_arena, ZMega(32), ZMega(1), 8192, 8192); + for (int i = 0; i < 3; ++i) + { + ui_arena->CreateSubArena(ZMega(9), &ZUIPayloadArenas[i]); + } Device->SwapchainPtr->OnSwapchainResized = [](uint32_t w, uint32_t h, void* ctx) { static_cast(ctx)->ResizeRenderTarget(w, h); }; Device->SwapchainPtr->OnSwapchainResizedCtx = this; - - for (size_t i = 0; i < MaxMailBoxBufferCount; ++i) - { - RenderPayloads[i].UIOverlay.IndexedCmds.resize(100); - RenderPayloads[i].UIOverlay.ScissorCmds.resize(100); - RenderPayloads[i].UIOverlay.TextureIds.resize(100); - } } void AppRenderPipeline::Shutdown() { SceneRenderer->Deinitialize(); - ImguiRenderer->Deinitialize(); + if (ZUIRenderer) + { + ZUIRenderer->Deinitialize(); + } + if (ZUICtx) + { + ZEngine::UI::ZUIContextDestroy(ZUICtx); + } + for (int i = 0; i < 3; ++i) + { + ZUIPayloadArenas[i].Shutdown(); + } } void AppRenderPipeline::ResizeRenderTarget(uint32_t w, uint32_t h) @@ -162,7 +179,7 @@ namespace ZEngine::Applications { auto* rrm = Device->RRM ? reinterpret_cast(Device->RRM) : nullptr; auto* mgr = Managers::AssetManager::Instance(); - auto scratch = ZGetScratch(&LocalArena); + auto scratch = ZGetScratch(&Engine::GetContext()->UIContextArena); Core::Containers::Array instances; scene->GetInstancesSnapshot(scratch.Arena, instances); @@ -269,83 +286,124 @@ namespace ZEngine::Applications SceneRenderer->DrawScene(frame_index, thread_index, CurrentCmdBuf, camera); } - void AppRenderPipeline::BeginOverlayFrame() + void AppRenderPipeline::BeginOverlayFrame(float dt) { - ImguiRenderer->NewFrame(); + if (ZUICtx) + { + // Use the logical window size (glfwGetWindowSize) not the physical swapchain + // size. Mouse positions from GLFW cursor callbacks are also in logical pixels, + // so panel positions and hit-testing must use the same coordinate space. + if (Device->CurrentWindow) + { + auto* native = static_cast(Device->CurrentWindow->GetNativeWindow()); + float content_scale = 1.f; + if (native) + { + float xs = 1.f, ys = 1.f; + glfwGetWindowContentScale(native, &xs, &ys); + content_scale = (xs > ys ? xs : ys); + if (content_scale < 0.5f) + content_scale = 1.f; + } + + // Exact ImGui approach (imgui_impl_glfw.cpp GetWindowSizeAndFramebufferScale): + // ScreenW/H = glfwGetWindowSize → logical screen coords (1512 on Retina) + // UIScale = glfwGetFramebufferSize / glfwGetWindowSize → physical/logical ratio (2.0 on Retina) + // Cursor from GLFW callback is already in logical space — no division needed. + // This is identical on all platforms; macOS just happens to have UIScale=2 on Retina. + if (native) + { + int win_w = 0, win_h = 0, fb_w = 0, fb_h = 0; + glfwGetWindowSize(native, &win_w, &win_h); + glfwGetFramebufferSize(native, &fb_w, &fb_h); + if (win_w > 0 && win_h > 0) + { + ZUICtx->ScreenW = (uint32_t) win_w; + ZUICtx->ScreenH = (uint32_t) win_h; + float s = (fb_w > 0) ? (float) fb_w / (float) win_w : 1.f; + ZUICtx->UIScale = (s > 0.5f) ? s : 1.f; + } + else + { + ZUICtx->ScreenW = Device->CurrentWindow->GetWidth(); + ZUICtx->ScreenH = Device->CurrentWindow->GetHeight(); + ZUICtx->UIScale = content_scale; + } + } + else + { + ZUICtx->ScreenW = Device->CurrentWindow->GetWidth(); + ZUICtx->ScreenH = Device->CurrentWindow->GetHeight(); + ZUICtx->UIScale = content_scale; + } + if (!ZUICtx->UIScaleLogged) + { + ZENGINE_CORE_INFO("[ZUI] UIScale={:.2f} Screen={}x{} (logical) ContentScale={:.2f}", ZUICtx->UIScale, ZUICtx->ScreenW, ZUICtx->ScreenH, content_scale); + ZUICtx->UIScaleLogged = true; + } + } + else + { + ZUICtx->ScreenW = Device->SwapchainPtr->SwapchainImageWidth; + ZUICtx->ScreenH = Device->SwapchainPtr->SwapchainImageHeight; + } + ZEngine::UI::ZUIBeginFrame(ZUICtx, dt); + } } - void AppRenderPipeline::FillOverlayPayload(Rendering::Renderers::RenderOverlayPayload& payload) + void AppRenderPipeline::FillOverlayPayload(RenderPayload& payload) { - ImguiRenderer->PreparePayload(payload); + if (ZUIRenderer && ZUICtx && ZUICtx->Root) + { + uint32_t slot = MailBoxBufferHead.value.load(std::memory_order_relaxed); + ZUIPayloadArenas[slot].Clear(); + ZUIRenderer->PreparePayload(ZUICtx, &payload.ZUIOverlay, &ZUIPayloadArenas[slot]); + } } - void AppRenderPipeline::RenderOverlay(const Rendering::Renderers::RenderOverlayPayload& payload) + void AppRenderPipeline::RenderOverlay(const RenderPayload& payload) { - if (payload.VertexCount == 0 && payload.IndexCount == 0) + if (ZUIRenderer) { - return; + ZUIRenderer->Submit(CurrentCmdBuf, payload.ZUIOverlay); } + } - auto swpachain = Device->SwapchainPtr; - auto frame_index = swpachain->CurrentFrame->Index; - auto thread_index = RenderMainThreadIndex; - - auto current_framebuffer = Device->SwapchainPtr->SwapchainFramebuffers[Device->SwapchainPtr->CurrentFrame->ImageIndex]; - - // Resolve per-frame ImGui buffers now that BeginFrame has set CurrentFrame. - uint32_t fi = frame_index % Rendering::Renderers::ImGUIRenderer::FRAMES_IN_FLIGHT; - auto vb = ImguiRenderer->VBHandles[fi]; - auto ib = ImguiRenderer->IdxBHandles[fi]; - - CurrentCmdBuf->BeginRenderPass(ImguiRenderer->UIPass, current_framebuffer, true); + void AppRenderPipeline::EndOverlayFrame() + { + if (ZUICtx) { - // Direct HOST_VISIBLE writes — one buffer per frame-in-flight, no WAR hazard. - auto* rrm = Device->RRM ? reinterpret_cast(Device->RRM) : nullptr; - if (rrm) - { - rrm->UpdateBuffer(vb, payload.VertexData.data(), payload.VertexData.size() * sizeof(payload.VertexData[0])); - rrm->UpdateBuffer(ib, payload.IndexData.data(), payload.IndexData.size() * sizeof(payload.IndexData[0])); - } - - auto ui_second_cb = Device->CommandBufferMgr->GetCommandBuffer(Rendering::QueueType::GRAPHIC_QUEUE, Device->SwapchainPtr->CurrentFrame->Index, RenderMainThreadIndex, UICommandBufferIndex, false); - ui_second_cb->ResetState(); - ui_second_cb->BeginSecondary(ImguiRenderer->UIPass, current_framebuffer); - ui_second_cb->SetViewport(ImguiRenderer->UIPass->GetRenderAreaWidth(), ImguiRenderer->UIPass->GetRenderAreaHeight()); - - ui_second_cb->BindPipeline(Rendering::Specifications::PipelineBindPoint::GRAPHIC, ImguiRenderer->UIPass->Pipeline); - - ui_second_cb->BindVertexBuffer(vb); - ui_second_cb->BindIndexBuffer(ib, payload.IsIndexBufferUint16 ? VK_INDEX_TYPE_UINT16 : VK_INDEX_TYPE_UINT32); - - Rendering::Renderers::PushConstantData pc_data = {}; - pc_data.Scale[0] = payload.Pc[0]; - pc_data.Scale[1] = payload.Pc[1]; - - pc_data.Translate[0] = payload.Pc[2]; - pc_data.Translate[1] = payload.Pc[3]; + ZEngine::UI::ZUIEndFrame(ZUICtx); + } - for (uint32_t i = 0; i < payload.DrawDataIndex; ++i) + // Apply resize cursor from the ZUI divider hover state + flush clipboard writes + if (Device && Device->CurrentWindow) + { + auto* gw = static_cast(Device->CurrentWindow->GetNativeWindow()); + if (gw) { - const auto& scissor_cmd = payload.ScissorCmds[i]; - const auto& indexed_cmd = payload.IndexedCmds[i]; - - ui_second_cb->SetScissor(scissor_cmd.w, scissor_cmd.h, scissor_cmd.x, scissor_cmd.y); - pc_data.TextureId = payload.TextureIds[i]; - ui_second_cb->PushConstants(VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(Rendering::Renderers::PushConstantData), &pc_data); - ui_second_cb->BindDescriptorSets(Device->SwapchainPtr->CurrentFrame->Index); - ui_second_cb->DrawIndexed(indexed_cmd.IdxCount, indexed_cmd.InstanceCount, indexed_cmd.FirstIndex, indexed_cmd.VertexOffset, indexed_cmd.FirstInstance); + // Flush Ctrl+C clipboard write (set by ZUITextField when focused) + if (ZUICtx && ZUICtx->ClipboardWrite[0] != '\0') + { + glfwSetClipboardString(gw, ZUICtx->ClipboardWrite); + ZUICtx->ClipboardWrite[0] = '\0'; + } + int req = ZUICtx ? ZUICtx->ResizeCursor : 0; + // Lazily create standard cursors (created once, never destroyed — app lifetime) + static GLFWcursor* s_hresize = nullptr; + static GLFWcursor* s_vresize = nullptr; + if (!s_hresize) + s_hresize = glfwCreateStandardCursor(GLFW_HRESIZE_CURSOR); + if (!s_vresize) + s_vresize = glfwCreateStandardCursor(GLFW_VRESIZE_CURSOR); + + if (req == 1 && s_hresize) + glfwSetCursor(gw, s_hresize); + else if (req == 2 && s_vresize) + glfwSetCursor(gw, s_vresize); + else + glfwSetCursor(gw, nullptr); // restore default } - - ui_second_cb->End(); - - CurrentCmdBuf->ExecuteSecondaryCommandBuffers(ArrayView{ui_second_cb, 1}); } - - CurrentCmdBuf->EndRenderPass(); - } - - void AppRenderPipeline::EndOverlayFrame() - { - ImguiRenderer->EndFrame(); } } // namespace ZEngine::Applications diff --git a/ZEngine/ZEngine/Applications/AppRenderPipeline.h b/ZEngine/ZEngine/Applications/AppRenderPipeline.h index 475e9d1bf..b53d18961 100644 --- a/ZEngine/ZEngine/Applications/AppRenderPipeline.h +++ b/ZEngine/ZEngine/Applications/AppRenderPipeline.h @@ -1,19 +1,24 @@ #pragma once #include #include -#include +#include + +namespace ZEngine::UI +{ + struct ZUIContext; +} namespace ZEngine::Applications { struct RenderPayload { - uint32_t RenderTargetW = 0; - uint32_t RenderTargetH = 0; - PaddedAtomic RenderUIOverlay = {.value = false}; - PaddedAtomic ResizeRenderTarget = {.value = false}; - Rendering::Cameras::CameraPtr Camera = nullptr; - Rendering::Scenes::RenderScenePtr Scene = nullptr; - Rendering::Renderers::RenderOverlayPayload UIOverlay = {}; + uint32_t RenderTargetW = 0; + uint32_t RenderTargetH = 0; + PaddedAtomic RenderUIOverlay = {.value = false}; + PaddedAtomic ResizeRenderTarget = {.value = false}; + Rendering::Cameras::CameraPtr Camera = nullptr; + Rendering::Scenes::RenderScenePtr Scene = nullptr; + Rendering::Renderers::ZUIRenderPayload ZUIOverlay = {}; }; struct AppRenderPipeline @@ -21,15 +26,15 @@ namespace ZEngine::Applications const uint8_t MaxMailBoxBufferCount = 3; const uint8_t RenderMainThreadIndex = 0; uint8_t RenderWorkerThreadCount = 0; - uint8_t UICommandBufferIndex = 0xff; uint32_t CurrentMailBoxBufferHead = 0; PaddedAtomic MailBoxBufferHead = {.value = 0}; PaddedAtomic MailBoxBufferTail = {.value = 0}; RenderPayload RenderPayloads[3] = {}; - ZEngine::Core::Memory::ArenaAllocator LocalArena = {}; + ZEngine::Core::Memory::ArenaAllocator ZUIPayloadArenas[3] = {}; + ZEngine::UI::ZUIContext* ZUICtx = nullptr; Hardwares::VulkanDevicePtr Device = nullptr; Rendering::Renderers::GraphicRendererPtr SceneRenderer = nullptr; - Rendering::Renderers::ImGUIRendererPtr ImguiRenderer = nullptr; + Rendering::Renderers::ZUIRendererPtr ZUIRenderer = nullptr; Hardwares::CommandBufferPtr CurrentCmdBuf = nullptr; void Initialize(Hardwares::VulkanDevicePtr device); @@ -37,17 +42,15 @@ namespace ZEngine::Applications void ResizeRenderTarget(uint32_t w, uint32_t h); - // Returns false when the frame was aborted (OUT_OF_DATE at acquire or - // zero-size surface). The caller must skip all rendering work for that frame. bool BeginFrame(); void EndFrame(); void RenderScene(Rendering::Cameras::CameraPtr camera, Rendering::Scenes::RenderScenePtr scene); - void BeginOverlayFrame(); + void BeginOverlayFrame(float dt = 0.f); void EndOverlayFrame(); - void FillOverlayPayload(Rendering::Renderers::RenderOverlayPayload& payload); - void RenderOverlay(const Rendering::Renderers::RenderOverlayPayload& payload); + void FillOverlayPayload(RenderPayload& payload); + void RenderOverlay(const RenderPayload& payload); }; ZDEFINE_PTR(AppRenderPipeline); diff --git a/ZEngine/ZEngine/Applications/GameApplication.h b/ZEngine/ZEngine/Applications/GameApplication.h index 3b5515fe3..81978729b 100644 --- a/ZEngine/ZEngine/Applications/GameApplication.h +++ b/ZEngine/ZEngine/Applications/GameApplication.h @@ -43,7 +43,7 @@ namespace ZEngine::Applications void Initialize(Core::Memory::MemoryManager* memory); void Update(Core::TimeStep dt); - void ProcessEvent(Core::CoreEvent&); + virtual void ProcessEvent(Core::CoreEvent&); void Run(); void PrepareScene(RenderPayload&); void Shutdown(); diff --git a/ZEngine/ZEngine/CMakeLists.txt b/ZEngine/ZEngine/CMakeLists.txt index b9f47ce9c..180b15308 100644 --- a/ZEngine/ZEngine/CMakeLists.txt +++ b/ZEngine/ZEngine/CMakeLists.txt @@ -74,6 +74,9 @@ file(GLOB_RECURSE ZENGINE_SOURCES_INPUT CONFIGURE_DEPENDS file(GLOB_RECURSE ZENGINE_SOURCES_HARDWARE CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/Hardwares/*.cpp ) +file(GLOB_RECURSE ZENGINE_SOURCES_UI CONFIGURE_DEPENDS + ${CMAKE_CURRENT_SOURCE_DIR}/UI/*.cpp +) if(APPLE) list(APPEND ZENGINE_SOURCES_CRASH_HANDLERS @@ -113,6 +116,7 @@ source_group(TREE ${PROJECT_SOURCE_DIR}/ZEngine PREFIX "Source Files" FILES ${ZENGINE_SOURCES_INPUT} ${ZENGINE_SOURCES_HARDWARE} ${ZENGINE_SOURCES_CRASH_HANDLERS} + ${ZENGINE_SOURCES_UI} ) source_group(TREE ${PROJECT_SOURCE_DIR}/../Resources PREFIX "Resources Files" FILES ${ZENGINE_RESOURCES}) @@ -135,6 +139,7 @@ target_sources(zEngineLib PRIVATE ${ZENGINE_SOURCES_INPUT} ${ZENGINE_SOURCES_HARDWARE} ${ZENGINE_SOURCES_CRASH_HANDLERS} + ${ZENGINE_SOURCES_UI} ${CMAKE_CURRENT_SOURCE_DIR}/Engine.cpp ) diff --git a/ZEngine/ZEngine/Engine.cpp b/ZEngine/ZEngine/Engine.cpp index fb60f5976..6fe6794cd 100644 --- a/ZEngine/ZEngine/Engine.cpp +++ b/ZEngine/ZEngine/Engine.cpp @@ -98,6 +98,7 @@ namespace ZEngine // parent (glTF 64 MB + Assimp 128 MB + envmap 32 MB + editor ~414 MB). // Each Import() call ends with Arena.Clear() so the sub-arena is reused, not consumed. memory->CreateBudgetedArena(memory->Budget.ImportPipeline, &g_engine_ctx->ImportPipelineArena); + memory->CreateBudgetedArena(memory->Budget.UIContext, &g_engine_ctx->UIContextArena); g_engine_ctx->ImportCoordinator = ZPushStructCtor(&g_engine_ctx->AssetArena, Importers::ImportCoordinator); g_engine_ctx->ImportCoordinator->Initialize(&g_engine_ctx->AssetArena, g_engine_ctx->VFS, Managers::AssetManager::Instance()->Registry); @@ -277,17 +278,16 @@ namespace ZEngine if (next == tail) continue; // buffer full — drop frame - auto& r_payload = pipeline->RenderPayloads[head]; - r_payload.UIOverlay.DrawDataIndex = 0; + auto& r_payload = pipeline->RenderPayloads[head]; r_payload.RenderUIOverlay.value.store(false, std::memory_order_release); if (g_app->EnableRenderOverlay) { - pipeline->BeginOverlayFrame(); + pipeline->BeginOverlayFrame(raw_dt); g_app->OnRenderUI(); pipeline->EndOverlayFrame(); r_payload.RenderUIOverlay.value.store(true, std::memory_order_release); - pipeline->FillOverlayPayload(r_payload.UIOverlay); + pipeline->FillOverlayPayload(r_payload); } if (g_engine_ctx->Scene && g_app->CurrentScene) @@ -355,7 +355,7 @@ namespace ZEngine { pipeline->RenderScene(r_payload.Camera, r_payload.Scene); if (r_payload.RenderUIOverlay.value.load(std::memory_order_acquire)) - pipeline->RenderOverlay(r_payload.UIOverlay); + pipeline->RenderOverlay(r_payload); } pipeline->EndFrame(); diff --git a/ZEngine/ZEngine/Engine.h b/ZEngine/ZEngine/Engine.h index 0efee234d..e6613f21b 100644 --- a/ZEngine/ZEngine/Engine.h +++ b/ZEngine/ZEngine/Engine.h @@ -30,6 +30,7 @@ namespace ZEngine Core::Memory::ArenaAllocator InputArena = {}; Core::Memory::ArenaAllocator ECSArena = {}; Core::Memory::ArenaAllocator ImportPipelineArena = {}; + Core::Memory::ArenaAllocator UIContextArena = {}; // Pointers (8 bytes each — grouped to pack cleanly) Hardwares::VulkanDevicePtr Device = nullptr; diff --git a/ZEngine/ZEngine/Rendering/Renderers/IRenderer.h b/ZEngine/ZEngine/Rendering/Renderers/IRenderer.h index b59ef7c6e..2e7f0dc2c 100644 --- a/ZEngine/ZEngine/Rendering/Renderers/IRenderer.h +++ b/ZEngine/ZEngine/Rendering/Renderers/IRenderer.h @@ -4,22 +4,7 @@ namespace ZEngine::Rendering::Renderers { - struct ScissorCmd - { - uint32_t w = 0; - uint32_t h = 0; - int32_t x = 0; - int32_t y = 0; - }; - struct IndexedCmd - { - uint32_t IdxCount = 0; - uint32_t InstanceCount = 0; - uint32_t FirstIndex = 0; - int32_t VertexOffset = 0; - uint32_t FirstInstance = 0; - }; - + // Vertex layout shared by ZUIRenderer and zui_draw.vert / zui_draw.frag. struct UIDrawVert { typedef struct _vec2 @@ -31,22 +16,6 @@ namespace ZEngine::Rendering::Renderers unsigned int col; }; - struct RenderOverlayPayload - { - bool IsIndexBufferUint16 = false; - uint32_t VertexCount = 0; - uint32_t IndexCount = 0; - uint32_t DrawDataIndex = 0; - float Pc[4] = {0.0f}; // {Scale}-{Translate} - Core::Memory::BufferView VBHandle = {}; - Core::Memory::BufferView IdxBHandle = {}; - std::vector TextureIds = {}; - std::vector IndexData = {}; - std::vector VertexData = {}; - std::vector ScissorCmds = {}; - std::vector IndexedCmds = {}; - }; - struct RendererResourceName { inline static cstring FrameDepthRenderTargetName = "g_frame_depth_render_target"; diff --git a/ZEngine/ZEngine/Rendering/Renderers/ImGUIRenderer.cpp b/ZEngine/ZEngine/Rendering/Renderers/ImGUIRenderer.cpp deleted file mode 100644 index c804973bf..000000000 --- a/ZEngine/ZEngine/Rendering/Renderers/ImGUIRenderer.cpp +++ /dev/null @@ -1,302 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -// clang-format off -#include -#include -#include -// clang-format on - -using namespace ZEngine::Hardwares; -using namespace ZEngine::Rendering; -using namespace ZEngine::Rendering::Textures; -using namespace ZEngine::Helpers; -using namespace ZEngine::Core::Containers; - -namespace ZEngine::Rendering::Renderers -{ - void ImGUIRenderer::Initialize(Hardwares::VulkanDevicePtr device) - { - Device = device; - RenderGraph = ZPushStructCtorArgs(Device->Arena, Renderers::RenderGraph); - - RenderGraph->Initialize(Device); - - IMGUI_CHECKVERSION(); - ImGui::CreateContext(); - - ImGuiIO& io = ImGui::GetIO(); - io.ConfigViewportsNoTaskBarIcon = true; - io.ConfigViewportsNoDecoration = true; - io.BackendFlags |= ImGuiBackendFlags_RendererHasViewports; - io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset; - io.BackendFlags |= ImGuiBackendFlags_HasMouseHoveredViewport; - io.BackendRendererName = "ZEngine-Imgui"; - - { - auto* ctx = Engine::GetContext(); - auto ini_vfs_path = Core::VFS::VFSPath::Parse("/ZodiacEngine/Settings/DefaultLayout.ini"); - if (ini_vfs_path.Succeeded()) - { - auto exists = ctx->VFS->Exists(ini_vfs_path.Value()); - if (exists.Succeeded() && exists.Value()) - { - static char s_ini_path[MAX_FILE_PATH_COUNT]; - fmt::format_to_n(s_ini_path, sizeof(s_ini_path) - 1, "{}/Settings/DefaultLayout.ini", ctx->EngineAssetsBackend.NativeRoot()); - io.IniFilename = s_ini_path; - } - } - } - - io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; - io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; - // io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable; - - auto& style = ImGui::GetStyle(); - style.WindowBorderSize = 0.f; - style.ChildBorderSize = 0.f; - style.FrameRounding = 7.0f; - - { - auto* vfs = Engine::GetContext()->VFS; - auto font_res = Core::VFS::VFSPath::Parse("/ZodiacEngine/Settings/Fonts/OpenSans/OpenSans-Regular.ttf"); - if (font_res.Succeeded()) - { - auto file_res = vfs->Open(font_res.Value(), Core::VFS::VFSOpenFlags::Read); - if (file_res.Succeeded()) - { - auto* f = file_res.Value(); - auto size_res = f->Size(); - if (size_res.Succeeded()) - { - const uint64_t sz = size_res.Value(); - void* data = IM_ALLOC(static_cast(sz)); - Core::Containers::ArrayView view{static_cast(data), sz}; - f->ReadAll(view); - io.FontDefault = io.Fonts->AddFontFromMemoryTTF(data, static_cast(sz), 17.f); - } - vfs->Close(f); - } - } - } - - auto current_window = Device->CurrentWindow->GetNativeWindow(); - - ImGui_ImplGlfw_InitForVulkan(reinterpret_cast(current_window), false); - - // HOST_VISIBLE — one buffer per frame-in-flight to avoid write-after-read hazard. - for (uint32_t i = 0; i < FRAMES_IN_FLIGHT; ++i) - { - VBHandles[i] = Device->GpuMem.AllocateBuffer(ZMega(5), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, Core::Memory::GpuMemoryDomain::HostUniform, "ImguiVertexBuffer"); - IdxBHandles[i] = Device->GpuMem.AllocateBuffer(ZMega(5), VK_BUFFER_USAGE_INDEX_BUFFER_BIT, Core::Memory::GpuMemoryDomain::HostUniform, "ImguiIndexBuffer"); - } - - /* - * Font uploading — RRM creates the texture and copies pixel data into an - * owned buffer. The GPU upload is deferred to the first BeginFrame call. - */ - unsigned char* pixels; - int width, height; - io.Fonts->GetTexDataAsRGBA32(&pixels, &width, &height); - - if (Device->RRM) - { - auto* rrm = static_cast(Device->RRM); - FontTexture = rrm->UploadFontAtlas(pixels, static_cast(width), static_cast(height)); - } - - Device->TextureHandleToUpdates.Enqueue(FontTexture); - io.Fonts->TexID = (ImTextureID) FontTexture.Index; - - auto pass_builder = RenderGraph->RenderPassBuilder; - pass_builder->SetName("Imgui Pass") - .SetPipelineName("Imgui-Pipeline") - .EnablePipelineBlending(true) - .SetInputBindingCount(1) - .SetStride(0, sizeof(ImDrawVert)) - .SetRate(0, VK_VERTEX_INPUT_RATE_VERTEX) - - .SetInputAttributeCount(3) - .SetLocation(0, 0) - .SetBinding(0, 0) - .SetFormat(0, Specifications::ImageFormat::R32G32_SFLOAT) - .SetOffset(0, offsetof(ImDrawVert, pos)) - .SetLocation(1, 1) - .SetBinding(1, 0) - .SetFormat(1, Specifications::ImageFormat::R32G32_SFLOAT) - .SetOffset(1, offsetof(ImDrawVert, uv)) - .SetLocation(2, 2) - .SetBinding(2, 0) - .SetFormat(2, Specifications::ImageFormat::R8G8B8A8_UNORM) - .SetOffset(2, offsetof(ImDrawVert, col)) - - .UseShader("imgui") - // .SetShaderOverloadMaxSet(2000) // Todo : deprecated API - should be removed - - .UseSwapchainAsRenderTarget(); - - UIPass = Device->CreateRenderPass(pass_builder->Detach()); - UIPass->UseTextureArray("TextureArray"); - UIPass->SetSampler("LinearClampSampler", Device->GlobalLinearClampToEdgeSamplerImageInfo); - UIPass->Verify(); - UIPass->Bake(); - } - - void ImGUIRenderer::Deinitialize() - { - RenderGraph->Dispose(); - UIPass->Dispose(); - - if (FontTexture.Valid()) - Device->TextureHandleToDispose.Enqueue(FontTexture); - - for (uint32_t i = 0; i < FRAMES_IN_FLIGHT; ++i) - { - if (VBHandles[i]) - Device->GpuMem.FreeBuffer(VBHandles[i]); - if (IdxBHandles[i]) - Device->GpuMem.FreeBuffer(IdxBHandles[i]); - } - - ImGui_ImplGlfw_Shutdown(); - ImGui::DestroyContext(); - } - - void ImGUIRenderer::NewFrame() - { - ImGui_ImplGlfw_NewFrame(); - ImGui::NewFrame(); - ImGuizmo::BeginFrame(); - } - void ImGUIRenderer::EndFrame() - { - // The render method has EndFrame() - ImGui::Render(); - ImGuiIO& io = ImGui::GetIO(); - if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) - { - ImGui::UpdatePlatformWindows(); - ImGui::RenderPlatformWindowsDefault(); - } - } - - void ImGUIRenderer::PreparePayload(RenderOverlayPayload& r_payload) - { - ImDrawData* draw_data = ImGui::GetDrawData(); - - if (!draw_data) - { - return; - } - // Avoid rendering when minimized, scale coordinates for retina displays (screen coordinates != framebuffer - // coordinates) - int fb_width = (int) (draw_data->DisplaySize.x * draw_data->FramebufferScale.x); - int fb_height = (int) (draw_data->DisplaySize.y * draw_data->FramebufferScale.y); - if (fb_width <= 0 || fb_height <= 0) - { - return; - } - - int vertex_count = draw_data->TotalVtxCount; - int index_count = draw_data->TotalIdxCount; - - if (vertex_count == 0 && index_count == 0) - { - return; - } - - r_payload.VertexCount = vertex_count; - r_payload.IndexCount = index_count; - r_payload.IsIndexBufferUint16 = sizeof(ImDrawIdx) == 2; - // VBHandle/IdxBHandle are set in RenderOverlay after BeginFrame sets CurrentFrame. - - r_payload.VertexData.clear(); - r_payload.IndexData.clear(); - r_payload.VertexData.shrink_to_fit(); - r_payload.IndexData.shrink_to_fit(); - - r_payload.VertexData.resize(vertex_count); - r_payload.IndexData.resize(index_count); - - UIDrawVert* vertex_data_ptr = r_payload.VertexData.data(); - for (int n = 0; n < draw_data->CmdListsCount; ++n) - { - const ImDrawList* cmd_list = draw_data->CmdLists[n]; - const size_t data_size = cmd_list->VtxBuffer.Size * sizeof(ImDrawVert); - Helpers::secure_memcpy(vertex_data_ptr, data_size, cmd_list->VtxBuffer.Data, data_size); - vertex_data_ptr += cmd_list->VtxBuffer.Size; - } - - unsigned short* index_data_ptr = r_payload.IndexData.data(); - for (int n = 0; n < draw_data->CmdListsCount; ++n) - { - const ImDrawList* cmd_list = draw_data->CmdLists[n]; - const size_t data_size = cmd_list->IdxBuffer.Size * sizeof(ImDrawIdx); - Helpers::secure_memcpy(index_data_ptr, data_size, cmd_list->IdxBuffer.Data, data_size); - index_data_ptr += cmd_list->IdxBuffer.Size; - } - - // Setup scale and translation: - // Our visible imgui space lies from draw_data->DisplayPps (top left) to - // draw_data->DisplayPos+data_data->DisplaySize (bottom right). DisplayPos is (0,0) for single viewport apps. - - r_payload.Pc[0] = 2.0f / draw_data->DisplaySize.x; - r_payload.Pc[1] = 2.0f / draw_data->DisplaySize.y; - r_payload.Pc[2] = -1.0f - draw_data->DisplayPos.x * r_payload.Pc[0]; - r_payload.Pc[3] = -1.0f - draw_data->DisplayPos.y * r_payload.Pc[1]; - - // Will project scissor/clipping rectangles into framebuffer space - ImVec2 clip_off = draw_data->DisplayPos; // (0,0) unless using multi-viewports - ImVec2 clip_scale = draw_data->FramebufferScale; // (1,1) unless using retina display which are often (2,2) - - // Render command lists - // (Because we merged all buffers into a single one, we maintain our own offset into them) - - int global_vtx_offset = 0; - int global_idx_offset = 0; - for (int n = 0; n < draw_data->CmdListsCount; n++) - { - const ImDrawList* cmd_list = draw_data->CmdLists[n]; - for (int cmd_i = 0; cmd_i < cmd_list->CmdBuffer.Size; cmd_i++) - { - const ImDrawCmd* pcmd = &cmd_list->CmdBuffer[cmd_i]; - if (pcmd->UserCallback != nullptr) - { - pcmd->UserCallback(cmd_list, pcmd); - } - else - { - // Project scissor/clipping rectangles into framebuffer space - ImVec4 clip_rect; - clip_rect.x = std::max(0.f, (pcmd->ClipRect.x - clip_off.x) * clip_scale.x); - clip_rect.y = std::max(0.f, (pcmd->ClipRect.y - clip_off.y) * clip_scale.y); - clip_rect.z = std::max(0.f, (pcmd->ClipRect.z - clip_off.x) * clip_scale.x); - clip_rect.w = std::max(0.f, (pcmd->ClipRect.w - clip_off.y) * clip_scale.y); - - if (clip_rect.x >= 0 && clip_rect.x < fb_width && clip_rect.y >= 0 && clip_rect.y < fb_height && clip_rect.z >= 0.0f && clip_rect.w >= 0.0f) - { - // Apply scissor/clipping rectangle - VkRect2D scissor; - scissor.offset.x = (int32_t) (clip_rect.x); - scissor.offset.y = (int32_t) (clip_rect.y); - scissor.extent.width = (uint32_t) (clip_rect.z - clip_rect.x); - scissor.extent.height = (uint32_t) (clip_rect.w - clip_rect.y); - - r_payload.TextureIds[r_payload.DrawDataIndex] = (uint32_t) (intptr_t) pcmd->GetTexID(); - r_payload.ScissorCmds[r_payload.DrawDataIndex] = ScissorCmd{scissor.extent.width, scissor.extent.height, scissor.offset.x, scissor.offset.y}; - r_payload.IndexedCmds[r_payload.DrawDataIndex] = IndexedCmd{pcmd->ElemCount, 1, pcmd->IdxOffset + global_idx_offset, (int32_t) (pcmd->VtxOffset + global_vtx_offset), 0}; - - r_payload.DrawDataIndex++; - } - } - } - global_idx_offset += cmd_list->IdxBuffer.Size; - global_vtx_offset += cmd_list->VtxBuffer.Size; - } - } -} // namespace ZEngine::Rendering::Renderers diff --git a/ZEngine/ZEngine/Rendering/Renderers/ImGUIRenderer.h b/ZEngine/ZEngine/Rendering/Renderers/ImGUIRenderer.h deleted file mode 100644 index d5ab45ca0..000000000 --- a/ZEngine/ZEngine/Rendering/Renderers/ImGUIRenderer.h +++ /dev/null @@ -1,35 +0,0 @@ -#pragma once -#include -#include -#include -#include - -namespace ZEngine::Rendering::Renderers -{ - struct PushConstantData - { - float Scale[2] = {0}; - float Translate[2] = {0}; - uint32_t TextureId = 0xFFFFFFFFu; - uint32_t padding = 0xFFFFFFFFu; - }; - - struct ImGUIRenderer : public IRenderer - { - - static constexpr uint32_t FRAMES_IN_FLIGHT = 3; - RenderPasses::RenderPass* UIPass = nullptr; - Core::Memory::BufferView VBHandles[FRAMES_IN_FLIGHT] = {}; - Core::Memory::BufferView IdxBHandles[FRAMES_IN_FLIGHT] = {}; - Rendering::Textures::TextureHandle FontTexture = {}; - - void Initialize(Hardwares::VulkanDevicePtr device) override; - void Deinitialize() override; - - void NewFrame(); - void EndFrame(); - void PreparePayload(RenderOverlayPayload& payload); - }; - - ZDEFINE_PTR(ImGUIRenderer); -} // namespace ZEngine::Rendering::Renderers diff --git a/ZEngine/ZEngine/Rendering/Renderers/Pipelines/RendererPipeline.cpp b/ZEngine/ZEngine/Rendering/Renderers/Pipelines/RendererPipeline.cpp index 5d6a74f1b..d12fd2c50 100644 --- a/ZEngine/ZEngine/Rendering/Renderers/Pipelines/RendererPipeline.cpp +++ b/ZEngine/ZEngine/Rendering/Renderers/Pipelines/RendererPipeline.cpp @@ -54,7 +54,10 @@ namespace ZEngine::Rendering::Renderers::Pipelines * Vertex Input */ Array vertex_input_bindings = {}; - vertex_input_bindings.init(scratch.Arena, 5); + { + uint32_t n = Specification.VertexInputBindingSpecifications.size(); + vertex_input_bindings.init(scratch.Arena, n > 0 ? n : 1); + } for (unsigned i = 0; i < Specification.VertexInputBindingSpecifications.size(); ++i) { auto& input = Specification.VertexInputBindingSpecifications[i]; @@ -62,7 +65,10 @@ namespace ZEngine::Rendering::Renderers::Pipelines } Array vertex_input_attributes = {}; - vertex_input_attributes.init(scratch.Arena, 5); + { + uint32_t n = Specification.VertexInputAttributeSpecifications.size(); + vertex_input_attributes.init(scratch.Arena, n > 0 ? n : 1); + } for (unsigned i = 0; i < Specification.VertexInputAttributeSpecifications.size(); ++i) { auto& input = Specification.VertexInputAttributeSpecifications[i]; diff --git a/ZEngine/ZEngine/Rendering/Renderers/ZUIRenderer.cpp b/ZEngine/ZEngine/Rendering/Renderers/ZUIRenderer.cpp new file mode 100644 index 000000000..c796333d9 --- /dev/null +++ b/ZEngine/ZEngine/Rendering/Renderers/ZUIRenderer.cpp @@ -0,0 +1,561 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace ZEngine::Core::Memory; +using namespace ZEngine::UI; +using namespace ZEngine::Rendering::Specifications; + +namespace ZEngine::Rendering::Renderers +{ + // --------------------------------------------------------------- + // Initialize / Deinitialize + // --------------------------------------------------------------- + + void ZUIRenderer::Initialize(Hardwares::VulkanDevicePtr device) + { + Device = device; + RenderGraph = ZPushStructCtorArgs(Device->Arena, Renderers::RenderGraph); + RenderGraph->Initialize(Device); + + // Per-frame vertex + index buffers + // GPU buffer sizes — must match or exceed the draw list's max capacity. + // GrowVtx doubles the CPU buffer when exceeded; if CPU > GPU the upload + // overflows and corrupts GPU memory (observed as flickering triangles). + // 65536 vertices × 20 bytes = 1.3 MB/frame + // 131072 indices × 2 bytes = 256 KB/frame + static constexpr uint32_t kMaxVtx = 65536; + static constexpr uint32_t kMaxIdx = 131072; + for (uint32_t i = 0; i < FRAMES_IN_FLIGHT; ++i) + { + VtxBHandles[i] = Device->GpuMem.AllocateBuffer(sizeof(ZUIDrawVtx) * kMaxVtx, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, GpuMemoryDomain::HostUniform, "ZUIVertexBuffer"); + IdxBHandles[i] = Device->GpuMem.AllocateBuffer(sizeof(uint16_t) * kMaxIdx, VK_BUFFER_USAGE_INDEX_BUFFER_BIT, GpuMemoryDomain::HostUniform, "ZUIIndexBuffer"); + } + + // Pipeline: vertex-rate, 3 attributes (pos=R32G32, uv=R32G32, col=R8G8B8A8_UNORM) + // Col is packed uint32 RGBA8 — hardware unpacks to vec4 automatically. + auto pass_builder = RenderGraph->RenderPassBuilder; + pass_builder->SetName("ZUI Draw Pass") + .SetPipelineName("ZUI-Draw-Pipeline") + .EnablePipelineBlending(true) + .SetInputBindingCount(1) + .SetStride(0, (uint32_t) sizeof(ZUIDrawVtx)) + .SetRate(0, VK_VERTEX_INPUT_RATE_VERTEX) + + .SetInputAttributeCount(3) + // location 0: position (x, y) + .SetLocation(0, 0) + .SetBinding(0, 0) + .SetFormat(0, ImageFormat::R32G32_SFLOAT) + .SetOffset(0, (uint32_t) offsetof(ZUIDrawVtx, x)) + // location 1: UV (u, v) + .SetLocation(1, 1) + .SetBinding(1, 0) + .SetFormat(1, ImageFormat::R32G32_SFLOAT) + .SetOffset(1, (uint32_t) offsetof(ZUIDrawVtx, u)) + // location 2: color (RGBA8 UNORM packed uint32) + .SetLocation(2, 2) + .SetBinding(2, 0) + .SetFormat(2, ImageFormat::R8G8B8A8_UNORM) + .SetOffset(2, (uint32_t) offsetof(ZUIDrawVtx, col)) + + .UseShader("zui_draw") + .UseSwapchainAsRenderTarget(); + + DrawPass = Device->CreateRenderPass(pass_builder->Detach()); + DrawPass->UseTextureArray("TextureArray"); + DrawPass->SetSampler("LinearClampSampler", Device->GlobalLinearClampToEdgeSamplerImageInfo); + DrawPass->Verify(); + DrawPass->Bake(); + } + + void ZUIRenderer::Deinitialize() + { + RenderGraph->Dispose(); + if (DrawPass) + DrawPass->Dispose(); + for (uint32_t i = 0; i < FRAMES_IN_FLIGHT; ++i) + { + if (VtxBHandles[i]) + Device->GpuMem.FreeBuffer(VtxBHandles[i]); + if (IdxBHandles[i]) + Device->GpuMem.FreeBuffer(IdxBHandles[i]); + } + } + + // --------------------------------------------------------------- + // PreparePayload — walk box tree, emit draw list + // --------------------------------------------------------------- + + void ZUIRenderer::PreparePayload(UI::ZUIContext* ctx, ZUIRenderPayload* out, Core::Memory::ArenaAllocator* payload_arena) + { + if (!ctx || !ctx->Root || !out || !payload_arena) + { + return; + } + + // Match GPU buffer sizes exactly — GrowVtx can expand CPU buffers; + // if CPU > GPU at upload time we get memory corruption (flickering). + const uint32_t kMaxVtx = 65536; + const uint32_t kMaxIdx = 131072; + const uint32_t max_boxes = ctx->MaxBoxesPerFrame; + + float fb_w = ctx->ScreenW > 0 ? (float) ctx->ScreenW : (float) Device->SwapchainPtr->SwapchainImageWidth; + float fb_h = ctx->ScreenH > 0 ? (float) ctx->ScreenH : (float) Device->SwapchainPtr->SwapchainImageHeight; + + out->Scale[0] = 2.f / fb_w; + out->Scale[1] = 2.f / fb_h; + out->Translate[0] = -1.f; + out->Translate[1] = -1.f; + out->FramebufferScale = ctx->UIScale > 0.f ? ctx->UIScale : 1.f; + + uint32_t atlas_idx = ctx->Atlas ? ctx->Atlas->Handle.Index : 0; + float wu = ctx->Atlas ? ctx->Atlas->WhiteU : 0.f; + float wv = ctx->Atlas ? ctx->Atlas->WhiteV : 0.f; + + // Init draw list into payload_arena + ZUIDrawListInit(&ctx->DrawList, payload_arena, kMaxVtx, kMaxIdx, wu, wv, atlas_idx); + ZUIDrawListPushClipRect(&ctx->DrawList, 0.f, 0.f, fb_w, fb_h, false); + + // --------------------------------------------------------------- + // Helpers + // --------------------------------------------------------------- + + auto PackBoxColor = [](const float c[4][4], bool& all_same) -> uint32_t { + all_same = true; + for (int k = 1; k < 4; ++k) + for (int ch = 0; ch < 4; ++ch) + if (c[k][ch] != c[0][ch]) + { + all_same = false; + break; + } + return ZUIPackColor(c[0]); + }; + + auto CornersRadius = [](const float r[4]) -> float { + float mx = r[0]; + for (int i = 1; i < 4; ++i) + if (r[i] > mx) + mx = r[i]; + return mx; + }; + + auto IsAncestor = [](const ZUIBox* a, const ZUIBox* b) -> bool { + for (const ZUIBox* p = b->Parent; p; p = p->Parent) + if (p == a) + return true; + return false; + }; + + // --------------------------------------------------------------- + // Clip stack (same ancestor-based approach as the old renderer) + // --------------------------------------------------------------- + static constexpr uint32_t kClipDepth = 8; + const ZUIBox* clip_stack[kClipDepth] = {}; + uint32_t clip_top = 0; + + auto PushBoxClip = [&](const ZUIBox* box) { + if (clip_top < kClipDepth) + { + clip_stack[clip_top++] = box; + float x0 = box->ScreenMin[0], y0 = box->ScreenMin[1]; + float x1 = box->ScreenMax[0], y1 = box->ScreenMax[1]; + ZUIDrawListPushClipRect(&ctx->DrawList, x0, y0, x1, y1, true); + } + }; + auto PopToAncestor = [&](const ZUIBox* box) { + while (clip_top > 0 && !IsAncestor(clip_stack[clip_top - 1], box)) + { + --clip_top; + ZUIDrawListPopClipRect(&ctx->DrawList); + } + }; + + // --------------------------------------------------------------- + // DFS walk (identical traversal order to old PreparePayload) + // --------------------------------------------------------------- + ZUIBox** nodes = ZPushArray(&ctx->FrameArena, ZUIBox*, max_boxes); + ZUIBox** dfs_stack = ZPushArray(&ctx->FrameArena, ZUIBox*, max_boxes); + uint32_t node_count = 0, stack_top = 0; + + dfs_stack[stack_top++] = ctx->Root; + while (stack_top > 0 && node_count < max_boxes) + { + ZUIBox* box = dfs_stack[--stack_top]; + nodes[node_count++] = box; + for (ZUIBox* c = box->LastChild; c; c = c->PrevSib) + if (stack_top < max_boxes) + dfs_stack[stack_top++] = c; + } + + for (uint32_t i = 0; i < node_count; ++i) + { + ZUIBox* box = nodes[i]; + float bx0 = box->ScreenMin[0], by0 = box->ScreenMin[1]; + float bx1 = box->ScreenMax[0], by1 = box->ScreenMax[1]; + + // Maintain clip stack + PopToAncestor(box); + if (box->Flags & ZUI_ClipChildren) + PushBoxClip(box); + if (bx1 <= bx0 || by1 <= by0) + { + continue; + } // zero-area + + float cr = CornersRadius(box->CornerRadii); + + // --- Drop shadow (emitted first, renders behind everything) --- + if (box->Flags & ZUI_DropShadow) + { + float offset = ctx->Style.DropShadowOffset; + uint32_t scol = ZUIPackColor(0.f, 0.f, 0.f, ctx->Style.DropShadowAlpha); + ZUIDrawListAddRectFilled(&ctx->DrawList, bx0 + offset, by0 + offset, bx1 + offset, by1 + offset, scol, cr); + } + + // Per-corner round_flags from CornerRadii — allows top-only, bottom-only, etc. + // ZUIBox index→PathRect bit: TL=0→0x1, TR=1→0x2, BR=3→0x4, BL=2→0x8 + uint32_t rf = 0; + if (box->CornerRadii[0] > 0.f) + rf |= 0x1; // TL + if (box->CornerRadii[1] > 0.f) + rf |= 0x2; // TR + if (box->CornerRadii[3] > 0.f) + rf |= 0x4; // BR + if (box->CornerRadii[2] > 0.f) + rf |= 0x8; // BL + if (rf == 0 && cr > 0.f) + rf = 0xF; // fallback if all are equal nonzero + + // --- Hover overlay for clickable boxes without background --- + if ((box->Flags & ZUI_Clickable) && !(box->Flags & ZUI_DrawBackground)) + { + auto* ps = ZUIStateGetOrInsert(&ctx->StateStore, box->Key); + if (ps && ps->HotT > 0.01f) + { + uint32_t oc = ZUIPackColor(0.5f, 0.5f, 0.56f, ps->HotT * ctx->Style.HoverOverlayAlpha); + ZUIDrawListAddRectFilled(&ctx->DrawList, bx0, by0, bx1, by1, oc, cr, rf); + } + } + + // --- Background --- + if (box->Flags & ZUI_DrawBackground) + { + bool all_same = true; + uint32_t col0 = PackBoxColor(box->Colors, all_same); + + if (box->TextureIndex != 0xFFFFFFFFu) + { + ZUIDrawListAddImage(&ctx->DrawList, box->TextureIndex, bx0, by0, bx1, by1, 0.f, 0.f, 1.f, 1.f, ZUIPackColor(1.f, 1.f, 1.f, 1.f)); + } + else if (all_same) + { + ZUIDrawListAddRectFilled(&ctx->DrawList, bx0, by0, bx1, by1, col0, cr, rf); + } + else + { + // Gradient quad — flat (no rounding), per-corner colors + ZUIDrawListAddRectFilledMultiColor( + &ctx->DrawList, + bx0, + by0, + bx1, + by1, + ZUIPackColor(box->Colors[0]), // TL + ZUIPackColor(box->Colors[1]), // TR + ZUIPackColor(box->Colors[2]), // BL + ZUIPackColor(box->Colors[3])); // BR + } + } + + // --- Border --- + if ((box->Flags & ZUI_DrawBorder) && box->BorderThickness > 0.f) + { + uint32_t bcol = ZUIPackColor(box->BorderColor); + if ((bcol >> 24) > 2) + ZUIDrawListAddRect(&ctx->DrawList, bx0, by0, bx1, by1, bcol, cr, rf ? rf : 0xF, box->BorderThickness); + } + + // --- Text --- + if ((box->Flags & ZUI_DrawText) && box->Label.Ptr && ctx->GetFont(box->FontSize)) + { + const ZUIFont* font = ctx->GetFont(box->FontSize); + float fs = font->FontScale > 0.f ? font->FontScale : 1.f; + float lh = font->LineHeight * fs; + float box_h = by1 - by0; + float text_top = floorf(by0 + (box_h - lh) * 0.5f); + float baseline = text_top + font->Ascent * fs; + float indent = box->Padding[0] > 0.f ? box->Padding[0] : ctx->Style.FramePadding[0]; + float cx = floorf(bx0 + indent); + + if (box->TextAlign != ZUITextAlign::Left) + { + float ts[2] = {0.f, 0.f}; + ZUIMeasureText(font, box->Label.Ptr, box->Label.Len, ts); + if (box->TextAlign == ZUITextAlign::Center) + cx = floorf(bx0 + ((bx1 - bx0) - ts[0]) * 0.5f); + else + cx = floorf(bx1 - ts[0] - ctx->Style.FramePadding[0]); + } + + uint32_t text_col = ZUIPackColor(box->TextColor); + + for (uint32_t ci = 0; ci < box->Label.Len; ++ci) + { + uint32_t cp = (uint8_t) box->Label.Ptr[ci]; + uint32_t idx = cp - font->FirstCodepoint; + if (cp < font->FirstCodepoint || idx >= font->GlyphCount) + { + continue; + } + + const ZUIGlyph& g = font->Glyphs[idx]; + float gx0 = cx + g.OffsetX * fs; + float gy0 = baseline + g.OffsetY * fs; + float gx1 = gx0 + g.Width * fs; + float gy1 = gy0 + g.Height * fs; + + // Pass raw subpixel coords — OversampleH=3 atlas has sub-pixel columns + // that the GPU bilinear filter selects. floorf() here wastes the entire + // oversampling benefit (matched against ImGui RenderText lines 5940-5943). + ZUIDrawListAddImage(&ctx->DrawList, atlas_idx, gx0, gy0, gx1, gy1, g.U0, g.V0, g.U1, g.V1, text_col); + cx += g.AdvanceX * fs; + } + } + + // --- Checkmark (✓ polyline stroke) --- + if (box->Flags & ZUI_DrawCheckmark) + { + float w = bx1 - bx0, h = by1 - by0; + // Three-point tick: (25%,55%) → (42%,75%) → (75%,28%) + float pts_x[3] = {bx0 + w * 0.20f, bx0 + w * 0.42f, bx0 + w * 0.78f}; + float pts_y[3] = {by0 + h * 0.52f, by0 + h * 0.76f, by0 + h * 0.24f}; + uint32_t cc = ZUIPackColor(box->TextColor); + float thick = (w < 14.f ? 1.5f : 2.0f); + ZUIDrawListAddLine(&ctx->DrawList, pts_x[0], pts_y[0], pts_x[1], pts_y[1], cc, thick); + ZUIDrawListAddLine(&ctx->DrawList, pts_x[1], pts_y[1], pts_x[2], pts_y[2], cc, thick); + } + + // --- Circle fill (inscribed in box center) --- + if (box->Flags & ZUI_DrawCircleFill) + { + float cx = (bx0 + bx1) * 0.5f; + float cy = (by0 + by1) * 0.5f; + float r = ((bx1 - bx0) < (by1 - by0) ? (bx1 - bx0) : (by1 - by0)) * 0.32f; + uint32_t cc = ZUIPackColor(box->TextColor); + ZUIDrawListAddCircleFilled(&ctx->DrawList, cx, cy, r, cc); + } + + // --- Plot lines (ZUIPlotLines) --- + if ((box->Flags & ZUI_DrawPlotLines) && box->Label.Ptr && box->Label.Len >= 2) + { + const float* data = (const float*) box->Label.Ptr; + int n = (int) box->Label.Len; + float v_min = box->Padding[0]; + float v_max = box->Padding[2]; + float range = v_max - v_min; + if (range < 1e-6f) + range = 1.f; + float pw = bx1 - bx0, ph = by1 - by0; + uint32_t pcol = ZUIPackColor(ctx->Theme.PlotLines); + // Emit line segments (n-1 segments for n data points) + float prev_x = bx0; + float v0 = (data[0] - v_min) / range; + if (v0 < 0.f) + v0 = 0.f; + if (v0 > 1.f) + v0 = 1.f; + float prev_y = by1 - v0 * ph; + for (int i = 1; i < n; ++i) + { + float t = (float) i / (float) (n - 1); + float v = (data[i] - v_min) / range; + if (v < 0.f) + v = 0.f; + if (v > 1.f) + v = 1.f; + float cx = bx0 + t * pw; + float cy = by1 - v * ph; + ZUIDrawListAddLine(&ctx->DrawList, prev_x, prev_y, cx, cy, pcol, 1.5f); + prev_x = cx; + prev_y = cy; + } + } + + // --- Plot histogram (ZUIPlotHistogram) --- + if ((box->Flags & ZUI_DrawPlotBars) && box->Label.Ptr && box->Label.Len >= 1) + { + const float* data = (const float*) box->Label.Ptr; + int n = (int) box->Label.Len; + float v_min = box->Padding[0]; + float v_max = box->Padding[2]; + float range = v_max - v_min; + if (range < 1e-6f) + range = 1.f; + float pw = bx1 - bx0, ph = by1 - by0; + float bar_w = pw / (float) n; + uint32_t pcol = ZUIPackColor(ctx->Theme.PlotHistogram); + for (int i = 0; i < n; ++i) + { + float v = (data[i] - v_min) / range; + if (v < 0.f) + v = 0.f; + if (v > 1.f) + v = 1.f; + float x0 = bx0 + (float) i * bar_w + 1.f; + float x1 = x0 + bar_w - 2.f; + float y0 = by1 - v * ph; + ZUIDrawListAddRectFilled(&ctx->DrawList, x0, y0, x1, by1, pcol, 0.f); + } + } + + // --- Triangle arrow (collapse indicator) --- + // Geometry matches ImGui RenderArrow() exactly: + // r = FontSize * 0.40 (5.2px at FontSize=13) + // Down ▼: a=(0,0.75)*r b=(-0.866,-0.75)*r c=(0.866,-0.75)*r + // Right ►: a=(0.75,0)*r b=(-0.75,0.866)*r c=(-0.75,-0.866)*r + // We scale r from box height so the arrow is proportional to the row. + if (box->Flags & ZUI_DrawTriArrow) + { + auto* ps = ZUIStateGetOrInsert(&ctx->StateStore, box->Key); + float udata = ps ? ps->UserData : 0.f; + float cx = (bx0 + bx1) * 0.5f; + float cy = (by0 + by1) * 0.5f; + uint32_t cc = ZUIPackColor(box->TextColor); + float fs = ctx->Style.FontSize; + + if (udata > 1.5f) + { + float bw = bx1 - bx0; + float bh = by1 - by0; + // 90° opening angle: hw = 2 × hh → cos(θ)=0 → arms at 45° each + // hh derived from the shorter box dimension so the chevron stays compact + float hh = fminf(bw, bh) * 0.185f; + float hw = hh * 2.0f; + if (udata < 2.5f) + ZUIDrawListAddChevronDown(&ctx->DrawList, cx, cy, hw, hh, cc, 1.0f); + else + ZUIDrawListAddChevronRight(&ctx->DrawList, cx, cy, hw, hh, cc, 1.0f); + } + else + { + // Filled equilateral triangle (tree nodes, collapsing headers) + float r = (by1 - by0) * (0.40f * 13.f / 19.f); + bool down = udata > 0.5f; + if (down) + { + // ▼ Down + ZUIDrawListAddTriangleFilled(&ctx->DrawList, cx + 0.000f * r, cy + 0.750f * r, cx - 0.866f * r, cy - 0.750f * r, cx + 0.866f * r, cy - 0.750f * r, cc); + } + else + { + // ► Right + ZUIDrawListAddTriangleFilled(&ctx->DrawList, cx + 0.750f * r, cy + 0.000f * r, cx - 0.750f * r, cy + 0.866f * r, cx - 0.750f * r, cy - 0.866f * r, cc); + } + } + } + } + + // Pop remaining clip rects + while (clip_top > 0) + { + --clip_top; + ZUIDrawListPopClipRect(&ctx->DrawList); + } + + // Fill payload from draw list + out->Vtx = ctx->DrawList.Vtx; + out->VtxCount = ctx->DrawList.VtxCount; + out->Idx = ctx->DrawList.Idx; + out->IdxCount = ctx->DrawList.IdxCount; + out->Cmds = ctx->DrawList.Cmds; + out->CmdCount = ctx->DrawList.CmdCount; + } + + // --------------------------------------------------------------- + // Submit + // --------------------------------------------------------------- + + void ZUIRenderer::Submit(Hardwares::CommandBuffer* primary_cmd, const ZUIRenderPayload& payload) + { + if (payload.VtxCount == 0 || payload.CmdCount == 0) + { + return; + } + + auto swapchain = Device->SwapchainPtr; + auto frame_index = swapchain->CurrentFrame->Index; + auto current_fb = swapchain->SwapchainFramebuffers[swapchain->CurrentFrame->ImageIndex]; + uint32_t fi = frame_index % FRAMES_IN_FLIGHT; + + auto* rrm = Device->RRM ? reinterpret_cast(Device->RRM) : nullptr; + if (!rrm) + { + return; + } + + // Clamp to GPU buffer capacity (65536 vtx, 131072 idx) — safety net + static constexpr uint32_t kVtxCap = 65536; + static constexpr uint32_t kIdxCap = 131072; + uint32_t vtx_upload = (payload.VtxCount > kVtxCap) ? kVtxCap : payload.VtxCount; + uint32_t idx_upload = (payload.IdxCount > kIdxCap) ? kIdxCap : payload.IdxCount; + + rrm->UpdateBuffer(VtxBHandles[fi], payload.Vtx, vtx_upload * sizeof(ZUIDrawVtx)); + rrm->UpdateBuffer(IdxBHandles[fi], payload.Idx, idx_upload * sizeof(uint16_t)); + + primary_cmd->BeginRenderPass(DrawPass, current_fb, true); + { + auto secondary_cb = Device->CommandBufferMgr->GetCommandBuffer(Rendering::QueueType::GRAPHIC_QUEUE, frame_index, 0, ZUICommandBufferIndex, false); + secondary_cb->ResetState(); + secondary_cb->BeginSecondary(DrawPass, current_fb); + secondary_cb->SetViewport(DrawPass->GetRenderAreaWidth(), DrawPass->GetRenderAreaHeight()); + secondary_cb->BindPipeline(Specifications::PipelineBindPoint::GRAPHIC, DrawPass->Pipeline); + secondary_cb->BindVertexBuffer(VtxBHandles[fi]); + secondary_cb->BindIndexBuffer(IdxBHandles[fi], VK_INDEX_TYPE_UINT16); + + float fs = payload.FramebufferScale; + + for (uint32_t i = 0; i < payload.CmdCount; ++i) + { + const ZUIDrawListCmd& cmd = payload.Cmds[i]; + if (cmd.ElemCount == 0) + { + continue; + } + // Skip commands that reference indices beyond the clamped upload range + if (cmd.IdxOffset + cmd.ElemCount > idx_upload) + { + continue; + } + + // Logical → physical pixel scissor + secondary_cb->SetScissor((uint32_t) (cmd.ClipW * fs), (uint32_t) (cmd.ClipH * fs), (int32_t) (cmd.ClipX * fs), (int32_t) (cmd.ClipY * fs)); + + ZUIDrawPushConstant pc = {}; + pc.Scale[0] = payload.Scale[0]; + pc.Scale[1] = payload.Scale[1]; + pc.Translate[0] = payload.Translate[0]; + pc.Translate[1] = payload.Translate[1]; + pc.TexIdx = cmd.TexIdx; + pc.FbScale = payload.FramebufferScale; + + secondary_cb->PushConstants(VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(ZUIDrawPushConstant), &pc); + secondary_cb->BindDescriptorSets(frame_index); + secondary_cb->DrawIndexed(cmd.ElemCount, 1, cmd.IdxOffset, 0, 0); + } + + secondary_cb->End(); + Core::Containers::ArrayView cbs{secondary_cb, 1}; + primary_cmd->ExecuteSecondaryCommandBuffers(cbs); + } + primary_cmd->EndRenderPass(); + } + +} // namespace ZEngine::Rendering::Renderers diff --git a/ZEngine/ZEngine/Rendering/Renderers/ZUIRenderer.h b/ZEngine/ZEngine/Rendering/Renderers/ZUIRenderer.h new file mode 100644 index 000000000..db9c27986 --- /dev/null +++ b/ZEngine/ZEngine/Rendering/Renderers/ZUIRenderer.h @@ -0,0 +1,63 @@ +#pragma once +#include +#include +#include +#include +#include + +namespace ZEngine::UI +{ + struct ZUIContext; +} + +namespace ZEngine::Rendering::Renderers +{ + // --------------------------------------------------------------- + // ZUIRenderPayload — draw-list backed (replaces ZUIRectInst approach) + // --------------------------------------------------------------- + struct ZUIRenderPayload + { + UI::ZUIDrawVtx* Vtx = nullptr; + uint32_t VtxCount = 0; + uint16_t* Idx = nullptr; + uint32_t IdxCount = 0; + UI::ZUIDrawListCmd* Cmds = nullptr; + uint32_t CmdCount = 0; + float FramebufferScale = 1.f; // physical / logical scale for scissor + float Scale[2] = {}; // NDC scale (2/ScreenW, 2/ScreenH) + float Translate[2] = {}; // NDC offset (-1, -1) + }; + + // Push constant shared with zui_draw.vert + struct ZUIDrawPushConstant + { + float Scale[2] = {}; + float Translate[2] = {}; + uint32_t TexIdx = 0; + float FbScale = 1.f; // matches uFbScale in zui_draw.vert + }; + + struct ZUIRenderer : public IRenderer + { + static constexpr uint32_t FRAMES_IN_FLIGHT = 3; + static constexpr uint32_t ZUICommandBufferIndex = 1; + + RenderPasses::RenderPass* DrawPass = nullptr; // zui_draw pipeline + + // Per-frame vertex + index buffers + Core::Memory::BufferView VtxBHandles[FRAMES_IN_FLIGHT] = {}; + Core::Memory::BufferView IdxBHandles[FRAMES_IN_FLIGHT] = {}; + + void Initialize(Hardwares::VulkanDevicePtr device) override; + void Deinitialize() override; + + // Translate the ZUIBox tree into a flat ZUIRenderPayload. + void PreparePayload(UI::ZUIContext* ctx, ZUIRenderPayload* out, Core::Memory::ArenaAllocator* payload_arena); + + // Submit to Vulkan. + void Submit(Hardwares::CommandBuffer* primary_cmd, const ZUIRenderPayload& payload); + }; + + ZDEFINE_PTR(ZUIRenderer); + +} // namespace ZEngine::Rendering::Renderers diff --git a/ZEngine/ZEngine/UI/ZUIBox.h b/ZEngine/ZEngine/UI/ZUIBox.h new file mode 100644 index 000000000..8989edbd3 --- /dev/null +++ b/ZEngine/ZEngine/UI/ZUIBox.h @@ -0,0 +1,170 @@ +#pragma once +#include +#include +#include + +namespace ZEngine::UI +{ + struct ZUIStr + { + const char* Ptr = nullptr; + uint32_t Len = 0; + }; + + enum class ZUISizeKind : uint8_t + { + Pixels, + Text, + ChildrenSum, + ParentPercent, + Fill + }; + + // Strictness: 1.0 = rigid (never shrunk), 0.0 = fully flexible (absorbed first). + struct ZUISize + { + ZUISizeKind Kind = ZUISizeKind::Pixels; + float Value = 0.f; + float Strictness = 1.f; + }; + + enum ZUIBoxFlags : uint32_t + { + ZUI_None = 0, + ZUI_DrawBackground = 1 << 0, + ZUI_DrawBorder = 1 << 1, + ZUI_DrawText = 1 << 2, + ZUI_Clickable = 1 << 3, + ZUI_Scrollable = 1 << 4, + ZUI_ClipChildren = 1 << 5, + ZUI_FloatX = 1 << 6, + ZUI_FloatY = 1 << 7, + // Draw-list shape overlays — rendered by PreparePayload after background/border. + // All use TextColor as the stroke/fill color. + ZUI_DrawCheckmark = 1 << 8, // ✓ polyline stroke inside the box + ZUI_DrawCircleFill = 1 << 9, // filled circle inscribed in box center + ZUI_DrawTriArrow = 1 << 10, // collapse arrow; direction from UserData (0=right, 1=down) + ZUI_DropShadow = 1 << 11, // dark offset rect emitted behind background + ZUI_DrawPlotLines = 1 << 12, // line chart; data in Label.Ptr/Len, range in Padding + ZUI_DrawPlotBars = 1 << 13, // bar chart; same data layout as DrawPlotLines + }; + + inline ZUIBoxFlags operator|(ZUIBoxFlags a, ZUIBoxFlags b) + { + return static_cast(static_cast(a) | static_cast(b)); + } + + enum class ZUIAxis : uint8_t + { + X, + Y + }; + enum class ZUITextAlign : uint8_t + { + Left = 0, + Center, + Right + }; + enum class ZUIFontSize : uint8_t + { + Small = 0, + Body = 1, + Header = 2 + }; + + // Corner order: [0]=TL [1]=TR [2]=BL [3]=BR + // Matches gl_VertexID order in the SDF vertex shader. + static constexpr int ZUI_CORNER_TL = 0; + static constexpr int ZUI_CORNER_TR = 1; + static constexpr int ZUI_CORNER_BL = 2; + static constexpr int ZUI_CORNER_BR = 3; + + struct ZUIBox + { + uint64_t Key = 0; + ZUIStr Label = {}; + + // tree — arena pointers valid for one frame only + ZUIBox* Parent = nullptr; + ZUIBox* FirstChild = nullptr; + ZUIBox* LastChild = nullptr; + ZUIBox* NextSib = nullptr; + ZUIBox* PrevSib = nullptr; + + // build-time spec + ZUIBoxFlags Flags = ZUI_None; + ZUISize Size[2] = {}; + ZUIAxis LayoutAxis = ZUIAxis::Y; + ZUITextAlign TextAlign = ZUITextAlign::Left; + ZUIFontSize FontSize = ZUIFontSize::Body; + + // Per-corner colors (RAD approach). + // Colors[0]=TL [1]=TR [2]=BL [3]=BR, each RGBA float[4]. + // Use ZUIBoxSetColor / ZUIBoxSetGradientV helpers below. + float Colors[4][4] = {}; // all transparent by default + float TextColor[4] = {}; + float BorderColor[4] = {}; + + // Per-corner radii + edge softness (RAD SDF renderer). + // CornerRadii[0]=TL [1]=TR [2]=BL [3]=BR. + // EdgeSoftness: AA ramp width in pixels (0.5 = 1 pixel, 0 = hard edge). + float CornerRadii[4] = {}; + float EdgeSoftness = 0.5f; + + float BorderThickness = 0.f; + float FloatPos[2] = {}; + float Padding[4] = {}; // left, top, right, bottom + uint32_t TextureIndex = 0xFFFFFFFFu; + + // layout output — filled by ZUILayout::Solve + float ComputedSize[2] = {}; + float ScreenMin[2] = {}; + float ScreenMax[2] = {}; + }; + + // Inline helpers — set colors and radii in a single call + + inline void ZUIBoxSetColor(ZUIBox* b, float r, float g, float bl, float a) + { + for (int i = 0; i < 4; ++i) + { + b->Colors[i][0] = r; + b->Colors[i][1] = g; + b->Colors[i][2] = bl; + b->Colors[i][3] = a; + } + } + + inline void ZUIBoxSetColorArr(ZUIBox* b, const float c[4]) + { + ZUIBoxSetColor(b, c[0], c[1], c[2], c[3]); + } + + // Vertical gradient: top row gets `top`, bottom row gets `bot`. + inline void ZUIBoxSetGradientV(ZUIBox* b, const float top[4], const float bot[4]) + { + for (int ch = 0; ch < 4; ++ch) + { + b->Colors[ZUI_CORNER_TL][ch] = top[ch]; + b->Colors[ZUI_CORNER_TR][ch] = top[ch]; + b->Colors[ZUI_CORNER_BL][ch] = bot[ch]; + b->Colors[ZUI_CORNER_BR][ch] = bot[ch]; + } + } + + inline void ZUIBoxSetCornerRadius(ZUIBox* b, float r) + { + b->CornerRadii[0] = b->CornerRadii[1] = b->CornerRadii[2] = b->CornerRadii[3] = r; + } + + inline void ZUIBoxSetTopRadius(ZUIBox* b, float r) + { + b->CornerRadii[ZUI_CORNER_TL] = b->CornerRadii[ZUI_CORNER_TR] = r; + } + + inline void ZUIBoxSetBottomRadius(ZUIBox* b, float r) + { + b->CornerRadii[ZUI_CORNER_BL] = b->CornerRadii[ZUI_CORNER_BR] = r; + } + +} // namespace ZEngine::UI diff --git a/ZEngine/ZEngine/UI/ZUIContext.cpp b/ZEngine/ZEngine/UI/ZUIContext.cpp new file mode 100644 index 000000000..0805262c5 --- /dev/null +++ b/ZEngine/ZEngine/UI/ZUIContext.cpp @@ -0,0 +1,377 @@ +#include +#include +#include +#include + +namespace ZEngine::UI +{ + using namespace ZEngine::Core::Memory; + + void ZUIContextInit(ZUIContext* ctx, ArenaAllocator* parent, size_t FrameArenaBytes, size_t PersistentArenaBytes, uint32_t StateCapacity, uint32_t MaxBoxesPerFrame) + { + parent->CreateSubArena(FrameArenaBytes, &ctx->FrameArena); + parent->CreateSubArena(PersistentArenaBytes, &ctx->PersistentArena); + + ctx->StateStore.Slots = ZPushArray(&ctx->PersistentArena, ZUIPersistentSlot, StateCapacity); + ctx->StateStore.Capacity = StateCapacity; + ctx->StateStore.Count = 0; + ctx->MaxBoxesPerFrame = MaxBoxesPerFrame; + } + + void ZUIContextDestroy(ZUIContext* ctx) + { + ctx->FrameArena.Shutdown(); + ctx->PersistentArena.Shutdown(); + } + + void ZUIBeginFrame(ZUIContext* ctx, float dt) + { + ZUIStyleUpdate(&ctx->Style); // recompute FrameHeight = FontSize + FramePadding.y*2 + ctx->FrameArena.Clear(); + ctx->Root = nullptr; + ctx->Current = nullptr; + ctx->DeltaTime = dt; + ctx->Time += dt; + ctx->ResizeCursor = 0; + ctx->PopupBuildDepth = 0; // reset render depth; rebuilt during each BuildUI pass + // Clear stale popup Box* pointers — boxes are re-created each frame in FrameArena + for (uint32_t i = 0; i < ctx->PopupStackSize; i++) + ctx->PopupStack[i].Box = nullptr; + // TextInputLen, BackspacePressed, MousePressed/Released, ScrollDelta are NOT + // cleared here — GLFW events fire before BeginFrame (in window->PollEvent) and + // must survive until ZUIEndFrame runs the interaction pass and widget logic. + } + + void ZUIEndFrame(ZUIContext* ctx) + { + // Clear drop result from the previous frame before the interaction pass may set a new one + ctx->DragDropFired = false; + ctx->DragTargetKey = 0; + // ViewportHovered is written fresh each BuildUI; reset it so a missing panel = false + ctx->ViewportHovered = false; + + ZUILayoutSolve(ctx); + ZUIInteractionPass(ctx); + + // Save mouse position before clearing edge states — used for drag delta next frame + ctx->PrevMousePos[0] = ctx->MousePos[0]; + ctx->PrevMousePos[1] = ctx->MousePos[1]; + + // Key repeat: fire BackspacePressed again when held past the delay + if (ctx->BackspaceHeld && ctx->FocusKey != 0) + { + ctx->KeyRepeatTimer += ctx->DeltaTime; + if (ctx->KeyRepeatTimer >= ZUIContext::kRepeatDelay) + { + float excess = ctx->KeyRepeatTimer - ZUIContext::kRepeatDelay; + if ((int) (excess / ZUIContext::kRepeatRate) != (int) ((excess - ctx->DeltaTime) / ZUIContext::kRepeatRate)) + { + ctx->BackspacePressed = true; // fire repeat event + } + } + } + + // Key repeat: Arrow left/right and Delete when held + bool any_arrow = ctx->ArrowLeftHeld || ctx->ArrowRightHeld || ctx->ArrowUpHeld || ctx->ArrowDownHeld || ctx->DeleteHeld; + if (any_arrow && ctx->FocusKey != 0) + { + ctx->ArrowRepeatTimer += ctx->DeltaTime; + if (ctx->ArrowRepeatTimer >= ZUIContext::kRepeatDelay) + { + float excess = ctx->ArrowRepeatTimer - ZUIContext::kRepeatDelay; + bool fire = (int) (excess / ZUIContext::kRepeatRate) != (int) ((excess - ctx->DeltaTime) / ZUIContext::kRepeatRate); + if (fire) + { + if (ctx->ArrowLeftHeld) + ctx->ArrowLeftPressed = true; + if (ctx->ArrowRightHeld) + ctx->ArrowRightPressed = true; + if (ctx->ArrowUpHeld) + ctx->ArrowUpPressed = true; + if (ctx->ArrowDownHeld) + ctx->ArrowDownPressed = true; + if (ctx->DeleteHeld) + ctx->DeletePressed = true; + } + } + } + if (!any_arrow) + ctx->ArrowRepeatTimer = 0.f; + + // Escape / Enter: clear focus + if (ctx->EscapePressed || ctx->EnterPressed) + { + ctx->FocusKey = 0; + } + ctx->EscapePressed = false; + ctx->EnterPressed = false; + ctx->SpacePressed = false; + + // Popup keyboard navigation (applies to the innermost open popup) + if (ctx->PopupStackSize > 0) + { + int count = ctx->PopupBuildCount > 0 ? ctx->PopupBuildCount : 1; + if (ctx->ArrowDownPressed) + ctx->PopupNavIdx = (ctx->PopupNavIdx + 1) % count; + if (ctx->ArrowUpPressed) + ctx->PopupNavIdx = (ctx->PopupNavIdx <= 0) ? (count - 1) : (ctx->PopupNavIdx - 1); + if (ctx->ArrowLeftPressed && ctx->PopupStackSize > 1) + { + ctx->PopupStackSize--; // close innermost submenu + ctx->PopupNavIdx = -1; + } + if (ctx->EscapePressed || ctx->TabPressed || ctx->ShiftTabPressed) + { + ctx->PopupStackSize = 0; // close all popups on escape + ctx->PopupNavIdx = -1; + } + } + + // Tab focus navigation — apply after interaction pass so click-focus wins + if (ctx->TabPressed) + { + uint64_t next = ctx->TabNavNextKey ? ctx->TabNavNextKey : ctx->TabNavFirstKey; + if (next) + { + ctx->FocusKey = next; + } + } + if (ctx->ShiftTabPressed) + { + uint64_t prev = ctx->TabNavPrevKey ? ctx->TabNavPrevKey : ctx->TabNavLastKey; + if (prev) + { + ctx->FocusKey = prev; + } + } + ctx->TabPressed = false; + ctx->ShiftTabPressed = false; + ctx->TabNavNextKey = 0; + ctx->TabNavPrevKey = 0; + ctx->TabNavFirstKey = 0; + ctx->TabNavLastKey = 0; + ctx->TabNavSeenFocus = false; + + // Clear per-frame edge states now that the interaction pass has consumed them + for (int i = 0; i < 3; ++i) + { + ctx->MousePressed[i] = false; + ctx->MouseReleased[i] = false; + } + ctx->ScrollDelta = 0.f; + ctx->TextInputLen = 0; + ctx->BackspacePressed = false; + ctx->ArrowUpPressed = false; + ctx->ArrowDownPressed = false; + ctx->ArrowLeftPressed = false; + ctx->ArrowRightPressed = false; + ctx->HomePressed = false; + ctx->EndPressed = false; + ctx->CtrlCPressed = false; + ctx->CtrlXPressed = false; + ctx->CtrlBackspacePressed = false; + ctx->CtrlAPressed = false; + ctx->CtrlZPressed = false; + ctx->CtrlYPressed = false; + ctx->DeletePressed = false; + + // Apply pending popup open — truncate stack to target depth then push. + if (ctx->PendingPopupKey != 0) + { + uint32_t depth = ctx->PendingPopupDepth; + if (depth <= ctx->PopupStackSize) // can only open at/above current depth + { + ctx->PopupStackSize = depth; // close any deeper popups + ctx->PopupStack[ctx->PopupStackSize++] = {ctx->PendingPopupKey, nullptr, nullptr, ctx->PendingPopupPosX, ctx->PendingPopupPosY}; + } + ctx->PendingPopupKey = 0; + } + } + + ZUIBox* ZUIPushBox(ZUIContext* ctx, const char* key, uint32_t key_len, ZUIBoxFlags flags) + { + ZUIBox* box = ZPushStructCtor(&ctx->FrameArena, ZUIBox); + ZENGINE_VALIDATE_ASSERT(box != nullptr, "ZUI FrameArena exhausted — increase FrameArenaBytes"); + box->Flags = flags; + + // split key on '##': part before is the visible label, full string hashes the key + const char* hash_start = key; + uint32_t label_len = key_len; + for (uint32_t i = 0; i + 1 < key_len; ++i) + { + if (key[i] == '#' && key[i + 1] == '#') + { + label_len = i; + hash_start = key; + break; + } + } + + box->Key = ZUIHashStr(hash_start, key_len); + box->Label = (label_len > 0) ? ZUIPushStr(&ctx->FrameArena, key, label_len) : ZUIStr{nullptr, 0}; + + // link into tree + if (ctx->Current) + { + box->Parent = ctx->Current; + if (ctx->Current->LastChild) + { + ctx->Current->LastChild->NextSib = box; + box->PrevSib = ctx->Current->LastChild; + } + else + { + ctx->Current->FirstChild = box; + } + ctx->Current->LastChild = box; + } + else + { + ctx->Root = box; + } + + ctx->Current = box; + return box; + } + + void ZUIPopBox(ZUIContext* ctx) + { + if (ctx->Current) + { + ctx->Current = ctx->Current->Parent; + } + } + + ZUIPersistentState* ZUIStateGetOrInsert(ZUIPersistentStore* store, uint64_t key) + { + uint32_t idx = (uint32_t) (key & (uint64_t) (store->Capacity - 1)); + for (uint32_t i = 0; i < store->Capacity; ++i) + { + uint32_t slot_idx = (idx + i) & (store->Capacity - 1); + ZUIPersistentSlot* slot = &store->Slots[slot_idx]; + if (slot->Key == 0) + { + slot->Key = key; + ++store->Count; + // Arena zero-fills memory, overriding C++ default initializers. + // Explicitly set the sentinel so first-use detection works. + slot->State.UserData = -1.f; + return &slot->State; + } + if (slot->Key == key) + { + return &slot->State; + } + } + // Table full — widgets that dereference the returned nullptr will crash. + // Increase StateCapacity in ZUIContextInit. + ZENGINE_VALIDATE_ASSERT(false, "ZUI state table full — increase StateCapacity"); + return nullptr; + } + + ZUIStr ZUIPushStr(ArenaAllocator* arena, const char* str, uint32_t len) + { + char* buf = ZPushString(arena, len + 1); + memcpy(buf, str, len); + buf[len] = '\0'; + return {buf, len}; + } + + // FNV-1a 64-bit + uint64_t ZUIHashStr(const char* str, uint32_t len) + { + uint64_t hash = 14695981039346656037ULL; + for (uint32_t i = 0; i < len; ++i) + { + hash ^= (uint8_t) str[i]; + hash *= 1099511628211ULL; + } + return hash ? hash : 1; // 0 is reserved for empty slots + } + + // ZUIStylePushFloat / ZUIStylePop + // Maps a ZUIStyleVar enum to the corresponding float in ctx->Style, + // saves the old value on the stack, writes the new value. + + static float* StyleVarToPtr(ZUIStyle* s, ZUIStyleVar var) + { + switch (var) + { + case ZUIStyleVar_Alpha: + return &s->Alpha; + case ZUIStyleVar_DisabledAlpha: + return &s->DisabledAlpha; + case ZUIStyleVar_FramePaddingX: + return &s->FramePadding[0]; + case ZUIStyleVar_FramePaddingY: + return &s->FramePadding[1]; + case ZUIStyleVar_ItemSpacingX: + return &s->ItemSpacing[0]; + case ZUIStyleVar_ItemSpacingY: + return &s->ItemSpacing[1]; + case ZUIStyleVar_ItemInnerSpacingX: + return &s->ItemInnerSpacing[0]; + case ZUIStyleVar_ItemInnerSpacingY: + return &s->ItemInnerSpacing[1]; + case ZUIStyleVar_FrameRounding: + return &s->FrameRounding; + case ZUIStyleVar_PopupRounding: + return &s->PopupRounding; + case ZUIStyleVar_ScrollbarRounding: + return &s->ScrollbarRounding; + case ZUIStyleVar_GrabRounding: + return &s->GrabRounding; + case ZUIStyleVar_TabRounding: + return &s->TabRounding; + case ZUIStyleVar_WindowBorderSize: + return &s->WindowBorderSize; + case ZUIStyleVar_FrameBorderSize: + return &s->FrameBorderSize; + case ZUIStyleVar_PopupBorderSize: + return &s->PopupBorderSize; + case ZUIStyleVar_TabBarBorderSize: + return &s->TabBarBorderSize; + case ZUIStyleVar_TabBarOverlineSize: + return &s->TabBarOverlineSize; + case ZUIStyleVar_IndentSpacing: + return &s->IndentSpacing; + case ZUIStyleVar_ScrollbarSize: + return &s->ScrollbarSize; + case ZUIStyleVar_GrabMinSize: + return &s->GrabMinSize; + case ZUIStyleVar_DockingFocusBorderWidth: + return &s->DockingFocusBorderWidth; + case ZUIStyleVar_HoverAnimSpeed: + return &s->HoverAnimSpeed; + case ZUIStyleVar_ActiveAnimSpeed: + return &s->ActiveAnimSpeed; + default: + ZENGINE_VALIDATE_ASSERT(false, "ZUIStylePushFloat: unknown ZUIStyleVar"); + return nullptr; + } + } + + void ZUIStylePushFloat(ZUIContext* ctx, ZUIStyleVar var, float val) + { + ZENGINE_VALIDATE_ASSERT(ctx->StyleStackDepth < 64, "ZUIStyle push/pop stack overflow"); + float* ptr = StyleVarToPtr(&ctx->Style, var); + if (!ptr) + return; + ctx->StyleStack[ctx->StyleStackDepth++] = {var, *ptr}; + *ptr = val; + ZUIStyleUpdate(&ctx->Style); // recompute derived fields if FramePadding changed + } + + void ZUIStylePop(ZUIContext* ctx) + { + ZENGINE_VALIDATE_ASSERT(ctx->StyleStackDepth > 0, "ZUIStyle pop with empty stack"); + if (ctx->StyleStackDepth == 0) + return; + const auto& entry = ctx->StyleStack[--ctx->StyleStackDepth]; + float* ptr = StyleVarToPtr(&ctx->Style, entry.Id); + if (ptr) + *ptr = entry.Old; + ZUIStyleUpdate(&ctx->Style); + } + +} // namespace ZEngine::UI diff --git a/ZEngine/ZEngine/UI/ZUIContext.h b/ZEngine/ZEngine/UI/ZUIContext.h new file mode 100644 index 000000000..b67f1a606 --- /dev/null +++ b/ZEngine/ZEngine/UI/ZUIContext.h @@ -0,0 +1,568 @@ +#pragma once +#include +#include +#include +#include +#include + +namespace ZEngine::UI +{ + + // ZUITheme — single source of truth for all colors. + // Swap the whole struct at runtime to change the entire editor theme. + struct ZUITheme + { + // ZodiacEngine Dark — cool blue-dark palette with teal #4EC9B0 accent. + // Inspired by VS Code Dark+, GitHub Dark Dimmed and One Dark. + // Subtle +5% blue tint on backgrounds avoids flat gray monotony. + // All values sRGB [0,1]; hex refs are approximate. + + // --- Backgrounds (darkest → lightest) --- + float WindowBg[4] = {0.067f, 0.067f, 0.082f, 1.00f}; // #111115 editor area + float PanelBg[4] = {0.102f, 0.102f, 0.125f, 1.00f}; // #1a1a20 panel content + float PanelBgAlt[4] = {0.133f, 0.133f, 0.161f, 1.00f}; // #222229 alt rows / headers + float TitleBarBg[4] = {0.157f, 0.157f, 0.192f, 1.00f}; // #282831 tab bar (unfocused panel) + float TitleBgActive[4] = {0.176f, 0.176f, 0.216f, 1.00f}; // slightly lighter — focused panel + float HeaderBg[4] = {0.306f, 0.788f, 0.690f, 0.18f}; // teal 18% collapsing header + float MenuBarBg[4] = {0.180f, 0.180f, 0.220f, 1.00f}; // #2e2e38 menu + toolbar bars + float InputBg[4] = {0.180f, 0.180f, 0.220f, 1.00f}; // #2e2e38 input fields + + // Buttons — teal family + float ButtonBg[4] = {0.035f, 0.384f, 0.322f, 1.00f}; // #093e35 rest (dark teal) + float ButtonHoveredBg[4] = {0.055f, 0.529f, 0.431f, 1.00f}; // #0e876e hover + float ButtonActiveBg[4] = {0.192f, 0.627f, 0.541f, 1.00f}; // #31a08a active + + // Input interactive states + float InputHoveredBg[4] = {0.216f, 0.216f, 0.259f, 1.00f}; // #373742 + float InputActiveBg[4] = {0.216f, 0.216f, 0.259f, 1.00f}; // same + float HeaderHoveredBg[4] = {0.306f, 0.788f, 0.690f, 0.50f}; // teal 50% — ImGui ~0.80 + float HeaderActiveBg[4] = {0.306f, 0.788f, 0.690f, 0.70f}; // teal 70% + + // --- Tabs (4-state machine matching ImGui ImGuiCol_Tab* exactly) --- + // Visual hierarchy lightest→darkest: + // TabHoveredBg > TabInactiveBg > TitleBarBg(bar) > TabDimmedBg > TabDimmedSelectedBg > TabActiveBg + float TabActiveBg[4] = {0.102f, 0.102f, 0.125f, 1.00f}; // selected, focused — = PanelBg (sinks into content) + float TabInactiveBg[4] = {0.180f, 0.180f, 0.220f, 0.90f}; // inactive, focused — lighter than bar (floats on it) + float TabHoveredBg[4] = {0.196f, 0.196f, 0.240f, 1.00f}; // hovered inactive — even lighter + float TabDimmedBg[4] = {0.145f, 0.145f, 0.178f, 0.85f}; // inactive, unfocused panel — muted + float TabDimmedSelectedBg[4] = {0.118f, 0.118f, 0.147f, 1.00f}; // selected, unfocused panel + float TabActiveBorder[4] = {0.306f, 0.788f, 0.690f, 1.00f}; // #4EC9B0 teal overline + float TabInactiveBorder[4] = {0.235f, 0.235f, 0.280f, 0.40f}; // subtle border + float TabAccent[4] = {0.306f, 0.788f, 0.690f, 1.00f}; // teal + + // --- Rows --- + float RowHoverBg[4] = {0.306f, 0.788f, 0.690f, 0.09f}; // teal 9% + float RowSelectedBg[4] = {0.306f, 0.788f, 0.690f, 0.22f}; // teal 22% + float RowRootBg[4] = {0.306f, 0.788f, 0.690f, 0.11f}; // teal 11% + float SelectionBg[4] = {0.207f, 0.514f, 0.894f, 0.45f}; // text selection (VS Code blue) + + // --- Status bar --- + float StatusBarBg[4] = {0.200f, 0.627f, 0.537f, 1.00f}; // slightly dark teal + + // --- Text --- + float TextDefault[4] = {0.843f, 0.843f, 0.886f, 1.00f}; // #d7d7e2 cool near-white + float TextDim[4] = {0.431f, 0.431f, 0.510f, 1.00f}; // #6e6e82 blue-gray + float TextAccent[4] = {0.306f, 0.788f, 0.690f, 1.00f}; // #4EC9B0 teal + float TextWarn[4] = {0.949f, 0.741f, 0.141f, 1.00f}; // #f2bd24 + float TextError[4] = {0.937f, 0.325f, 0.314f, 1.00f}; // #ef5350 + + // --- Widget accent marks --- + float CheckMark[4] = {0.306f, 0.788f, 0.690f, 1.00f}; // teal + float SliderGrab[4] = {0.200f, 0.627f, 0.537f, 1.00f}; // teal dark + float SliderGrabActive[4] = {0.306f, 0.788f, 0.690f, 1.00f}; // teal full + + // Scrollbar + float ScrollbarBg[4] = {0.000f, 0.000f, 0.000f, 0.00f}; // transparent + float ScrollbarGrab[4] = {0.420f, 0.420f, 0.420f, 0.40f}; // VS Code scrollbarSlider.background + float ScrollbarGrabHov[4] = {0.620f, 0.620f, 0.620f, 0.65f}; // VS Code scrollbarSlider.hoverBackground + float ScrollbarGrabAct[4] = {0.740f, 0.740f, 0.740f, 0.80f}; // VS Code scrollbarSlider.activeBackground + + // Plot + float PlotLines[4] = {0.306f, 0.788f, 0.690f, 0.80f}; + float PlotLinesHov[4] = {0.306f, 0.788f, 0.690f, 1.00f}; + float PlotHistogram[4] = {0.200f, 0.627f, 0.537f, 0.90f}; + float PlotHistogramHov[4] = {0.306f, 0.788f, 0.690f, 1.00f}; + + // Table + float TableRowBgAlt[4] = {1.000f, 1.000f, 1.000f, 0.03f}; + + // --- Borders --- + float PanelBorder[4] = {0.235f, 0.235f, 0.290f, 1.00f}; // cool gray border + float PanelFocusBorder[4] = {0.306f, 0.788f, 0.690f, 1.00f}; // teal focus strip + float PanelInactiveOverlay[4] = {0.000f, 0.000f, 0.000f, 0.05f}; // 5% dim + float ButtonBorder[4] = {0.000f, 0.000f, 0.000f, 0.00f}; // none + float InputBorder[4] = {0.235f, 0.235f, 0.290f, 0.80f}; // cool gray + float InputFocusBorder[4] = {0.306f, 0.788f, 0.690f, 1.00f}; // teal + float Separator[4] = {0.235f, 0.235f, 0.290f, 0.50f}; // cool gray 50% + + // --- Popup --- + float PopupBg[4] = {0.184f, 0.184f, 0.224f, 1.00f}; // ImGui: ImGuiCol_PopupBg + + // --- DataTable --- + float TableHeaderBg[4] = {0.165f, 0.165f, 0.204f, 1.00f}; // header row background + float TableBorderLight[4] = {0.220f, 0.220f, 0.270f, 1.00f}; // inner cell borders + float TableBorderStrong[4] = {0.350f, 0.350f, 0.420f, 1.00f}; // outer border / resize grip + }; + + // ZUIStyle — all dimensional/behavioral properties. + // Analogous to ImGui's ImGuiStyle (non-color fields). + // Swap or push/pop individual properties at runtime. + // DO NOT mutate ctx->Style.* directly — use ZUIStylePushFloat/ZUIStylePop. + struct ZUIStyle + { + // Global + float Alpha = 1.f; + float DisabledAlpha = 0.38f; // ImGui: DisabledAlpha + + // Font + float FontSize = 13.f; // ImGui: g.FontSize — sync after ZUIFontAtlasBake + + // Padding / Spacing + float WindowPadding[2] = {8.f, 8.f}; + float FramePadding[2] = {4.f, 3.f}; // ImGui: FramePadding + float ItemSpacing[2] = {8.f, 4.f}; // ImGui: ItemSpacing + float ItemInnerSpacing[2] = {4.f, 4.f}; // ImGui: ItemInnerSpacing + float CellPadding[2] = {4.f, 2.f}; // ImGui: CellPadding + + // Rounding + float WindowRounding = 0.f; + float ChildRounding = 0.f; + float FrameRounding = 3.f; // ImGui: FrameRounding + float PopupRounding = 4.f; // ImGui: PopupRounding + float ScrollbarRounding = 9.f; // ImGui: ScrollbarRounding + float GrabRounding = 3.f; // ImGui: GrabRounding + float TabRounding = 3.f; // ImGui: TabRounding (top corners only) + + // Border Sizes + float WindowBorderSize = 1.f; + float ChildBorderSize = 1.f; + float FrameBorderSize = 0.f; + float PopupBorderSize = 1.f; + float TabBorderSize = 0.f; + float TabBarBorderSize = 1.f; // ImGui: TabBarBorderSize + float TabBarOverlineSize = 2.f; // ImGui: TabBarOverlineSize + float SeparatorTextBorderSize = 3.f; + + // Tabs + float TabMinWidthForClose = 0.f; // ImGui: TabMinWidthForCloseButton + + // Scrollbar + float ScrollbarSize = 10.f; // VS Code: 10px thin scrollbar + float ScrollbarMinThumbPx = 20.f; // VS Code: reasonable minimum thumb + float ScrollbarAutoHideAlpha = 0.15f; // floor alpha at rest — VS Code sidebar keeps a faint hint + + // Grab + float GrabMinSize = 12.f; // ImGui: GrabMinSize + + // Tree + float IndentSpacing = 21.f; // ImGui: IndentSpacing + float ColumnsMinSpacing = 6.f; + + // Alignment + float ButtonTextAlign[2] = {0.5f, 0.5f}; + float SelectableTextAlign[2] = {0.f, 0.f}; + float SeparatorTextAlign[2] = {0.f, 0.5f}; + float SeparatorTextPadding[2] = {20.f, 3.f}; + + // Popup + float PopupMinWidth = 200.f; + float PopupInnerPaddingX = 2.f; + + // Docking (tab/panel specifics) + float TabIconSize = 8.f; // colored icon dot size in tab/title strip + float TabGhostContentH = 39.f; // drag ghost content area height + float DataTableDefaultColumnW = 100.f; // ZUIDataTable fallback column width + + // Animation + float HoverAnimSpeed = 20.f; // expf(-HoverAnimSpeed * dt) + float ActiveAnimSpeed = 30.f; // expf(-ActiveAnimSpeed * dt) + float CursorBlinkRate = 1.f; // text cursor blink period in seconds + + // Docking + float DockingSeparatorSize = 2.f; // ImGui: DockingSeparatorSize + float DockingSeparatorSizeRest = 1.f; + float DockingGrabWidth = 6.f; + float DockingHoverBandWidth = 6.f; ///< Width of the tinted band shown on divider hover (px). Set to 0 to disable. + float DockingDropZoneEdge = 0.25f; + float DockingDropPreviewAlpha = 0.12f; + float DockingDragThreshold = 8.f; + float DockingTabReorderThreshold = 5.f; + float DockingUndockVertical = 12.f; + float DockingMinTabWidth = 40.f; + float DockingFocusBorderWidth = 2.f; + // Show a left-edge accent strip on the focused panel. + // Default false: the active tab's teal overline is sufficient visual focus indicator. + bool ShowFocusBorder = false; + + // Renderer + float DropShadowOffset = 4.f; + float DropShadowAlpha = 0.38f; + float HoverOverlayAlpha = 0.15f; + float MouseScrollSpeed = 48.f; // px per scroll unit (VS Code uses ~50) + float ScrollSmoothSpeed = 20.f; // exponential-lerp speed toward ScrollYTarget + + // Plot + float PlotLineThickness = 1.5f; + + // Non-pushable config (bool; not compatible with float push/pop stack) + bool DefaultAutoHideTabBar = false; // ImGui default: always show tab bar + + // Derived — set by ZUIStyleUpdate(), never manually + float FrameHeight = 19.f; // = FontSize + FramePadding[1] * 2 + }; + + // Call after changing FontSize or FramePadding to recompute derived fields. + // ZUIBeginFrame calls this automatically each frame. + inline void ZUIStyleUpdate(ZUIStyle* s) + { + s->FrameHeight = s->FontSize + s->FramePadding[1] * 2.f; + } + + // Style push/pop (ImGui PushStyleVar / PopStyleVar equivalent) + // These are the ONLY legal way to temporarily override a float style property. + // Mutating ctx->Style.* directly without push/pop is a contract violation. + enum ZUIStyleVar : uint32_t + { + ZUIStyleVar_Alpha = 0, + ZUIStyleVar_DisabledAlpha, + ZUIStyleVar_FramePaddingX, + ZUIStyleVar_FramePaddingY, + ZUIStyleVar_ItemSpacingX, + ZUIStyleVar_ItemSpacingY, + ZUIStyleVar_ItemInnerSpacingX, + ZUIStyleVar_ItemInnerSpacingY, + ZUIStyleVar_FrameRounding, + ZUIStyleVar_PopupRounding, + ZUIStyleVar_ScrollbarRounding, + ZUIStyleVar_GrabRounding, + ZUIStyleVar_TabRounding, + ZUIStyleVar_WindowBorderSize, + ZUIStyleVar_FrameBorderSize, + ZUIStyleVar_PopupBorderSize, + ZUIStyleVar_TabBarBorderSize, + ZUIStyleVar_TabBarOverlineSize, + ZUIStyleVar_IndentSpacing, + ZUIStyleVar_ScrollbarSize, + ZUIStyleVar_GrabMinSize, + ZUIStyleVar_DockingFocusBorderWidth, + ZUIStyleVar_HoverAnimSpeed, + ZUIStyleVar_ActiveAnimSpeed, + ZUIStyleVar_COUNT + }; + + struct ZUIPersistentState + { + float HotT = 0.f; + float ActiveT = 0.f; + float ScrollX = 0.f; // current (animated) scroll position + float ScrollY = 0.f; + float ScrollXTarget = 0.f; // destination; wheel input writes here; ScrollX lerps toward it + float ScrollYTarget = 0.f; + float MaxScrollY = 0.f; // set by layout solver; clamped in interaction pass + float ScrollbarShowTimer = 0.f; // seconds remaining; reset on scroll/drag; drives scrollbar alpha + float UserData = -1.f; + float ScreenMinX = 0.f; + float ScreenMinY = 0.f; + float ScreenMaxX = 0.f; + float ScreenMaxY = 0.f; + float MaxScrollX = 0.f; // set by layout solver for ZUI_Scrollable+LayoutAxis::X + int32_t SelectStart = -1; // text selection anchor (-1 = no selection) + }; + + struct ZUIPersistentSlot + { + uint64_t Key = 0; // 0 = empty + ZUIPersistentState State = {}; + }; + + struct ZUIPersistentStore + { + ZUIPersistentSlot* Slots = nullptr; + uint32_t Capacity = 0; // must be a power of two + uint32_t Count = 0; + }; + + struct ZUIContext + { + // sub-arenas carved from the engine's main arena via ZUIContextInit + ZEngine::Core::Memory::ArenaAllocator FrameArena; // Clear()-ed each BeginFrame; all ZUIBox* are stale after + ZEngine::Core::Memory::ArenaAllocator PersistentArena; // never cleared; holds the persistent state table + + // box tree — all pointers into FrameArena + ZUIBox* Root = nullptr; + ZUIBox* Current = nullptr; + + // persistent state — open-addressing hash table in PersistentArena + ZUIPersistentStore StateStore; + + // Single shared font atlas (ImGui approach: one texture, all fonts). + // Set by Editor::OnInitialized after ZUIFontAtlasBake. + ZUIFontAtlas* Atlas = nullptr; + + // Convenience accessors — delegate to Atlas + ZUIFont* GetFont(ZUIFontSize size) const + { + if (!Atlas) + { + return nullptr; + } + if (size == ZUIFontSize::Small && Atlas->Small) + return Atlas->Small; + if (size == ZUIFontSize::Header && Atlas->Header) + return Atlas->Header; + return Atlas->Body; + } + + // input state — written by ZUILayer each frame before ZUIBeginFrame + float MousePos[2] = {}; + float PrevMousePos[2] = {}; // saved at end of ZUIEndFrame for drag-delta + bool MouseDown[3] = {}; + bool MousePressed[3] = {}; + bool MouseReleased[3] = {}; + float ScrollDelta = 0.f; + float DeltaTime = 0.f; + float Time = 0.f; // accumulated seconds since init + bool BackspacePressed = false; // set by ZUILayer::OnKeyPressed, cleared in ZUIEndFrame + bool BackspaceHeld = false; // true while key is physically down + + // interaction state — updated by ZUIInteractionPass + uint64_t HotKey = 0; + uint64_t ActiveKey = 0; + uint64_t FocusKey = 0; + + // text input — written by OnTextInputRaised + char TextInput[32] = {}; + uint32_t TextInputLen = 0; + // Clipboard write request — written by ZUITextField (Ctrl+C), read+cleared by ZUILayer + char ClipboardWrite[512] = {}; + + // capacity caps used by layout and interaction passes + uint32_t MaxBoxesPerFrame = 0; + + // Style system — metrics, spacing, rounding (ImGui: ImGuiStyle non-color fields) + ZUIStyle Style; + + // Push/pop stack for temporary style overrides (64 slots, no dynamic allocation) + struct ZUIStyleEntry + { + ZUIStyleVar Id; + float Old; + }; + ZUIStyleEntry StyleStack[64] = {}; + uint32_t StyleStackDepth = 0; + + // active color theme — swap to retheme the whole UI at runtime + ZUITheme Theme; + + // Vector draw list — populated by PreparePayload each frame (FrameArena-backed) + ZUIDrawList DrawList; + + // current swapchain dimensions — set by AppRenderPipeline::BeginOverlayFrame each frame + uint32_t ScreenW = 1280; + uint32_t ScreenH = 720; + // display content scale (glfwGetWindowContentScale); 1.0=standard, 2.0=Retina. + // Widgets multiply logical pixel sizes by this to stay readable at any DPI. + float UIScale = 1.f; + // guard against per-frame ContentScale log spam — log only once + bool UIScaleLogged = false; + + // drag-and-drop — source is set by ZUIBeginDragSource while a box is held+moving; + // drop result (DragDropFired/DragTargetKey) is set by ZUIInteractionPass on mouse-release + // and cleared at the START of the next ZUIEndFrame so BuildUI can read it + uint64_t DragSourceKey = 0; + char DragPayload[512] = {}; + uint32_t DragPayloadLen = 0; + bool DragDropFired = false; + uint64_t DragTargetKey = 0; + + // set by ZUISceneViewportComponent each BuildUI frame; read by Editor::ProcessEvent + // to gate camera-controller mouse routing + bool ViewportHovered = false; + int ResizeCursor = 0; // 0=default 1=H-resize 2=V-resize; set by panel dividers, read by ZUILayer + + // Modifier key state — written by ZUILayer::OnKeyPressed/Released + bool CtrlDown = false; + bool ShiftDown = false; + bool AltDown = false; + + // Tab focus navigation — set by ZUILayer, consumed by ZUIEndFrame + bool TabPressed = false; + bool ShiftTabPressed = false; + bool EscapePressed = false; // clear FocusKey + bool EnterPressed = false; // confirm / deactivate field + bool SpacePressed = false; // activate focused button + bool ArrowUpPressed = false; // nudge drag float / combo nav + bool ArrowDownPressed = false; + bool ArrowLeftPressed = false; // text cursor left + bool ArrowRightPressed = false; // text cursor right + bool HomePressed = false; // cursor to start of field + bool EndPressed = false; // cursor to end of field + bool CtrlCPressed = false; + bool CtrlXPressed = false; + bool CtrlBackspacePressed = false; + bool CtrlAPressed = false; + bool CtrlZPressed = false; // undo + bool CtrlYPressed = false; // redo + bool DeletePressed = false; // forward-delete at cursor + // Held state for key-repeat (same mechanism as BackspaceHeld) + bool ArrowLeftHeld = false; + bool ArrowRightHeld = false; + bool ArrowUpHeld = false; + bool ArrowDownHeld = false; + bool DeleteHeld = false; + float ArrowRepeatTimer = 0.f; + // Per-frame tracking updated in ZUISignalFromBox during the build pass + uint64_t TabNavNextKey = 0; // first clickable after FocusKey + uint64_t TabNavPrevKey = 0; // last clickable before FocusKey + uint64_t TabNavFirstKey = 0; // first clickable seen (wraparound) + uint64_t TabNavLastKey = 0; // last clickable seen (Shift+Tab wrap) + bool TabNavSeenFocus = false; + + // Input repeat — ZUIEndFrame advances the timer; after RepeatDelay + // it fires BackspacePressed / ArrowPressed at RepeatRate hz + float KeyRepeatTimer = 0.f; + static constexpr float kRepeatDelay = 0.45f; // s before first repeat + static constexpr float kRepeatRate = 0.04f; // s between repeats + + // ZUIBeginDisabled / ZUIEndDisabled — widgets skip Clickable and dim colours + bool Disabled = false; + int DisabledDepth = 0; // supports nesting + + // Popup stack — supports nested popups (menus + submenus). + // ZUIOpenPopup queues a push at the current PopupBuildDepth. + // ZUIEndFrame applies the pending push (truncating deeper entries first). + // ZUIBeginPopup renders if the key matches PopupStack[PopupBuildDepth] + // and increments PopupBuildDepth for nested content. + // ZUIClosePopup clears the entire stack. + // Interaction pass pops from innermost outward when pressing outside. + static constexpr uint32_t kMaxPopupDepth = 8; + struct ZUIPopupEntry + { + uint64_t Key = 0; + ZUIBox* Box = nullptr; // set by ZUIBeginPopup; valid this frame + ZUIBox* SavedParent = nullptr; // ctx->Current before popup opened; restored on End + float PosX = 0.f; + float PosY = 0.f; + }; + ZUIPopupEntry PopupStack[kMaxPopupDepth] = {}; + uint32_t PopupStackSize = 0; // active popup count + uint32_t PopupBuildDepth = 0; // current render depth (reset in BeginFrame) + uint64_t PendingPopupKey = 0; // queued by ZUIOpenPopup, applied in EndFrame + uint32_t PendingPopupDepth = 0; + float PendingPopupPosX = 0.f; + float PendingPopupPosY = 0.f; + uint64_t ActiveModalKey = 0; // modal (cannot close by clicking outside) + + // Tab bar state (single-level; reset by ZUIBeginTabBar) + uint64_t TabBarKey = 0; // hash of active tab bar + int TabBarSelectedIdx = 0; // which tab is open + int TabBarCurrentIdx = 0; // iteration counter + bool TabItemWasSelected = false; // did last BeginTabItem match? + ZUIBox* TabBarRowBox = nullptr; + + // Basic table state (ZUIBeginTable / ZUIEndTable) + int TableColumns = 0; + int TableCurrentCol = -1; + float* TableColWidths = nullptr; // FrameArena array + ZUIBox* TableRowBox = nullptr; + + // TreeView state (ZUIBeginTreeView / ZUIEndTreeView) + int TV_Depth = 0; + float TV_IndentPx = 21.f; // px per depth level — ImGui IndentSpacing + float TV_RowH = 22.f; // logical row height + + // DataTable state (ZUIBeginDataTable / ZUIEndDataTable) + uint64_t DT_Key = 0; + int DT_ColCount = 0; + int DT_CurCol = -1; + int DT_RowIndex = 0; + bool DT_InRow = false; + ZUIBox* DT_RowBox = nullptr; + float* DT_ColWidths = nullptr; // FrameArena, size = DT_ColCount + const void* DT_Cols = nullptr; // ZUIDataTableColumn* stored by BeginDataTable + int DT_SortCol = -1; // -1 = unsorted + bool DT_SortAsc = true; + bool DT_SortChanged = false; + + // GridView state (ZUIBeginGridView / ZUIEndGridView) + float GV_ItemW = 0.f; + float GV_ItemH = 0.f; + int GV_MaxCols = 1; + int GV_CurCol = 0; + int GV_CurRow = 0; + bool GV_RowOpen = false; + float PopupPos[2] = {}; // unused — kept for ABI; pos now in PopupStack entry + float PopupDesiredW = 0.f; // optional fixed width (set by ZUIBeginCombo) + ZUIBox* ModalSavedParent = nullptr; // ctx->Current saved by ZUIBeginModal + // Text field undo / redo + // Per-field stacks stored in context (only the focused field uses them). + // Undo: push BEFORE edit → Ctrl+Z pops and restores. Redo: push current + // state when undoing → Ctrl+Y pops and restores. + struct ZUIUndoEntry + { + char Buf[512]; + int32_t Cursor; + }; + static constexpr uint32_t kUndoDepth = 8; + uint64_t UndoFieldKey = 0; + ZUIUndoEntry UndoStack[kUndoDepth] = {}; + int32_t UndoTop = 0; // next push index (0 = empty) + ZUIUndoEntry RedoStack[kUndoDepth] = {}; + int32_t RedoTop = 0; + + // Keyboard navigation inside open popups (combos, menus) + int PopupNavIdx = -1; // keyboard-highlighted item index; -1 = none + int PopupBuildIdx = 0; // incremented per ZUIComboItem/ZUISelectable in popup + int PopupBuildCount = 0; // item count from previous popup frame (for clamping) + }; + + ZDEFINE_PTR(ZUIContext); + + // Lifecycle + void ZUIContextInit(ZUIContext* ctx, ZEngine::Core::Memory::ArenaAllocator* parent, size_t FrameArenaBytes, size_t PersistentArenaBytes, uint32_t StateCapacity, uint32_t MaxBoxesPerFrame); + void ZUIContextDestroy(ZUIContext* ctx); + + // Per-frame + void ZUIBeginFrame(ZUIContext* ctx, float dt); + void ZUIEndFrame(ZUIContext* ctx); + + // Box tree helpers + ZUIBox* ZUIPushBox(ZUIContext* ctx, const char* key, uint32_t key_len, ZUIBoxFlags flags); + void ZUIPopBox(ZUIContext* ctx); + + // Utilities + ZUIPersistentState* ZUIStateGetOrInsert(ZUIPersistentStore* store, uint64_t key); + ZUIStr ZUIPushStr(ZEngine::Core::Memory::ArenaAllocator* arena, const char* str, uint32_t len); + uint64_t ZUIHashStr(const char* str, uint32_t len); + + // Style push/pop — ONLY legal mechanism for per-scope style overrides + void ZUIStylePushFloat(ZUIContext* ctx, ZUIStyleVar var, float val); + void ZUIStylePop(ZUIContext* ctx); + + // Helper inlines (prefer these over ctx->Style.* direct reads) + inline float ZUIGetFrameHeight(const ZUIContext* ctx) + { + return ctx->Style.FrameHeight; + } + inline float ZUIGetFramePadX(const ZUIContext* ctx) + { + return ctx->Style.FramePadding[0]; + } + inline float ZUIGetFramePadY(const ZUIContext* ctx) + { + return ctx->Style.FramePadding[1]; + } + inline float ZUIGetItemSpacX(const ZUIContext* ctx) + { + return ctx->Style.ItemSpacing[0]; + } + inline float ZUIGetItemSpacY(const ZUIContext* ctx) + { + return ctx->Style.ItemSpacing[1]; + } + inline float ZUIGetInnerSpac(const ZUIContext* ctx) + { + return ctx->Style.ItemInnerSpacing[0]; + } + +} // namespace ZEngine::UI diff --git a/ZEngine/ZEngine/UI/ZUIDockSerial.cpp b/ZEngine/ZEngine/UI/ZUIDockSerial.cpp new file mode 100644 index 000000000..e82065de8 --- /dev/null +++ b/ZEngine/ZEngine/UI/ZUIDockSerial.cpp @@ -0,0 +1,482 @@ +#include +#include +#include +#include +#include + +namespace ZEngine::UI +{ + using namespace ZEngine::Core::Memory; + + // Node collection helpers (DFS, parent-before-children) + + static constexpr uint32_t kMaxSerialNodes = 128; + + struct NodeRecord + { + ZUIDockNode* Ptr; + int ParentId; // -1 = root + }; + + static void CollectNodes(ZUIDockNode* node, int parent_id, NodeRecord records[], uint32_t* count) + { + if (!node || *count >= kMaxSerialNodes) + { + return; + } + int my_id = (int) (*count)++; + records[my_id] = {node, parent_id}; + for (ZUIDockNode* c = node->First; c; c = c->Next) + CollectNodes(c, my_id, records, count); + } + + // ZUIDockSave (v3 format — adds AutoHideTabBar; incompatible with v2) + + void ZUIDockSave(ZUIPanelManager* manager, const char* path) + { + if (!manager || !path || !path[0]) + { + return; + } + FILE* f = fopen(path, "w"); + if (!f) + { + return; + } + + fprintf(f, "# ZUI Layout v3\n"); + + // --- Dock tree --- + // node [ ] + if (manager->DockTree && manager->DockTree->Root) + { + NodeRecord records[kMaxSerialNodes]; + uint32_t count = 0; + CollectNodes(manager->DockTree->Root, -1, records, &count); + + for (uint32_t i = 0; i < count; ++i) + { + ZUIDockNode* n = records[i].Ptr; + bool is_leaf = (n->ContentKey != 0); + // Compute save fraction from SizePx (runtime source of truth). + // Falls back to PctOfParent if SizePx not yet initialized. + float save_pct = n->PctOfParent; + if (n->Parent && n->SizePx > 0.f) + { + float sib_sum = 0.f; + for (ZUIDockNode* s = n->Parent->First; s; s = s->Next) + sib_sum += s->SizePx; + if (sib_sum > 1e-6f) + save_pct = n->SizePx / sib_sum; + } + fprintf(f, "node %u %d %d %d %f", i, records[i].ParentId, is_leaf ? 1 : 0, (n->SplitAxis == ZUIAxis::X) ? 0 : 1, (double) save_pct); + if (is_leaf) + fprintf(f, " %016llx %d %d", (unsigned long long) n->ContentKey, n->IsCentral ? 1 : 0, n->AutoHideTabBar ? 1 : 0); + fprintf(f, "\n"); + } + } + + // --- Panels --- + for (uint32_t i = 0; i < manager->PanelCount; ++i) + { + ZUIPanel* p = &manager->Panels[i]; + // panel + // followed by one `view ` line per view + fprintf(f, "panel %016llx %u %d %u\n", (unsigned long long) p->DockKey, p->ActiveTab, p->Hidden ? 1 : 0, p->ViewCount); + for (uint32_t vi = 0; vi < p->ViewCount; ++vi) + { + const char* title = (p->Views[vi] && p->Views[vi]->Title) ? p->Views[vi]->Title : ""; + fprintf(f, "view %s\n", title); + } + } + + fclose(f); + } + + // ZUIDockLoad (v3 format only — v2 files are intentionally incompatible) + + bool ZUIDockLoad(ZUIPanelManager* manager, const char* path, ZUIPanelView** views, uint32_t view_count) + { + if (!manager || !path || !path[0]) + { + return false; + } + FILE* f = fopen(path, "r"); + if (!f) + { + return false; + } + + // --- Version check (v3 only; v2 files are discarded, not migrated) --- + char line[512]; + if (!fgets(line, sizeof(line), f) || strncmp(line, "# ZUI Layout v3", 15) != 0) + { + fclose(f); + return false; + } + + // --- Pass 1: parse all node records --- + struct LoadNode + { + int parent_id; + int is_leaf; + int axis; // 0=X 1=Y + float pct; + uint64_t content_key; // 0 if split + int is_central; + int auto_hide_tab_bar; + }; + + LoadNode load_nodes[kMaxSerialNodes]; + uint32_t node_count = 0; + + // Also buffer panel lines for pass 2 + struct LoadPanel + { + uint64_t key; + uint32_t active_tab; + int hidden; + uint32_t view_count; + }; + static constexpr uint32_t kMaxPanels = 32; + LoadPanel load_panels[kMaxPanels]; + char panel_views[kMaxPanels][kMaxTabsPerPanel][64]; // title buffers + uint32_t panel_count = 0; + int pending_panel = -1; // index of panel we're reading views for + uint32_t panel_view_idx = 0; + + while (fgets(line, sizeof(line), f)) + { + // Strip trailing newline + uint32_t len = (uint32_t) strlen(line); + while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) + { + line[--len] = '\0'; + } + + if (line[0] == '#' || line[0] == '\0') + { + continue; + } + + if (strncmp(line, "node ", 5) == 0 && node_count < kMaxSerialNodes) + { + pending_panel = -1; // end any ongoing panel read + LoadNode& n = load_nodes[node_count]; + n.content_key = 0; + n.is_central = 0; + n.auto_hide_tab_bar = 0; + unsigned long long ck = 0; + int parsed = sscanf(line + 5, "%*u %d %d %d %f %llx %d %d", &n.parent_id, &n.is_leaf, &n.axis, &n.pct, &ck, &n.is_central, &n.auto_hide_tab_bar); + n.content_key = (uint64_t) ck; + if (parsed >= 4) + { + ++node_count; + } + } + else if (strncmp(line, "panel ", 6) == 0 && panel_count < kMaxPanels) + { + LoadPanel& p = load_panels[panel_count]; + unsigned long long k = 0; + if (sscanf(line + 6, "%llx %u %d %u", &k, &p.active_tab, &p.hidden, &p.view_count) == 4) + { + p.key = (uint64_t) k; + pending_panel = (int) panel_count; + panel_view_idx = 0; + ++panel_count; + } + } + else if (strncmp(line, "view ", 5) == 0 && pending_panel >= 0) + { + if (panel_view_idx < kMaxTabsPerPanel) + { + snprintf(panel_views[pending_panel][panel_view_idx], sizeof(panel_views[0][0]), "%s", line + 5); + ++panel_view_idx; + } + } + } + fclose(f); + + if (node_count == 0) + { + return false; + } + + // --- Rebuild dock tree --- + if (!manager->DockTree || !manager->DockTree->Arena) + { + return false; + } + ArenaAllocator* arena = manager->DockTree->Arena; + + // Allocate all nodes upfront + ZUIDockNode* new_nodes[kMaxSerialNodes] = {}; + for (uint32_t i = 0; i < node_count; ++i) + { + new_nodes[i] = ZPushStructCtor(arena, ZUIDockNode); + if (!new_nodes[i]) + { + return false; + } + } + + // Wire the tree + for (uint32_t i = 0; i < node_count; ++i) + { + LoadNode& ln = load_nodes[i]; + ZUIDockNode* n = new_nodes[i]; + + n->SplitAxis = (ln.axis == 0) ? ZUIAxis::X : ZUIAxis::Y; + n->PctOfParent = ln.pct; + n->ContentKey = ln.content_key; + n->IsCentral = (ln.is_central != 0); + n->AutoHideTabBar = (ln.auto_hide_tab_bar != 0); + + if (ln.parent_id >= 0 && (uint32_t) ln.parent_id < node_count) + { + ZUIDockNode* parent = new_nodes[ln.parent_id]; + n->Parent = parent; + n->Prev = parent->Last; + if (parent->Last) + { + parent->Last->Next = n; + } + else + { + parent->First = n; + } + parent->Last = n; + ++parent->ChildCount; + } + } + + manager->DockTree->Root = new_nodes[0]; + manager->DockTree->Focused = nullptr; + + // --- Restore panel state + view assignments --- + // Snapshot current view assignments before clearing so we can restore + // panels that are NOT in the ini (stale DockKey after a rename, etc.). + // Without this, unmatched panels end up with ViewCount=0 → invisible. + struct ViewSnapshot + { + ZUIPanelView* Views[kMaxTabsPerPanel]; + uint32_t Count; + }; + ViewSnapshot snapshots[kMaxPanels]; + for (uint32_t i = 0; i < manager->PanelCount; ++i) + { + snapshots[i].Count = manager->Panels[i].ViewCount; + for (uint32_t v = 0; v < manager->Panels[i].ViewCount; ++v) + snapshots[i].Views[v] = manager->Panels[i].Views[v]; + manager->Panels[i].ViewCount = 0; + } + + for (uint32_t pi = 0; pi < panel_count; ++pi) + { + LoadPanel& lp = load_panels[pi]; + ZUIPanel* p = manager->FindPanel(lp.key); + if (!p) + { + continue; + } + + p->ActiveTab = lp.active_tab; + p->Hidden = (lp.hidden != 0); + + // Assign views by matching titles + for (uint32_t vi = 0; vi < lp.view_count && p->ViewCount < kMaxTabsPerPanel; ++vi) + { + const char* title = panel_views[pi][vi]; + for (uint32_t gi = 0; gi < view_count; ++gi) + { + if (views[gi] && views[gi]->Title && strcmp(views[gi]->Title, title) == 0) + { + p->Views[p->ViewCount++] = views[gi]; + break; + } + } + } + + if (p->ActiveTab >= p->ViewCount && p->ViewCount > 0) + p->ActiveTab = p->ViewCount - 1; + } + + // Pass 2b — match drag-created (DragKeySeq) panel records by view title. + // Panels are always created as AddPanel(ZUIDockHashName(view->Title)), so + // ZUIDockHashName(view->Title) == original_panel->DockKey — no snapshots needed. + // For each unmatched record: redirect its leaf ContentKey to the original panel + // and assign views directly. This makes drag-split positions survive restarts. + for (uint32_t pi = 0; pi < panel_count; ++pi) + { + LoadPanel& lp = load_panels[pi]; + if (manager->FindPanel(lp.key)) + { + continue; + } // already matched + if (lp.view_count == 0) + { + continue; + } // nothing to restore + + // Identify the owner panel from the first view title + const char* first_title = panel_views[pi][0]; + ZUIPanel* owner = nullptr; + for (uint32_t gi = 0; gi < view_count && !owner; ++gi) + { + if (!views[gi] || !views[gi]->Title) + { + continue; + } + if (strcmp(views[gi]->Title, first_title) != 0) + { + continue; + } + uint64_t owner_key = ZUIDockHashName(views[gi]->Title); + owner = manager->FindPanel(owner_key); + } + if (!owner) + { + continue; + } + + // Redirect the DragKeySeq leaf to the owner panel (if it has no leaf yet) + if (!ZUIDockFindLeaf(manager->DockTree, owner->DockKey)) + { + ZUIDockNode* leaf = ZUIDockFindLeaf(manager->DockTree, lp.key); + if (leaf) + leaf->ContentKey = owner->DockKey; + } + + // Assign all views from the drag-created record to the owner panel + owner->ViewCount = 0; + owner->Hidden = false; + owner->ActiveTab = lp.active_tab; + for (uint32_t vi = 0; vi < lp.view_count && owner->ViewCount < kMaxTabsPerPanel; ++vi) + { + const char* t = panel_views[pi][vi]; + for (uint32_t gi = 0; gi < view_count; ++gi) + { + if (views[gi] && views[gi]->Title && strcmp(views[gi]->Title, t) == 0) + { + owner->Views[owner->ViewCount++] = views[gi]; + break; + } + } + } + if (owner->ActiveTab >= owner->ViewCount && owner->ViewCount > 0) + owner->ActiveTab = owner->ViewCount - 1; + } + + // Restore view assignments for panels not found in the ini (stale/renamed keys). + // These keep their default AddView() assignments so the editor still renders. + for (uint32_t i = 0; i < manager->PanelCount; ++i) + { + ZUIPanel* p = &manager->Panels[i]; + if (p->ViewCount == 0 && snapshots[i].Count > 0) + { + p->ViewCount = snapshots[i].Count; + for (uint32_t v = 0; v < snapshots[i].Count; ++v) + p->Views[v] = snapshots[i].Views[v]; + // Keep Hidden=false (panel was previously visible before the ini mismatch) + p->Hidden = false; + } + } + + // Collapse orphaned leaves — leaves whose ContentKey has NO registered panel at all. + // This only removes drag-created slots (session-unique DragKeySeq keys that don't + // exist in the current session's Panels[] array). Leaves with a registered panel + // (even one with ViewCount==0) are left alone; hidden-panel collapse handles them. + { + uint64_t orphan_keys[kMaxSerialNodes]; + uint32_t orphan_count = 0; + + ZUIDockNode* stk[kMaxSerialNodes]; + uint32_t stk_top = 0; + if (manager->DockTree->Root) + stk[stk_top++] = manager->DockTree->Root; + while (stk_top > 0 && orphan_count < kMaxSerialNodes) + { + ZUIDockNode* n = stk[--stk_top]; + if (!n->First && n->ContentKey != 0) + { + // Leaf — orphaned only when NO panel is registered with this key. + if (!manager->FindPanel(n->ContentKey)) + orphan_keys[orphan_count++] = n->ContentKey; + } + for (ZUIDockNode* c = n->First; c; c = c->Next) + if (stk_top < kMaxSerialNodes) + stk[stk_top++] = c; + } + + for (uint32_t i = 0; i < orphan_count; ++i) + { + ZUIDockNode* leaf = ZUIDockFindLeaf(manager->DockTree, orphan_keys[i]); + if (leaf) + ZUIDockCollapseLeaf(manager->DockTree, leaf); + } + } + + // Re-insert panels that lost their leaf (all DragKeySeq leaves collapsed → no rect). + // Claim any orphaned/unclaimed leaf, or split an existing one. + for (uint32_t i = 0; i < manager->PanelCount; ++i) + { + ZUIPanel* p = &manager->Panels[i]; + if (p->Hidden || p->ViewCount == 0) + { + continue; + } + if (ZUIDockFindLeaf(manager->DockTree, p->DockKey)) + { + continue; + } // already placed + + // DFS: find the first leaf (may be an orphaned root) + ZUIDockNode* tgt = nullptr; + { + ZUIDockNode* s[kMaxSerialNodes]; + uint32_t st = 0; + if (manager->DockTree->Root) + s[st++] = manager->DockTree->Root; + while (st > 0 && !tgt) + { + ZUIDockNode* n = s[--st]; + if (!n->First) + { + tgt = n; + break; + } // leaf or empty root + for (ZUIDockNode* c = n->First; c; c = c->Next) + if (st < kMaxSerialNodes) + s[st++] = c; + } + } + if (!tgt) + { + break; + } // no tree + + if (!manager->FindPanel(tgt->ContentKey)) + tgt->ContentKey = p->DockKey; // claim orphaned leaf + else + ZUIDockSplitH(manager->DockTree, tgt, 0.5f, tgt->ContentKey, p->DockKey); + } + + // Collapse hidden panels + for (uint32_t i = 0; i < manager->PanelCount; ++i) + { + ZUIPanel* p = &manager->Panels[i]; + if (!p->Hidden) + { + continue; + } + ZUIDockNode* leaf = ZUIDockFindLeaf(manager->DockTree, p->DockKey); + if (leaf) + { + ZUIDockCollapseLeaf(manager->DockTree, leaf); + } + } + + return true; + } + +} // namespace ZEngine::UI diff --git a/ZEngine/ZEngine/UI/ZUIDockSerial.h b/ZEngine/ZEngine/UI/ZUIDockSerial.h new file mode 100644 index 000000000..a41b40177 --- /dev/null +++ b/ZEngine/ZEngine/UI/ZUIDockSerial.h @@ -0,0 +1,20 @@ +#pragma once +#include <ZEngine/UI/ZUIPanel.h> + +namespace ZEngine::UI +{ + // Save the full dock layout to path: + // - complete node tree (axes, pcts, content keys, central flags) + // - per-panel: ActiveTab, Hidden, ordered view titles + // Called automatically by ZUIPanelManager::BuildUI when LayoutDirty is set. + void ZUIDockSave(ZUIPanelManager* manager, const char* path); + + // Load a previously saved layout from path. + // - Rebuilds the dock tree from saved node records (old orphaned nodes stay in arena) + // - Restores panel state and view assignments by matching saved titles + // against the provided view list (pass ALL registered ZUIPanelView* pointers) + // - Hidden panels are collapsed in the tree + // Returns true on success, false if file not found or version mismatch. + bool ZUIDockLoad(ZUIPanelManager* manager, const char* path, ZUIPanelView** views, uint32_t view_count); + +} // namespace ZEngine::UI diff --git a/ZEngine/ZEngine/UI/ZUIDockspace.cpp b/ZEngine/ZEngine/UI/ZUIDockspace.cpp new file mode 100644 index 000000000..29c6a7e57 --- /dev/null +++ b/ZEngine/ZEngine/UI/ZUIDockspace.cpp @@ -0,0 +1,314 @@ +#include <ZEngine/Core/Memory/Allocator.h> +#include <ZEngine/UI/ZUIDockspace.h> + +namespace ZEngine::UI +{ + using namespace ZEngine::Core::Memory; + + // Helpers + + static ZUIDockNode* AllocNode(ZUIDockTree* tree) + { + return ZPushStructCtor(tree->Arena, ZUIDockNode); + } + + static void AppendChild(ZUIDockNode* parent, ZUIDockNode* child) + { + child->Parent = parent; + child->Next = nullptr; + child->Prev = parent->Last; + if (parent->Last) + { + parent->Last->Next = child; + } + else + { + parent->First = child; + } + parent->Last = child; + ++parent->ChildCount; + } + + static constexpr float kMinPanelPx = 60.f; // minimum panel size in logical pixels + + // Recursive rect computation using absolute SizePx values. + // On first call (SizePx==0), seeds from PctOfParent then switches to absolute storage. + // Last child always snaps to the exact parent edge — no sub-pixel gaps from rounding. + static void LayoutNode(ZUIDockNode* node) + { + if (!node->First) + { + return; + } // leaf — rect already set by parent + + float x0 = node->RectMin[0], y0 = node->RectMin[1]; + float x1 = node->RectMax[0], y1 = node->RectMax[1]; + float total = (node->SplitAxis == ZUIAxis::X) ? (x1 - x0) : (y1 - y0); + float cursor = (node->SplitAxis == ZUIAxis::X) ? x0 : y0; + + // If SizePx uninitialized, seed from PctOfParent fractions. + float size_sum = 0.f; + for (ZUIDockNode* c = node->First; c; c = c->Next) + size_sum += c->SizePx; + if (size_sum < 1e-6f) + { + float pct_sum = 0.f; + for (ZUIDockNode* c = node->First; c; c = c->Next) + pct_sum += c->PctOfParent; + if (pct_sum < 1e-6f) + pct_sum = 1.f; + for (ZUIDockNode* c = node->First; c; c = c->Next) + c->SizePx = total * (c->PctOfParent / pct_sum); + size_sum = total; + } + + for (ZUIDockNode* c = node->First; c; c = c->Next) + { + // Last child takes exact remainder — eliminates sub-pixel gaps. + float span = c->Next ? (total * c->SizePx / size_sum) : ((node->SplitAxis == ZUIAxis::X) ? (x1 - cursor) : (y1 - cursor)); + if (node->SplitAxis == ZUIAxis::X) + { + c->RectMin[0] = cursor; + c->RectMin[1] = y0; + c->RectMax[0] = cursor + span; + c->RectMax[1] = y1; + } + else + { + c->RectMin[0] = x0; + c->RectMin[1] = cursor; + c->RectMax[0] = x1; + c->RectMax[1] = cursor + span; + } + cursor += span; + LayoutNode(c); + } + } + + // Build API + + ZUIDockTree* ZUIDockTreeCreate(ArenaAllocator* persistent_arena) + { + auto* tree = ZPushStructCtor(persistent_arena, ZUIDockTree); + tree->Arena = persistent_arena; + tree->Root = AllocNode(tree); + tree->Root->PctOfParent = 1.f; + return tree; + } + + ZUIDockNode* ZUIDockSplitH(ZUIDockTree* tree, ZUIDockNode* node, float left_pct, uint64_t left_key, uint64_t right_key) + { + float avail = node->RectMax[0] - node->RectMin[0]; // >0 if already laid out + + node->SplitAxis = ZUIAxis::X; + node->ContentKey = 0; + + auto* left = AllocNode(tree); + auto* right = AllocNode(tree); + left->PctOfParent = left_pct; + right->PctOfParent = 1.f - left_pct; + left->ContentKey = left_key; + right->ContentKey = right_key; + // Seed SizePx from parent rect when available; LayoutNode seeds from PctOfParent otherwise. + if (avail > 1.f) + { + left->SizePx = avail * left_pct; + right->SizePx = avail * (1.f - left_pct); + } + + AppendChild(node, left); + AppendChild(node, right); + return left; + } + + ZUIDockNode* ZUIDockSplitV(ZUIDockTree* tree, ZUIDockNode* node, float top_pct, uint64_t top_key, uint64_t bot_key) + { + float avail = node->RectMax[1] - node->RectMin[1]; + + node->SplitAxis = ZUIAxis::Y; + node->ContentKey = 0; + + auto* top = AllocNode(tree); + auto* bot = AllocNode(tree); + top->PctOfParent = top_pct; + bot->PctOfParent = 1.f - top_pct; + top->ContentKey = top_key; + bot->ContentKey = bot_key; + if (avail > 1.f) + { + top->SizePx = avail * top_pct; + bot->SizePx = avail * (1.f - top_pct); + } + + AppendChild(node, top); + AppendChild(node, bot); + return top; + } + + // Runtime API + + void ZUIDockLayout(ZUIDockTree* tree, const float root_rect[4]) + { + if (!tree || !tree->Root) + { + return; + } + tree->Root->RectMin[0] = root_rect[0]; + tree->Root->RectMin[1] = root_rect[1]; + tree->Root->RectMax[0] = root_rect[2]; + tree->Root->RectMax[1] = root_rect[3]; + LayoutNode(tree->Root); + } + + static ZUIDockNode* FindLeaf(ZUIDockNode* node, uint64_t key) + { + if (node->ContentKey == key && !node->First) + { + return node; + } + for (ZUIDockNode* c = node->First; c; c = c->Next) + { + auto* found = FindLeaf(c, key); + if (found) + { + return found; + } + } + return nullptr; + } + + bool ZUIDockRectForKey(ZUIDockTree* tree, uint64_t key, float out_rect[4]) + { + if (!tree || !tree->Root) + { + return false; + } + auto* leaf = FindLeaf(tree->Root, key); + if (!leaf) + { + return false; + } + out_rect[0] = leaf->RectMin[0]; + out_rect[1] = leaf->RectMin[1]; + out_rect[2] = leaf->RectMax[0]; + out_rect[3] = leaf->RectMax[1]; + return true; + } + + void ZUIDockResize(ZUIDockTree* tree, ZUIDockNode* node, float delta_px) + { + if (!tree || !node || !node->Parent) + { + return; + } + auto* sibling = node->Next ? node->Next : node->Prev; + if (!sibling) + { + return; + } + + // Direct pixel delta — no percentage conversion, no drift. + if (node->Next == sibling) + { + node->SizePx += delta_px; + sibling->SizePx -= delta_px; + } + else + { + node->SizePx -= delta_px; + sibling->SizePx += delta_px; + } + + // Hard minimum size floor — clamp and compensate on the other side. + auto clamp = [](ZUIDockNode* a, ZUIDockNode* b) { + if (a->SizePx < kMinPanelPx) + { + b->SizePx -= kMinPanelPx - a->SizePx; + a->SizePx = kMinPanelPx; + if (b->SizePx < kMinPanelPx) + b->SizePx = kMinPanelPx; + } + }; + clamp(node, sibling); + clamp(sibling, node); + } + + ZUIDockNode* ZUIDockFindLeaf(ZUIDockTree* tree, uint64_t key) + { + if (!tree || !tree->Root) + { + return nullptr; + } + return FindLeaf(tree->Root, key); + } + + void ZUIDockCollapseLeaf(ZUIDockTree* tree, ZUIDockNode* leaf) + { + if (!tree || !leaf) + { + return; + } + ZUIDockNode* parent = leaf->Parent; + if (!parent) + { + return; + } // root leaf — nothing to collapse into + + // Find the sibling (the other child of the binary split parent) + ZUIDockNode* sibling = (parent->First == leaf) ? leaf->Next : leaf->Prev; + if (!sibling) + { + return; + } + + ZUIDockNode* gp = parent->Parent; + // Sibling inherits parent's share — both storage values for full compatibility. + sibling->PctOfParent = parent->PctOfParent; + sibling->SizePx = parent->SizePx; + sibling->Parent = gp; + // Patch grandparent's sibling links (sibling replaces parent in the list) + sibling->Prev = parent->Prev; + sibling->Next = parent->Next; + if (parent->Prev) + parent->Prev->Next = sibling; + if (parent->Next) + parent->Next->Prev = sibling; + + if (gp) + { + if (gp->First == parent) + gp->First = sibling; + if (gp->Last == parent) + gp->Last = sibling; + } + else + { + // parent was the root — sibling becomes the new root + tree->Root = sibling; + } + + // Orphan the removed nodes (arena-allocated, cannot free) + leaf->Parent = nullptr; + parent->First = nullptr; + parent->Last = nullptr; + parent->Parent = nullptr; + } + + uint64_t ZUIDockHashName(const char* name) + { + uint64_t h = 14695981039346656037ULL; + for (const char* p = name; *p; ++p) + h = (h ^ (uint8_t) *p) * 1099511628211ULL; + return h ? h : 1; + } + + void ZUIDockMarkCentral(ZUIDockTree* tree, uint64_t content_key) + { + ZUIDockNode* leaf = ZUIDockFindLeaf(tree, content_key); + if (leaf) + { + leaf->IsCentral = true; + } + } + +} // namespace ZEngine::UI diff --git a/ZEngine/ZEngine/UI/ZUIDockspace.h b/ZEngine/ZEngine/UI/ZUIDockspace.h new file mode 100644 index 000000000..cf10745c5 --- /dev/null +++ b/ZEngine/ZEngine/UI/ZUIDockspace.h @@ -0,0 +1,118 @@ +#pragma once +#include <ZEngine/Core/Memory/Allocator.h> +#include <ZEngine/UI/ZUIBox.h> +#include <cstdint> + +namespace ZEngine::UI +{ + /// @brief Arena-based binary split tree for panel docking. + /// + /// Each node is either a SPLIT node (has children, ContentKey==0) or a + /// LEAF node (no children, ContentKey != 0, assigned a rect each frame). + /// The tree is built once at startup and mutated by Split/Resize/Collapse + /// operations; rects are recomputed every frame from the root rect. + /// + /// Typical usage: + /// @code + /// ZUIDockSplitH(tree, root, 0.25f, kLeftKey, kRightKey); + /// ZUIDockLayout(tree, root_rect); + /// ZUIDockRectForKey(tree, kLeftKey, out_rect); + /// @endcode + struct ZUIDockNode + { + ZUIDockNode* Parent = nullptr; + ZUIDockNode* First = nullptr; ///< First child + ZUIDockNode* Last = nullptr; ///< Last child + ZUIDockNode* Next = nullptr; ///< Next sibling + ZUIDockNode* Prev = nullptr; ///< Previous sibling + uint32_t ChildCount = 0; + + ZUIAxis SplitAxis = ZUIAxis::X; + float PctOfParent = 1.f; ///< Serialization seed — ZUIDockLoad reads it, ZUIDockSave derives from SizePx/sum + float SizePx = 0.f; ///< Runtime absolute px along the parent split axis (0 = uninitialized, seeded from PctOfParent on first layout) + + uint64_t ContentKey = 0; ///< Hashed panel name; 0 on split nodes + + float RectMin[2] = {}; ///< Computed each frame by ZUIDockLayout + float RectMax[2] = {}; + + bool IsCentral = false; ///< Passthrough — no chrome, no click interception (e.g. 3D viewport) + bool AutoHideTabBar = false; ///< When true and ViewCount==1 shows a title strip instead of a tab bar + }; + + struct ZUIDockTree + { + ZUIDockNode* Root = nullptr; + ZUIDockNode* Focused = nullptr; ///< Leaf with keyboard focus + ZEngine::Core::Memory::ArenaAllocator* Arena = nullptr; + }; + + /// @brief Allocate a fresh dock tree in @p persistent_arena. + /// @param persistent_arena Must outlive the tree (arena-allocated, no heap). + /// @return Pointer to the newly created ZUIDockTree. + ZUIDockTree* ZUIDockTreeCreate(ZEngine::Core::Memory::ArenaAllocator* persistent_arena); + + /// @brief Split @p node into a left and right child along the X axis. + /// @param tree Owning tree (used for node allocation). + /// @param node Node to split — may be a leaf or an existing split. + /// @param left_pct Fraction [0,1] of the parent rect given to the left child. + /// @param left_key ContentKey for the left leaf (0 = intermediate split node). + /// @param right_key ContentKey for the right leaf. + /// @return The left child node (caller may further split it). + ZUIDockNode* ZUIDockSplitH(ZUIDockTree* tree, ZUIDockNode* node, float left_pct, uint64_t left_key, uint64_t right_key); + + /// @brief Split @p node into a top and bottom child along the Y axis. + /// @param tree Owning tree. + /// @param node Node to split. + /// @param top_pct Fraction [0,1] given to the top child. + /// @param top_key ContentKey for the top leaf. + /// @param bot_key ContentKey for the bottom leaf. + /// @return The top child node. + ZUIDockNode* ZUIDockSplitV(ZUIDockTree* tree, ZUIDockNode* node, float top_pct, uint64_t top_key, uint64_t bot_key); + + /// @brief Recompute all node rects by walking the tree from the root. + /// @param tree Target tree. + /// @param root_rect Bounding rect {x0, y0, x1, y1} for the root node. + void ZUIDockLayout(ZUIDockTree* tree, const float root_rect[4]); + + /// @brief Get the screen rect for the leaf whose ContentKey equals @p key. + /// @param tree Target tree. + /// @param key ContentKey to search for. + /// @param out_rect Receives {x0, y0, x1, y1} if found. + /// @return true if the leaf was found and @p out_rect was written. + bool ZUIDockRectForKey(ZUIDockTree* tree, uint64_t key, float out_rect[4]); + + /// @brief Move the divider between two siblings by @p delta_px pixels. + /// @param tree Target tree. + /// @param node A LEAF node whose sibling will absorb the delta. + /// @param delta_px Positive moves the divider toward the next sibling. + /// @note Enforces a minimum panel size (kMinPanelPx) on both sides. + void ZUIDockResize(ZUIDockTree* tree, ZUIDockNode* node, float delta_px); + + /// @brief Hash a panel name string to a stable 64-bit ContentKey. + /// @param name Null-terminated panel name (e.g. "Hierarchy"). + /// @return Non-zero FNV-1a hash — guaranteed non-zero. + uint64_t ZUIDockHashName(const char* name); + + /// @brief Find the leaf node whose ContentKey equals @p key. + /// @param tree Target tree. + /// @param key ContentKey to search for. + /// @return Pointer to the leaf, or nullptr if not found. + ZUIDockNode* ZUIDockFindLeaf(ZUIDockTree* tree, uint64_t key); + + /// @brief Remove a leaf from the tree; the sibling absorbs the freed space. + /// @param tree Owning tree. + /// @param leaf Leaf node to remove. Must have a parent (root leaf is a no-op). + /// @note Nodes are arena-allocated and cannot be freed — orphaned nodes are + /// simply disconnected. Call before re-inserting a view elsewhere. + void ZUIDockCollapseLeaf(ZUIDockTree* tree, ZUIDockNode* leaf); + + /// @brief Mark a leaf as the central passthrough node. + /// + /// A central node receives no chrome (no tab bar, no border, no focus strip) + /// and does not intercept clicks, making it suitable for a 3D viewport. + /// @param tree Owning tree. + /// @param content_key ContentKey of the leaf to mark. + void ZUIDockMarkCentral(ZUIDockTree* tree, uint64_t content_key); + +} // namespace ZEngine::UI diff --git a/ZEngine/ZEngine/UI/ZUIDrawList.cpp b/ZEngine/ZEngine/UI/ZUIDrawList.cpp new file mode 100644 index 000000000..ba102024a --- /dev/null +++ b/ZEngine/ZEngine/UI/ZUIDrawList.cpp @@ -0,0 +1,729 @@ +#include <ZEngine/Helpers/MemoryOperations.h> +#include <ZEngine/UI/ZUIDrawList.h> +#include <cmath> +#include <cstring> + +namespace ZEngine::UI +{ + using namespace ZEngine::Core::Memory; + + // Constants matching ImGui's geometry quality + static constexpr float kPI = 3.14159265358979f; + static constexpr float kCircleMaxError = 0.30f; // px + static constexpr int kArcFastSize = 48; // unit circle LUT size + + // 48-sample unit circle LUT (precomputed, matches ImGui ArcFastVtx) + static float s_CircleLUTX[kArcFastSize]; + static float s_CircleLUTY[kArcFastSize]; + static bool s_CircleLUTInited = false; + + static void InitCircleLUT() + { + if (s_CircleLUTInited) + return; + for (int i = 0; i < kArcFastSize; ++i) + { + float a = (float) i * (2.f * kPI) / (float) kArcFastSize; + s_CircleLUTX[i] = cosf(a); + s_CircleLUTY[i] = sinf(a); + } + s_CircleLUTInited = true; + } + + // Compute segment count for a circle of given radius + static int CircleSegments(float radius) + { + if (radius <= 0.f) + return 4; + int n = (int) ceilf(kPI / acosf(1.f - kCircleMaxError / radius)); + if (n < 4) + n = 4; + if (n > 512) + n = 512; + return n & ~1; // round to even + } + + // Internal allocation helpers + + static void GrowVtx(ZUIDrawList* dl, ArenaAllocator* arena, uint32_t needed) + { + if (dl->VtxCount + needed <= dl->VtxCapacity) + return; + uint32_t new_cap = dl->VtxCapacity ? dl->VtxCapacity * 2 : 4096; + while (new_cap < dl->VtxCount + needed) + new_cap *= 2; + ZUIDrawVtx* nb = ZPushArray(arena, ZUIDrawVtx, new_cap); + if (dl->Vtx) + memcpy(nb, dl->Vtx, dl->VtxCount * sizeof(ZUIDrawVtx)); + dl->Vtx = nb; + dl->VtxCapacity = new_cap; + } + + static void GrowIdx(ZUIDrawList* dl, ArenaAllocator* arena, uint32_t needed) + { + if (dl->IdxCount + needed <= dl->IdxCapacity) + return; + uint32_t new_cap = dl->IdxCapacity ? dl->IdxCapacity * 2 : 8192; + while (new_cap < dl->IdxCount + needed) + new_cap *= 2; + uint16_t* nb = ZPushArray(arena, uint16_t, new_cap); + if (dl->Idx) + memcpy(nb, dl->Idx, dl->IdxCount * sizeof(uint16_t)); + dl->Idx = nb; + dl->IdxCapacity = new_cap; + } + + static void GrowPath(ZUIDrawList* dl, ArenaAllocator* arena, uint32_t needed) + { + if (dl->PathCount + needed <= dl->PathCap) + return; + uint32_t new_cap = dl->PathCap ? dl->PathCap * 2 : 256; + while (new_cap < dl->PathCount + needed) + new_cap *= 2; + float* nx = ZPushArray(arena, float, new_cap); + float* ny = ZPushArray(arena, float, new_cap); + if (dl->PathX) + { + memcpy(nx, dl->PathX, dl->PathCount * sizeof(float)); + memcpy(ny, dl->PathY, dl->PathCount * sizeof(float)); + } + dl->PathX = nx; + dl->PathY = ny; + dl->PathCap = new_cap; + } + + // We store the arena for growth in the draw list calls. + // For simplicity thread it as a file-global (ZUIContext owns a single DL per frame). + static ArenaAllocator* s_Arena = nullptr; + + // Lifecycle + + void ZUIDrawListInit(ZUIDrawList* dl, ArenaAllocator* frame_arena, uint32_t vtx_cap, uint32_t idx_cap, float white_u, float white_v, uint32_t atlas_idx) + { + InitCircleLUT(); + s_Arena = frame_arena; + dl->VtxCapacity = vtx_cap; + dl->IdxCapacity = idx_cap; + dl->Vtx = vtx_cap ? ZPushArray(frame_arena, ZUIDrawVtx, vtx_cap) : nullptr; + dl->Idx = idx_cap ? ZPushArray(frame_arena, uint16_t, idx_cap) : nullptr; + dl->CmdCapacity = 512; + dl->Cmds = ZPushArray(frame_arena, ZUIDrawListCmd, dl->CmdCapacity); + dl->PathCap = 256; + dl->PathX = ZPushArray(frame_arena, float, dl->PathCap); + dl->PathY = ZPushArray(frame_arena, float, dl->PathCap); + dl->WhiteU = white_u; + dl->WhiteV = white_v; + dl->AtlasTexIdx = atlas_idx; + dl->FringeScale = 1.0f; + ZUIDrawListReset(dl); + } + + void ZUIDrawListReset(ZUIDrawList* dl) + { + dl->VtxCount = 0; + dl->IdxCount = 0; + dl->CmdCount = 0; + dl->PathCount = 0; + dl->ClipDepth = 0; + // Open a default command with no clip + if (dl->Cmds && dl->CmdCapacity > 0) + { + dl->CmdCount = 1; + dl->Cmds[0] = {}; + dl->Cmds[0].TexIdx = dl->AtlasTexIdx; + } + } + + // Ensure current command matches clip rect + tex; open new cmd if needed + + static void EnsureCmd(ZUIDrawList* dl, float cx0, float cy0, float cx1, float cy1, uint32_t tex_idx) + { + if (dl->CmdCount == 0) + { + if (dl->CmdCount >= dl->CmdCapacity) + return; + dl->Cmds[dl->CmdCount++] = {}; + } + ZUIDrawListCmd& cur = dl->Cmds[dl->CmdCount - 1]; + bool same = (cur.ClipX == cx0 && cur.ClipY == cy0 && cur.ClipW == cx1 - cx0 && cur.ClipH == cy1 - cy0 && cur.TexIdx == tex_idx); + if (same) + return; + // Open new cmd + if (dl->CmdCount >= dl->CmdCapacity) + return; + ZUIDrawListCmd nc = {}; + nc.ClipX = cx0; + nc.ClipY = cy0; + nc.ClipW = cx1 - cx0; + nc.ClipH = cy1 - cy0; + nc.TexIdx = tex_idx; + nc.IdxOffset = dl->IdxCount; + nc.ElemCount = 0; + dl->Cmds[dl->CmdCount++] = nc; + } + + static void GetCurrentClip(const ZUIDrawList* dl, float& x0, float& y0, float& x1, float& y1) + { + if (dl->ClipDepth > 0) + { + const float* r = dl->ClipStack[dl->ClipDepth - 1]; + x0 = r[0]; + y0 = r[1]; + x1 = r[2]; + y1 = r[3]; + } + else + { + x0 = -1e9f; + y0 = -1e9f; + x1 = 1e9f; + y1 = 1e9f; + } + } + + static void FlushCmd(ZUIDrawList* dl) + { + float cx0, cy0, cx1, cy1; + GetCurrentClip(dl, cx0, cy0, cx1, cy1); + EnsureCmd(dl, cx0, cy0, cx1, cy1, dl->AtlasTexIdx); + } + + // Clip rect stack + + void ZUIDrawListPushClipRect(ZUIDrawList* dl, float x0, float y0, float x1, float y1, bool intersect) + { + if (intersect && dl->ClipDepth > 0) + { + const float* prev = dl->ClipStack[dl->ClipDepth - 1]; + if (x0 < prev[0]) + x0 = prev[0]; + if (y0 < prev[1]) + y0 = prev[1]; + if (x1 > prev[2]) + x1 = prev[2]; + if (y1 > prev[3]) + y1 = prev[3]; + } + if (dl->ClipDepth < ZUIDrawList::kMaxClipDepth) + { + float* slot = dl->ClipStack[dl->ClipDepth++]; + slot[0] = x0; + slot[1] = y0; + slot[2] = x1; + slot[3] = y1; + } + FlushCmd(dl); + } + + void ZUIDrawListPopClipRect(ZUIDrawList* dl) + { + if (dl->ClipDepth > 0) + --dl->ClipDepth; + FlushCmd(dl); + } + + // Primitive reservation + + // Reserve `vtx` vertices and `idx` indices; return write pointer. + // Caller must write exactly that many. + static ZUIDrawVtx* PrimReserve(ZUIDrawList* dl, uint32_t vtx, uint32_t idx) + { + FlushCmd(dl); + GrowVtx(dl, s_Arena, vtx); + GrowIdx(dl, s_Arena, idx); + ZUIDrawVtx* vw = dl->Vtx + dl->VtxCount; + uint16_t* iw = dl->Idx + dl->IdxCount; + uint16_t base = (uint16_t) dl->VtxCount; + dl->VtxCount += vtx; + dl->IdxCount += idx; + // Update current cmd elem count + dl->Cmds[dl->CmdCount - 1].ElemCount += idx; + return vw; + (void) iw; // caller writes indices via separate helpers + } + + // Fast flat colored rect (no AA, no rounding) — 4 vtx, 6 idx + + void ZUIDrawListAddRectFilledNoAA(ZUIDrawList* dl, float x0, float y0, float x1, float y1, uint32_t col) + { + FlushCmd(dl); + GrowVtx(dl, s_Arena, 4); + GrowIdx(dl, s_Arena, 6); + uint16_t base = (uint16_t) dl->VtxCount; + ZUIDrawVtx* v = dl->Vtx + dl->VtxCount; + uint16_t* i = dl->Idx + dl->IdxCount; + v[0] = {x0, y0, dl->WhiteU, dl->WhiteV, col}; + v[1] = {x1, y0, dl->WhiteU, dl->WhiteV, col}; + v[2] = {x1, y1, dl->WhiteU, dl->WhiteV, col}; + v[3] = {x0, y1, dl->WhiteU, dl->WhiteV, col}; + i[0] = base; + i[1] = (uint16_t) (base + 1); + i[2] = (uint16_t) (base + 2); + i[3] = base; + i[4] = (uint16_t) (base + 2); + i[5] = (uint16_t) (base + 3); + dl->VtxCount += 4; + dl->IdxCount += 6; + dl->Cmds[dl->CmdCount - 1].ElemCount += 6; + } + + void ZUIDrawListAddRectFilledMultiColor(ZUIDrawList* dl, float x0, float y0, float x1, float y1, uint32_t col_tl, uint32_t col_tr, uint32_t col_bl, uint32_t col_br) + { + FlushCmd(dl); + GrowVtx(dl, s_Arena, 4); + GrowIdx(dl, s_Arena, 6); + uint16_t base = (uint16_t) dl->VtxCount; + ZUIDrawVtx* v = dl->Vtx + dl->VtxCount; + uint16_t* i = dl->Idx + dl->IdxCount; + float wu = dl->WhiteU, wv = dl->WhiteV; + v[0] = {x0, y0, wu, wv, col_tl}; + v[1] = {x1, y0, wu, wv, col_tr}; + v[2] = {x1, y1, wu, wv, col_br}; + v[3] = {x0, y1, wu, wv, col_bl}; + i[0] = base; + i[1] = (uint16_t) (base + 1); + i[2] = (uint16_t) (base + 2); + i[3] = base; + i[4] = (uint16_t) (base + 2); + i[5] = (uint16_t) (base + 3); + dl->VtxCount += 4; + dl->IdxCount += 6; + dl->Cmds[dl->CmdCount - 1].ElemCount += 6; + } + + // Path helpers + + static void PathClear(ZUIDrawList* dl) + { + dl->PathCount = 0; + } + + static void PathLineTo(ZUIDrawList* dl, float x, float y) + { + GrowPath(dl, s_Arena, 1); + dl->PathX[dl->PathCount] = x; + dl->PathY[dl->PathCount] = y; + ++dl->PathCount; + } + + // Append arc to path using the fast 48-sample LUT. + // angle_min/max in turns [0..1], a_min_sample/a_max_sample in LUT indices. + static void PathArcToFast(ZUIDrawList* dl, float cx, float cy, float r, int a_min, int a_max) + { + if (r <= 0.f || a_min > a_max) + { + PathLineTo(dl, cx, cy); + return; + } + GrowPath(dl, s_Arena, a_max - a_min + 1); + for (int i = a_min; i <= a_max; ++i) + { + int idx = i % kArcFastSize; + dl->PathX[dl->PathCount] = cx + s_CircleLUTX[idx] * r; + dl->PathY[dl->PathCount] = cy + s_CircleLUTY[idx] * r; + ++dl->PathCount; + } + } + + // Append rounded rect path. round_flags: bit 0=TL 1=TR 2=BR 3=BL + static void PathRect(ZUIDrawList* dl, float x0, float y0, float x1, float y1, float r, uint32_t flags = 0xF) + { + if (r < 0.5f) + { + flags = 0; + } + bool tl = (flags & 1) != 0; + bool tr = (flags & 2) != 0; + bool br = (flags & 4) != 0; + bool bl = (flags & 8) != 0; + // clamp radius + float half = (x1 - x0 < y1 - y0 ? x1 - x0 : y1 - y0) * 0.5f; + if (r > half) + r = half; + // 48-step LUT indices matching ImGui's 12-step convention (multiply by 4): + // TL: 24-36 (180°→270° = left→up), TR: 36-48 (270°→360° = up→right) + // BR: 0-12 ( 0°→ 90° = right→down), BL: 12-24 ( 90°→180° = down→left) + if (tl) + { + PathArcToFast(dl, x0 + r, y0 + r, r, 24, 36); + } + else + PathLineTo(dl, x0, y0); + if (tr) + { + PathArcToFast(dl, x1 - r, y0 + r, r, 36, 48); + } + else + PathLineTo(dl, x1, y0); + if (br) + { + PathArcToFast(dl, x1 - r, y1 - r, r, 0, 12); + } + else + PathLineTo(dl, x1, y1); + if (bl) + { + PathArcToFast(dl, x0 + r, y1 - r, r, 12, 24); + } + else + PathLineTo(dl, x0, y1); + } + + // PathFillConvex — AA filled polygon (ImGui AddConvexPolyFilled) + static void PathFillConvex(ZUIDrawList* dl, uint32_t col) + { + int n = (int) dl->PathCount; + if (n < 3) + { + PathClear(dl); + return; + } + + float fs = dl->FringeScale; + uint32_t col_trans = col & 0x00FFFFFFu; // alpha = 0 + + FlushCmd(dl); + GrowVtx(dl, s_Arena, (uint32_t) (n * 2)); + GrowIdx(dl, s_Arena, (uint32_t) ((n - 2) * 3 + n * 6)); + + uint16_t base = (uint16_t) dl->VtxCount; + ZUIDrawVtx* vw = dl->Vtx + dl->VtxCount; + uint16_t* iw = dl->Idx + dl->IdxCount; + float wu = dl->WhiteU, wv = dl->WhiteV; + + // Compute per-vertex normals (dm = miter direction) + // dm[i] = average of normals of edge (i-1,i) and edge (i,i+1) + GrowPath(dl, s_Arena, n); // temp space for dm_x, dm_y reusing path + + float* dmx = dl->PathX + dl->PathCount; // NOT modifying PathCount, using tail as scratch + float* dmy = dl->PathY + dl->PathCount; + // (ensure capacity) + if (dl->PathCount + (uint32_t) n > dl->PathCap) + { + GrowPath(dl, s_Arena, n); + dmx = dl->PathX + dl->PathCount; + dmy = dl->PathY + dl->PathCount; + } + + for (int i = 0; i < n; ++i) + { + int ni = (i + 1) % n; + float ex = dl->PathX[ni] - dl->PathX[i]; + float ey = dl->PathY[ni] - dl->PathY[i]; + float len = sqrtf(ex * ex + ey * ey); + if (len > 1e-6f) + { + ex /= len; + ey /= len; + } + // Store edge normal (rotated 90° inward) + dmx[i] = ey; + dmy[i] = -ex; + } + // Average adjacent normals + for (int i = 0; i < n; ++i) + { + int pi = (i + n - 1) % n; + float avg_x = (dmx[i] + dmx[pi]) * 0.5f; + float avg_y = (dmy[i] + dmy[pi]) * 0.5f; + float dot = avg_x * avg_x + avg_y * avg_y; + if (dot > 1e-9f) + { + float inv = 1.f / dot; + if (inv > 100.f) + inv = 100.f; // match ImGui IM_FIXNORMAL2F_MAX_INVLEN2 + avg_x *= inv; + avg_y *= inv; + } + dmx[i] = avg_x * fs * 0.5f; + dmy[i] = avg_y * fs * 0.5f; + } + + // Emit vertices: inner (col) + outer fringe (col_trans) + for (int i = 0; i < n; ++i) + { + float px = dl->PathX[i], py = dl->PathY[i]; + vw[i * 2 + 0] = {px - dmx[i], py - dmy[i], wu, wv, col}; + vw[i * 2 + 1] = {px + dmx[i], py + dmy[i], wu, wv, col_trans}; + } + dl->VtxCount += (uint32_t) (n * 2); + + // Fill indices: fan over inner vertices + uint32_t fill_idx = (uint32_t) ((n - 2) * 3); + for (int i = 2; i < n; ++i) + { + iw[0] = base; + iw[1] = (uint16_t) (base + (uint16_t) ((i - 1) * 2)); + iw[2] = (uint16_t) (base + (uint16_t) (i * 2)); + iw += 3; + } + // Fringe quads + for (int i = 0; i < n; ++i) + { + int ni = (i + 1) % n; + uint16_t i0 = (uint16_t) (base + (uint16_t) (i * 2)); + uint16_t i1 = (uint16_t) (base + (uint16_t) (i * 2 + 1)); + uint16_t i2 = (uint16_t) (base + (uint16_t) (ni * 2 + 1)); + uint16_t i3 = (uint16_t) (base + (uint16_t) (ni * 2)); + iw[0] = i0; + iw[1] = i1; + iw[2] = i2; + iw[3] = i0; + iw[4] = i2; + iw[5] = i3; + iw += 6; + } + uint32_t total_idx = (uint32_t) (fill_idx + (uint32_t) (n * 6)); + dl->IdxCount += total_idx; + dl->Cmds[dl->CmdCount - 1].ElemCount += total_idx; + + PathClear(dl); + } + + // PathStroke — AA stroked polyline (ImGui AddPolyline) + // closed: whether last point connects back to first + static void PathStroke(ZUIDrawList* dl, uint32_t col, bool closed, float thickness) + { + int n = (int) dl->PathCount; + int count = closed ? n : n - 1; + if (count <= 0 || n < 2) + { + PathClear(dl); + return; + } + + float fs = dl->FringeScale; + float half = thickness * 0.5f; + uint32_t col_trans = col & 0x00FFFFFFu; + float wu = dl->WhiteU, wv = dl->WhiteV; + + // Pass 1: compute per-segment left normals (outward = left of direction) + // Matches ImGui AddPolyline temp_normals[] pattern exactly. + GrowPath(dl, s_Arena, (uint32_t) (n)); // use path tail as scratch for normals + float* snx = dl->PathX + dl->PathCount; // segment normal X (scratch after path pts) + float* sny = dl->PathY + dl->PathCount; + // Ensure capacity for 2×n extra floats in path scratch + if (dl->PathCount + (uint32_t) (n * 2) > dl->PathCap) + { + GrowPath(dl, s_Arena, (uint32_t) (n * 2)); + snx = dl->PathX + dl->PathCount; + sny = dl->PathY + dl->PathCount; + } + + for (int i = 0; i < n; ++i) + { + int j = (i + 1) < n ? i + 1 : 0; // next index (wraps for closed) + float dx = dl->PathX[j] - dl->PathX[i]; + float dy = dl->PathY[j] - dl->PathY[i]; + float len = sqrtf(dx * dx + dy * dy); + if (len < 1e-6f) + len = 1.f; + snx[i] = dy / len; // left normal + sny[i] = -dx / len; + } + // For open lines: copy last segment normal to last vertex + // (endpoint inherits its only adjacent segment, not the phantom wrap) + if (!closed) + { + snx[n - 1] = snx[n - 2]; + sny[n - 1] = sny[n - 2]; + } + + // Pass 2: per vertex — average adjacent segment normals (miter), + // apply IM_FIXNORMAL2F with 100.0 cap, split into inner + outer fringe. + // 4 vertices per point: outer-fringe-left, inner-left, inner-right, outer-fringe-right + FlushCmd(dl); + GrowVtx(dl, s_Arena, (uint32_t) (n * 4)); + GrowIdx(dl, s_Arena, (uint32_t) (count * 18)); + + uint16_t base = (uint16_t) dl->VtxCount; + ZUIDrawVtx* vw = dl->Vtx + dl->VtxCount; + uint16_t* iw = dl->Idx + dl->IdxCount; + + for (int i = 0; i < n; ++i) + { + // Average with previous segment normal (miter) + int prev = closed ? (i + n - 1) % n : (i > 0 ? i - 1 : 0); + float dm_x = (snx[prev] + snx[i]) * 0.5f; + float dm_y = (sny[prev] + sny[i]) * 0.5f; + + // IM_FIXNORMAL2F: scale by 1/|dm|² capped at 100 to handle sharp angles + float d2 = dm_x * dm_x + dm_y * dm_y; + if (d2 > 1e-6f) + { + float inv = 1.f / d2; + if (inv > 100.f) + inv = 100.f; + dm_x *= inv; + dm_y *= inv; + } + + float x = dl->PathX[i], y = dl->PathY[i]; + float ox = dm_x * (half + fs), oy = dm_y * (half + fs); // outer (fringe) + float ix = dm_x * half, iy = dm_y * half; // inner + + vw[i * 4 + 0] = {x + ox, y + oy, wu, wv, col_trans}; // outer-left AA + vw[i * 4 + 1] = {x + ix, y + iy, wu, wv, col}; // inner-left + vw[i * 4 + 2] = {x - ix, y - iy, wu, wv, col}; // inner-right + vw[i * 4 + 3] = {x - ox, y - oy, wu, wv, col_trans}; // outer-right AA + } + dl->VtxCount += (uint32_t) (n * 4); + + // Indices: 3 quads (18 indices) per segment + for (int i1 = 0; i1 < count; ++i1) + { + int i2 = (i1 + 1) < n ? i1 + 1 : 0; + uint16_t b1 = (uint16_t) (base + (uint16_t) (i1 * 4)); + uint16_t b2 = (uint16_t) (base + (uint16_t) (i2 * 4)); + // left fringe quad + iw[0] = b1; + iw[1] = (uint16_t) (b1 + 1); + iw[2] = (uint16_t) (b2 + 1); + iw[3] = b1; + iw[4] = (uint16_t) (b2 + 1); + iw[5] = b2; + // fill quad + iw[6] = (uint16_t) (b1 + 1); + iw[7] = (uint16_t) (b1 + 2); + iw[8] = (uint16_t) (b2 + 2); + iw[9] = (uint16_t) (b1 + 1); + iw[10] = (uint16_t) (b2 + 2); + iw[11] = (uint16_t) (b2 + 1); + // right fringe quad + iw[12] = (uint16_t) (b1 + 2); + iw[13] = (uint16_t) (b1 + 3); + iw[14] = (uint16_t) (b2 + 3); + iw[15] = (uint16_t) (b1 + 2); + iw[16] = (uint16_t) (b2 + 3); + iw[17] = (uint16_t) (b2 + 2); + iw += 18; + } + uint32_t total_idx = (uint32_t) (count * 18); + dl->IdxCount += total_idx; + dl->Cmds[dl->CmdCount - 1].ElemCount += total_idx; + + PathClear(dl); + } + + // Public shape functions + + void ZUIDrawListAddLine(ZUIDrawList* dl, float x0, float y0, float x1, float y1, uint32_t col, float thickness) + { + PathLineTo(dl, x0 + 0.5f, y0 + 0.5f); + PathLineTo(dl, x1 + 0.5f, y1 + 0.5f); + PathStroke(dl, col, false, thickness); + } + + void ZUIDrawListAddChevronDown(ZUIDrawList* dl, float cx, float cy, float half_w, float half_h, uint32_t col, float thickness) + { + PathLineTo(dl, cx - half_w + 0.5f, cy - half_h + 0.5f); // top-left + PathLineTo(dl, cx + 0.5f, cy + half_h + 0.5f); // apex (bottom-center) + PathLineTo(dl, cx + half_w + 0.5f, cy - half_h + 0.5f); // top-right + PathStroke(dl, col, false, thickness); + } + + void ZUIDrawListAddChevronRight(ZUIDrawList* dl, float cx, float cy, float half_w, float half_h, uint32_t col, float thickness) + { + // 90° CW rotation of ChevronDown: half_w → vertical span, half_h → horizontal span + PathLineTo(dl, cx - half_h + 0.5f, cy - half_w + 0.5f); // top-left + PathLineTo(dl, cx + half_h + 0.5f, cy + 0.5f); // apex (right-center) + PathLineTo(dl, cx - half_h + 0.5f, cy + half_w + 0.5f); // bottom-left + PathStroke(dl, col, false, thickness); + } + + void ZUIDrawListAddPolylineFilled(ZUIDrawList* dl, const float* xs, const float* ys, int n, uint32_t col) + { + for (int i = 0; i < n; ++i) + PathLineTo(dl, xs[i], ys[i]); + PathFillConvex(dl, col); + } + + void ZUIDrawListAddRectFilled(ZUIDrawList* dl, float x0, float y0, float x1, float y1, uint32_t col, float rounding, uint32_t round_flags) + { + if ((col >> 24) == 0) + { + PathClear(dl); + return; + } + if (rounding < 0.5f) + { + ZUIDrawListAddRectFilledNoAA(dl, x0, y0, x1, y1, col); + return; + } + // Nudge inward by 0.5px like ImGui (pixel-center convention) + PathRect(dl, x0 + 0.5f, y0 + 0.5f, x1 - 0.5f, y1 - 0.5f, rounding, round_flags); + PathFillConvex(dl, col); + } + + void ZUIDrawListAddRect(ZUIDrawList* dl, float x0, float y0, float x1, float y1, uint32_t col, float rounding, uint32_t round_flags, float thickness) + { + if ((col >> 24) == 0) + { + PathClear(dl); + return; + } + PathRect(dl, x0 + 0.5f, y0 + 0.5f, x1 - 0.5f, y1 - 0.5f, rounding, round_flags); + PathStroke(dl, col, true, thickness); + } + + void ZUIDrawListAddCircleFilled(ZUIDrawList* dl, float cx, float cy, float r, uint32_t col, int num_segments) + { + if ((col >> 24) == 0 || r <= 0.f) + return; + int n = (num_segments > 0) ? num_segments : CircleSegments(r); + float step = 2.f * kPI / (float) n; + for (int i = 0; i < n; ++i) + PathLineTo(dl, cx + cosf(i * step) * r, cy + sinf(i * step) * r); + PathFillConvex(dl, col); + } + + void ZUIDrawListAddCircle(ZUIDrawList* dl, float cx, float cy, float r, uint32_t col, int num_segments, float thickness) + { + if ((col >> 24) == 0 || r <= 0.f) + return; + int n = (num_segments > 0) ? num_segments : CircleSegments(r); + float step = 2.f * kPI / (float) n; + for (int i = 0; i < n; ++i) + PathLineTo(dl, cx + cosf(i * step) * (r - 0.5f), cy + sinf(i * step) * (r - 0.5f)); + PathStroke(dl, col, true, thickness); + } + + void ZUIDrawListAddTriangleFilled(ZUIDrawList* dl, float ax, float ay, float bx, float by, float cx, float cy, uint32_t col) + { + PathLineTo(dl, ax, ay); + PathLineTo(dl, bx, by); + PathLineTo(dl, cx, cy); + PathFillConvex(dl, col); + } + + void ZUIDrawListAddImage(ZUIDrawList* dl, uint32_t tex_idx, float x0, float y0, float x1, float y1, float u0, float v0, float u1, float v1, uint32_t col) + { + // Switch texture for this draw + float cx0, cy0, cx1, cy1; + GetCurrentClip(dl, cx0, cy0, cx1, cy1); + EnsureCmd(dl, cx0, cy0, cx1, cy1, tex_idx); + + GrowVtx(dl, s_Arena, 4); + GrowIdx(dl, s_Arena, 6); + uint16_t base = (uint16_t) dl->VtxCount; + ZUIDrawVtx* v = dl->Vtx + dl->VtxCount; + uint16_t* i = dl->Idx + dl->IdxCount; + v[0] = {x0, y0, u0, v0, col}; + v[1] = {x1, y0, u1, v0, col}; + v[2] = {x1, y1, u1, v1, col}; + v[3] = {x0, y1, u0, v1, col}; + i[0] = base; + i[1] = (uint16_t) (base + 1); + i[2] = (uint16_t) (base + 2); + i[3] = base; + i[4] = (uint16_t) (base + 2); + i[5] = (uint16_t) (base + 3); + dl->VtxCount += 4; + dl->IdxCount += 6; + dl->Cmds[dl->CmdCount - 1].ElemCount += 6; + + // Restore atlas texture + EnsureCmd(dl, cx0, cy0, cx1, cy1, dl->AtlasTexIdx); + } + +} // namespace ZEngine::UI diff --git a/ZEngine/ZEngine/UI/ZUIDrawList.h b/ZEngine/ZEngine/UI/ZUIDrawList.h new file mode 100644 index 000000000..bc84a364c --- /dev/null +++ b/ZEngine/ZEngine/UI/ZUIDrawList.h @@ -0,0 +1,129 @@ +#pragma once +#include <ZEngine/Core/Memory/Allocator.h> +#include <cstdint> + +// ZUIDrawList — immediate-mode vector draw list. +// +// Mirrors ImDrawList's architecture: +// - VtxBuffer / IdxBuffer → flat GPU-ready buffers +// - CmdBuffer → scissor/texture batches +// - _Path → scratch path points (CPU only) +// +// AA fringe width = FringeScale (1.0 logical px, matches ImGui). +// Solid-color fills use the atlas white pixel (WhiteU/WhiteV). + +namespace ZEngine::UI +{ + + // Vertex — 20 bytes, matches zui_draw.vert attribute layout + struct ZUIDrawVtx + { + float x, y; // screen position (logical px) + float u, v; // atlas UV + uint32_t col; // RGBA8 packed (R in low byte) + }; + static_assert(sizeof(ZUIDrawVtx) == 20, "ZUIDrawVtx size mismatch"); + + // Draw command — one scissored draw call. + struct ZUIDrawListCmd + { + uint32_t IdxOffset = 0; // first index in IdxBuffer + uint32_t ElemCount = 0; // index count for this call + float ClipX = 0.f; // scissor rect (logical px) + float ClipY = 0.f; + float ClipW = 0.f; + float ClipH = 0.f; + uint32_t TexIdx = 0; // bindless texture array index + }; + + // ZUIDrawList + struct ZUIDrawList + { + // GPU-bound buffers (allocated from FrameArena each frame) + ZUIDrawVtx* Vtx = nullptr; + uint32_t VtxCount = 0; + uint32_t VtxCapacity = 0; + + uint16_t* Idx = nullptr; + uint32_t IdxCount = 0; + uint32_t IdxCapacity = 0; + + // Scissor command buffer + ZUIDrawListCmd* Cmds = nullptr; + uint32_t CmdCount = 0; + uint32_t CmdCapacity = 0; + + // CPU-only path scratch (not sent to GPU) + float* PathX = nullptr; + float* PathY = nullptr; + uint32_t PathCount = 0; + uint32_t PathCap = 0; + + // Clip rect stack + static constexpr int kMaxClipDepth = 16; + float ClipStack[kMaxClipDepth][4] = {}; // [x0,y0,x1,y1] + int ClipDepth = 0; + + // AA fringe width — 1.0f = 1 logical pixel (matches ImGui) + float FringeScale = 1.0f; + + // White pixel UV for solid-color fills + float WhiteU = 0.f; + float WhiteV = 0.f; + + // Active texture index (atlas slot) + uint32_t AtlasTexIdx = 0; + }; + + // Lifecycle + void ZUIDrawListInit(ZUIDrawList* dl, ZEngine::Core::Memory::ArenaAllocator* frame_arena, uint32_t vtx_cap, uint32_t idx_cap, float white_u, float white_v, uint32_t atlas_idx); + void ZUIDrawListReset(ZUIDrawList* dl); + + // Clip rect stack + void ZUIDrawListPushClipRect(ZUIDrawList* dl, float x0, float y0, float x1, float y1, bool intersect_with_current = true); + void ZUIDrawListPopClipRect(ZUIDrawList* dl); + + // Shape primitives + void ZUIDrawListAddLine(ZUIDrawList* dl, float x0, float y0, float x1, float y1, uint32_t col, float thickness = 1.f); + /// @brief Draw a VS Code-style "∨" down chevron (combo open, section expanded). + void ZUIDrawListAddChevronDown(ZUIDrawList* dl, float cx, float cy, float half_w, float half_h, uint32_t col, float thickness = 1.5f); + /// @brief Draw a VS Code-style "›" right chevron (section collapsed). + void ZUIDrawListAddChevronRight(ZUIDrawList* dl, float cx, float cy, float half_w, float half_h, uint32_t col, float thickness = 1.5f); + + // Anti-aliased filled convex polygon from an array of (x,y) pairs. + void ZUIDrawListAddPolylineFilled(ZUIDrawList* dl, const float* xs, const float* ys, int n, uint32_t col); + + void ZUIDrawListAddRectFilled(ZUIDrawList* dl, float x0, float y0, float x1, float y1, uint32_t col, float rounding = 0.f, uint32_t round_flags = 0xF); + + // Stroked (outline) rect + void ZUIDrawListAddRect(ZUIDrawList* dl, float x0, float y0, float x1, float y1, uint32_t col, float rounding = 0.f, uint32_t round_flags = 0xF, float thickness = 1.f); + + void ZUIDrawListAddCircleFilled(ZUIDrawList* dl, float cx, float cy, float r, uint32_t col, int num_segments = 0); + + void ZUIDrawListAddCircle(ZUIDrawList* dl, float cx, float cy, float r, uint32_t col, int num_segments = 0, float thickness = 1.f); + + void ZUIDrawListAddTriangleFilled(ZUIDrawList* dl, float ax, float ay, float bx, float by, float cx, float cy, uint32_t col); + + // Textured quad (for glyph rendering) + void ZUIDrawListAddImage(ZUIDrawList* dl, uint32_t tex_idx, float x0, float y0, float x1, float y1, float u0, float v0, float u1, float v1, uint32_t col); + + // Flat (no AA) colored rect — fast path for solid opaque rects + void ZUIDrawListAddRectFilledNoAA(ZUIDrawList* dl, float x0, float y0, float x1, float y1, uint32_t col); + + // Per-corner colored rect (gradient quad) — used for per-corner color boxes + void ZUIDrawListAddRectFilledMultiColor(ZUIDrawList* dl, float x0, float y0, float x1, float y1, uint32_t col_tl, uint32_t col_tr, uint32_t col_bl, uint32_t col_br); + + // Colour helpers + // Pack linear float[4] RGBA → uint32_t RGBA8 (matches shader unpackUnorm4x8) + inline uint32_t ZUIPackColor(const float c[4]) + { + auto clamp01 = [](float v) -> uint8_t { return v <= 0.f ? 0 : v >= 1.f ? 255 : (uint8_t) (v * 255.f + 0.5f); }; + return (uint32_t) clamp01(c[0]) | ((uint32_t) clamp01(c[1]) << 8) | ((uint32_t) clamp01(c[2]) << 16) | ((uint32_t) clamp01(c[3]) << 24); + } + inline uint32_t ZUIPackColor(float r, float g, float b, float a) + { + float c[4] = {r, g, b, a}; + return ZUIPackColor(c); + } + +} // namespace ZEngine::UI diff --git a/ZEngine/ZEngine/UI/ZUIFont.cpp b/ZEngine/ZEngine/UI/ZUIFont.cpp new file mode 100644 index 000000000..0b1011280 --- /dev/null +++ b/ZEngine/ZEngine/UI/ZUIFont.cpp @@ -0,0 +1,267 @@ +// stb_rect_pack is kept for atlas layout; stb_truetype replaced by FreeType. +#define STB_RECT_PACK_IMPLEMENTATION +#include <ZEngine/Core/Containers/Array.h> +#include <ZEngine/Core/VFS/IVFSFile.h> +#include <ZEngine/Core/VFS/VFSPath.h> +#include <ZEngine/Engine.h> +#include <ZEngine/Hardwares/VulkanDevice.h> +#include <ZEngine/Rendering/RenderResourceManager.h> +#include <ZEngine/UI/ZUIFont.h> +#include <ft2build.h> +#include <stb/stb_rect_pack.h> +#include FT_FREETYPE_H + +using namespace ZEngine::Core::VFS; +using namespace ZEngine::Core::Containers; +using namespace ZEngine::Rendering; + +namespace ZEngine::UI +{ + using namespace ZEngine::Core::Memory; + + // Load a TTF file from the VFS into temp_arena. Returns nullptr on failure. + static uint8_t* LoadTTFFromVFS(const char* vfs_path, ArenaAllocator* temp_arena, uint64_t* out_size) + { + auto* vfs = Engine::GetContext()->VFS; + auto path_res = VFSPath::Parse(vfs_path); + if (!path_res.Succeeded()) + return nullptr; + + auto file_res = vfs->Open(path_res.Value(), VFSOpenFlags::Read); + if (!file_res.Succeeded()) + return nullptr; + + auto* file = file_res.Value(); + auto size_res = file->Size(); + if (!size_res.Succeeded()) + { + vfs->Close(file); + return nullptr; + } + + uint64_t sz = size_res.Value(); + uint8_t* data = ZPushArray(temp_arena, uint8_t, (uint32_t) sz); + ArrayView<uint8_t> view{data, sz}; + file->ReadAll(view); + vfs->Close(file); + + if (out_size) + *out_size = sz; + return data; + } + + ZUIFontAtlas* ZUIFontAtlasBake(ArenaAllocator* persistent_arena, ArenaAllocator* temp_arena, Hardwares::VulkanDevice* device, const char* vfs_path, float size_small, float size_body, float size_header, uint32_t first_codepoint, uint32_t codepoint_count, const char* header_vfs_path) + { + // 1. Load TTF data + uint64_t ttf_size = 0; + uint8_t* ttf_data = LoadTTFFromVFS(vfs_path, temp_arena, &ttf_size); + if (!ttf_data) + return nullptr; + + uint64_t hdr_size = ttf_size; + uint8_t* hdr_ttf_data = ttf_data; + if (header_vfs_path) + { + uint8_t* hdr = LoadTTFFromVFS(header_vfs_path, temp_arena, &hdr_size); + if (hdr) + hdr_ttf_data = hdr; + } + + // 2. Initialize FreeType + FT_Library ft_lib = nullptr; + if (FT_Init_FreeType(&ft_lib) != 0) + return nullptr; + + FT_Face ft_body = nullptr; + FT_Face ft_header = nullptr; + FT_New_Memory_Face(ft_lib, ttf_data, (FT_Long) ttf_size, 0, &ft_body); + if (hdr_ttf_data != ttf_data) + FT_New_Memory_Face(ft_lib, hdr_ttf_data, (FT_Long) hdr_size, 0, &ft_header); + else + ft_header = ft_body; + + const float kSizes[3] = {size_small, size_body, size_header}; + FT_Face kFaces[3] = {ft_body, ft_body, ft_header}; + const int kGlyphPad = 1; // 1-px border so bilinear sampling never bleeds + + // 3. Two-pass atlas packing + // Pass A: render each glyph to measure its bitmap dimensions. + // Pass B: render again into the atlas at stb_rect_pack positions. + const uint32_t kTotalGlyphs = 3 * codepoint_count; + stbrp_rect* rects = ZPushArray(temp_arena, stbrp_rect, kTotalGlyphs); + + // Pass A — measure + for (int fi = 0; fi < 3; ++fi) + { + FT_Set_Pixel_Sizes(kFaces[fi], 0, (FT_UInt) kSizes[fi]); + for (uint32_t gi = 0; gi < codepoint_count; ++gi) + { + uint32_t ri = (uint32_t) fi * codepoint_count + gi; + rects[ri].id = (int) ri; + FT_UInt glyph_idx = FT_Get_Char_Index(kFaces[fi], first_codepoint + gi); + if (glyph_idx == 0 || FT_Load_Glyph(kFaces[fi], glyph_idx, FT_LOAD_RENDER | FT_LOAD_FORCE_AUTOHINT) != 0) + { + rects[ri].w = rects[ri].h = 0; + continue; + } + rects[ri].w = (stbrp_coord) (kFaces[fi]->glyph->bitmap.width + 2 * kGlyphPad); + rects[ri].h = (stbrp_coord) (kFaces[fi]->glyph->bitmap.rows + 2 * kGlyphPad); + } + } + + // Pack + const uint32_t kAtlasW = 1024; + const uint32_t kAtlasH = 2048; + stbrp_context pack_ctx = {}; + stbrp_node* pack_nodes = ZPushArray(temp_arena, stbrp_node, kAtlasW); + stbrp_init_target(&pack_ctx, (int) kAtlasW, (int) kAtlasH, pack_nodes, (int) kAtlasW); + stbrp_pack_rects(&pack_ctx, rects, (int) kTotalGlyphs); + + // Pass B — render into atlas + uint8_t* atlas_px = ZPushArray(temp_arena, uint8_t, kAtlasW* kAtlasH); + // White texel at (0,0) — used by solid-color draws + atlas_px[0] = 255u; + + for (int fi = 0; fi < 3; ++fi) + { + FT_Set_Pixel_Sizes(kFaces[fi], 0, (FT_UInt) kSizes[fi]); + for (uint32_t gi = 0; gi < codepoint_count; ++gi) + { + uint32_t ri = (uint32_t) fi * codepoint_count + gi; + if (!rects[ri].was_packed || rects[ri].w == 0) + continue; + + FT_UInt glyph_idx = FT_Get_Char_Index(kFaces[fi], first_codepoint + gi); + if (glyph_idx == 0 || FT_Load_Glyph(kFaces[fi], glyph_idx, FT_LOAD_RENDER | FT_LOAD_FORCE_AUTOHINT) != 0) + continue; + + FT_Bitmap& bmp = kFaces[fi]->glyph->bitmap; + int dst_x = (int) rects[ri].x + kGlyphPad; + int dst_y = (int) rects[ri].y + kGlyphPad; + + for (int row = 0; row < (int) bmp.rows; ++row) + { + for (int col = 0; col < (int) bmp.width; ++col) + { + int dst = (dst_y + row) * (int) kAtlasW + (dst_x + col); + atlas_px[dst] = bmp.buffer[row * abs(bmp.pitch) + col]; + } + } + } + } + + // 4. Expand single-channel → RGBA8 (white text, alpha-masked) + uint8_t* rgba = ZPushArray(temp_arena, uint8_t, kAtlasW * kAtlasH * 4); + for (uint32_t i = 0; i < kAtlasW * kAtlasH; ++i) + { + rgba[i * 4 + 0] = 255; + rgba[i * 4 + 1] = 255; + rgba[i * 4 + 2] = 255; + rgba[i * 4 + 3] = atlas_px[i]; + } + + // 5. Upload to GPU + Rendering::Textures::TextureHandle gpu_handle = {}; + if (device->RRM) + { + auto* rrm = static_cast<RenderResourceManager*>(device->RRM); + gpu_handle = rrm->UploadFontAtlas(rgba, kAtlasW, kAtlasH); + } + device->TextureHandleToUpdates.Enqueue(gpu_handle); + + // 6. Build ZUIFontAtlas + ZUIFontAtlas* atlas = ZPushStruct(persistent_arena, ZUIFontAtlas); + atlas->Handle = gpu_handle; + atlas->Width = kAtlasW; + atlas->Height = kAtlasH; + atlas->WhiteU = 0.5f / (float) kAtlasW; + atlas->WhiteV = 0.5f / (float) kAtlasH; + + float inv_w = 1.f / (float) kAtlasW; + float inv_h = 1.f / (float) kAtlasH; + + ZUIFont** kDsts[3] = {&atlas->Small, &atlas->Body, &atlas->Header}; + + for (int fi = 0; fi < 3; ++fi) + { + FT_Set_Pixel_Sizes(kFaces[fi], 0, (FT_UInt) kSizes[fi]); + + ZUIFont* font = ZPushStruct(persistent_arena, ZUIFont); + font->Glyphs = ZPushArray(persistent_arena, ZUIGlyph, codepoint_count); + font->GlyphCount = codepoint_count; + font->FirstCodepoint = first_codepoint; + font->FontSize = kSizes[fi]; + + // FreeType metrics are in 26.6 fixed-point — shift right 6 to get pixels + font->Ascent = (float) (kFaces[fi]->size->metrics.ascender >> 6); + font->Descent = (float) (kFaces[fi]->size->metrics.descender >> 6); // negative + font->LineGap = 0.f; + font->LineHeight = (float) (kFaces[fi]->size->metrics.height >> 6); + + for (uint32_t gi = 0; gi < codepoint_count; ++gi) + { + ZUIGlyph& g = font->Glyphs[gi]; + uint32_t ri = (uint32_t) fi * codepoint_count + gi; + + FT_UInt glyph_idx = FT_Get_Char_Index(kFaces[fi], first_codepoint + gi); + if (glyph_idx == 0 || FT_Load_Glyph(kFaces[fi], glyph_idx, FT_LOAD_RENDER | FT_LOAD_FORCE_AUTOHINT) != 0) + { + // Missing glyph — advance only, no visible quad + g = {}; + g.AdvanceX = kSizes[fi] * 0.5f; // fallback width + continue; + } + + FT_GlyphSlot slot = kFaces[fi]->glyph; + + g.OffsetX = (float) slot->bitmap_left; + g.OffsetY = -(float) slot->bitmap_top; // FT: positive = above baseline; ZUI: positive = below + g.Width = (float) slot->bitmap.width; + g.Height = (float) slot->bitmap.rows; + g.AdvanceX = (float) (slot->advance.x >> 6); + + if (rects[ri].was_packed && rects[ri].w > 0) + { + int px0 = (int) rects[ri].x + kGlyphPad; + int py0 = (int) rects[ri].y + kGlyphPad; + g.U0 = (float) px0 * inv_w; + g.V0 = (float) py0 * inv_h; + g.U1 = (float) (px0 + (int) slot->bitmap.width) * inv_w; + g.V1 = (float) (py0 + (int) slot->bitmap.rows) * inv_h; + } + } + + *kDsts[fi] = font; + } + + // 7. Clean up FreeType (its own heap, not arena-tracked) + if (ft_header != ft_body) + FT_Done_Face(ft_header); + FT_Done_Face(ft_body); + FT_Done_FreeType(ft_lib); + + return atlas; + } + + void ZUIMeasureText(const ZUIFont* font, const char* str, uint32_t len, float out_size[2]) + { + if (!font || !str || len == 0) + { + out_size[0] = out_size[1] = 0.f; + return; + } + + float width = 0.f; + for (uint32_t i = 0; i < len; ++i) + { + uint32_t cp = (uint8_t) str[i]; + uint32_t idx = cp - font->FirstCodepoint; + if (cp < font->FirstCodepoint || idx >= font->GlyphCount) + continue; + width += font->Glyphs[idx].AdvanceX; + } + out_size[0] = width * font->FontScale; + out_size[1] = font->LineHeight * font->FontScale; + } + +} // namespace ZEngine::UI diff --git a/ZEngine/ZEngine/UI/ZUIFont.h b/ZEngine/ZEngine/UI/ZUIFont.h new file mode 100644 index 000000000..c61a71fba --- /dev/null +++ b/ZEngine/ZEngine/UI/ZUIFont.h @@ -0,0 +1,67 @@ +#pragma once +#include <ZEngine/Core/Memory/Allocator.h> +#include <ZEngine/Rendering/Textures/Texture.h> +#include <ZEngine/ZEngineDef.h> +#include <cstdint> + +namespace ZEngine::Hardwares +{ + struct VulkanDevice; +} + +namespace ZEngine::UI +{ + + struct ZUIGlyph + { + float U0, V0; // atlas UV top-left + float U1, V1; // atlas UV bottom-right + float OffsetX; // pen X offset (logical px) + float OffsetY; // pen Y offset from baseline (logical px) + float Width; // screen width (logical px) + float Height; // screen height (logical px) + float AdvanceX; // cursor advance (logical px) + }; + + struct ZUIFont + { + ZUIGlyph* Glyphs = nullptr; + uint32_t GlyphCount = 0; + uint32_t FirstCodepoint = 32; + float FontSize = 0.f; + float Ascent = 0.f; + float Descent = 0.f; + float LineGap = 0.f; + float LineHeight = 0.f; + // Converts atlas-pixel metrics to logical screen coordinates. + // Set to 1/UIScale (= 0.5 on Retina) after baking so glyph quads + // render at the correct logical size even when baked at physical density. + float FontScale = 1.f; + }; + + // Single shared texture atlas — all fonts packed together (ImGui approach). + // WhiteU/WhiteV is the UV of the 1×1 white texel at pixel (0,0). + // Use it for solid-color quads: sampling white × vertex color = vertex color. + struct ZUIFontAtlas + { + ZUIFont* Small = nullptr; + ZUIFont* Body = nullptr; + ZUIFont* Header = nullptr; + Rendering::Textures::TextureHandle Handle = {}; + uint32_t Width = 0; + uint32_t Height = 0; + float WhiteU = 0.f; + float WhiteV = 0.f; + }; + + /// @brief Pack Small + Body + Header fonts into one shared atlas texture. + /// + /// Uses FreeType with FT_LOAD_FORCE_AUTOHINT for cross-platform quality. + /// Atlas layout handled by stb_rect_pack (1-px glyph padding to prevent UV bleed). + /// Permanent allocations (ZUIFontAtlas, ZUIFont[3], ZUIGlyph arrays) go to @p persistent_arena. + /// Temporary baking buffers (TTF bytes, FreeType bitmaps, pixel maps) go to @p temp_arena. + ZUIFontAtlas* ZUIFontAtlasBake(ZEngine::Core::Memory::ArenaAllocator* persistent_arena, ZEngine::Core::Memory::ArenaAllocator* temp_arena, Hardwares::VulkanDevice* device, const char* vfs_path, float size_small, float size_body, float size_header, uint32_t first_codepoint, uint32_t codepoint_count, const char* header_vfs_path = nullptr); + + void ZUIMeasureText(const ZUIFont* font, const char* str, uint32_t len, float out_size[2]); + +} // namespace ZEngine::UI diff --git a/ZEngine/ZEngine/UI/ZUIInput.cpp b/ZEngine/ZEngine/UI/ZUIInput.cpp new file mode 100644 index 000000000..70f269d59 --- /dev/null +++ b/ZEngine/ZEngine/UI/ZUIInput.cpp @@ -0,0 +1,105 @@ +#include <ZEngine/UI/ZUIContext.h> +#include <ZEngine/UI/ZUIInput.h> +#include <cstring> + +namespace ZEngine::UI +{ + void ZUIFeedBeginFrame(ZUIContext* ctx) + { + if (!ctx) + { + return; + } + ctx->MousePressed[0] = ctx->MousePressed[1] = ctx->MousePressed[2] = false; + ctx->MouseReleased[0] = ctx->MouseReleased[1] = ctx->MouseReleased[2] = false; + ctx->ScrollDelta = 0.f; + ctx->TextInputLen = 0; + ctx->TextInput[0] = '\0'; + ctx->BackspacePressed = false; + } + + void ZUIFeedMousePos(ZUIContext* ctx, float x, float y) + { + if (!ctx) + { + return; + } + ctx->MousePos[0] = x; + ctx->MousePos[1] = y; + } + + void ZUIFeedMouseButton(ZUIContext* ctx, int btn, bool pressed) + { + if (!ctx || btn < 0 || btn > 2) + { + return; + } + if (pressed) + { + ctx->MouseDown[btn] = true; + ctx->MousePressed[btn] = true; + } + else + { + ctx->MouseDown[btn] = false; + ctx->MouseReleased[btn] = true; + } + } + + void ZUIFeedScroll(ZUIContext* ctx, float delta) + { + if (!ctx) + { + return; + } + ctx->ScrollDelta += delta; + } + + void ZUIFeedText(ZUIContext* ctx, uint32_t codepoint) + { + if (!ctx) + { + return; + } + // Encode codepoint as UTF-8 into TextInput buffer + if (codepoint < 0x80 && ctx->TextInputLen < 30) + { + ctx->TextInput[ctx->TextInputLen++] = (char) codepoint; + } + else if (codepoint < 0x800 && ctx->TextInputLen < 29) + { + ctx->TextInput[ctx->TextInputLen++] = (char) (0xC0 | (codepoint >> 6)); + ctx->TextInput[ctx->TextInputLen++] = (char) (0x80 | (codepoint & 0x3F)); + } + else if (codepoint < 0x10000 && ctx->TextInputLen < 28) + { + ctx->TextInput[ctx->TextInputLen++] = (char) (0xE0 | (codepoint >> 12)); + ctx->TextInput[ctx->TextInputLen++] = (char) (0x80 | ((codepoint >> 6) & 0x3F)); + ctx->TextInput[ctx->TextInputLen++] = (char) (0x80 | (codepoint & 0x3F)); + } + ctx->TextInput[ctx->TextInputLen] = '\0'; + } + + void ZUIFeedKey(ZUIContext* ctx, ZUIKey key, bool pressed, bool ctrl, bool shift, bool alt) + { + if (!ctx) + { + return; + } + + ctx->CtrlDown = ctrl; + ctx->ShiftDown = shift; + ctx->AltDown = alt; + + if (key == ZUIKey::Backspace) + { + ctx->BackspaceHeld = pressed; + ctx->BackspacePressed = pressed; + if (!pressed) + { + ctx->KeyRepeatTimer = 0.f; + } + } + } + +} // namespace ZEngine::UI diff --git a/ZEngine/ZEngine/UI/ZUIInput.h b/ZEngine/ZEngine/UI/ZUIInput.h new file mode 100644 index 000000000..b5ed63624 --- /dev/null +++ b/ZEngine/ZEngine/UI/ZUIInput.h @@ -0,0 +1,42 @@ +#pragma once +#include <ZEngine/UI/ZUIKey.h> +#include <cstdint> + +namespace ZEngine::UI +{ + struct ZUIContext; + + // ZUI Input Feed API + // + // Callers (e.g. Editor::ProcessEvent) translate their native event + // types into these calls. ZUI lib has no dependency on the host's + // event system. + // + // Call order each frame: + // 1. ZUIFeedBeginFrame(ctx) — reset per-frame transient state + // 2. ZUIFeedMousePos / Button / ... — any order + // 3. ZUIBeginFrame(ctx, dt) — ZUI frame starts + // 4. (build UI) + // 5. ZUIEndFrame(ctx) + + // Must be called once before feeding events, before ZUIBeginFrame. + // Clears MousePressed[], MouseReleased[], TextInput, ScrollDelta. + void ZUIFeedBeginFrame(ZUIContext* ctx); + + // Mouse position in logical pixels (matches glfwGetCursorPos units). + void ZUIFeedMousePos(ZUIContext* ctx, float x, float y); + + // btn: 0=left, 1=right, 2=middle + void ZUIFeedMouseButton(ZUIContext* ctx, int btn, bool pressed); + + // Vertical scroll delta (positive = up, negative = down). + void ZUIFeedScroll(ZUIContext* ctx, float delta); + + // Single Unicode codepoint from text input (e.g. glfwSetCharCallback). + void ZUIFeedText(ZUIContext* ctx, uint32_t codepoint); + + // Key press or release. Modifier flags are the current state when the + // key event fires (not just the modifier keys themselves). + void ZUIFeedKey(ZUIContext* ctx, ZUIKey key, bool pressed, bool ctrl, bool shift, bool alt); + +} // namespace ZEngine::UI diff --git a/ZEngine/ZEngine/UI/ZUIInteraction.cpp b/ZEngine/ZEngine/UI/ZUIInteraction.cpp new file mode 100644 index 000000000..071edadc3 --- /dev/null +++ b/ZEngine/ZEngine/UI/ZUIInteraction.cpp @@ -0,0 +1,265 @@ +#include <ZEngine/UI/ZUIInteraction.h> +#include <cmath> + +namespace ZEngine::UI +{ + static bool PointInBox(const float pt[2], const ZUIBox* box) + { + return pt[0] >= box->ScreenMin[0] && pt[0] <= box->ScreenMax[0] && pt[1] >= box->ScreenMin[1] && pt[1] <= box->ScreenMax[1]; + } + + void ZUIInteractionPass(ZUIContext* ctx) + { + if (!ctx->Root) + { + ctx->HotKey = 0; + return; + } + + uint32_t max = ctx->MaxBoxesPerFrame; + auto scratch = ZGetScratch(&ctx->FrameArena); + ZUIBox** stack = ZPushArray(&ctx->FrameArena, ZUIBox*, max); + uint32_t stack_top = 0; + + uint64_t new_hot = 0; + uint64_t new_scroll_key = 0; + ZUIAxis new_scroll_axis = ZUIAxis::Y; // axis of the nearest scrollable box + + stack[stack_top++] = ctx->Root; + while (stack_top > 0) + { + ZUIBox* box = stack[--stack_top]; + if (!box) + { + continue; + } + + bool under_cursor = PointInBox(ctx->MousePos, box); + + if (under_cursor) + { + // When a popup is open, only boxes INSIDE the popup receive hover — + // exactly as ImGui blocks input to windows below the modal/popup stack. + // A box can be hovered when no popup is open, or when it is inside + // any popup in the stack (supports nested menus + submenus). + bool can_hover = (ctx->PopupStackSize == 0); + if (!can_hover) + { + for (ZUIBox* p = box; p && !can_hover; p = p->Parent) + for (uint32_t pi = 0; pi < ctx->PopupStackSize && !can_hover; pi++) + if (p == ctx->PopupStack[pi].Box) + can_hover = true; + } + + if (can_hover) + { + if (box->Flags & ZUI_Clickable) + { + new_hot = box->Key; + } + if (box->Flags & ZUI_Scrollable) + { + new_scroll_key = box->Key; + new_scroll_axis = box->LayoutAxis; + } + } + } + + for (ZUIBox* child = box->FirstChild; child; child = child->NextSib) + { + if (stack_top < max) + { + stack[stack_top++] = child; + } + // else: subtree silently skipped (acceptable degradation, not corruption) + } + } + + ZReleaseScratch(scratch); + + // Scroll input — both wheel and keyboard write to ScrollYTarget. + // ZUIBeginScrollRegion lerps ScrollY toward ScrollYTarget each frame (smooth scroll). + // MaxScrollY is owned by the layout pass; we only read it here for clamping. + if (new_scroll_key != 0) + { + ZUIPersistentState* ps = ZUIStateGetOrInsert(&ctx->StateStore, new_scroll_key); + if (ps) + { + float max_y = fmaxf(ps->MaxScrollY, 0.f); + float max_x = fmaxf(ps->MaxScrollX, 0.f); + bool changed = false; + + // Clamp helper — only upper-clamps when max > 0 (layout may not have run yet) + auto clampScroll = [](float v, float lo, float hi) { + if (v < lo) + v = lo; + if (hi > 0.f && v > hi) + v = hi; + return v; + }; + + // Mouse wheel — sets target for smooth animation; also nudges ScrollY + // directly so the first tick is always visible even before the lerp catches up. + if (ctx->ScrollDelta != 0.f) + { + if (new_scroll_axis == ZUIAxis::X) + { + ps->ScrollXTarget = clampScroll(ps->ScrollXTarget - ctx->ScrollDelta * ctx->Style.MouseScrollSpeed, 0.f, max_x); + ps->ScrollX = clampScroll(ps->ScrollX - ctx->ScrollDelta * ctx->Style.MouseScrollSpeed, 0.f, max_x); + } + else + { + ps->ScrollYTarget = clampScroll(ps->ScrollYTarget - ctx->ScrollDelta * ctx->Style.MouseScrollSpeed, 0.f, max_y); + ps->ScrollY = clampScroll(ps->ScrollY - ctx->ScrollDelta * ctx->Style.MouseScrollSpeed, 0.f, max_y); + } + changed = true; + } + + // Keyboard scroll — active when no widget holds focus. + // Uses instant snap (sets ScrollY = ScrollYTarget) for crisp keypress feel. + // Step = 5 × FrameHeight ≈ ImGui's 5 × FontSize rule. + if (ctx->FocusKey == 0) + { + float kLine = ctx->Style.FrameHeight * 5.f; + bool kb = false; + if (ctx->ArrowUpPressed) + { + ps->ScrollYTarget = clampScroll(ps->ScrollYTarget - kLine, 0.f, max_y); + kb = true; + } + if (ctx->ArrowDownPressed) + { + ps->ScrollYTarget = clampScroll(ps->ScrollYTarget + kLine, 0.f, max_y); + kb = true; + } + if (ctx->HomePressed) + { + ps->ScrollYTarget = 0.f; + kb = true; + } + if (ctx->EndPressed) + { + ps->ScrollYTarget = max_y; + kb = true; + } + if (kb) + { + ps->ScrollY = ps->ScrollYTarget; // instant snap for keyboard + changed = true; + } + } + + if (changed) + ps->ScrollbarShowTimer = 0.5f; // show scrollbar for 500ms after any scroll input + } + } + + // Close popups when pressing outside — pops from innermost outward. + // Pressing inside popup N but outside popup N+1 closes only popup N+1. + bool any_pressed = ctx->MousePressed[0] || ctx->MousePressed[1]; + if (any_pressed && ctx->PopupStackSize > 0) + { + // Find the deepest popup whose box contains the cursor + int inside = -1; + for (int pi = (int) ctx->PopupStackSize - 1; pi >= 0; pi--) + { + if (ctx->PopupStack[pi].Box && PointInBox(ctx->MousePos, ctx->PopupStack[pi].Box)) + { + inside = pi; + break; + } + } + // Close all popups deeper than 'inside' (inside == -1 → close all) + ctx->PopupStackSize = (inside < 0) ? 0u : (uint32_t) (inside + 1); + } + + // Hot / active + if (!ctx->MouseDown[0]) + { + ctx->HotKey = new_hot; + } + + if (ctx->MousePressed[0] && ctx->HotKey) + { + ctx->ActiveKey = ctx->HotKey; + } + // Clear keyboard focus when the user clicks empty space (no widget under cursor). + // Mirrors VS Code/ImGui: section header border, text field cursor, etc. all clear. + if (ctx->MousePressed[0] && !ctx->HotKey) + { + ctx->FocusKey = 0; + } + if (ctx->MouseReleased[0]) + { + if (ctx->DragSourceKey != 0) + { + ctx->DragDropFired = true; + ctx->DragTargetKey = ctx->HotKey; + ctx->DragSourceKey = 0; + ctx->DragPayloadLen = 0; + } + ctx->ActiveKey = 0; + } + } + + ZUISignal ZUISignalFromBox(ZUIContext* ctx, ZUIBox* box) + { + ZUISignal signal = {}; + + bool hovered = (ctx->HotKey == box->Key); + bool active = (ctx->ActiveKey == box->Key); + + if (hovered) + { + signal.Flags |= ZUI_SignalHovered; + } + if (active && ctx->MouseDown[0]) + { + signal.Flags |= ZUI_SignalHeld; + signal.DragDelta[0] = ctx->MousePos[0] - ctx->PrevMousePos[0]; + signal.DragDelta[1] = ctx->MousePos[1] - ctx->PrevMousePos[1]; + } + if (ctx->MousePressed[0] && hovered) + { + signal.Flags |= ZUI_SignalPressed; + } + if (ctx->MouseReleased[0] && active && hovered) + { + signal.Flags |= ZUI_SignalClicked | ZUI_SignalReleased; + } + if (ctx->ScrollDelta != 0.f && hovered) + { + signal.Flags |= ZUI_SignalScrolled; + signal.ScrollDelta = ctx->ScrollDelta; + } + + // Tab focus order tracking — builds prev/next chain during the widget build pass + if ((ctx->TabPressed || ctx->ShiftTabPressed) && (box->Flags & ZUI_Clickable)) + { + uint64_t k = box->Key; + if (ctx->TabNavFirstKey == 0) + ctx->TabNavFirstKey = k; + if (!ctx->TabNavSeenFocus) + ctx->TabNavPrevKey = k; + if (ctx->TabNavSeenFocus && !ctx->TabNavNextKey) + ctx->TabNavNextKey = k; + if (k == ctx->FocusKey) + ctx->TabNavSeenFocus = true; + ctx->TabNavLastKey = k; + } + + // Animate hot/active + ZUIPersistentState* state = ZUIStateGetOrInsert(&ctx->StateStore, box->Key); + if (state) + { + float dt = ctx->DeltaTime > 0.f ? ctx->DeltaTime : (1.f / 60.f); + float hot_target = hovered ? 1.f : 0.f; + float act_target = active ? 1.f : 0.f; + state->HotT += (hot_target - state->HotT) * (1.f - expf(-ctx->Style.HoverAnimSpeed * dt)); + state->ActiveT += (act_target - state->ActiveT) * (1.f - expf(-ctx->Style.ActiveAnimSpeed * dt)); + } + + return signal; + } + +} // namespace ZEngine::UI diff --git a/ZEngine/ZEngine/UI/ZUIInteraction.h b/ZEngine/ZEngine/UI/ZUIInteraction.h new file mode 100644 index 000000000..0e5a3fda2 --- /dev/null +++ b/ZEngine/ZEngine/UI/ZUIInteraction.h @@ -0,0 +1,33 @@ +#pragma once +#include <ZEngine/UI/ZUIContext.h> + +namespace ZEngine::UI +{ + enum ZUISignalFlags : uint32_t + { + ZUI_SignalNone = 0, + ZUI_SignalHovered = 1 << 0, + ZUI_SignalPressed = 1 << 1, + ZUI_SignalReleased = 1 << 2, + ZUI_SignalHeld = 1 << 3, + ZUI_SignalClicked = 1 << 4, + ZUI_SignalDoubleClicked = 1 << 5, + ZUI_SignalScrolled = 1 << 6, + ZUI_SignalKeyboardFocus = 1 << 7, + }; + + struct ZUISignal + { + uint32_t Flags = ZUI_SignalNone; + float DragDelta[2] = {}; + float ScrollDelta = 0.f; + }; + + // Called from ZUIEndFrame — walks the box tree and updates HotKey / ActiveKey on ctx + void ZUIInteractionPass(ZUIContext* ctx); + + // Called per-widget after the build phase to query interaction state for a specific box. + // Also advances HotT / ActiveT animation on the box's persistent state. + ZUISignal ZUISignalFromBox(ZUIContext* ctx, ZUIBox* box); + +} // namespace ZEngine::UI diff --git a/ZEngine/ZEngine/UI/ZUIKey.h b/ZEngine/ZEngine/UI/ZUIKey.h new file mode 100644 index 000000000..48872f118 --- /dev/null +++ b/ZEngine/ZEngine/UI/ZUIKey.h @@ -0,0 +1,110 @@ +#pragma once +#include <cstdint> + +namespace ZEngine::UI +{ + // Platform-agnostic key codes — consumers map OS keys to these. + // Covers keyboard input needed by UI widgets. + enum class ZUIKey : uint32_t + { + None = 0, + + // Printable (A-Z) + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M, + N, + O, + P, + Q, + R, + S, + T, + U, + V, + W, + X, + Y, + Z, + + // Digits (top row) + D0, + D1, + D2, + D3, + D4, + D5, + D6, + D7, + D8, + D9, + + // Function + F1, + F2, + F3, + F4, + F5, + F6, + F7, + F8, + F9, + F10, + F11, + F12, + + // Navigation + Left, + Right, + Up, + Down, + Home, + End, + PageUp, + PageDown, + Tab, + + // Editing + Enter, + Backspace, + Delete, + Insert, + Escape, + Space, + + // Modifiers (used as keys, not just flags) + LeftCtrl, + RightCtrl, + LeftShift, + RightShift, + LeftAlt, + RightAlt, + + // Misc + CapsLock, + Grave, // `~ + Minus, // -_ + Equal, // =+ + LeftBracket, // [{ + RightBracket, // ]} + Backslash, // \| + Semicolon, // ;: + Apostrophe, // '" + Comma, // ,< + Period, // .> + Slash, // /? + + Count + }; + +} // namespace ZEngine::UI diff --git a/ZEngine/ZEngine/UI/ZUILayout.cpp b/ZEngine/ZEngine/UI/ZUILayout.cpp new file mode 100644 index 000000000..d628595cd --- /dev/null +++ b/ZEngine/ZEngine/UI/ZUILayout.cpp @@ -0,0 +1,331 @@ +#include <ZEngine/UI/ZUIFont.h> +#include <ZEngine/UI/ZUILayout.h> + +namespace ZEngine::UI +{ + // Padding helpers: [0]=left [1]=top [2]=right [3]=bottom + static inline float PadStart(const ZUIBox* b, int axis) + { + return (axis == 0) ? b->Padding[0] : b->Padding[1]; + } + static inline float PadEnd(const ZUIBox* b, int axis) + { + return (axis == 0) ? b->Padding[2] : b->Padding[3]; + } + + void ZUILayoutSolve(ZUIContext* ctx) + { + if (!ctx->Root) + { + return; + } + + uint32_t max = ctx->MaxBoxesPerFrame; + + ZUIBox** nodes = ZPushArray(&ctx->FrameArena, ZUIBox*, max); + ZUIBox** dfs_stack = ZPushArray(&ctx->FrameArena, ZUIBox*, max); + uint32_t node_count = 0; + uint32_t stack_top = 0; + + dfs_stack[stack_top++] = ctx->Root; + while (stack_top > 0 && node_count < max) + { + ZUIBox* box = dfs_stack[--stack_top]; + nodes[node_count++] = box; + for (ZUIBox* c = box->LastChild; c; c = c->PrevSib) + if (stack_top < max) + { + dfs_stack[stack_top++] = c; + } + } + + // Pass 1 — post-order: intrinsic sizes (Pixels, Text, ChildrenSum) + // Padding is included in ChildrenSum totals. + for (uint32_t i = node_count; i > 0; --i) + { + ZUIBox* box = nodes[i - 1]; + int layout = (int) box->LayoutAxis; + + for (int axis = 0; axis < 2; ++axis) + { + ZUISize s = box->Size[axis]; + switch (s.Kind) + { + case ZUISizeKind::Pixels: + box->ComputedSize[axis] = s.Value; + break; + + case ZUISizeKind::Text: + { + float text_size[2] = {0.f, 0.f}; + if (ctx->GetFont(box->FontSize) && box->Label.Ptr) + ZUIMeasureText(ctx->GetFont(box->FontSize), box->Label.Ptr, box->Label.Len, text_size); + // Include padding so text never clips its containing box + box->ComputedSize[axis] = text_size[axis] + PadStart(box, axis) + PadEnd(box, axis); + break; + } + + case ZUISizeKind::ChildrenSum: + { + float ps = PadStart(box, axis); + float pe = PadEnd(box, axis); + float accum = ps + pe; + if (axis == layout) + { + for (ZUIBox* c = box->FirstChild; c; c = c->NextSib) + accum += c->ComputedSize[axis]; + } + else + { + float mx = 0.f; + for (ZUIBox* c = box->FirstChild; c; c = c->NextSib) + if (c->ComputedSize[axis] > mx) + mx = c->ComputedSize[axis]; + accum = mx + ps + pe; + } + box->ComputedSize[axis] = accum; + break; + } + + default: + break; // Fill / ParentPercent deferred to Pass 2 + } + } + } + + // Pass 2 — pre-order: extrinsic sizes + screen positions + // Fill subtracts parent padding from available space. + // Child placement starts at parent->ScreenMin + parent padding. + // Pass 2a: resolve extrinsic sizes only (no positions yet) + for (uint32_t i = 0; i < node_count; ++i) + { + ZUIBox* box = nodes[i]; + ZUIBox* parent = box->Parent; + int layout = parent ? (int) parent->LayoutAxis : 0; + + for (int axis = 0; axis < 2; ++axis) + { + ZUISize s = box->Size[axis]; + switch (s.Kind) + { + case ZUISizeKind::ParentPercent: + box->ComputedSize[axis] = parent ? parent->ComputedSize[axis] * s.Value : 0.f; + break; + + case ZUISizeKind::Fill: + { + if (!parent) + { + box->ComputedSize[axis] = 0.f; + break; + } + float ps = PadStart(parent, axis); + float pe = PadEnd(parent, axis); + if (axis != layout) + { + box->ComputedSize[axis] = parent->ComputedSize[axis] - ps - pe; + } + else + { + float non_fill = 0.f; + uint32_t fill_n = 0; + for (ZUIBox* sib = parent->FirstChild; sib; sib = sib->NextSib) + { + // Floated siblings are positioned absolutely — they do + // not participate in the flow layout or Fill budget. + bool floated = (axis == 0) ? !!(sib->Flags & ZUI_FloatX) : !!(sib->Flags & ZUI_FloatY); + if (floated) + { + continue; + } + if (sib->Size[axis].Kind == ZUISizeKind::Fill) + ++fill_n; + else + non_fill += sib->ComputedSize[axis]; + } + float remaining = parent->ComputedSize[axis] - ps - pe - non_fill; + box->ComputedSize[axis] = (fill_n > 0) ? remaining / (float) fill_n : 0.f; + } + break; + } + + default: + break; + } + } + } + + // Pass 2.5 — enforce constraints: when children overflow a parent + // along its layout axis, shrink flexible (low-strictness) children + // proportionally to absorb the overflow. Prevents panel content from + // bleeding outside its bounds. Strictness 1.0 = rigid, 0.0 = fully + // flexible. ZFill defaults to Strictness=0, ZPx to Strictness=1. + for (uint32_t i = 0; i < node_count; ++i) + { + ZUIBox* box = nodes[i]; + if (!box->FirstChild) + { + continue; + } + + int axis = (int) box->LayoutAxis; + float ps = PadStart(box, axis); + float pe = PadEnd(box, axis); + float available = box->ComputedSize[axis] - ps - pe; + + float total = 0.f; + for (ZUIBox* c = box->FirstChild; c; c = c->NextSib) + { + bool floated = (axis == 0) ? !!(c->Flags & ZUI_FloatX) : !!(c->Flags & ZUI_FloatY); + if (!floated) + total += c->ComputedSize[axis]; + } + + float overflow = total - available; + if (overflow <= 0.001f) + { + continue; + } + + // Weighted flexibility pool: each child contributes (1-strictness) fraction + float flex_pool = 0.f; + for (ZUIBox* c = box->FirstChild; c; c = c->NextSib) + { + bool floated = (axis == 0) ? !!(c->Flags & ZUI_FloatX) : !!(c->Flags & ZUI_FloatY); + if (floated) + { + continue; + } + float flex = 1.f - c->Size[axis].Strictness; + if (flex > 0.f) + { + flex_pool += c->ComputedSize[axis] * flex; + } + } + if (flex_pool <= 0.f) + { + continue; + } + + for (ZUIBox* c = box->FirstChild; c; c = c->NextSib) + { + bool floated = (axis == 0) ? !!(c->Flags & ZUI_FloatX) : !!(c->Flags & ZUI_FloatY); + if (floated) + { + continue; + } + float flex = 1.f - c->Size[axis].Strictness; + if (flex <= 0.f) + { + continue; + } + float share = c->ComputedSize[axis] * flex / flex_pool; + c->ComputedSize[axis] -= overflow * share; + if (c->ComputedSize[axis] < 0.f) + { + c->ComputedSize[axis] = 0.f; + } + } + } + + // Pass 3 — assign screen positions (top-down, after sizes finalized) + for (uint32_t i = 0; i < node_count; ++i) + { + ZUIBox* box = nodes[i]; + ZUIBox* parent = box->Parent; + int layout = parent ? (int) parent->LayoutAxis : 0; + + if (!parent) + { + box->ScreenMin[0] = 0.f; + box->ScreenMin[1] = 0.f; + } + else + { + for (int axis = 0; axis < 2; ++axis) + { + bool floating = (axis == 0) ? !!(box->Flags & ZUI_FloatX) : !!(box->Flags & ZUI_FloatY); + if (floating) + { + box->ScreenMin[axis] = parent->ScreenMin[axis] + box->FloatPos[axis]; + } + else if (axis == layout) + { + float ps = PadStart(parent, axis); + float scroll = 0.f; + if (!box->PrevSib && (parent->Flags & ZUI_Scrollable)) + { + ZUIPersistentState* ps_state = ZUIStateGetOrInsert(&ctx->StateStore, parent->Key); + if (ps_state) + scroll = (axis == 1) ? ps_state->ScrollY : ps_state->ScrollX; + } + // Skip floated siblings — they are positioned absolutely and must not + // advance the flow cursor for the non-floated children that follow them. + ZUIBox* prev_flow = box->PrevSib; + while (prev_flow) + { + bool pf = (axis == 0) ? !!(prev_flow->Flags & ZUI_FloatX) : !!(prev_flow->Flags & ZUI_FloatY); + if (!pf) + { + break; + } + prev_flow = prev_flow->PrevSib; + } + box->ScreenMin[axis] = prev_flow ? prev_flow->ScreenMax[axis] : parent->ScreenMin[axis] + ps - scroll; + } + else + { + float ps = PadStart(parent, axis); + box->ScreenMin[axis] = parent->ScreenMin[axis] + ps; + } + } + } + + box->ScreenMax[0] = box->ScreenMin[0] + box->ComputedSize[0]; + box->ScreenMax[1] = box->ScreenMin[1] + box->ComputedSize[1]; + + // Persist screen rect for next-frame access (e.g. slider width, draw-list positions) + if (box->Key && (box->Flags & ZUI_Clickable)) + { + ZUIPersistentState* ps = ZUIStateGetOrInsert(&ctx->StateStore, box->Key); + if (ps) + { + ps->ScreenMinX = box->ScreenMin[0]; + ps->ScreenMinY = box->ScreenMin[1]; + ps->ScreenMaxX = box->ScreenMax[0]; + ps->ScreenMaxY = box->ScreenMax[1]; + } + } + } + + // Pass 4 — compute MaxScrollY / MaxScrollX for every ZUI_Scrollable box. + for (uint32_t i = 0; i < node_count; ++i) + { + ZUIBox* box = nodes[i]; + if (!(box->Flags & ZUI_Scrollable)) + { + continue; + } + + int layout = (int) box->LayoutAxis; + float content = 0.f; + for (ZUIBox* c = box->FirstChild; c; c = c->NextSib) + content += c->ComputedSize[layout]; + + float visible = box->ComputedSize[layout]; + float max_scroll = content - visible; + if (max_scroll < 0.f) + max_scroll = 0.f; + + ZUIPersistentState* ps = ZUIStateGetOrInsert(&ctx->StateStore, box->Key); + if (ps) + { + if (layout == (int) ZUIAxis::X) + ps->MaxScrollX = max_scroll; + else + ps->MaxScrollY = max_scroll; + } + } + } + +} // namespace ZEngine::UI diff --git a/ZEngine/ZEngine/UI/ZUILayout.h b/ZEngine/ZEngine/UI/ZUILayout.h new file mode 100644 index 000000000..7236874a5 --- /dev/null +++ b/ZEngine/ZEngine/UI/ZUILayout.h @@ -0,0 +1,16 @@ +#pragma once +#include <ZEngine/UI/ZUIContext.h> + +namespace ZEngine::UI +{ + // Two-pass layout solver. + // + // Pass 1 (post-order) — resolves intrinsic sizes: Pixels, Text (stub), ChildrenSum. + // Pass 2 (pre-order) — resolves extrinsic sizes: ParentPercent, Fill. + // Then computes ScreenMin / ScreenMax for every box. + // + // After this call, every ZUIBox in the tree has valid ComputedSize, ScreenMin, ScreenMax. + // Phase 3 will hook in real text measurement; until then, ZUISizeKind::Text resolves to 0. + void ZUILayoutSolve(ZUIContext* ctx); + +} // namespace ZEngine::UI diff --git a/ZEngine/ZEngine/UI/ZUIPanel.cpp b/ZEngine/ZEngine/UI/ZUIPanel.cpp new file mode 100644 index 000000000..38dff0871 --- /dev/null +++ b/ZEngine/ZEngine/UI/ZUIPanel.cpp @@ -0,0 +1,1538 @@ +#include <ZEngine/Helpers/MemoryOperations.h> +#include <ZEngine/Logging/LoggerDefinition.h> +#include <ZEngine/UI/ZUIDockSerial.h> +#include <ZEngine/UI/ZUIFont.h> +#include <ZEngine/UI/ZUIPanel.h> +#include <ZEngine/UI/ZUIWidgets.h> +#include <cmath> +#include <cstdio> +#include <cstring> + +namespace ZEngine::UI +{ + using namespace ZEngine::Core::Memory; + + // Init / Registration + + void ZUIPanelManager::Init(ArenaAllocator* arena) + { + DockTree = ZUIDockTreeCreate(arena); + PanelCount = 0; + m_split_divider_count = 0; + } + + void ZUIPanelManager::Shutdown() {} + + void ZUIPanelManager::SetLayoutPath(const char* path) + { + snprintf(LayoutPath, sizeof(LayoutPath), "%s", path ? path : ""); + } + + ZUIPanel* ZUIPanelManager::AddPanel(uint64_t dock_key) + { + if (PanelCount >= kMaxPanels) + { + return nullptr; + } + ZUIPanel* p = &Panels[PanelCount++]; + p->DockKey = dock_key; + p->ViewCount = 0; + p->ActiveTab = 0; + p->Hidden = false; + return p; + } + + void ZUIPanelManager::AddView(ZUIPanel* panel, ZUIPanelView* view) + { + if (!panel || panel->ViewCount >= kMaxTabsPerPanel) + { + return; + } + panel->Views[panel->ViewCount++] = view; + } + + // Insert a view at a specific index; idx >= ViewCount appends at end. + static void InsertViewAt(ZUIPanel* panel, ZUIPanelView* view, uint32_t idx) + { + if (!panel || panel->ViewCount >= kMaxTabsPerPanel) + { + return; + } + if (idx > panel->ViewCount) + idx = panel->ViewCount; + for (uint32_t i = panel->ViewCount; i > idx; --i) + panel->Views[i] = panel->Views[i - 1]; + panel->Views[idx] = view; + ++panel->ViewCount; + } + + ZUIPanel* ZUIPanelManager::FindPanel(uint64_t dock_key) + { + for (uint32_t i = 0; i < PanelCount; ++i) + if (Panels[i].DockKey == dock_key) + return &Panels[i]; + return nullptr; + } + + static ZUIDockNode* FindLargestLeaf(ZUIDockNode* node) + { + if (!node) + { + return nullptr; + } + ZUIDockNode* best = nullptr; + float best_area = -1.f; + ZUIDockNode* stack[64]; + int top = 0; + stack[top++] = node; + while (top > 0) + { + ZUIDockNode* n = stack[--top]; + if (n->ContentKey != 0) + { + float area = (n->RectMax[0] - n->RectMin[0]) * (n->RectMax[1] - n->RectMin[1]); + if (area > best_area) + { + best_area = area; + best = n; + } + } + for (ZUIDockNode* c = n->First; c; c = c->Next) + if (top < 64) + stack[top++] = c; + } + return best; + } + + void ZUIPanelManager::FocusPanel(uint32_t idx) + { + FocusedPanelIdx = idx; + } + + // SplitDivider state + + bool ZUIPanelManager::GetSplitDividerDragging(ZUIDockNode* node) const + { + for (uint32_t i = 0; i < m_split_divider_count; ++i) + if (m_split_dividers[i].Node == node) + return m_split_dividers[i].Dragging; + return false; + } + + void ZUIPanelManager::SetSplitDividerDragging(ZUIDockNode* node, bool v) + { + for (uint32_t i = 0; i < m_split_divider_count; ++i) + { + if (m_split_dividers[i].Node == node) + { + m_split_dividers[i].Dragging = v; + return; + } + } + if (m_split_divider_count < kMaxSplitDividers) + m_split_dividers[m_split_divider_count++] = {node, v}; + } + + void ZUIPanelManager::SyncSplitDividers() + { + if (!DockTree || !DockTree->Root) + return; + ZUIDockNode* stack[64]; + int top = 0; + stack[top++] = DockTree->Root; + ZUIDockNode* seen[kMaxSplitDividers]; + uint32_t seen_count = 0; + while (top > 0) + { + ZUIDockNode* node = stack[--top]; + if (!node) + { + continue; + } + if (node->ContentKey == 0 && node->First) + { + seen[seen_count++] = node; + bool found = false; + for (uint32_t i = 0; i < m_split_divider_count; ++i) + if (m_split_dividers[i].Node == node) + { + found = true; + break; + } + if (!found && m_split_divider_count < kMaxSplitDividers) + m_split_dividers[m_split_divider_count++] = {node, false}; + } + for (ZUIDockNode* c = node->First; c; c = c->Next) + if (top < 64) + stack[top++] = c; + } + for (uint32_t i = 0; i < m_split_divider_count;) + { + bool alive = false; + for (uint32_t j = 0; j < seen_count; ++j) + if (seen[j] == m_split_dividers[i].Node) + { + alive = true; + break; + } + if (!alive) + m_split_dividers[i] = m_split_dividers[--m_split_divider_count]; + else + ++i; + } + } + + // PreDetectCloseEvents — Clay-style pre-pass + // Detects close button clicks using ctx->ActiveKey + ctx->MouseReleased + // BEFORE any panel box is built, so ZUIDockLayout sees the correct tree. + // ctx->ActiveKey is set by the previous frame's ZUIInteractionPass and is + // still valid during the current frame's build phase. + + void ZUIPanelManager::PreDetectCloseEvents(ZUIContext* ctx) + { + if (!ctx->MouseReleased[0] || ctx->ActiveKey == 0) + { + return; + } + + for (uint32_t i = 0; i < PanelCount; ++i) + { + ZUIPanel* p = &Panels[i]; + if (p->Hidden || p->ViewCount == 0) + { + continue; + } + + // Tab bar close buttons + for (uint32_t ti = 0; ti < p->ViewCount; ++ti) + { + char xkey[64]; + snprintf(xkey, sizeof(xkey), "x##x_%llx_%u", (unsigned long long) p->DockKey, ti); + uint64_t xhash = ZUIHashStr(xkey, (uint32_t) strlen(xkey)); + + if (ctx->ActiveKey == xhash && ctx->HotKey == xhash) + { + if (p->ViewCount > 1) + { + // Remove this tab — panel keeps rendering with fewer tabs + for (uint32_t j = ti; j + 1 < p->ViewCount; ++j) + p->Views[j] = p->Views[j + 1]; + --p->ViewCount; + if (p->ActiveTab >= p->ViewCount && p->ViewCount > 0) + p->ActiveTab = p->ViewCount - 1; + } + else + { + // Last tab — panel disappears this frame + if (PendingCloseCount < kMaxPanels) + PendingCloseKeys[PendingCloseCount++] = p->DockKey; + } + LayoutDirty = true; + goto next_panel; // only one close per panel per frame + } + } + + // Single-view title strip close button + { + char txk[56]; + snprintf(txk, sizeof(txk), "x##tx_%llx", (unsigned long long) p->DockKey); + uint64_t txhash = ZUIHashStr(txk, (uint32_t) strlen(txk)); + + if (ctx->ActiveKey == txhash && ctx->HotKey == txhash) + { + if (PendingCloseCount < kMaxPanels) + PendingCloseKeys[PendingCloseCount++] = p->DockKey; + LayoutDirty = true; + } + } + + next_panel:; + } + } + + // BuildUI — top-level per-frame entry + + void ZUIPanelManager::BuildUI(ZUIContext* ctx, float menu_h, float status_h) + { + float sw = (float) ctx->ScreenW; + float sh = (float) ctx->ScreenH; + + // Clay-style pre-pass: detect close events BEFORE layout so the + // sibling fills the space in the same frame the panel disappears. + PreDetectCloseEvents(ctx); + + // Flush deferred close queue FIRST — must run before ZUIDockLayout so the + // sibling fills freed space in the same frame, and before ZUIDockSave so the + // saved tree reflects the actual post-close state (not one frame stale). + for (uint32_t ci = 0; ci < PendingCloseCount; ++ci) + { + ZUIPanel* cp = FindPanel(PendingCloseKeys[ci]); + if (cp) + { + cp->Hidden = true; + } + if (DockTree) + { + ZUIDockNode* leaf = ZUIDockFindLeaf(DockTree, PendingCloseKeys[ci]); + if (leaf) + ZUIDockCollapseLeaf(DockTree, leaf); + } + LayoutDirty = true; + } + PendingCloseCount = 0; + + // Save layout AFTER close flush so the file always reflects the current state. + if (LayoutDirty && LayoutPath[0]) + { + ZUIDockSave(this, LayoutPath); + LayoutDirty = false; + } + + if (DockTree) + { + float root_rect[4] = {0.f, menu_h, sw, sh - status_h}; + ZUIDockLayout(DockTree, root_rect); + SyncSplitDividers(); + } + + ZUIBox* bg = ZUIBeginColumn(ctx, "##pm_bg", ZPx(sw), ZPx(sh)); + bg->Flags = bg->Flags | ZUI_DrawBackground | ZUI_FloatX | ZUI_FloatY; + bg->FloatPos[0] = 0.f; + bg->FloatPos[1] = 0.f; + ZUIBoxSetColorArr(bg, ctx->Theme.WindowBg); + bg->EdgeSoftness = 0.f; + + BuildMenuBar(ctx, sw, menu_h); + + // Input pass — hit zones added first (early children of ##pm_bg). + // LIFO traversal processes early children last → win ctx->HotKey over panel content. + BuildDividerHitZones(ctx); + + Drag.HoverNode = nullptr; + Drag.DropZone = ZUIDropZone::None; // reset each frame; set by BuildDropZones below + if (DockTree) + { + float mx = ctx->MousePos[0], my = ctx->MousePos[1]; + for (uint32_t i = 0; i < PanelCount; ++i) + { + ZUIPanel* p = &Panels[i]; + if (p->Hidden) + { + continue; + } + float r[4]; + if (!ZUIDockRectForKey(DockTree, p->DockKey, r)) + { + continue; + } + + if (Drag.Active && mx >= r[0] && mx <= r[2] && my >= r[1] && my <= r[3]) + { + // Always update HoverNode — including source panel so edge-zone splits + // are proposed even when all views are merged into one tab group. + Drag.HoverNode = ZUIDockFindLeaf(DockTree, p->DockKey); + } + + BuildDockedPanel(ctx, p, r); + + if (ctx->MousePressed[0] && !Drag.Active && mx >= r[0] && mx <= r[2] && my >= r[1] && my <= r[3]) + FocusPanel(i); + } + } + + // Status bar + if (status_h > 0.f) + { + ZUIBox* sbar = ZUIBeginRow(ctx, "##pm_sbar", ZPx(sw), ZPx(status_h)); + sbar->Flags = sbar->Flags | ZUI_DrawBackground | ZUI_FloatX | ZUI_FloatY; + sbar->FloatPos[0] = 0.f; + sbar->FloatPos[1] = sh - status_h; + ZUIBoxSetColorArr(sbar, ctx->Theme.StatusBarBg); + sbar->EdgeSoftness = 0.f; + + // Left — engine identity + ZUISpacer(ctx, 12.f); + ZUILabel(ctx, "ZEngine", ctx->Theme.TextDefault); + ZUISpacer(ctx, 6.f); + ZUILabel(ctx, "Editor", ctx->Theme.TextDim); + + // Fill + { + char fk[] = "##sbf"; + ZUIBox* sf = ZUIPushBox(ctx, fk, 5, ZUI_None); + sf->Size[0] = ZFill(); + sf->Size[1] = ZPx(status_h); + ZUIPopBox(ctx); + } + + // Right — performance + scale + { + float fps = (ctx->DeltaTime > 0.0005f && ctx->DeltaTime < 1.f) ? (1.f / ctx->DeltaTime) : 0.f; + + // UIScale + { + char scale_buf[24]; + snprintf(scale_buf, sizeof(scale_buf), "UIScale %.1f", (double) ctx->UIScale); + ZUILabel(ctx, scale_buf, ctx->Theme.TextDim); + } + + ZUISpacer(ctx, 10.f); + static const float kSep[4] = {1.f, 1.f, 1.f, 0.25f}; + ZUILabel(ctx, "|", kSep); + ZUISpacer(ctx, 10.f); + + // FPS — color-coded: normal / warn / error + if (fps > 0.f) + { + const float* fps_col = (fps >= 55.f) ? ctx->Theme.TextDefault : (fps >= 30.f) ? ctx->Theme.TextWarn : ctx->Theme.TextError; + char fps_buf[24]; + snprintf(fps_buf, sizeof(fps_buf), "%.0f fps", (double) fps); + ZUILabel(ctx, fps_buf, fps_col); + } + } + ZUISpacer(ctx, 14.f); + ZUIEndRow(ctx); + } + + // Render pass — visual lines added last (late children of ##pm_bg → drawn on top of panels). + BuildDividerVisuals(ctx); + + // Drag ghost + if (Drag.Active) + { + Drag.GhostX = ctx->MousePos[0]; + Drag.GhostY = ctx->MousePos[1]; + + if (ctx->MouseReleased[0] && Drag.DropZone == ZUIDropZone::None) + { + Drag.Active = false; + Drag.SrcPanel = nullptr; + } + + if (Drag.Active) + { + ZUIPanel* sp = Drag.SrcPanel; + const char* title = "Panel"; + if (Drag.SrcTabIdx == kWholePanel) + { + if (sp && sp->ViewCount > 0 && sp->Views[0]) + title = sp->Views[0]->Title; + } + else if (sp && Drag.SrcTabIdx < sp->ViewCount && sp->Views[Drag.SrcTabIdx]) + { + title = sp->Views[Drag.SrcTabIdx]->Title; + } + + ZUIDockGhostHeader(ctx, "##drag_ghost", title, Drag.GhostX, Drag.GhostY); + } + } + + ZUIEndColumn(ctx); + } + + // BuildMenuBar + + void ZUIPanelManager::BuildMenuBar(ZUIContext* ctx, float sw, float mh) + { + ZUIBox* bar = ZUIBeginRow(ctx, "##pm_menubar", ZPx(sw), ZPx(mh)); + bar->Flags = bar->Flags | ZUI_DrawBackground; + ZUIBoxSetColorArr(bar, ctx->Theme.MenuBarBg); + bar->EdgeSoftness = 0.f; + bar->BorderColor[0] = ctx->Theme.Separator[0]; + bar->BorderColor[1] = ctx->Theme.Separator[1]; + bar->BorderColor[2] = ctx->Theme.Separator[2]; + bar->BorderColor[3] = ctx->Theme.Separator[3]; + bar->BorderThickness = 1.f; + bar->Flags = bar->Flags | ZUI_DrawBorder; + + ZUISpacer(ctx, 10.f); + if (ZUIBeginMenu(ctx, "File")) + { + ZUIMenuItem(ctx, "New Scene"); + ZUIMenuItem(ctx, "Open Scene..."); + ZUIMenuItem(ctx, "Save Scene"); + ZUISeparator(ctx); + ZUIMenuItem(ctx, "Quit"); + ZUIEndMenu(ctx); + } + ZUISpacer(ctx, 2.f); + if (ZUIBeginMenu(ctx, "Edit")) + { + ZUIMenuItem(ctx, "Undo"); + ZUIMenuItem(ctx, "Redo"); + ZUISeparator(ctx); + ZUIMenuItem(ctx, "Preferences..."); + ZUIEndMenu(ctx); + } + ZUISpacer(ctx, 2.f); + if (ZUIBeginMenu(ctx, "Window")) + { + // Reset Layout + if (ZUIMenuItem(ctx, "Reset Layout")) + { + // Unhide all hidden panels and re-insert them into the tree + for (uint32_t i = 0; i < PanelCount; ++i) + { + ZUIPanel* p = &Panels[i]; + if (!p->Hidden || p->ViewCount == 0) + { + continue; + } + p->Hidden = false; + if (DockTree) + { + ZUIDockNode* target = FindLargestLeaf(DockTree->Root); + if (target) + ZUIDockSplitH(DockTree, target, 0.5f, target->ContentKey, p->DockKey); + } + } + // Delete saved layout so it restarts from default next launch + if (LayoutPath[0]) + { + remove(LayoutPath); + } + LayoutDirty = true; + } + + ZUISeparator(ctx); + + // Panels submenu — fly-out to the right (popup stack supports nested menus) + if (ZUIBeginSubMenu(ctx, "Panels")) + { + static const char* kPanelNames[] = {"Hierarchy", "Console", "Inspector", "Viewport"}; + static constexpr uint32_t kPanelNameCount = 4; + + for (uint32_t ni = 0; ni < kPanelNameCount; ++ni) + { + const char* target_name = kPanelNames[ni]; + + ZUIPanel* match = nullptr; + for (uint32_t i = 0; i < PanelCount; ++i) + { + ZUIPanel* p = &Panels[i]; + const char* t = (p->ViewCount > 0 && p->Views[0]) ? p->Views[0]->Title : ""; + if (strcmp(t, target_name) == 0) + { + match = p; + break; + } + } + + bool visible = match && !match->Hidden; + if (ZUIMenuItemEx(ctx, target_name, nullptr, visible) && match) + { + if (match->Hidden) + { + match->Hidden = false; + if (DockTree) + { + ZUIDockNode* tgt = FindLargestLeaf(DockTree->Root); + if (tgt) + ZUIDockSplitH(DockTree, tgt, 0.5f, tgt->ContentKey, match->DockKey); + } + } + else if (PendingCloseCount < kMaxPanels) + PendingCloseKeys[PendingCloseCount++] = match->DockKey; + LayoutDirty = true; + } + } + ZUIEndSubMenu(ctx); + } + ZUIEndMenu(ctx); + } + ZUISpacer(ctx, 2.f); + if (ZUIBeginMenu(ctx, "Help")) + { + ZUIMenuItem(ctx, "About ZEngine"); + ZUIEndMenu(ctx); + } + { + char fk[] = "##mb_fill"; + ZUIBox* f = ZUIPushBox(ctx, fk, 8, ZUI_None); + f->Size[0] = ZFill(); + f->Size[1] = ZPx(mh); + ZUIPopBox(ctx); + } + ZUILabel(ctx, "ZEngine", ctx->Theme.TextDim); + ZUISpacer(ctx, 12.f); + ZUIEndRow(ctx); + } + + // BuildDockedPanel + + void ZUIPanelManager::BuildDockedPanel(ZUIContext* ctx, ZUIPanel* p, float rect[4]) + { + if (!p || p->ViewCount == 0) + { + return; + } + float header_h = ZUIGetFrameHeight(ctx); // 19px + + // Central node check: read IsCentral from the dock node, not a manager key + ZUIDockNode* leaf = ZUIDockFindLeaf(DockTree, p->DockKey); + bool is_central = leaf && leaf->IsCentral; + + if (is_central) + { + // Pure passthrough — multi-tab gets minimal tab bar, single gets nothing + if (p->ViewCount > 1) + { + float tab_rect[4] = {rect[0], rect[1], rect[2], rect[1] + header_h}; + BuildTabBar(ctx, p, tab_rect); + float cr[4] = {rect[0], rect[1] + header_h, rect[2], rect[3]}; + ZUIPanelView* view = (p->ActiveTab < p->ViewCount) ? p->Views[p->ActiveTab] : nullptr; + if (view) + { + view->BuildContent(ctx, cr); + } + } + else + { + ZUIPanelView* view = (p->ActiveTab < p->ViewCount) ? p->Views[p->ActiveTab] : nullptr; + if (view) + { + view->BuildContent(ctx, rect); + } + } + return; + } + + bool is_focused = false; + for (uint32_t pi = 0; pi < PanelCount; ++pi) + if (&Panels[pi] == p && pi == FocusedPanelIdx) + { + is_focused = true; + break; + } + + char panel_key[32]; + snprintf(panel_key, sizeof(panel_key), "##panel_%llx", (unsigned long long) p->DockKey); + + ZUIBox* panel = ZUIBeginColumn(ctx, panel_key, ZPx(rect[2] - rect[0]), ZPx(rect[3] - rect[1])); + panel->Flags = panel->Flags | ZUI_DrawBackground | ZUI_DrawBorder | ZUI_FloatX | ZUI_FloatY; + panel->FloatPos[0] = rect[0]; + panel->FloatPos[1] = rect[1]; + ZUIBoxSetColorArr(panel, ctx->Theme.PanelBg); + panel->BorderColor[0] = ctx->Theme.PanelBorder[0]; + panel->BorderColor[1] = ctx->Theme.PanelBorder[1]; + panel->BorderColor[2] = ctx->Theme.PanelBorder[2]; + panel->BorderColor[3] = ctx->Theme.PanelBorder[3]; + panel->BorderThickness = 1.f; + panel->EdgeSoftness = 0.f; + + // AutoHideTabBar: use node flag, defaulting to false if no node found + // (false = ImGui default: always show tab bar even for single-view nodes) + bool auto_hide = leaf ? leaf->AutoHideTabBar : false; + bool show_tabs = (p->ViewCount > 1) || !auto_hide; + + if (show_tabs) + { + float tab_rect[4] = {rect[0], rect[1], rect[2], rect[1] + header_h}; + BuildTabBar(ctx, p, tab_rect); + } + else + { + // VS Code single-view title strip — style-driven + const char* view_title = (p->ViewCount > 0 && p->Views[0]) ? p->Views[0]->Title : "Panel"; + float btn_h = ctx->Style.FontSize; // was header_h * 0.60 + + char hk[48]; + snprintf(hk, sizeof(hk), "##tbar_%llx", (unsigned long long) p->DockKey); + ZUIBox* strip = ZUIBeginRow(ctx, hk, ZFill(), ZPx(header_h)); + strip->Flags = strip->Flags | ZUI_DrawBackground | ZUI_Clickable; + // Background: focus-aware + ZUIBoxSetColorArr(strip, is_focused ? ctx->Theme.TitleBgActive : ctx->Theme.TitleBarBg); + strip->EdgeSoftness = 0.f; + + // Left padding from style + ZUISpacer(ctx, ZUIGetFramePadX(ctx)); + // Icon dot — size from style + { + const float* ic = (p->Views[0] && p->Views[0]->TabColor[3] > 0.01f) ? p->Views[0]->TabColor : ctx->Theme.PanelFocusBorder; + char ik[48]; + snprintf(ik, sizeof(ik), "##tic_%llx", (unsigned long long) p->DockKey); + float icon_sz = ctx->Style.TabIconSize; + ZUIBox* icon = ZUIPushBox(ctx, ik, (uint32_t) strlen(ik), ZUI_DrawBackground); + icon->Size[0] = ZPx(icon_sz); + icon->Size[1] = ZPx(icon_sz); + ZUIBoxSetColorArr(icon, ic); + ZUIBoxSetCornerRadius(icon, icon_sz * 0.5f); + icon->EdgeSoftness = 0.5f; + ZUIPopBox(ctx); + } + // Icon-to-label gap from style + ZUISpacer(ctx, ZUIGetInnerSpac(ctx)); + ZUILabel(ctx, view_title, ctx->Theme.TextDefault); + + // Fill + { + char fk[48]; + snprintf(fk, sizeof(fk), "##tf_%llx", (unsigned long long) p->DockKey); + ZUIBox* f = ZUIPushBox(ctx, fk, (uint32_t) strlen(fk), ZUI_None); + f->Size[0] = ZFill(); + f->Size[1] = ZPx(header_h); + ZUIPopBox(ctx); + } + + // x close (hover-only) + bool should_close = false; + bool ph = (ctx->MousePos[0] >= rect[0] && ctx->MousePos[0] <= rect[2] && ctx->MousePos[1] >= rect[1] && ctx->MousePos[1] <= rect[1] + header_h); + if (ph) + { + char xk[56]; + snprintf(xk, sizeof(xk), "x##tx_%llx", (unsigned long long) p->DockKey); + ZUIBox* xb = ZUIPushBox(ctx, xk, (uint32_t) strlen(xk), ZUI_DrawText | ZUI_Clickable); + xb->Size[0] = ZPx(btn_h); + xb->Size[1] = ZPx(btn_h); + xb->TextAlign = ZUITextAlign::Center; + bool xh = (ctx->HotKey == xb->Key); + xb->TextColor[0] = xh ? 1.f : 0.55f; + xb->TextColor[1] = xh ? 0.4f : 0.55f; + xb->TextColor[2] = xh ? 0.4f : 0.55f; + xb->TextColor[3] = 1.f; + ZUISignal xs = ZUISignalFromBox(ctx, xb); + ZUIPopBox(ctx); + if (xs.Flags & ZUI_SignalClicked) + should_close = true; + } + // Right spacer from style + ZUISpacer(ctx, ZUIGetFramePadX(ctx)); + + ZUISignal strip_sig = ZUISignalFromBox(ctx, strip); + ZUIEndRow(ctx); + + // Record press start + if (strip_sig.Flags & ZUI_SignalPressed) + { + Drag.StartX = ctx->MousePos[0]; + Drag.StartY = ctx->MousePos[1]; + } + + // Drag threshold from style + if (!should_close && (strip_sig.Flags & ZUI_SignalHeld) && !Drag.Active) + { + float dx = fabsf(ctx->MousePos[0] - Drag.StartX); + float dy = fabsf(ctx->MousePos[1] - Drag.StartY); + if (dx + dy > ctx->Style.DockingDragThreshold) + { + Drag.Active = true; + Drag.SrcPanel = p; + Drag.SrcTabIdx = kWholePanel; + Drag.GhostX = ctx->MousePos[0]; + Drag.GhostY = ctx->MousePos[1]; + } + } + + // Close already handled by PreDetectCloseEvents pre-pass. + (void) should_close; + } + + // Content — no WindowPadding for docked panels (editor panels fill edge-to-edge). + // WindowPadding will be applied when floating windows are implemented. + ZUIPanelView* view = (p->ActiveTab < p->ViewCount) ? p->Views[p->ActiveTab] : nullptr; + if (view) + { + ZUIBox* content = ZUIBeginColumn(ctx, "##dc", ZFill(), ZFill()); + content->Flags = content->Flags | ZUI_ClipChildren; + content->EdgeSoftness = 0.f; + float cr[4] = {rect[0], rect[1] + header_h, rect[2], rect[3]}; + view->BuildContent(ctx, cr); + ZUIEndColumn(ctx); + } + + ZUIEndColumn(ctx); + + // Optional left-edge focus strip (ZUIStyle.ShowFocusBorder). + // Off by default — the active tab's overline already indicates focus. + if (is_focused && ctx->Style.ShowFocusBorder) + { + char fk[48]; + snprintf(fk, sizeof(fk), "##pfocus_%llx", (unsigned long long) p->DockKey); + ZUIBox* fb = ZUIPushBox(ctx, fk, (uint32_t) strlen(fk), ZUI_DrawBackground | ZUI_FloatX | ZUI_FloatY); + fb->Size[0] = ZPx(ctx->Style.DockingFocusBorderWidth); + fb->Size[1] = ZPx(rect[3] - rect[1]); + fb->FloatPos[0] = rect[0]; + fb->FloatPos[1] = rect[1]; + ZUIBoxSetColorArr(fb, ctx->Theme.PanelFocusBorder); + fb->EdgeSoftness = 0.f; + ZUIPopBox(ctx); + } + + // Drop zones — edge zones shown even on source panel (allows splitting a merged tab group) + if (Drag.Active && Drag.HoverNode && Drag.HoverNode->ContentKey == p->DockKey) + BuildDropZones(ctx, p, rect); + } + + // BuildTabBar + + void ZUIPanelManager::BuildTabBar(ZUIContext* ctx, ZUIPanel* p, float rect[4]) + { + float tab_h = ZUIGetFrameHeight(ctx); + char bar_key[40]; + snprintf(bar_key, sizeof(bar_key), "##tabbar_%llx", (unsigned long long) p->DockKey); + + bool panel_focused = false; + for (uint32_t pi = 0; pi < PanelCount; ++pi) + if (&Panels[pi] == p && pi == FocusedPanelIdx) + { + panel_focused = true; + break; + } + const float* bar_bg = panel_focused ? ctx->Theme.TitleBgActive : ctx->Theme.TitleBarBg; + + ZUIBox* bar = ZUIBeginRow(ctx, bar_key, ZFill(), ZPx(tab_h)); + bar->Flags = bar->Flags | ZUI_DrawBackground | ZUI_Clickable | ZUI_Scrollable | ZUI_ClipChildren; + ZUIBoxSetColorArr(bar, bar_bg); + bar->EdgeSoftness = 0.f; + + ZUISpacer(ctx, ZUIGetFramePadX(ctx)); + + for (uint32_t ti = 0; ti < p->ViewCount; ++ti) + { + ZUIPanelView* view = p->Views[ti]; + if (!view) + { + continue; + } + bool is_active = (ti == p->ActiveTab); + bool tab_hovered = false; // determined after box push (uses previous-frame HotKey) + + char tab_key[64]; + snprintf(tab_key, sizeof(tab_key), "##tab_%llx_%u", (unsigned long long) p->DockKey, ti); + ZUIBox* tab = ZUIBeginRow(ctx, tab_key, ZFit(), ZPx(tab_h)); + tab->Flags = tab->Flags | ZUI_DrawBackground | ZUI_Clickable; + tab->EdgeSoftness = 0.f; + // Vertical padding: centers label + close button (FramePadding.y top and bottom) + float fpy = ZUIGetFramePadY(ctx); + tab->Padding[1] = fpy; + tab->Padding[3] = fpy; + ZUIBoxSetTopRadius(tab, ctx->Style.TabRounding); + + tab_hovered = (ctx->HotKey == tab->Key); + + // 4-state color + if (is_active && panel_focused) + ZUIBoxSetColorArr(tab, ctx->Theme.TabActiveBg); + else if (is_active && !panel_focused) + ZUIBoxSetColorArr(tab, ctx->Theme.TabDimmedSelectedBg); + else + { + const float* rest = panel_focused ? ctx->Theme.TabInactiveBg : ctx->Theme.TabDimmedBg; + ZUIBoxSetColorArr(tab, tab_hovered ? ctx->Theme.TabHoveredBg : rest); + } + + // Label + ZUISpacer(ctx, ZUIGetFramePadX(ctx)); + { + uint32_t tlen = (uint32_t) strlen(view->Title); + ZUIBox* lbl = ZUIPushBox(ctx, view->Title, tlen, ZUI_DrawText); + lbl->Size[0] = ZText(); + lbl->Size[1] = ZFill(); + lbl->TextColor[0] = is_active ? ctx->Theme.TextDefault[0] : ctx->Theme.TextDim[0]; + lbl->TextColor[1] = is_active ? ctx->Theme.TextDefault[1] : ctx->Theme.TextDim[1]; + lbl->TextColor[2] = is_active ? ctx->Theme.TextDefault[2] : ctx->Theme.TextDim[2]; + lbl->TextColor[3] = 1.f; + ZUIPopBox(ctx); + } + + // Close button — space ALWAYS reserved so tab width is stable on hover. + // Pre-compute hash for hover detection before box creation. + bool tab_closed = false; + { + float btn_sz = ctx->Style.FontSize; + bool show_close = is_active || tab_hovered; + ZUISpacer(ctx, ZUIGetInnerSpac(ctx)); + + char xkey[64]; + snprintf(xkey, sizeof(xkey), "x##x_%llx_%u", (unsigned long long) p->DockKey, ti); + uint64_t xhash = ZUIHashStr(xkey, (uint32_t) strlen(xkey)); + bool xh = show_close && (ctx->HotKey == xhash || ctx->ActiveKey == xhash); + + ZUIBoxFlags xflags = ZUI_None; + if (show_close) + xflags = (ZUIBoxFlags) (ZUI_DrawText | ZUI_Clickable | (xh ? ZUI_DrawBackground : 0)); + ZUIBox* xbtn = ZUIPushBox(ctx, xkey, (uint32_t) strlen(xkey), xflags); + xbtn->Size[0] = ZPx(btn_sz); + xbtn->Size[1] = ZFill(); + xbtn->TextAlign = ZUITextAlign::Center; + if (show_close) + { + if (xh) + { + // Hovered: red fill + white × + ZUIBoxSetColor(xbtn, 0.65f, 0.15f, 0.15f, 0.80f); + ZUIBoxSetCornerRadius(xbtn, ctx->Style.FrameRounding); + xbtn->TextColor[0] = 1.f; + xbtn->TextColor[1] = 1.f; + xbtn->TextColor[2] = 1.f; + xbtn->TextColor[3] = 1.f; + } + else + { + // Visible default: brighter than before + float a = is_active ? 0.70f : 0.50f; + xbtn->TextColor[0] = a; + xbtn->TextColor[1] = a; + xbtn->TextColor[2] = a; + xbtn->TextColor[3] = 1.f; + } + } + ZUISignal xsig = ZUISignalFromBox(ctx, xbtn); + ZUIPopBox(ctx); + if (show_close && (xsig.Flags & ZUI_SignalClicked)) + { + tab_closed = true; + } + ZUISpacer(ctx, ZUIGetFramePadX(ctx)); + } + + // Overline — 2px accent strip built INSIDE the active+focused tab so it + // inherits the exact tab width/position with no separate approximation. + // FloatPos is relative to the tab box's ScreenMin (parent-relative per convention). + if (is_active && panel_focused) + { + char ok[64]; + snprintf(ok, sizeof(ok), "##tov_%llx_%u", (unsigned long long) p->DockKey, ti); + ZUIBox* ov = ZUIPushBox(ctx, ok, (uint32_t) strlen(ok), ZUI_DrawBackground | ZUI_FloatX | ZUI_FloatY); + ov->Size[0] = {ZUISizeKind::ParentPercent, 1.f, 1.f}; // full tab width + ov->Size[1] = ZPx(ctx->Style.TabBarOverlineSize); // 2px + ov->FloatPos[0] = 0.f; // tab-relative: left edge + ov->FloatPos[1] = 0.f; // tab-relative: top edge (outside 3px padding) + ZUIBoxSetColorArr(ov, ctx->Theme.TabActiveBorder); + ov->EdgeSoftness = 0.f; + ZUIPopBox(ctx); + } + + ZUISignal sig = ZUISignalFromBox(ctx, tab); + ZUIEndRow(ctx); + + // Close already handled by PreDetectCloseEvents pre-pass. + // Nothing to do here — just break to exit the tab loop cleanly. + if (tab_closed) + { + break; + } + + if (!tab_closed && (sig.Flags & ZUI_SignalClicked)) + { + if (p->ActiveTab != ti) + { + LayoutDirty = true; + } + p->ActiveTab = ti; + for (uint32_t pi = 0; pi < PanelCount; ++pi) + if (&Panels[pi] == p) + { + FocusPanel(pi); + break; + } + } + + if (sig.Flags & ZUI_SignalPressed) + { + Drag.StartX = ctx->MousePos[0]; + Drag.StartY = ctx->MousePos[1]; + p->ReorderActive = false; + p->ReorderAccumX = 0.f; + } + + if (!tab_closed && (sig.Flags & ZUI_SignalHeld) && !Drag.Active) + { + float dx = fabsf(ctx->MousePos[0] - Drag.StartX); + float dy = fabsf(ctx->MousePos[1] - Drag.StartY); + float total = dx + dy; + if (total > ctx->Style.DockingTabReorderThreshold) + { + bool mostly_horiz = (dx > dy * 1.5f); + if (mostly_horiz && p->ViewCount > 1) + { + if (!p->ReorderActive) + { + p->ReorderActive = true; + p->ReorderTabIdx = ti; + p->ReorderAccumX = 0.f; + } + if (p->ReorderActive && p->ReorderTabIdx == ti) + { + p->ReorderAccumX += sig.DragDelta[0]; + float tab_slot = (rect[2] - rect[0]) / (float) p->ViewCount; + if (tab_slot < ctx->Style.DockingMinTabWidth) + tab_slot = ctx->Style.DockingMinTabWidth; + float threshold = tab_slot * 0.5f; + if (p->ReorderAccumX > threshold && ti + 1 < p->ViewCount) + { + ZUIPanelView* tmp = p->Views[ti]; + p->Views[ti] = p->Views[ti + 1]; + p->Views[ti + 1] = tmp; + if (p->ActiveTab == ti) + p->ActiveTab = ti + 1; + else if (p->ActiveTab == ti + 1) + p->ActiveTab = ti; + p->ReorderTabIdx = ti + 1; + p->ReorderAccumX -= threshold; + break; + } + else if (p->ReorderAccumX < -threshold && ti > 0) + { + ZUIPanelView* tmp = p->Views[ti]; + p->Views[ti] = p->Views[ti - 1]; + p->Views[ti - 1] = tmp; + if (p->ActiveTab == ti) + p->ActiveTab = ti - 1; + else if (p->ActiveTab == ti - 1) + p->ActiveTab = ti; + p->ReorderTabIdx = ti - 1; + p->ReorderAccumX += threshold; + break; + } + } + } + else if (!mostly_horiz || dy > ctx->Style.DockingUndockVertical) + { + p->ReorderActive = false; + Drag.Active = true; + Drag.SrcPanel = p; + Drag.SrcTabIdx = ti; + Drag.GhostX = ctx->MousePos[0]; + Drag.GhostY = ctx->MousePos[1]; + } + } + } + + if (ctx->MouseReleased[0] && p->ReorderActive) + { + p->ReorderActive = false; + p->ReorderAccumX = 0.f; + } + + // Inter-tab spacing from style + ZUISpacer(ctx, ZUIGetInnerSpac(ctx)); + } + + // Bar signal — drag empty space = whole-panel drag + // Scroll arrows — appear when tabs overflow the bar width. + // Floated children of the bar so they pin to bar edges regardless of ScrollX. + { + ZUIPersistentState* bps = ZUIStateGetOrInsert(&ctx->StateStore, bar->Key); + if (bps && bps->MaxScrollX > 1.f) + { + float bar_w = rect[2] - rect[0]; + float arrow_w = tab_h; // square arrow button + const float* dim = ctx->Theme.TextDim; + + // Left arrow + if (bps->ScrollX > 0.5f) + { + char lk[64]; + snprintf(lk, sizeof(lk), "##tsl_%llx", (unsigned long long) p->DockKey); + ZUIBox* la = ZUIPushBox(ctx, lk, (uint32_t) strlen(lk), ZUI_DrawBackground | ZUI_DrawText | ZUI_Clickable | ZUI_FloatX | ZUI_FloatY); + la->Size[0] = ZPx(arrow_w); + la->Size[1] = ZPx(tab_h); + la->FloatPos[0] = 0.f; + la->FloatPos[1] = 0.f; + la->TextAlign = ZUITextAlign::Center; + ZUIBoxSetColorArr(la, ctx->Theme.TitleBgActive); + la->Label = ZUIPushStr(&ctx->FrameArena, "<", 1); + la->TextColor[0] = dim[0]; + la->TextColor[1] = dim[1]; + la->TextColor[2] = dim[2]; + la->TextColor[3] = 1.f; + ZUISignal lsig = ZUISignalFromBox(ctx, la); + ZUIPopBox(ctx); + if (lsig.Flags & ZUI_SignalClicked) + { + bps->ScrollX -= tab_h * 3.f; + if (bps->ScrollX < 0.f) + bps->ScrollX = 0.f; + } + } + + // Right arrow + if (bps->ScrollX < bps->MaxScrollX - 0.5f) + { + char rk[64]; + snprintf(rk, sizeof(rk), "##tsr_%llx", (unsigned long long) p->DockKey); + ZUIBox* ra = ZUIPushBox(ctx, rk, (uint32_t) strlen(rk), ZUI_DrawBackground | ZUI_DrawText | ZUI_Clickable | ZUI_FloatX | ZUI_FloatY); + ra->Size[0] = ZPx(arrow_w); + ra->Size[1] = ZPx(tab_h); + ra->FloatPos[0] = bar_w - arrow_w; + ra->FloatPos[1] = 0.f; + ra->TextAlign = ZUITextAlign::Center; + ZUIBoxSetColorArr(ra, ctx->Theme.TitleBgActive); + ra->Label = ZUIPushStr(&ctx->FrameArena, ">", 1); + ra->TextColor[0] = dim[0]; + ra->TextColor[1] = dim[1]; + ra->TextColor[2] = dim[2]; + ra->TextColor[3] = 1.f; + ZUISignal rsig = ZUISignalFromBox(ctx, ra); + ZUIPopBox(ctx); + if (rsig.Flags & ZUI_SignalClicked) + { + bps->ScrollX += tab_h * 3.f; + if (bps->ScrollX > bps->MaxScrollX) + bps->ScrollX = bps->MaxScrollX; + } + } + } + } + + ZUISignal bar_sig = ZUISignalFromBox(ctx, bar); + if (bar_sig.Flags & ZUI_SignalPressed) + { + Drag.StartX = ctx->MousePos[0]; + Drag.StartY = ctx->MousePos[1]; + } + if ((bar_sig.Flags & ZUI_SignalHeld) && !Drag.Active) + { + float dx = fabsf(ctx->MousePos[0] - Drag.StartX), dy = fabsf(ctx->MousePos[1] - Drag.StartY); + if (dx + dy > ctx->Style.DockingDragThreshold) + { + float panel_r[4] = {}; + if (!ZUIDockRectForKey(DockTree, p->DockKey, panel_r)) + { + panel_r[0] = rect[0]; + panel_r[1] = rect[1]; + panel_r[2] = rect[2]; + panel_r[3] = rect[3] + 200.f; + } + Drag.Active = true; + Drag.SrcPanel = p; + Drag.SrcTabIdx = kWholePanel; + Drag.GhostX = ctx->MousePos[0]; + Drag.GhostY = ctx->MousePos[1]; + } + } + + ZUIEndRow(ctx); + } + + // BuildDropZones — position-based ImGui-style subdivision + // Zone is determined by WHERE the mouse is within the target panel, + // not by hovering explicit indicator widgets. Only a split preview + // overlay is drawn — no directional arrows, no target boxes. + + // Coordinate convention for all overlay boxes in ZUIPanelManager: + // + // Parent FloatPos convention + // ##pm_bg absolute screen coords (ScreenMin=0,0 → FloatPos == screen pos) + // panel column panel-relative coords (ScreenMin=rect[0,1] → FloatPos offset from panel origin) + // + // Elements and their parent: + // Drop zone preview → ##pm_bg (absolute) + // Dividers → ##pm_bg (absolute) + // Focus strip → ##pm_bg (absolute) + // Drag ghost → ##pm_bg (absolute) + // Overline → panel column (panel-relative, inside BuildTabBar before ZUIEndRow) + // + // BuildDropZones is called AFTER ZUIEndColumn for the panel box, so + // ctx->Current == ##pm_bg here. All FloatPos values must be absolute screen coords. + void ZUIPanelManager::BuildDropZones(ZUIContext* ctx, ZUIPanel* p, float rect[4]) + { + float w = rect[2] - rect[0]; + float h = rect[3] - rect[1]; + float mx = ctx->MousePos[0]; + float my = ctx->MousePos[1]; + float rx = (w > 0.f) ? (mx - rect[0]) / w : 0.5f; // 0..1 across width + float ry = (h > 0.f) ? (my - rect[1]) / h : 0.5f; // 0..1 down height + + float tab_h = ZUIGetFrameHeight(ctx); + bool over_tab_bar = (my >= rect[1] && my <= rect[1] + tab_h); + // When dragging over the source panel itself, only edge splits are valid — + // center zone would merge with self. Tab bar on source panel also uses edge detection. + bool is_src = (Drag.SrcPanel == p); + + ZUIDropZone zone; + if (over_tab_bar && !is_src) + { + // Dropping on another panel's tab bar: tab merge with insertion index. + zone = ZUIDropZone::Center; + + uint32_t insert_idx = p->ViewCount; // default: append at end + float insert_x = rect[2]; // default: right edge of tab bar + + for (uint32_t ti = 0; ti < p->ViewCount; ++ti) + { + char tk[64]; + snprintf(tk, sizeof(tk), "##tab_%llx_%u", (unsigned long long) p->DockKey, ti); + uint64_t thash = ZUIHashStr(tk, (uint32_t) strlen(tk)); + ZUIPersistentState* ps = ZUIStateGetOrInsert(&ctx->StateStore, thash); + + // ps valid and has been laid out (non-zero width from a previous frame) + if (ps && ps->ScreenMaxX > ps->ScreenMinX) + { + float tab_mid = (ps->ScreenMinX + ps->ScreenMaxX) * 0.5f; + if (mx < tab_mid) + { + insert_idx = ti; + insert_x = ps->ScreenMinX; + break; + } + // Gap between this tab and the next + insert_x = ps->ScreenMaxX + ZUIGetInnerSpac(ctx) * 0.5f; + } + } + Drag.DropTabInsertIdx = insert_idx; + + // Draw VS Code-style 2px white vertical insertion bar. + char ik[48]; + snprintf(ik, sizeof(ik), "##dz_ins_%llx", (unsigned long long) p->DockKey); + ZUIBox* ins = ZUIPushBox(ctx, ik, (uint32_t) strlen(ik), ZUI_DrawBackground | ZUI_FloatX | ZUI_FloatY); + ins->Size[0] = ZPx(2.f); + ins->Size[1] = ZPx(tab_h); + ins->FloatPos[0] = insert_x - 1.f; // absolute screen coords (##pm_bg parent) + ins->FloatPos[1] = rect[1]; + ZUIBoxSetColor(ins, 1.f, 1.f, 1.f, 0.90f); + ins->EdgeSoftness = 0.f; + ZUIPopBox(ctx); + } + else + { + Drag.DropTabInsertIdx = kWholePanel; + float kEdge = ctx->Style.DockingDropZoneEdge; + if (rx < kEdge) + zone = ZUIDropZone::Left; + else if (rx > 1.f - kEdge) + zone = ZUIDropZone::Right; + else if (ry < kEdge) + zone = ZUIDropZone::Top; + else if (ry > 1.f - kEdge) + zone = ZUIDropZone::Bottom; + else + zone = is_src ? ZUIDropZone::None // no self-center merge + : ZUIDropZone::Center; + + // Teal overlay for split/merge preview (skipped when center zone blocked on src panel) + float px0 = rect[0], py0 = rect[1], px1 = rect[2], py1 = rect[3]; + if (zone == ZUIDropZone::None) + { + Drag.DropZone = zone; + return; + } + switch (zone) + { + case ZUIDropZone::Left: + px1 = rect[0] + w * 0.5f; + break; + case ZUIDropZone::Right: + px0 = rect[0] + w * 0.5f; + break; + case ZUIDropZone::Top: + py1 = rect[1] + h * 0.5f; + break; + case ZUIDropZone::Bottom: + py0 = rect[1] + h * 0.5f; + break; + default: + break; + } + char pk[40]; + snprintf(pk, sizeof(pk), "##dz_prev_%llx", (unsigned long long) p->DockKey); + ZUIDropZoneFill(ctx, pk, px0, py0, px1 - px0, py1 - py0); + } + + Drag.DropZone = zone; + + if (ctx->MouseReleased[0] && zone != ZUIDropZone::None && Drag.SrcPanel) + { + ZUIDockNode* dst_node = ZUIDockFindLeaf(DockTree, p->DockKey); + if (dst_node) + CommitDrop(Drag.SrcPanel, Drag.SrcTabIdx, dst_node, zone); + Drag.Active = false; + Drag.SrcPanel = nullptr; + Drag.DropZone = ZUIDropZone::None; + Drag.HoverNode = nullptr; + Drag.DropTabInsertIdx = kWholePanel; + } + } + + // BuildDividers — dynamic from dock tree + + // Input pass — added FIRST to ##pm_bg so hit zones are last-processed by the LIFO + // interaction traversal → always win ctx->HotKey over any panel content underneath. + void ZUIPanelManager::BuildDividerHitZones(ZUIContext* ctx) + { + if (!DockTree) + return; + float grab_half = ctx->Style.DockingGrabWidth * 0.5f; + float band_w = ctx->Style.DockingHoverBandWidth; + + for (uint32_t di = 0; di < m_split_divider_count; ++di) + { + ZUIDockNode* snode = m_split_dividers[di].Node; + if (!snode || !snode->First || !snode->First->Next) + continue; + ZUIDockNode* child1 = snode->First; + bool horizontal = (snode->SplitAxis == ZUIAxis::Y); + float dx0, dy0, dx1, dy1; + if (!horizontal) + { + float ex = child1->RectMax[0]; + dx0 = ex - grab_half; + dy0 = snode->RectMin[1]; + dx1 = ex + grab_half; + dy1 = snode->RectMax[1]; + } + else + { + float ey = child1->RectMax[1]; + dx0 = snode->RectMin[0]; + dy0 = ey - grab_half; + dx1 = snode->RectMax[0]; + dy1 = ey + grab_half; + } + ZUISignal ha_sig = {}; + + if (band_w > 0.f) + { + float bx0, by0, bx1, by1; + if (!horizontal) + { + float cx = (dx0 + dx1) * 0.5f; + bx0 = cx - band_w * 0.5f; + by0 = dy0; + bx1 = cx + band_w * 0.5f; + by1 = dy1; + } + else + { + float cy = (dy0 + dy1) * 0.5f; + bx0 = dx0; + by0 = cy - band_w * 0.5f; + bx1 = dx1; + by1 = cy + band_w * 0.5f; + } + char hk[32]; + snprintf(hk, sizeof(hk), "##sdivh_%u", di); + ZUIBox* ha = ZUIPushBox(ctx, hk, (uint32_t) strlen(hk), ZUI_DrawBackground | ZUI_Clickable | ZUI_FloatX | ZUI_FloatY); + ha->Size[0] = ZPx(bx1 - bx0); + ha->Size[1] = ZPx(by1 - by0); + ha->FloatPos[0] = bx0; + ha->FloatPos[1] = by0; + ha->EdgeSoftness = 0.f; + ZUIBoxSetColor(ha, 0.f, 0.f, 0.f, 0.f); + ha_sig = ZUISignalFromBox(ctx, ha); + ZUIPopBox(ctx); + } + + bool& dragging = m_split_dividers[di].Dragging; + if (ha_sig.Flags & ZUI_SignalPressed) + dragging = true; + if (ctx->MouseReleased[0] && dragging) + { + dragging = false; + LayoutDirty = true; + } + if (ha_sig.Flags & ZUI_SignalHeld) + { + float delta = horizontal ? ha_sig.DragDelta[1] : ha_sig.DragDelta[0]; + if (delta != 0.f) + ZUIDockResize(DockTree, child1, delta); + } + + // Cursor — use in_rect for immediate feedback (signal has 1-frame lag) + float mx = ctx->MousePos[0], my = ctx->MousePos[1]; + if ((mx >= dx0 && mx <= dx1 && my >= dy0 && my <= dy1) || dragging) + ctx->ResizeCursor = horizontal ? 2 : 1; + } + } + + // Render pass — added LAST to ##pm_bg so the visual line is always drawn on top of panels. + void ZUIPanelManager::BuildDividerVisuals(ZUIContext* ctx) + { + if (!DockTree) + return; + float grab_half = ctx->Style.DockingGrabWidth * 0.5f; + float mx = ctx->MousePos[0], my = ctx->MousePos[1]; + + for (uint32_t di = 0; di < m_split_divider_count; ++di) + { + ZUIDockNode* snode = m_split_dividers[di].Node; + if (!snode || !snode->First || !snode->First->Next) + continue; + ZUIDockNode* child1 = snode->First; + bool horizontal = (snode->SplitAxis == ZUIAxis::Y); + float dx0, dy0, dx1, dy1; + if (!horizontal) + { + float ex = child1->RectMax[0]; + dx0 = ex - grab_half; + dy0 = snode->RectMin[1]; + dx1 = ex + grab_half; + dy1 = snode->RectMax[1]; + } + else + { + float ey = child1->RectMax[1]; + dx0 = snode->RectMin[0]; + dy0 = ey - grab_half; + dx1 = snode->RectMax[0]; + dy1 = ey + grab_half; + } + (void) child1; + + bool dragging = m_split_dividers[di].Dragging; + bool in_rect = (mx >= dx0 && mx <= dx1 && my >= dy0 && my <= dy1); + bool highlight = in_rect || dragging; + + float lw = highlight ? ctx->Style.DockingSeparatorSize : ctx->Style.DockingSeparatorSizeRest; + float vc[4] = {ctx->Theme.TabActiveBorder[0], ctx->Theme.TabActiveBorder[1], ctx->Theme.TabActiveBorder[2], highlight ? 1.f : 0.35f}; + if (!highlight) + { + vc[0] = ctx->Theme.Separator[0]; + vc[1] = ctx->Theme.Separator[1]; + vc[2] = ctx->Theme.Separator[2]; + } + + char vk[32]; + snprintf(vk, sizeof(vk), "##sdiv_%u", di); + ZUIBox* vis = ZUIPushBox(ctx, vk, (uint32_t) strlen(vk), ZUI_DrawBackground | ZUI_FloatX | ZUI_FloatY); + if (!horizontal) + { + vis->Size[0] = ZPx(lw); + vis->Size[1] = ZPx(dy1 - dy0); + vis->FloatPos[0] = (dx0 + dx1) * 0.5f - lw * 0.5f; + vis->FloatPos[1] = dy0; + } + else + { + vis->Size[0] = ZPx(dx1 - dx0); + vis->Size[1] = ZPx(lw); + vis->FloatPos[0] = dx0; + vis->FloatPos[1] = (dy0 + dy1) * 0.5f - lw * 0.5f; + } + vis->EdgeSoftness = 0.f; + ZUIBoxSetColorArr(vis, vc); + ZUIPopBox(ctx); + } + } + + // CommitDrop — moves a tab (or whole panel) to a new dock position + + void ZUIPanelManager::CommitDrop(ZUIPanel* src, uint32_t tab_idx, ZUIDockNode* dst, ZUIDropZone zone) + { + if (!src || !dst) + return; + LayoutDirty = true; // structural change → save next frame + + if (tab_idx == kWholePanel) + { + // Move entire panel + if (zone == ZUIDropZone::Center) + { + // Merge all source views into destination as tabs + ZUIPanel* dst_panel = FindPanel(dst->ContentKey); + if (dst_panel && dst_panel != src) + { + uint32_t ins = Drag.DropTabInsertIdx; + for (uint32_t i = 0; i < src->ViewCount; ++i) + InsertViewAt(dst_panel, src->Views[i], ins == kWholePanel ? ins : ins + i); + src->ViewCount = 0; + src->Hidden = true; + ZUIDockNode* src_leaf = ZUIDockFindLeaf(DockTree, src->DockKey); + if (src_leaf) + ZUIDockCollapseLeaf(DockTree, src_leaf); + } + } + else + { + if (dst->ContentKey == src->DockKey) + return; // cannot dock panel into itself + // Split and move panel to new slot + // First collapse source to free its slot + ZUIDockNode* src_leaf = ZUIDockFindLeaf(DockTree, src->DockKey); + if (src_leaf) + ZUIDockCollapseLeaf(DockTree, src_leaf); + // Re-find destination after potential tree change + ZUIDockNode* new_dst = ZUIDockFindLeaf(DockTree, dst->ContentKey); + if (!new_dst) + { + src->Hidden = true; + return; + } // tree changed under us — orphan safely + { + float split_pct = 0.5f; + if (zone == ZUIDropZone::Left || zone == ZUIDropZone::Right) + { + bool left = (zone == ZUIDropZone::Left); + ZUIDockSplitH(DockTree, new_dst, left ? split_pct : 1.f - split_pct, left ? src->DockKey : new_dst->ContentKey, left ? new_dst->ContentKey : src->DockKey); + } + else + { + bool top = (zone == ZUIDropZone::Top); + ZUIDockSplitV(DockTree, new_dst, top ? split_pct : 1.f - split_pct, top ? src->DockKey : new_dst->ContentKey, top ? new_dst->ContentKey : src->DockKey); + } + } + } + return; + } + + // Single tab move + if (tab_idx >= src->ViewCount) + return; + ZUIPanelView* view = src->Views[tab_idx]; + + if (zone == ZUIDropZone::Center) + { + ZUIPanel* dst_panel = FindPanel(dst->ContentKey); + if (dst_panel && dst_panel != src) + { + uint32_t ins = Drag.DropTabInsertIdx; + InsertViewAt(dst_panel, view, ins); + // Focus the newly inserted tab in the destination panel. + dst_panel->ActiveTab = (ins == kWholePanel || ins >= dst_panel->ViewCount) ? dst_panel->ViewCount - 1 : ins; + for (uint32_t i = tab_idx; i + 1 < src->ViewCount; ++i) + src->Views[i] = src->Views[i + 1]; + --src->ViewCount; + if (src->ActiveTab >= src->ViewCount && src->ViewCount > 0) + src->ActiveTab = src->ViewCount - 1; + if (src->ViewCount == 0) + { + ZUIDockNode* src_leaf = ZUIDockFindLeaf(DockTree, src->DockKey); + if (src_leaf) + ZUIDockCollapseLeaf(DockTree, src_leaf); + src->Hidden = true; + } + } + } + else + { + float split_pct = 0.5f; + // Use a session-unique key: avoids collisions with existing panels that + // share the same title (e.g. dragging "Hierarchy" tab to an edge zone + // would otherwise collide with the existing Hierarchy panel's DockKey). + uint64_t new_key = ++DragKeySeq; + + // Check capacity BEFORE mutating the tree — orphaned leaf otherwise. + ZUIPanel* new_panel = AddPanel(new_key); + if (!new_panel) + return; + + // Now it's safe to split dst. + if (zone == ZUIDropZone::Left || zone == ZUIDropZone::Right) + { + bool left = (zone == ZUIDropZone::Left); + ZUIDockSplitH(DockTree, dst, left ? split_pct : 1.f - split_pct, left ? new_key : dst->ContentKey, left ? dst->ContentKey : new_key); + } + else + { + bool top = (zone == ZUIDropZone::Top); + ZUIDockSplitV(DockTree, dst, top ? split_pct : 1.f - split_pct, top ? new_key : dst->ContentKey, top ? dst->ContentKey : new_key); + } + + AddView(new_panel, view); + for (uint32_t i = tab_idx; i + 1 < src->ViewCount; ++i) + src->Views[i] = src->Views[i + 1]; + --src->ViewCount; + if (src->ActiveTab >= src->ViewCount && src->ViewCount > 0) + src->ActiveTab = src->ViewCount - 1; + if (src->ViewCount == 0) + { + ZUIDockNode* src_leaf = ZUIDockFindLeaf(DockTree, src->DockKey); + if (src_leaf) + ZUIDockCollapseLeaf(DockTree, src_leaf); + src->Hidden = true; + } + } + } + +} // namespace ZEngine::UI diff --git a/ZEngine/ZEngine/UI/ZUIPanel.h b/ZEngine/ZEngine/UI/ZUIPanel.h new file mode 100644 index 000000000..3ba852ffd --- /dev/null +++ b/ZEngine/ZEngine/UI/ZUIPanel.h @@ -0,0 +1,161 @@ +#pragma once +#include <ZEngine/Core/Memory/Allocator.h> +#include <ZEngine/UI/ZUIBox.h> +#include <ZEngine/UI/ZUIContext.h> +#include <ZEngine/UI/ZUIDockspace.h> +#include <cstdint> + +namespace ZEngine::UI +{ + /// @brief Base class for any view that can live in a panel tab. + /// + /// Derive from ZUIPanelView and implement BuildContent() to provide the + /// panel's rendered content. Register the view with ZUIPanelManager::AddView(). + struct ZUIPanelView + { + const char* Title = "Panel"; ///< Displayed in the tab bar + uint64_t Key = 0; ///< Optional stable key; 0 = derive from Title + bool Visible = true; + float TabColor[4] = {0.f, 0.f, 0.f, 0.f}; ///< Per-tab accent (alpha=0 → theme default) + + virtual ~ZUIPanelView() = default; + + /// @brief Called every frame to build the view's widget tree. + /// @param ctx Active ZUI context. + /// @param rect Screen rect {x0, y0, x1, y1} allocated to this view. + virtual void BuildContent(ZUIContext* ctx, float rect[4]) = 0; + + /// @brief Called once after the ZUI font atlas is baked. Optional. + virtual void Initialize(ZUIContext*) {} + }; + + static constexpr uint32_t kMaxTabsPerPanel = 16; + + /// @brief One docked panel holding 1..N views as tabs. + struct ZUIPanel + { + uint64_t DockKey = 0; + ZUIPanelView* Views[kMaxTabsPerPanel] = {}; + uint32_t ViewCount = 0; + uint32_t ActiveTab = 0; + bool Hidden = false; ///< Closed by user; restorable from Window menu + + bool ReorderActive = false; + uint32_t ReorderTabIdx = 0; + float ReorderAccumX = 0.f; + }; + + /// SrcTabIdx == kWholePanel means the drag moves the entire panel, not a single tab. + static constexpr uint32_t kWholePanel = 0xFFFFFFFFu; + + /// @brief Target zone when a dragged panel is released over another panel. + enum class ZUIDropZone + { + None, + Center, + Left, + Right, + Top, + Bottom + }; + + /// @brief Transient drag-to-dock state — valid only while a drag is active. + struct ZUIDragDockState + { + bool Active = false; + ZUIPanel* SrcPanel = nullptr; + uint32_t SrcTabIdx = 0; ///< Tab index or kWholePanel + float GhostX = 0.f; + float GhostY = 0.f; + float StartX = 0.f; + float StartY = 0.f; + ZUIDockNode* HoverNode = nullptr; + ZUIDropZone DropZone = ZUIDropZone::None; + uint32_t DropTabInsertIdx = 0xFFFFFFFFu; ///< kWholePanel = append at end + }; + + static constexpr uint32_t kMaxPanels = 32; + + /// @brief Manages the panel layout: docking tree, tab bars, drag-to-dock, + /// divider resize, and ini persistence. + /// + /// Typical setup: + /// @code + /// ZUIPanelManager mgr; + /// mgr.Init(&arena); + /// ZUIDockSplitH(mgr.DockTree, mgr.DockTree->Root, 0.25f, kHierKey, kViewportKey); + /// auto* p = mgr.AddPanel(kHierKey); + /// mgr.AddView(p, &hierarchyView); + /// mgr.SetLayoutPath("layout.ini"); + /// // each frame: + /// mgr.BuildUI(ctx, menu_h, status_h); + /// @endcode + struct ZUIPanelManager + { + ZUIDockTree* DockTree = nullptr; + ZUIPanel Panels[kMaxPanels]; + uint32_t PanelCount = 0; + uint32_t FocusedPanelIdx = 0; + + char LayoutPath[256] = {}; ///< Set before first BuildUI; empty = no persistence + bool LayoutDirty = false; + + uint64_t PendingCloseKeys[kMaxPanels] = {}; + uint32_t PendingCloseCount = 0; + + ZUIDragDockState Drag; + uint64_t DragKeySeq = 0xD0C400000000ULL; ///< Monotone counter for drag-split panel keys + + /// @brief Allocate the dock tree and reset panel state. + /// @param arena Persistent arena; must outlive the manager. + void Init(ZEngine::Core::Memory::ArenaAllocator* arena); + void Shutdown(); + + /// @brief Register a new panel slot keyed by @p dock_key. + /// @param dock_key ZUIDockHashName("PanelName") or a drag-generated key. + /// @return Pointer to the new ZUIPanel, or nullptr if kMaxPanels is reached. + ZUIPanel* AddPanel(uint64_t dock_key); + + /// @brief Append @p view to @p panel's tab list. + /// @note No-op when panel is null or already at kMaxTabsPerPanel. + void AddView(ZUIPanel* panel, ZUIPanelView* view); + + /// @brief Build the entire panel UI for this frame. + /// @param ctx Active ZUI context. + /// @param menu_h Height of the menu bar in logical pixels. + /// @param status_h Height of the status bar in logical pixels. + void BuildUI(ZUIContext* ctx, float menu_h, float status_h); + + /// @brief Find the panel registered with @p dock_key. + /// @return Pointer to the panel, or nullptr if not found. + ZUIPanel* FindPanel(uint64_t dock_key); + + /// @brief Set the ini file path for layout persistence. + /// @param path Relative or absolute path; empty string disables saving. + void SetLayoutPath(const char* path); + + private: + struct SplitDivider + { + ZUIDockNode* Node = nullptr; + bool Dragging = false; + }; + static constexpr uint32_t kMaxSplitDividers = 32; + SplitDivider m_split_dividers[kMaxSplitDividers] = {}; + uint32_t m_split_divider_count = 0; + + void PreDetectCloseEvents(ZUIContext* ctx); + void BuildMenuBar(ZUIContext* ctx, float sw, float mh); + void BuildDockedPanel(ZUIContext* ctx, ZUIPanel* p, float rect[4]); + void BuildTabBar(ZUIContext* ctx, ZUIPanel* p, float rect[4]); + void BuildDropZones(ZUIContext* ctx, ZUIPanel* p, float rect[4]); + void BuildDividerHitZones(ZUIContext* ctx); ///< Input pass — first; owns ActiveKey for resize + void BuildDividerVisuals(ZUIContext* ctx); ///< Render pass — last; always on top of panels + void CommitDrop(ZUIPanel* src, uint32_t tab_idx, ZUIDockNode* dst, ZUIDropZone zone); + void FocusPanel(uint32_t idx); + void SyncSplitDividers(); + bool GetSplitDividerDragging(ZUIDockNode* node) const; + void SetSplitDividerDragging(ZUIDockNode* node, bool v); + }; + +} // namespace ZEngine::UI diff --git a/ZEngine/ZEngine/UI/ZUIWidgets.cpp b/ZEngine/ZEngine/UI/ZUIWidgets.cpp new file mode 100644 index 000000000..f4deb0fd9 --- /dev/null +++ b/ZEngine/ZEngine/UI/ZUIWidgets.cpp @@ -0,0 +1,4082 @@ +#include <ZEngine/Helpers/MemoryOperations.h> +#include <ZEngine/UI/ZUIWidgets.h> +#include <cmath> +#include <cstdio> +#include <cstdlib> +#include <cstring> + +namespace ZEngine::UI +{ + // Internal helpers + + static void SetTextColor(ZUIBox* box, const float c[4]) + { + box->TextColor[0] = c[0]; + box->TextColor[1] = c[1]; + box->TextColor[2] = c[2]; + box->TextColor[3] = c[3]; + } + + static void SetBgArr(ZUIBox* b, const float c[4]) + { + ZUIBoxSetColorArr(b, c); + } + static void SetBdrArr(ZUIBox* b, const float c[4]) + { + b->BorderColor[0] = c[0]; + b->BorderColor[1] = c[1]; + b->BorderColor[2] = c[2]; + b->BorderColor[3] = c[3]; + } + + // Lerp box background through rest → hover → active using HotT/ActiveT. + // Call AFTER ZUISignalFromBox so the persistent state is already updated this frame. + static void ApplyHotActive(ZUIBox* box, ZUIContext* ctx, const float rest[4], const float hov[4], const float act[4]) + { + ZUIPersistentState* st = ZUIStateGetOrInsert(&ctx->StateStore, box->Key); + if (!st) + return; + float ht = st->HotT, at = st->ActiveT; + for (int ch = 0; ch < 4; ++ch) + { + float col = rest[ch] + (hov[ch] - rest[ch]) * ht + (act[ch] - hov[ch]) * at; + for (int k = 0; k < 4; ++k) + box->Colors[k][ch] = col; + } + } + + // Layout containers + + ZUIBox* ZUIBeginColumn(ZUIContext* ctx, const char* key, ZUISize w, ZUISize h) + { + uint32_t len = (uint32_t) strlen(key); + ZUIBox* box = ZUIPushBox(ctx, key, len, ZUI_None); + box->Size[0] = w; + box->Size[1] = h; + box->LayoutAxis = ZUIAxis::Y; + return box; + } + + void ZUIEndColumn(ZUIContext* ctx) + { + ZUIPopBox(ctx); + } + + ZUIBox* ZUIBeginRow(ZUIContext* ctx, const char* key, ZUISize w, ZUISize h) + { + uint32_t len = (uint32_t) strlen(key); + ZUIBox* box = ZUIPushBox(ctx, key, len, ZUI_None); + box->Size[0] = w; + box->Size[1] = h; + box->LayoutAxis = ZUIAxis::X; + return box; + } + + void ZUIEndRow(ZUIContext* ctx) + { + ZUIPopBox(ctx); + } + + ZUIBox* ZUIBeginScrollRegion(ZUIContext* ctx, const char* key, ZUISize w, ZUISize h) + { + uint32_t len = (uint32_t) strlen(key); + ZUIBox* box = ZUIPushBox(ctx, key, len, ZUI_Scrollable | ZUI_ClipChildren | ZUI_Clickable); + box->Size[0] = w; + box->Size[1] = h; + box->LayoutAxis = ZUIAxis::Y; + + // Smooth scroll — RAD Debugger model: + // ScrollYTarget written by wheel input (ZUIInteractionPass); ScrollY animated toward it. + // MaxScrollY owned by the layout pass (bottom-up from child extents); read-only here. + // Snap < 2px prevents infinite micro-oscillation (RAD: abs(off - target) < 2 → snap). + ZUIPersistentState* ps = ZUIStateGetOrInsert(&ctx->StateStore, box->Key); + if (ps) + { + float dt = ctx->DeltaTime > 0.f ? ctx->DeltaTime : (1.f / 60.f); + float alpha = 1.f - expf(-ctx->Style.ScrollSmoothSpeed * dt); + + ps->ScrollY += (ps->ScrollYTarget - ps->ScrollY) * alpha; + if (fabsf(ps->ScrollYTarget - ps->ScrollY) < 2.f) + ps->ScrollY = ps->ScrollYTarget; + if (ps->ScrollY < 0.f) + ps->ScrollY = 0.f; + if (ps->MaxScrollY > 0.f && ps->ScrollY > ps->MaxScrollY) + ps->ScrollY = ps->MaxScrollY; + + ps->ScrollX += (ps->ScrollXTarget - ps->ScrollX) * alpha; + if (fabsf(ps->ScrollXTarget - ps->ScrollX) < 2.f) + ps->ScrollX = ps->ScrollXTarget; + if (ps->ScrollX < 0.f) + ps->ScrollX = 0.f; + if (ps->MaxScrollX > 0.f && ps->ScrollX > ps->MaxScrollX) + ps->ScrollX = ps->MaxScrollX; + } + + return box; + } + + void ZUIEndScrollRegion(ZUIContext* ctx) + { + // Before popping, inject a visible scrollbar if content overflows. + // We read prev-frame screen rect from ZUIPersistentState so the thumb + // is correctly positioned even though layout hasn't run yet this frame. + ZUIBox* sb = ctx->Current; + if (sb && sb->Key) + { + // Update HotT for the scroll region — drives scrollbar auto-hide alpha + ZUISignalFromBox(ctx, sb); + + ZUIPersistentState* ps = ZUIStateGetOrInsert(&ctx->StateStore, sb->Key); + if (ps && ps->MaxScrollY > 1.f && ps->ScreenMaxX > ps->ScreenMinX) + { + float sx0 = ps->ScreenMinX, sy0 = ps->ScreenMinY; + float sx1 = ps->ScreenMaxX, sy1 = ps->ScreenMaxY; + float dt = ctx->DeltaTime > 0.f ? ctx->DeltaTime : (1.f / 60.f); + + // Advance show timer; keep alive while smooth scroll animation is running + ps->ScrollbarShowTimer = fmaxf(0.f, ps->ScrollbarShowTimer - dt); + if (fabsf(ps->ScrollY - ps->ScrollYTarget) > 0.5f) + ps->ScrollbarShowTimer = fmaxf(ps->ScrollbarShowTimer, 0.3f); + + float track_h = sy1 - sy0; + float content_h = track_h + ps->MaxScrollY; + float thumb_h = (content_h > 0.f) ? (track_h * track_h / content_h) : track_h; + if (thumb_h < ctx->Style.ScrollbarMinThumbPx) + thumb_h = ctx->Style.ScrollbarMinThumbPx; + if (thumb_h > track_h) + thumb_h = track_h; + float thumb_y = (ps->MaxScrollY > 0.f) ? sy0 + (ps->ScrollY / ps->MaxScrollY) * (track_h - thumb_h) : sy0; + + // Visibility: floor (always discoverable) + scroll-timer + hover — VS Code model. + // Floor keeps a faint hint at rest; timer flashes full on wheel/drag; hover sustains it. + float vis_timer = ps->ScrollbarShowTimer / 0.5f; + float vis = fmaxf(fmaxf(ps->HotT, vis_timer), ctx->Style.ScrollbarAutoHideAlpha); + const float kBarW = ctx->Style.ScrollbarSize; + + // FloatPos is parent-relative (scroll region ScreenMin = (sx0, sy0)). + // sx1-kBarW and thumb_y are absolute screen coords — subtract sx0/sy0. + float bar_x = (sx1 - kBarW) - sx0; // right edge, relative to scroll region left + + // Track + char trk[64]; + snprintf(trk, sizeof(trk), "##sbtrk_%llu", (unsigned long long) sb->Key); + ZUIBox* track = ZUIPushBox(ctx, trk, (uint32_t) strlen(trk), ZUI_DrawBackground | ZUI_FloatX | ZUI_FloatY); + track->Size[0] = ZPx(kBarW); + track->Size[1] = ZPx(track_h); + track->FloatPos[0] = bar_x; + track->FloatPos[1] = 0.f; // sy0 - sy0 = 0 + ZUIBoxSetColor(track, ctx->Theme.ScrollbarGrab[0], ctx->Theme.ScrollbarGrab[1], ctx->Theme.ScrollbarGrab[2], 0.15f * vis); + track->EdgeSoftness = 0.f; + ZUIPopBox(ctx); + + // Thumb + char thm[64]; + snprintf(thm, sizeof(thm), "##sbthm_%llu", (unsigned long long) sb->Key); + ZUIBox* thumb = ZUIPushBox(ctx, thm, (uint32_t) strlen(thm), ZUI_DrawBackground | ZUI_Clickable | ZUI_FloatX | ZUI_FloatY); + thumb->Size[0] = ZPx(kBarW); + thumb->Size[1] = ZPx(thumb_h); + thumb->FloatPos[0] = bar_x; + thumb->FloatPos[1] = thumb_y - sy0; // relative to scroll region top + float grab[4] = {ctx->Theme.ScrollbarGrab[0], ctx->Theme.ScrollbarGrab[1], ctx->Theme.ScrollbarGrab[2], vis}; + float grab_hov[4] = {ctx->Theme.ScrollbarGrabHov[0], ctx->Theme.ScrollbarGrabHov[1], ctx->Theme.ScrollbarGrabHov[2], vis}; + float grab_act[4] = {ctx->Theme.ScrollbarGrabAct[0], ctx->Theme.ScrollbarGrabAct[1], ctx->Theme.ScrollbarGrabAct[2], 1.f}; + ZUIBoxSetColorArr(thumb, grab); + ZUIBoxSetCornerRadius(thumb, ctx->Style.ScrollbarRounding); + thumb->EdgeSoftness = 0.5f; + + ZUISignal tsig = ZUISignalFromBox(ctx, thumb); + ApplyHotActive(thumb, ctx, grab, grab_hov, grab_act); + + if ((tsig.Flags & ZUI_SignalHeld) && tsig.DragDelta[1] != 0.f) + { + float ratio = (track_h - thumb_h) > 0.f ? ps->MaxScrollY / (track_h - thumb_h) : 0.f; + float new_scroll = ps->ScrollY + tsig.DragDelta[1] * ratio; + if (new_scroll < 0.f) + new_scroll = 0.f; + if (new_scroll > ps->MaxScrollY) + new_scroll = ps->MaxScrollY; + // Sync both: immediate position (no lerp lag while dragging) + target + ps->ScrollY = new_scroll; + ps->ScrollYTarget = new_scroll; + ps->ScrollbarShowTimer = 0.5f; + } + ZUIPopBox(ctx); + } + + // Horizontal scrollbar (appears when content overflows on X) + if (ps && ps->MaxScrollX > 1.f && ps->ScreenMaxY > ps->ScreenMinY) + { + float sx0 = ps->ScreenMinX, sy0 = ps->ScreenMinY; + float sx1 = ps->ScreenMaxX, sy1 = ps->ScreenMaxY; + float track_w = sx1 - sx0; + float content_w = track_w + ps->MaxScrollX; + float thumb_w = (content_w > 0.f) ? (track_w * track_w / content_w) : track_w; + if (thumb_w < ctx->Style.ScrollbarMinThumbPx) + thumb_w = ctx->Style.ScrollbarMinThumbPx; + if (thumb_w > track_w) + thumb_w = track_w; + // FloatPos is parent-relative; convert absolute coords to scroll-region-relative + const float kBarH = ctx->Style.ScrollbarSize; + float thumb_x_rel = (ps->MaxScrollX > 0.f) ? (ps->ScrollX / ps->MaxScrollX) * (track_w - thumb_w) : 0.f; + float bar_y_h = (sy1 - kBarH) - sy0; // bottom edge, relative to scroll region top + float vis_h = fmaxf(fmaxf(ps->HotT, ps->ScrollbarShowTimer / 0.5f), ctx->Style.ScrollbarAutoHideAlpha); + + // Track + char htrk[64]; + snprintf(htrk, sizeof(htrk), "##hsbtrk_%llu", (unsigned long long) sb->Key); + ZUIBox* htrack = ZUIPushBox(ctx, htrk, (uint32_t) strlen(htrk), ZUI_DrawBackground | ZUI_FloatX | ZUI_FloatY); + htrack->Size[0] = ZPx(track_w); + htrack->Size[1] = ZPx(kBarH); + htrack->FloatPos[0] = 0.f; // sx0 - sx0 = 0 + htrack->FloatPos[1] = bar_y_h; + ZUIBoxSetColor(htrack, ctx->Theme.ScrollbarGrab[0], ctx->Theme.ScrollbarGrab[1], ctx->Theme.ScrollbarGrab[2], 0.15f * vis_h); + htrack->EdgeSoftness = 0.f; + ZUIPopBox(ctx); + + // Thumb + char hthm[64]; + snprintf(hthm, sizeof(hthm), "##hsbthm_%llu", (unsigned long long) sb->Key); + ZUIBox* hthumb = ZUIPushBox(ctx, hthm, (uint32_t) strlen(hthm), ZUI_DrawBackground | ZUI_Clickable | ZUI_FloatX | ZUI_FloatY); + hthumb->Size[0] = ZPx(thumb_w); + hthumb->Size[1] = ZPx(kBarH); + hthumb->FloatPos[0] = thumb_x_rel; + hthumb->FloatPos[1] = bar_y_h; + float grab[4] = {ctx->Theme.ScrollbarGrab[0], ctx->Theme.ScrollbarGrab[1], ctx->Theme.ScrollbarGrab[2], vis_h}; + float grab_hov[4] = {ctx->Theme.ScrollbarGrabHov[0], ctx->Theme.ScrollbarGrabHov[1], ctx->Theme.ScrollbarGrabHov[2], vis_h}; + float grab_act[4] = {ctx->Theme.ScrollbarGrabAct[0], ctx->Theme.ScrollbarGrabAct[1], ctx->Theme.ScrollbarGrabAct[2], 1.f}; + ZUIBoxSetColorArr(hthumb, grab); + ZUIBoxSetCornerRadius(hthumb, ctx->Style.ScrollbarRounding); + hthumb->EdgeSoftness = 0.5f; + + ZUISignal htsig = ZUISignalFromBox(ctx, hthumb); + ApplyHotActive(hthumb, ctx, grab, grab_hov, grab_act); + + if ((htsig.Flags & ZUI_SignalHeld) && htsig.DragDelta[0] != 0.f) + { + float ratio = (track_w - thumb_w) > 0.f ? ps->MaxScrollX / (track_w - thumb_w) : 0.f; + ps->ScrollX += htsig.DragDelta[0] * ratio; + if (ps->ScrollX < 0.f) + ps->ScrollX = 0.f; + if (ps->ScrollX > ps->MaxScrollX) + ps->ScrollX = ps->MaxScrollX; + } + ZUIPopBox(ctx); + } + } + ZUIPopBox(ctx); + } + + void ZUIScrollToBottom(ZUIContext* ctx, const char* key) + { + uint64_t hash = ZUIHashStr(key, (uint32_t) strlen(key)); + ZUIPersistentState* ps = ZUIStateGetOrInsert(&ctx->StateStore, hash); + if (ps) + ps->ScrollY = 1e9f; // clamped to MaxScrollY by layout pass + } + + float ZUIGetScrollY(ZUIContext* ctx, const char* key) + { + uint64_t hash = ZUIHashStr(key, (uint32_t) strlen(key)); + ZUIPersistentState* ps = ZUIStateGetOrInsert(&ctx->StateStore, hash); + return ps ? ps->ScrollY : 0.f; + } + + // ZUILabel + + void ZUILabel(ZUIContext* ctx, const char* text, const float color[4], ZUIFontSize size) + { + const float* c = color ? color : ctx->Theme.TextDefault; + uint32_t len = (uint32_t) strlen(text); + + ZUIBox* box = ZUIPushBox(ctx, text, len, ZUI_DrawText); + box->Size[0] = ZText(); + box->Size[1] = ZText(); + box->FontSize = size; + SetTextColor(box, c); + ZUIPopBox(ctx); + } + + // Disabled-state helpers + + void ZUIBeginDisabled(ZUIContext* ctx) + { + ++ctx->DisabledDepth; + ctx->Disabled = true; + } + void ZUIEndDisabled(ZUIContext* ctx) + { + if (ctx->DisabledDepth > 0) + --ctx->DisabledDepth; + ctx->Disabled = (ctx->DisabledDepth > 0); + } + + // Dim a single color array (e.g. TextColor) in-place when disabled. + static void ApplyDisabledDim(const ZUIContext* ctx, float c[4]) + { + c[3] *= ctx->Style.DisabledAlpha; + } + // Dim all per-corner background colors when disabled. + static void ApplyDisabledDimBox(const ZUIContext* ctx, ZUIBox* b) + { + for (int _c = 0; _c < 4; ++_c) + b->Colors[_c][3] *= ctx->Style.DisabledAlpha; + } + + // ZUIButton family + + ZUISignal ZUIButton(ZUIContext* ctx, const char* label, ZUISize w, ZUISize h) + { + uint32_t len = (uint32_t) strlen(label); + ZUIBoxFlags flags = ZUI_DrawBackground | ZUI_DrawText | ZUI_DrawBorder; + if (!ctx->Disabled) + flags = flags | ZUI_Clickable; + + ZUIBox* box = ZUIPushBox(ctx, label, len, flags); + box->Size[0] = w; + box->Size[1] = h; + box->Padding[0] = ZUIGetFramePadX(ctx); // ImGui FramePadding.x = 4px per side + box->Padding[2] = ZUIGetFramePadX(ctx); + SetBgArr(box, ctx->Theme.ButtonBg); + SetTextColor(box, ctx->Theme.TextDefault); + SetBdrArr(box, ctx->Theme.ButtonBorder); + box->BorderThickness = 1.f; + box->EdgeSoftness = 0.5f; + ZUIBoxSetCornerRadius(box, ctx->Style.FrameRounding); + if (ctx->Disabled) + { + ApplyDisabledDimBox(ctx, box); + ApplyDisabledDim(ctx, box->TextColor); + } + + // Keyboard focus: Tab can land on buttons; Space/Enter activates + bool is_focused = !ctx->Disabled && (ctx->FocusKey == box->Key); + if (is_focused) + { + SetBdrArr(box, ctx->Theme.InputFocusBorder); // teal focus ring + box->Flags = box->Flags | ZUI_DrawBorder; + } + + ZUISignal sig = ZUISignalFromBox(ctx, box); + if (!ctx->Disabled) + { + ApplyHotActive(box, ctx, ctx->Theme.ButtonBg, ctx->Theme.ButtonHoveredBg, ctx->Theme.ButtonActiveBg); + if (is_focused && (ctx->SpacePressed || ctx->EnterPressed)) + sig.Flags |= ZUI_SignalClicked; + } + ZUIPopBox(ctx); + return sig; + } + + ZUISignal ZUISmallButton(ZUIContext* ctx, const char* label) + { + uint32_t len = (uint32_t) strlen(label); + ZUIBoxFlags flags = ZUI_DrawBackground | ZUI_DrawText; + if (!ctx->Disabled) + flags = flags | ZUI_Clickable; + + ZUIBox* box = ZUIPushBox(ctx, label, len, flags); + box->Size[0] = ZText(); + box->Size[1] = ZPx(ZUIGetFrameHeight(ctx)); // ImGui GetFrameHeight + box->Padding[0] = ZUIGetFramePadX(ctx); // ImGui FramePadding.x + box->Padding[2] = ZUIGetFramePadX(ctx); + SetBgArr(box, ctx->Theme.ButtonBg); + SetTextColor(box, ctx->Theme.TextDefault); + box->EdgeSoftness = 0.5f; + ZUIBoxSetCornerRadius(box, ctx->Style.FrameRounding); + if (ctx->Disabled) + { + ApplyDisabledDimBox(ctx, box); + ApplyDisabledDim(ctx, box->TextColor); + } + + bool is_focused = !ctx->Disabled && (ctx->FocusKey == box->Key); + if (is_focused) + { + SetBdrArr(box, ctx->Theme.InputFocusBorder); + box->Flags = box->Flags | ZUI_DrawBorder; + } + + ZUISignal sig = ZUISignalFromBox(ctx, box); + if (!ctx->Disabled) + { + ApplyHotActive(box, ctx, ctx->Theme.ButtonBg, ctx->Theme.ButtonHoveredBg, ctx->Theme.ButtonActiveBg); + if (is_focused && (ctx->SpacePressed || ctx->EnterPressed)) + sig.Flags |= ZUI_SignalClicked; + } + ZUIPopBox(ctx); + return sig; + } + + ZUISignal ZUIInvisibleButton(ZUIContext* ctx, const char* key, ZUISize w, ZUISize h) + { + uint32_t len = (uint32_t) strlen(key); + ZUIBoxFlags flags = ctx->Disabled ? ZUI_None : ZUI_Clickable; + + ZUIBox* box = ZUIPushBox(ctx, key, len, flags); + box->Size[0] = w; + box->Size[1] = h; + + ZUISignal sig = ZUISignalFromBox(ctx, box); + ZUIPopBox(ctx); + return sig; + } + + bool ZUIToggleButton(ZUIContext* ctx, const char* label, bool* active, ZUISize w, ZUISize h) + { + uint32_t len = (uint32_t) strlen(label); + ZUIBoxFlags flags = ZUI_DrawBackground | ZUI_DrawText | ZUI_DrawBorder; + if (!ctx->Disabled) + flags = flags | ZUI_Clickable; + + ZUIBox* box = ZUIPushBox(ctx, label, len, flags); + box->Size[0] = w; + box->Size[1] = h; + box->Padding[0] = ZUIGetFramePadX(ctx); + box->Padding[2] = ZUIGetFramePadX(ctx); + box->BorderThickness = 1.f; + box->EdgeSoftness = 0.5f; + ZUIBoxSetCornerRadius(box, ctx->Style.FrameRounding); + + // Active state uses a lighter background + if (active && *active) + { + float bg[4] = {ctx->Theme.ButtonBg[0] + 0.14f, ctx->Theme.ButtonBg[1] + 0.14f, ctx->Theme.ButtonBg[2] + 0.14f, ctx->Theme.ButtonBg[3]}; + SetBgArr(box, bg); + SetBdrArr(box, ctx->Theme.InputFocusBorder); + } + else + { + SetBgArr(box, ctx->Theme.ButtonBg); + SetBdrArr(box, ctx->Theme.ButtonBorder); + } + SetTextColor(box, ctx->Theme.TextDefault); + if (ctx->Disabled) + { + ApplyDisabledDimBox(ctx, box); + ApplyDisabledDim(ctx, box->TextColor); + } + + ZUISignal sig = ZUISignalFromBox(ctx, box); + ZUIPopBox(ctx); + + if ((sig.Flags & ZUI_SignalClicked) && active) + { + *active = !(*active); + return true; + } + return false; + } + + ZUISignal ZUIImageButton(ZUIContext* ctx, const char* key, uint32_t texture_index, ZUISize w, ZUISize h) + { + uint32_t len = (uint32_t) strlen(key); + ZUIBoxFlags flags = ZUI_DrawBackground; + if (!ctx->Disabled) + flags = flags | ZUI_Clickable; + + ZUIBox* box = ZUIPushBox(ctx, key, len, flags); + box->Size[0] = w; + box->Size[1] = h; + box->TextureIndex = texture_index; + // Ensure bg alpha > 0 so PreparePayload emits the quad + ZUIBoxSetColor(box, 1.f, 1.f, 1.f, 1.f); + if (ctx->Disabled) + ApplyDisabledDimBox(ctx, box); + + ZUISignal sig = ZUISignalFromBox(ctx, box); + ZUIPopBox(ctx); + return sig; + } + + // ZUISeparator + + void ZUISeparator(ZUIContext* ctx) + { + ZUIBox* box = ZUIPushBox(ctx, "##zui_sep", 9, ZUI_DrawBackground); + box->Size[0] = ZFill(); + box->Size[1] = ZSPx(ctx, 2.f); + SetBgArr(box, ctx->Theme.Separator); + ZUIPopBox(ctx); + } + + // ZUISpacer + + void ZUISpacer(ZUIContext* ctx, float px) + { + ZUIBox* box = ZUIPushBox(ctx, "##zui_spacer", 12, ZUI_None); + // Size on both axes so it works in both row and column parents + box->Size[0] = ZPx(px); + box->Size[1] = ZPx(px); + ZUIPopBox(ctx); + } + + // ZUITreeNode + + ZUISignal ZUITreeNode(ZUIContext* ctx, const char* label, bool* open) + { + // Row box — the entire row is the clickable hit target + char row_key[256]; + snprintf(row_key, sizeof(row_key), "##tn_%s", label); + uint32_t row_key_len = (uint32_t) strlen(row_key); + + ZUIBox* row = ZUIPushBox(ctx, row_key, row_key_len, ZUI_Clickable); + row->Size[0] = ZFill(); + row->Size[1] = ZPx(ZUIGetFrameHeight(ctx)); + row->LayoutAxis = ZUIAxis::X; + + // Disclosure indicator drawn via ZUI_DrawTriArrow (same as tree view) + char ind_key[160]; + snprintf(ind_key, sizeof(ind_key), "##tnarr_%s", row_key); + ZUIBox* ind = ZUIPushBox(ctx, ind_key, (uint32_t) strlen(ind_key), ZUI_DrawTriArrow); + ind->Size[0] = ZPx(ctx->Style.FontSize + 1.f); + ind->Size[1] = ZPx(ZUIGetFrameHeight(ctx)); + float ind_col[4] = {0.55f, 0.55f, 0.60f, 1.f}; + SetTextColor(ind, ind_col); + { + auto* ps = ZUIStateGetOrInsert(&ctx->StateStore, ind->Key); + if (ps) + ps->UserData = (open && *open) ? 2.f : 3.f; // chevron: ∨ expanded, › collapsed + } + ZUIPopBox(ctx); + + // Label text + uint32_t label_len = (uint32_t) strlen(label); + ZUIBox* txt = ZUIPushBox(ctx, label, label_len, ZUI_DrawText); + txt->Size[0] = ZText(); + txt->Size[1] = ZPx(ZUIGetFrameHeight(ctx)); + SetTextColor(txt, ctx->Theme.TextDefault); + ZUIPopBox(ctx); // pop label + + bool is_focused = (ctx->FocusKey == row->Key); + + ZUISignal sig = ZUISignalFromBox(ctx, row); + ZUIPopBox(ctx); // pop row + + if (sig.Flags & ZUI_SignalClicked) + { + ctx->FocusKey = row->Key; + } + + bool toggle = (sig.Flags & ZUI_SignalClicked) || (is_focused && (ctx->SpacePressed || ctx->EnterPressed)); + if (open) + { + if (toggle) + { + *open = !(*open); + } + // Arrow Right opens, Arrow Left closes + if (is_focused && ctx->ArrowRightPressed && !(*open)) + *open = true; + if (is_focused && ctx->ArrowLeftPressed && (*open)) + *open = false; + } + + return sig; + } + + // Popup / overlay system + + void ZUIOpenPopup(ZUIContext* ctx, const char* key, float pos_x, float pos_y) + { + ctx->PendingPopupKey = ZUIHashStr(key, (uint32_t) strlen(key)); + ctx->PendingPopupDepth = ctx->PopupBuildDepth; // open at the caller's render depth + ctx->PendingPopupPosX = (pos_x >= 0.f) ? pos_x : ctx->MousePos[0]; + ctx->PendingPopupPosY = (pos_y >= 0.f) ? pos_y : ctx->MousePos[1]; + ctx->PopupNavIdx = -1; + ctx->PopupBuildIdx = 0; + } + + bool ZUIBeginPopup(ZUIContext* ctx, const char* key) + { + uint64_t hash = ZUIHashStr(key, (uint32_t) strlen(key)); + uint32_t depth = ctx->PopupBuildDepth; + if (depth >= ctx->PopupStackSize || ctx->PopupStack[depth].Key != hash) + return false; + + // Reset item counter each frame the popup builds + ctx->PopupBuildIdx = 0; + ctx->PopupBuildDepth++; // inner content is one level deeper + + // Escape to root so the popup renders on top of everything else. + // Save current parent per-entry so nested popups don't clobber each other. + ctx->PopupStack[depth].SavedParent = ctx->Current; + ctx->Current = ctx->Root; + + float px = ctx->PopupStack[depth].PosX; + float py = ctx->PopupStack[depth].PosY; + uint32_t len = (uint32_t) strlen(key); + ZUIBox* popup = ZUIPushBox(ctx, key, len, ZUI_DrawBackground | ZUI_DrawBorder | ZUI_DropShadow | ZUI_ClipChildren | ZUI_FloatX | ZUI_FloatY); + popup->Size[0] = ZFit(); + popup->Size[1] = ZFit(); + popup->FloatPos[0] = px; + popup->FloatPos[1] = py; + popup->LayoutAxis = ZUIAxis::Y; + popup->BorderThickness = 1.f; + popup->EdgeSoftness = 0.5f; + ZUIBoxSetCornerRadius(popup, ctx->Style.PopupRounding); + popup->Padding[0] = popup->Padding[2] = ctx->Style.PopupInnerPaddingX; + SetBgArr(popup, ctx->Theme.PopupBg); + SetBdrArr(popup, ctx->Theme.PanelBorder); + + ctx->PopupStack[depth].Box = popup; // register for hover + close-on-press detection + + // Minimum-width sizer: breaks the ZFit↔ZFill circular dependency. + // ZFit popup width = max(sizer_width, children widths). + // ZFill items can then fill this known minimum. Menu items use 200px, + // combos pass their button width via ctx->PopupDesiredW. + { + float min_w = (ctx->PopupDesiredW > 0.f) ? ctx->PopupDesiredW : ctx->Style.PopupMinWidth; + ctx->PopupDesiredW = 0.f; // consume + char sk[20] = "##popup_min_w"; + ZUIBox* sizer = ZUIPushBox(ctx, sk, 13, ZUI_None); + sizer->Size[0] = ZPx(min_w); + sizer->Size[1] = ZPx(0.f); // zero height — invisible + ZUIPopBox(ctx); + } + + return true; + } + + void ZUIEndPopup(ZUIContext* ctx) + { + ctx->PopupBuildCount = ctx->PopupBuildIdx; + if (ctx->PopupBuildDepth > 0) + ctx->PopupBuildDepth--; + ZUIPopBox(ctx); + // Restore ctx->Current from the per-entry saved parent (works for nested popups) + uint32_t depth = ctx->PopupBuildDepth; // depth of the popup we just closed + if (depth < ctx->PopupStackSize) + ctx->Current = ctx->PopupStack[depth].SavedParent; + } + + void ZUIClosePopup(ZUIContext* ctx) + { + ctx->PopupStackSize = 0; // close all open popups (menu item confirmed) + ctx->PopupBuildDepth = 0; + } + + bool ZUIBeginPopupContextItem(ZUIContext* ctx, const char* key, const ZUISignal& item_signal) + { + // Right-click while hovered → request popup at mouse position + if ((item_signal.Flags & ZUI_SignalHovered) && ctx->MousePressed[1]) + { + ZUIOpenPopup(ctx, key); + } + return ZUIBeginPopup(ctx, key); + } + + bool ZUIMenuItem(ZUIContext* ctx, const char* label, bool enabled) + { + uint32_t len = (uint32_t) strlen(label); + ZUIBoxFlags fl = ZUI_DrawText; + if (enabled) + fl = fl | ZUI_DrawBackground | ZUI_Clickable; + + ZUIBox* box = ZUIPushBox(ctx, label, len, fl); + box->Size[0] = ZFill(); + box->Size[1] = ZPx(ZUIGetFrameHeight(ctx)); + box->Padding[0] = ZUIGetFramePadX(ctx) * 2.f; // left indent (ImGui: FramePadding.x * 2) + box->Padding[2] = ZUIGetFramePadX(ctx) * 2.f; + + // Keyboard highlight via popup nav + bool kb_focus = enabled && (ctx->PopupNavIdx >= 0 && ctx->PopupBuildIdx == ctx->PopupNavIdx); + if (kb_focus) + ZUIBoxSetColorArr(box, ctx->Theme.RowHoverBg); + else + ZUIBoxSetColor(box, 0.f, 0.f, 0.f, 0.f); + SetTextColor(box, enabled ? ctx->Theme.TextDefault : ctx->Theme.TextDim); + + if (enabled) + ctx->PopupBuildIdx++; // disabled items are invisible to keyboard nav + + ZUISignal sig = ZUISignalFromBox(ctx, box); + + // ImGui uses INSTANT color switch (no lerp) for menu items. + // Reading ctx->HotKey / ctx->ActiveKey directly matches that behaviour. + if (enabled && !kb_focus) + { + bool is_hot = (ctx->HotKey == box->Key); + bool is_active = (ctx->ActiveKey == box->Key); + if (is_active) + ZUIBoxSetColorArr(box, ctx->Theme.HeaderActiveBg); + else if (is_hot) + ZUIBoxSetColorArr(box, ctx->Theme.HeaderHoveredBg); + } + ZUIPopBox(ctx); + + bool activated = (enabled && (sig.Flags & ZUI_SignalClicked)) || (kb_focus && (ctx->EnterPressed || ctx->SpacePressed)); + if (activated) + { + ZUIClosePopup(ctx); + ctx->PopupNavIdx = -1; + return true; + } + return false; + } + + bool ZUIMenuItemEx(ZUIContext* ctx, const char* label, const char* shortcut, bool selected, bool enabled) + { + char box_key[128]; + snprintf(box_key, sizeof(box_key), "##miex_%s", label); + + ZUIBox* box = ZUIBeginRow(ctx, box_key, ZFill(), ZPx(ZUIGetFrameHeight(ctx))); + box->Flags = ZUIBoxFlags((box->Flags & ~ZUI_DrawText) | ZUI_DrawBackground); + if (enabled) + box->Flags = box->Flags | ZUI_Clickable; + box->EdgeSoftness = 0.f; + + bool kb_focus = enabled && (ctx->PopupNavIdx >= 0 && ctx->PopupBuildIdx == ctx->PopupNavIdx); + if (kb_focus) + ZUIBoxSetColorArr(box, ctx->Theme.RowHoverBg); + else + ZUIBoxSetColor(box, 0.f, 0.f, 0.f, 0.f); + + if (enabled) + ctx->PopupBuildIdx++; + + // 1. Checkmark slot — fixed width = FontSize, always present for column alignment + { + char ck[128]; + snprintf(ck, sizeof(ck), "##miex_ck_%s", label); + ZUIBox* chk = ZUIPushBox(ctx, ck, (uint32_t) strlen(ck), selected ? ZUI_DrawText : ZUI_None); + chk->Size[0] = ZPx(ctx->Style.FontSize); + chk->Size[1] = ZFill(); + if (selected) + { + chk->Label = ZUIPushStr(&ctx->FrameArena, "\xe2\x9c\x93", 3); // UTF-8 checkmark + SetTextColor(chk, ctx->Theme.CheckMark); + } + ZUIPopBox(ctx); + } + + // 2. Left spacer + ZUISpacer(ctx, ZUIGetFramePadX(ctx)); + + // 3. Label — ZFill + { + char lk[128]; + snprintf(lk, sizeof(lk), "##miex_lbl_%s", label); + ZUIBox* lbl = ZUIPushBox(ctx, lk, (uint32_t) strlen(lk), ZUI_DrawText); + lbl->Size[0] = ZFill(); + lbl->Size[1] = ZFill(); + lbl->Label = ZUIPushStr(&ctx->FrameArena, label, (uint32_t) strlen(label)); + SetTextColor(lbl, enabled ? ctx->Theme.TextDefault : ctx->Theme.TextDim); + ZUIPopBox(ctx); + } + + // 4. Fill spacer — pushes shortcut to the right + { + char fk[128]; + snprintf(fk, sizeof(fk), "##miex_fill_%s", label); + ZUIBox* fill = ZUIPushBox(ctx, fk, (uint32_t) strlen(fk), ZUI_None); + fill->Size[0] = ZFill(); + fill->Size[1] = ZFill(); + ZUIPopBox(ctx); + } + + // 5. Shortcut text (optional) + if (shortcut && shortcut[0]) + { + char sk[128]; + snprintf(sk, sizeof(sk), "##miex_sc_%s", label); + ZUIBox* sc = ZUIPushBox(ctx, sk, (uint32_t) strlen(sk), ZUI_DrawText); + sc->Size[0] = ZText(); + sc->Size[1] = ZFill(); + sc->Label = ZUIPushStr(&ctx->FrameArena, shortcut, (uint32_t) strlen(shortcut)); + SetTextColor(sc, ctx->Theme.TextDim); + ZUIPopBox(ctx); + ZUISpacer(ctx, ZUIGetFramePadX(ctx) * 2.f); + } + + // 6. Right padding + ZUISpacer(ctx, ZUIGetFramePadX(ctx)); + + ZUISignal sig = ZUISignalFromBox(ctx, box); + + if (enabled && !kb_focus) + { + bool is_hot = (ctx->HotKey == box->Key); + bool is_active = (ctx->ActiveKey == box->Key); + if (is_active) + ZUIBoxSetColorArr(box, ctx->Theme.HeaderActiveBg); + else if (is_hot) + ZUIBoxSetColorArr(box, ctx->Theme.HeaderHoveredBg); + } + ZUIEndRow(ctx); + + // Drag-selection: mouse pressed elsewhere in popup, released over this item + bool drag_release = ctx->MouseReleased[0] && ctx->HotKey == box->Key && ctx->ActiveKey != 0 && ctx->ActiveKey != box->Key; + + bool activated = (enabled && (sig.Flags & ZUI_SignalClicked)) || drag_release || (kb_focus && (ctx->EnterPressed || ctx->SpacePressed)); + if (activated) + { + ZUIClosePopup(ctx); + ctx->PopupNavIdx = -1; + return true; + } + return false; + } + + bool ZUIComboItem(ZUIContext* ctx, const char* label, bool selected) + { + uint32_t len = (uint32_t) strlen(label); + ZUIBox* box = ZUIPushBox(ctx, label, len, ZUI_DrawBackground | ZUI_DrawText | ZUI_Clickable); + box->Size[0] = ZFill(); + box->Size[1] = ZPx(ZUIGetFrameHeight(ctx)); + box->Padding[0] = ZUIGetFramePadX(ctx) * 2.f; + box->Padding[2] = ZUIGetFramePadX(ctx) * 2.f; + + bool kb_focus = (ctx->PopupNavIdx >= 0 && ctx->PopupBuildIdx == ctx->PopupNavIdx); + bool kb_active = (ctx->PopupNavIdx >= 0); // any keyboard nav is happening + + // While keyboard nav is active, only the kb_focus item is highlighted — + // suppressing the `selected` state prevents two simultaneous highlights. + if (kb_focus) + ZUIBoxSetColorArr(box, ctx->Theme.HeaderHoveredBg); // distinct from RowSelectedBg + else if (selected && !kb_active) + ZUIBoxSetColorArr(box, ctx->Theme.RowSelectedBg); + else + ZUIBoxSetColor(box, 0.f, 0.f, 0.f, 0.f); + SetTextColor(box, ctx->Theme.TextDefault); + + ctx->PopupBuildIdx++; + + ZUISignal sig = ZUISignalFromBox(ctx, box); + if (!kb_focus) + { + bool is_hot = (ctx->HotKey == box->Key); + bool is_active = (ctx->ActiveKey == box->Key); + if (is_active) + ZUIBoxSetColorArr(box, ctx->Theme.HeaderActiveBg); + else if (is_hot) + ZUIBoxSetColorArr(box, ctx->Theme.HeaderHoveredBg); + else if (selected && !kb_active) + ZUIBoxSetColorArr(box, ctx->Theme.RowSelectedBg); + } + ZUIPopBox(ctx); + + bool activated = (sig.Flags & ZUI_SignalClicked) || (kb_focus && (ctx->EnterPressed || ctx->SpacePressed)); + if (activated) + { + ZUIClosePopup(ctx); + ctx->PopupNavIdx = -1; + return true; + } + return false; + } + + // Complex widgets + + void ZUIBeginTabBar(ZUIContext* ctx, const char* key) + { + uint64_t hash = ZUIHashStr(key, (uint32_t) strlen(key)); + ctx->TabBarKey = hash; + + // Read selected index from persistent state (stored in ScrollY) + ZUIPersistentState* ps = ZUIStateGetOrInsert(&ctx->StateStore, hash); + ctx->TabBarSelectedIdx = ps ? (int) ps->ScrollY : 0; + ctx->TabBarCurrentIdx = 0; + + ZUIBox* outer = ZUIBeginColumn(ctx, key, ZFill(), ZFit()); + + // Button row + char row_key[64]; + snprintf(row_key, sizeof(row_key), "##tbr_%s", key); + ZUIBox* row = ZUIBeginRow(ctx, row_key, ZFill(), ZSPx(ctx, 28.f)); + row->Flags = row->Flags | ZUI_DrawBackground; + SetBgArr(row, ctx->Theme.HeaderBg); + ctx->TabBarRowBox = row; + } + + bool ZUIBeginTabItem(ZUIContext* ctx, const char* label) + { + int idx = ctx->TabBarCurrentIdx++; + bool active = (idx == ctx->TabBarSelectedIdx); + + // Tab button + char btn_key[128]; + snprintf(btn_key, sizeof(btn_key), "%s##tbi_%d", label, idx); + ZUIBoxFlags fl = ZUI_DrawBackground | ZUI_DrawText | ZUI_Clickable; + ZUIBox* btn = ZUIPushBox(ctx, btn_key, (uint32_t) strlen(btn_key), fl); + btn->Size[0] = ZText(); + btn->Size[1] = ZSPx(ctx, 28.f); + if (active) + { + SetBgArr(btn, ctx->Theme.PanelBg); + SetTextColor(btn, ctx->Theme.TextDefault); + } + else + { + ZUIBoxSetColor(btn, 0.f, 0.f, 0.f, 0.f); + SetTextColor(btn, ctx->Theme.TextDim); + } + + ZUISignal sig = ZUISignalFromBox(ctx, btn); + ZUIPopBox(ctx); + + if (sig.Flags & ZUI_SignalClicked) + { + ctx->TabBarSelectedIdx = idx; + ZUIPersistentState* ps = ZUIStateGetOrInsert(&ctx->StateStore, ctx->TabBarKey); + if (ps) + ps->ScrollY = (float) idx; + } + + ctx->TabItemWasSelected = active; + if (!active) + return false; + + // Close the button row so content goes below it + ZUIEndRow(ctx); + ctx->TabBarRowBox = nullptr; + + // Push content column + char content_key[128]; + snprintf(content_key, sizeof(content_key), "##tbc_%s_%d", label, idx); + ZUIBeginColumn(ctx, content_key, ZFill(), ZFit()); + return true; + } + + void ZUIEndTabItem(ZUIContext* ctx) + { + ZUIEndColumn(ctx); // close content column + } + + void ZUIEndTabBar(ZUIContext* ctx) + { + if (ctx->TabBarRowBox) + { + ZUIEndRow(ctx); + } // close button row if no tab was active + ZUIEndColumn(ctx); // close outer column + ctx->TabBarKey = 0; + ctx->TabBarRowBox = nullptr; + } + + ZUIBox* ZUIBeginListBox(ZUIContext* ctx, const char* key, ZUISize w, ZUISize h) + { + ZUIBox* frame = ZUIBeginColumn(ctx, key, w, h); + frame->Flags = frame->Flags | ZUI_DrawBackground | ZUI_DrawBorder; + SetBgArr(frame, ctx->Theme.InputBg); + SetBdrArr(frame, ctx->Theme.InputBorder); + frame->BorderThickness = 1.f; + + // Content inside a scroll region + char sr_key[64]; + snprintf(sr_key, sizeof(sr_key), "##lbsr_%s", key); + ZUIBeginScrollRegion(ctx, sr_key, ZFill(), ZFill()); + return frame; + } + void ZUIEndListBox(ZUIContext* ctx) + { + ZUIEndScrollRegion(ctx); + ZUIEndColumn(ctx); + } + + bool ZUISliderFloat(ZUIContext* ctx, const char* key, float* value, float v_min, float v_max, ZUISize w, ZUISize h) + { + if (!value) + return false; + float range = v_max - v_min; + if (range <= 0.f) + range = 1.f; + float fraction = (*value - v_min) / range; + if (fraction < 0.f) + fraction = 0.f; + if (fraction > 1.f) + fraction = 1.f; + + uint32_t len = (uint32_t) strlen(key); + ZUIBox* track = ZUIPushBox(ctx, key, len, ZUI_DrawBackground | ZUI_DrawBorder | ZUI_Clickable); + track->Size[0] = w; + track->Size[1] = h; + track->LayoutAxis = ZUIAxis::X; + SetBgArr(track, ctx->Theme.InputBg); + SetBdrArr(track, ctx->Theme.InputBorder); + track->BorderThickness = 1.f; + + // Fill bar (ZPct of track width based on normalised value) + { + char fk[64]; + snprintf(fk, sizeof(fk), "##sf_fill_%s", key); + ZUIBox* fill = ZUIPushBox(ctx, fk, (uint32_t) strlen(fk), ZUI_DrawBackground); + fill->Size[0] = ZPct(fraction); + fill->Size[1] = ZFill(); + float fc[4] = {ctx->Theme.SliderGrab[0], ctx->Theme.SliderGrab[1], ctx->Theme.SliderGrab[2], 0.55f}; + ZUIBoxSetColorArr(fill, fc); + fill->EdgeSoftness = 0.f; + ZUIPopBox(ctx); + } + // Thumb: rounded rect grip centered on the track + { + char tk[64]; + snprintf(tk, sizeof(tk), "##sf_thumb_%s", key); + ZUIBox* thumb = ZUIPushBox(ctx, tk, (uint32_t) strlen(tk), ZUI_DrawBackground | ZUI_FloatX | ZUI_FloatY); + float tw = ctx->Style.GrabMinSize; // ImGui GrabMinSize + float th = ctx->Style.GrabMinSize + 2.f; + // Position uses prev-frame track screen coords + ZUIPersistentState* ps = ZUIStateGetOrInsert(&ctx->StateStore, track->Key); + float trk_x0 = ps ? ps->ScreenMinX : 0.f; + float trk_w = ps ? (ps->ScreenMaxX - ps->ScreenMinX) : 120.f; + float trk_y0 = ps ? ps->ScreenMinY : 0.f; + float trk_h = ps ? (ps->ScreenMaxY - ps->ScreenMinY) : 22.f; + thumb->FloatPos[0] = trk_x0 + fraction * trk_w - tw * 0.5f; + thumb->FloatPos[1] = trk_y0 + (trk_h - th) * 0.5f; + thumb->Size[0] = ZPx(tw); + thumb->Size[1] = ZPx(th); + ZUIBoxSetColorArr(thumb, ctx->Theme.SliderGrab); + ZUIBoxSetCornerRadius(thumb, ctx->Style.GrabRounding); + thumb->EdgeSoftness = 0.5f; + ZUIPopBox(ctx); + } + + bool is_focused = (ctx->FocusKey == track->Key); + if (is_focused) + { + SetBdrArr(track, ctx->Theme.InputFocusBorder); + } + + ZUISignal sig = ZUISignalFromBox(ctx, track); + ApplyHotActive(track, ctx, ctx->Theme.InputBg, ctx->Theme.InputHoveredBg, ctx->Theme.InputActiveBg); + ZUIPopBox(ctx); + + if (sig.Flags & ZUI_SignalClicked) + { + ctx->FocusKey = track->Key; + } + + bool changed = false; + { + ZUIPersistentState* ps = ZUIStateGetOrInsert(&ctx->StateStore, track->Key); + float box_w = (ps && ps->ScreenMaxX > ps->ScreenMinX) ? (ps->ScreenMaxX - ps->ScreenMinX) : 120.f; + + if (sig.Flags & ZUI_SignalHeld) + { + *value += sig.DragDelta[0] * (range / box_w); + if (*value < v_min) + *value = v_min; + if (*value > v_max) + *value = v_max; + changed = (sig.DragDelta[0] != 0.f); + } + else if (sig.Flags & ZUI_SignalPressed) + { + float pos = ctx->MousePos[0] - (ps ? ps->ScreenMinX : ctx->MousePos[0]); + if (box_w > 0.f) + { + *value = v_min + (pos / box_w) * range; + if (*value < v_min) + *value = v_min; + if (*value > v_max) + *value = v_max; + changed = true; + } + } + } + // Arrow key nudge when focused: 1% of range per press + if (is_focused) + { + float step = range / 100.f; + if (ctx->ArrowRightPressed || ctx->ArrowUpPressed) + { + *value += step; + if (*value > v_max) + *value = v_max; + changed = true; + } + if (ctx->ArrowLeftPressed || ctx->ArrowDownPressed) + { + *value -= step; + if (*value < v_min) + *value = v_min; + changed = true; + } + } + return changed; + } + + bool ZUIInputInt(ZUIContext* ctx, const char* key, int* value, int v_min, int v_max, ZUISize w) + { + if (!value) + return false; + + // Format as string, use TextField-style box + char buf[32]; + snprintf(buf, sizeof(buf), "%d", *value); + uint32_t len = (uint32_t) strlen(key); + + ZUIBox* field = ZUIPushBox(ctx, key, len, ZUI_DrawBackground | ZUI_DrawText | ZUI_Clickable | ZUI_DrawBorder); + field->Size[0] = w; + field->Size[1] = ZPx(ZUIGetFrameHeight(ctx)); + field->Padding[0] = ZUIGetFramePadX(ctx); + field->Padding[2] = ZUIGetFramePadX(ctx); + SetBgArr(field, ctx->Theme.InputBg); + SetTextColor(field, ctx->Theme.TextDefault); + SetBdrArr(field, ctx->Theme.InputBorder); + field->BorderThickness = 1.f; + field->EdgeSoftness = 0.5f; + ZUIBoxSetCornerRadius(field, ctx->Style.FrameRounding); + + bool focused = (ctx->FocusKey == field->Key); + if (focused) + { + // Append digits from text input + static char s_ibuf[32] = {}; + static uint64_t s_last_key = 0; + if (s_last_key != field->Key) + { + snprintf(s_ibuf, sizeof(s_ibuf), "%d", *value); + s_last_key = field->Key; + } + bool changed = false; + for (uint32_t i = 0; i < ctx->TextInputLen; ++i) + { + char c = ctx->TextInput[i]; + if ((c >= '0' && c <= '9') || (c == '-' && strlen(s_ibuf) == 0)) + { + size_t l = strlen(s_ibuf); + if (l < sizeof(s_ibuf) - 1) + { + s_ibuf[l] = c; + s_ibuf[l + 1] = '\0'; + changed = true; + } + } + } + if (ctx->BackspacePressed) + { + size_t l = strlen(s_ibuf); + if (l > 0) + { + s_ibuf[l - 1] = '\0'; + changed = true; + } + } + if (changed || focused) + { + int parsed = s_ibuf[0] ? atoi(s_ibuf) : 0; + if (parsed < v_min) + parsed = v_min; + if (parsed > v_max) + parsed = v_max; + *value = parsed; + } + + char disp[34]; + snprintf(disp, sizeof(disp), "%s|", s_ibuf); + field->Label = ZUIPushStr(&ctx->FrameArena, disp, (uint32_t) strlen(disp)); + + SetBdrArr(field, ctx->Theme.InputFocusBorder); + } + else + { + field->Label = ZUIPushStr(&ctx->FrameArena, buf, (uint32_t) strlen(buf)); + SetBdrArr(field, ctx->Theme.InputBorder); + } + + ZUISignal sig = ZUISignalFromBox(ctx, field); + ZUIPopBox(ctx); + + if (sig.Flags & ZUI_SignalClicked) + { + ctx->FocusKey = field->Key; + } + // Also support drag to change + if ((sig.Flags & ZUI_SignalHeld) && sig.DragDelta[0] != 0.f) + { + *value += (int) (sig.DragDelta[0] * 0.5f); + if (*value < v_min) + *value = v_min; + if (*value > v_max) + *value = v_max; + } + + return focused && ctx->TextInputLen > 0; + } + + bool ZUIInputTextMultiline(ZUIContext* ctx, const char* key, char* buf, uint32_t buf_size, ZUISize w, ZUISize h) + { + // Outer bordered frame + char frame_key[64]; + snprintf(frame_key, sizeof(frame_key), "##itmf_%s", key); + ZUIBox* frame = ZUIBeginColumn(ctx, frame_key, w, h); + frame->Flags = frame->Flags | ZUI_DrawBackground | ZUI_DrawBorder | ZUI_Clickable; + SetBgArr(frame, ctx->Theme.InputBg); + SetBdrArr(frame, ctx->Theme.InputBorder); + frame->BorderThickness = 1.f; + + // Scroll region inside + char sr_key[64]; + snprintf(sr_key, sizeof(sr_key), "##itmsr_%s", key); + ZUIBeginScrollRegion(ctx, sr_key, ZFill(), ZFill()); + + // The text as a label for now (full editing in a later pass) + bool focused = (ctx->FocusKey == frame->Key); + char disp[1024]; + if (focused) + snprintf(disp, sizeof(disp), "%s|", buf); + else + snprintf(disp, sizeof(disp), "%s", buf); + + ZUILabel(ctx, disp, ctx->Theme.TextDefault); + + ZUIEndScrollRegion(ctx); + + ZUISignal sig = ZUISignalFromBox(ctx, frame); + ZUIEndColumn(ctx); + + bool changed = false; + if ((sig.Flags & ZUI_SignalClicked)) + ctx->FocusKey = frame->Key; + if (focused) + { + for (uint32_t i = 0; i < ctx->TextInputLen; ++i) + { + char c = ctx->TextInput[i]; + uint32_t l = (uint32_t) Helpers::secure_strlen(buf); + if (l + 1 < buf_size) + { + buf[l] = c; + buf[l + 1] = '\0'; + changed = true; + } + } + if (ctx->BackspacePressed) + { + uint32_t l = (uint32_t) Helpers::secure_strlen(buf); + if (l > 0) + { + buf[l - 1] = '\0'; + changed = true; + } + } + } + return changed; + } + + bool ZUIColorPicker4(ZUIContext* ctx, const char* key, float color[4]) + { + // Simple version: color swatch + R/G/B/A sliders + char outer_key[64]; + snprintf(outer_key, sizeof(outer_key), "##cp_%s", key); + ZUIBeginColumn(ctx, outer_key, ZFill(), ZFit()); + + // Color swatch + char sw_key[64]; + snprintf(sw_key, sizeof(sw_key), "##cpswk_%s", key); + ZUIBox* swatch = ZUIPushBox(ctx, sw_key, (uint32_t) strlen(sw_key), ZUI_DrawBackground | ZUI_DrawBorder); + swatch->Size[0] = ZFill(); + swatch->Size[1] = ZPx(ZUIGetFrameHeight(ctx)); + ZUIBoxSetColorArr(swatch, color); + SetBdrArr(swatch, ctx->Theme.PanelBorder); + swatch->BorderThickness = 1.f; + ZUIPopBox(ctx); + + ZUISpacer(ctx, 4.f); + + bool changed = false; + // R/G/B/A sliders + const char* channel_names[] = {"R", "G", "B", "A"}; + for (int i = 0; i < 4; ++i) + { + ZUIBeginRow(ctx, channel_names[i], ZFill(), ZPx(ZUIGetFrameHeight(ctx))); + ZUIBox* lbl = ZUIPushBox(ctx, channel_names[i], 1, ZUI_DrawText); + lbl->Size[0] = ZPx(16.f); + lbl->Size[1] = ZPx(ZUIGetFrameHeight(ctx)); + SetTextColor(lbl, ctx->Theme.TextDim); + ZUIPopBox(ctx); + + char ch_key[32]; + snprintf(ch_key, sizeof(ch_key), "##cpch_%s_%d", key, i); + if (ZUISliderFloat(ctx, ch_key, &color[i], 0.f, 1.f, ZFill(), ZPx(ZUIGetFrameHeight(ctx)))) + changed = true; + ZUIEndRow(ctx); + } + + ZUIEndColumn(ctx); + return changed; + } + + // Layout helpers + + void ZUISameLine(ZUIContext* ctx, float /*spacing*/) + { + // Change the current parent's layout axis to X so the NEXT sibling + // is placed horizontally next to the previous one. + if (ctx->Current) + ctx->Current->LayoutAxis = ZUIAxis::X; + } + + void ZUIBeginTable(ZUIContext* ctx, const char* key, int columns, const float* widths, ZUISize h) + { + ctx->TableColumns = columns; + ctx->TableCurrentCol = -1; + ctx->TableRowBox = nullptr; + + // Allocate per-column widths in FrameArena + ctx->TableColWidths = ZPushArray(&ctx->FrameArena, float, columns); + for (int i = 0; i < columns; ++i) + ctx->TableColWidths[i] = widths ? widths[i] : 0.f; + + // Outer column container + ZUIBeginColumn(ctx, key, ZFill(), h); + } + + void ZUITableNextRow(ZUIContext* ctx) + { + // Close previous row if open + if (ctx->TableCurrentCol >= 0) + { + ZUIEndColumn(ctx); // close last cell column + ZUIEndRow(ctx); // close row + ctx->TableCurrentCol = -1; + } + // Open new row — key based on current parent so it's deterministic per-frame + char row_key[40]; + int child_count = 0; + if (ctx->Current) + { + for (auto* c = ctx->Current->FirstChild; c; c = c->NextSib) + ++child_count; + } + snprintf(row_key, sizeof(row_key), "##trow_%p_%d", (void*) ctx->Current, child_count); + ZUIBox* row = ZUIBeginRow(ctx, row_key, ZFill(), ZFit()); + row->LayoutAxis = ZUIAxis::X; + ctx->TableRowBox = row; + } + + void ZUITableSetColumn(ZUIContext* ctx, int col_index) + { + // Close previous cell + if (ctx->TableCurrentCol >= 0) + ZUIEndColumn(ctx); + + ctx->TableCurrentCol = col_index; + bool has_width = (col_index < ctx->TableColumns && ctx->TableColWidths && ctx->TableColWidths[col_index] > 0.f); + float w = has_width ? ctx->TableColWidths[col_index] : 0.f; + ZUISize cell_w = has_width ? ZSPx(ctx, w) : ZFill(); // 0 = fill remaining + + char cell_key[40]; + snprintf(cell_key, sizeof(cell_key), "##tcell_%d_%d", col_index, ctx->TableCurrentCol + (int) (uintptr_t) ctx->TableRowBox); + ZUIBeginColumn(ctx, cell_key, cell_w, ZFit()); + } + + void ZUIEndTable(ZUIContext* ctx) + { + // Close any open cell + row + if (ctx->TableCurrentCol >= 0) + { + ZUIEndColumn(ctx); + ZUIEndRow(ctx); + } + ZUIEndColumn(ctx); // outer table column + ctx->TableColumns = 0; + ctx->TableCurrentCol = -1; + ctx->TableColWidths = nullptr; + ctx->TableRowBox = nullptr; + } + + // Simple standalone widgets + + bool ZUICheckbox(ZUIContext* ctx, const char* label, bool* checked) + { + // Row: [16×16 box] [label] + char row_key[64]; + snprintf(row_key, sizeof(row_key), "##cb_%s", label); + + ZUIBoxFlags row_flags = ZUI_Clickable; + if (!ctx->Disabled) + { /* keep Clickable */ + } + else + row_flags = ZUI_None; + + ZUIBox* row = ZUIBeginRow(ctx, row_key, ZFit(), ZPx(ZUIGetFrameHeight(ctx))); + row->Flags = row->Flags | (ctx->Disabled ? ZUI_None : ZUI_Clickable); + row->LayoutAxis = ZUIAxis::X; + + // Tick box — bg lerps from InputBg (rest) → InputHoveredBg (hover) → InputActiveBg (active) + bool is_checked = checked && *checked; + char tick_key[32]; + snprintf(tick_key, sizeof(tick_key), "##tick_%s", label); + ZUIBoxFlags tick_draw = ZUI_DrawBackground | ZUI_DrawBorder; + if (is_checked) + tick_draw = tick_draw | ZUI_DrawCheckmark; + ZUIBox* box = ZUIPushBox(ctx, tick_key, (uint32_t) strlen(tick_key), tick_draw); + box->Size[0] = ZPx(ctx->Style.FontSize); + box->Size[1] = ZPx(ctx->Style.FontSize); // ImGui: checkbox = FontSize + SetBgArr(box, is_checked ? ctx->Theme.InputActiveBg : ctx->Theme.InputBg); + SetBdrArr(box, is_checked ? ctx->Theme.InputFocusBorder : ctx->Theme.InputBorder); + box->BorderThickness = 1.f; + ZUIBoxSetCornerRadius(box, ctx->Style.FrameRounding); + SetTextColor(box, ctx->Theme.CheckMark); + ZUISignal tick_sig = ZUISignalFromBox(ctx, box); + ApplyHotActive(box, ctx, ctx->Theme.InputBg, ctx->Theme.InputHoveredBg, ctx->Theme.InputActiveBg); + ZUIPopBox(ctx); + (void) tick_sig; + + ZUISpacer(ctx, ZUIGetInnerSpac(ctx)); // ImGui ItemInnerSpacing.x + ZUILabel(ctx, label, ctx->Disabled ? ctx->Theme.TextDim : ctx->Theme.TextDefault); + + bool is_focused = !ctx->Disabled && (ctx->FocusKey == row->Key); + if (is_focused) + { + SetBdrArr(box, ctx->Theme.InputFocusBorder); + } + + ZUISignal sig = ZUISignalFromBox(ctx, row); + ZUIEndRow(ctx); + + bool activated = (sig.Flags & ZUI_SignalClicked) || (is_focused && ctx->SpacePressed); + if (activated && checked) + { + *checked = !(*checked); + return true; + } + return false; + } + + bool ZUIRadioButton(ZUIContext* ctx, const char* label, int* selected, int index) + { + char row_key[64]; + snprintf(row_key, sizeof(row_key), "##rb_%s_%d", label, index); + + ZUIBox* row = ZUIBeginRow(ctx, row_key, ZFit(), ZPx(ZUIGetFrameHeight(ctx))); + row->Flags = row->Flags | (ctx->Disabled ? ZUI_None : ZUI_Clickable); + row->LayoutAxis = ZUIAxis::X; + + bool is_active = selected && (*selected == index); + char dot_key[64]; + snprintf(dot_key, sizeof(dot_key), "##dot_%s_%d", label, index); + ZUIBoxFlags dot_fl = ZUI_DrawBackground | ZUI_DrawBorder; + if (is_active) + dot_fl = dot_fl | ZUI_DrawCircleFill; + ZUIBox* circle = ZUIPushBox(ctx, dot_key, (uint32_t) strlen(dot_key), dot_fl); + circle->Size[0] = ZPx(ctx->Style.FontSize); + circle->Size[1] = ZPx(ctx->Style.FontSize); // ImGui: FontSize + SetBgArr(circle, is_active ? ctx->Theme.InputActiveBg : ctx->Theme.InputBg); + SetBdrArr(circle, is_active ? ctx->Theme.InputFocusBorder : ctx->Theme.InputBorder); + circle->BorderThickness = 1.f; + ZUIBoxSetCornerRadius(circle, ctx->Style.FontSize * 0.5f); // full circle + SetTextColor(circle, ctx->Theme.CheckMark); + ZUISignalFromBox(ctx, circle); + ApplyHotActive(circle, ctx, ctx->Theme.InputBg, ctx->Theme.InputHoveredBg, ctx->Theme.InputActiveBg); + ZUIPopBox(ctx); + + ZUISpacer(ctx, ZUIGetInnerSpac(ctx)); // ImGui ItemInnerSpacing.x + ZUILabel(ctx, label, ctx->Disabled ? ctx->Theme.TextDim : ctx->Theme.TextDefault); + + bool is_focused = !ctx->Disabled && (ctx->FocusKey == row->Key); + if (is_focused) + { + SetBdrArr(circle, ctx->Theme.InputFocusBorder); + } + + ZUISignal sig = ZUISignalFromBox(ctx, row); + ZUIEndRow(ctx); + + bool activated = ((sig.Flags & ZUI_SignalClicked) || (is_focused && ctx->SpacePressed)) && selected && !ctx->Disabled; + if (activated) + { + *selected = index; + return true; + } + return false; + } + + void ZUIProgressBar(ZUIContext* ctx, const char* key, float fraction, ZUISize w, ZUISize h, const char* overlay_text) + { + fraction = fraction < 0.f ? 0.f : (fraction > 1.f ? 1.f : fraction); + uint32_t len = (uint32_t) strlen(key); + + // Track + ZUIBox* track = ZUIBeginRow(ctx, key, w, h); + track->Flags = track->Flags | ZUI_DrawBackground | ZUI_DrawBorder; + SetBgArr(track, ctx->Theme.InputBg); + SetBdrArr(track, ctx->Theme.InputBorder); + track->BorderThickness = 1.f; + track->LayoutAxis = ZUIAxis::X; + + // Fill bar + char fill_key[64]; + snprintf(fill_key, sizeof(fill_key), "##fill_%s", key); + ZUIBox* fill = ZUIPushBox(ctx, fill_key, (uint32_t) strlen(fill_key), ZUI_DrawBackground | (overlay_text ? ZUI_DrawText : ZUI_None)); + fill->Size[0] = ZPct(fraction); + fill->Size[1] = ZFill(); + { + float _c[4] = {(ctx->Theme.InputFocusBorder)[0], (ctx->Theme.InputFocusBorder)[1], (ctx->Theme.InputFocusBorder)[2], 0.80f}; + ZUIBoxSetColorArr(fill, _c); + } + if (overlay_text) + { + fill->Label = ZUIPushStr(&ctx->FrameArena, overlay_text, (uint32_t) Helpers::secure_strlen(overlay_text)); + SetTextColor(fill, ctx->Theme.TextDefault); + } + ZUIPopBox(ctx); + + ZUIEndRow(ctx); + } + + void ZUISetTooltip(ZUIContext* ctx, const ZUISignal& sig, const char* text) + { + if (!(sig.Flags & ZUI_SignalHovered) || !text) + { + return; + } + + // Open a popup-style floating box near the cursor + float tx = ctx->MousePos[0] + 14.f; + float ty = ctx->MousePos[1] + 14.f; + + // Clamp to screen edges (rough) + if (tx + 200.f > (float) ctx->ScreenW) + tx = ctx->MousePos[0] - 200.f; + if (ty + 32.f > (float) ctx->ScreenH) + ty = ctx->MousePos[1] - 32.f; + + // Build as a root-level floated box (same parent-escape as popups) + ZUIBox* saved = ctx->Current; + ctx->Current = ctx->Root; + + char ttkey[64]; + snprintf(ttkey, sizeof(ttkey), "##tt_%p", (void*) text); + ZUIBox* tip = ZUIPushBox(ctx, ttkey, (uint32_t) strlen(ttkey), ZUI_DrawBackground | ZUI_DrawBorder | ZUI_DrawText | ZUI_FloatX | ZUI_FloatY); + tip->Size[0] = ZFit(); + tip->Size[1] = ZFit(); + tip->FloatPos[0] = tx; + tip->FloatPos[1] = ty; + tip->Label = ZUIPushStr(&ctx->FrameArena, text, (uint32_t) Helpers::secure_strlen(text)); + ZUIBoxSetColorArr(tip, ctx->Theme.HeaderBg); + SetBdrArr(tip, ctx->Theme.PanelBorder); + tip->BorderThickness = 1.f; + SetTextColor(tip, ctx->Theme.TextDefault); + ZUIPopBox(ctx); + + ctx->Current = saved; + } + + ZUISignal ZUICollapsingHeader(ZUIContext* ctx, const char* label, bool* open, const float* bg_color, bool show_focus_border) + { + char key[256]; + snprintf(key, sizeof(key), "##ch_%s", label); + + ZUIBox* hdr = ZUIBeginRow(ctx, key, ZFill(), ZPx(ZUIGetFrameHeight(ctx))); + hdr->Flags = hdr->Flags | ZUI_DrawBackground | ZUI_Clickable; + hdr->Padding[0] = ZUIGetFramePadX(ctx); + static const float kNone[4] = {}; + ZUIBoxSetColorArr(hdr, bg_color ? bg_color : kNone); + hdr->LayoutAxis = ZUIAxis::X; + hdr->EdgeSoftness = 0.f; + + bool is_open = open && *open; + bool is_focused = (ctx->FocusKey == hdr->Key); + + // VS Code chevron: ∨ expanded (UserData 2), › collapsed (UserData 3) + char ak[272]; + snprintf(ak, sizeof(ak), "##ch_arr_%s", label); + ZUIBox* arrow = ZUIPushBox(ctx, ak, (uint32_t) strlen(ak), ZUI_DrawTriArrow); + arrow->Size[0] = ZPx(ctx->Style.FontSize); + arrow->Size[1] = ZFill(); + SetTextColor(arrow, ctx->Theme.TextDim); + { + auto* ps = ZUIStateGetOrInsert(&ctx->StateStore, arrow->Key); + if (ps) + ps->UserData = is_open ? 2.f : 3.f; + } + ZUIPopBox(ctx); + + ZUISpacer(ctx, ZUIGetInnerSpac(ctx)); + ZUILabel(ctx, label, ctx->Theme.TextDefault); + + ZUISignal sig = ZUISignalFromBox(ctx, hdr); + ApplyHotActive(hdr, ctx, bg_color ? bg_color : kNone, ctx->Theme.TitleBgActive, ctx->Theme.TitleBgActive); + + // 1px teal focus border — only when caller opts in (show_focus_border = true) + if (is_focused && show_focus_border) + { + hdr->Flags = hdr->Flags | ZUI_DrawBorder; + hdr->BorderColor[0] = ctx->Theme.TabActiveBorder[0]; + hdr->BorderColor[1] = ctx->Theme.TabActiveBorder[1]; + hdr->BorderColor[2] = ctx->Theme.TabActiveBorder[2]; + hdr->BorderColor[3] = 0.60f; + hdr->BorderThickness = 1.f; + } + + ZUIEndRow(ctx); + + bool activated = (sig.Flags & ZUI_SignalClicked) || (is_focused && (ctx->SpacePressed || ctx->EnterPressed)); + if (activated) + { + ctx->FocusKey = hdr->Key; + if (open) + *open = !(*open); + } + return sig; + } + + // Greedy cascade for ZUIPaneSash. + // Walks up-side [boundary..0] absorbing delta, then down-side [boundary+1..n-1] + // absorbing the remainder (with opposite sign). + static void PaneSashResize(float* heights, const bool* opens, int n, int boundary, float delta, float min_h) + { + if (fabsf(delta) < 0.05f) + return; + + // Pass 1: apply +delta upward from boundary + float rem = delta; + for (int i = boundary; i >= 0 && fabsf(rem) > 0.05f; --i) + { + if (!opens[i]) + continue; + if (rem > 0.f) + { + heights[i] += rem; // grow freely (no hard max) + rem = 0.f; + } + else + { + float give = fminf(-rem, heights[i] - min_h); + heights[i] -= give; + rem += give; + } + } + + // Pass 2: compensate downward — down side absorbs -(total absorbed by up) + float compensate = -(delta - rem); + for (int i = boundary + 1; i < n && fabsf(compensate) > 0.05f; ++i) + { + if (!opens[i]) + continue; + if (compensate < 0.f) + { + // down side must shrink + float give = fminf(-compensate, heights[i] - min_h); + heights[i] -= give; + compensate += give; + } + else + { + // down side grows + heights[i] += compensate; + compensate = 0.f; + } + } + } + + void ZUIPaneSash(ZUIContext* ctx, const char* key, float* heights, const bool* opens, int n, int boundary, float min_h) + { + // Hit zone: 4 px transparent clickable strip — wide enough to grab easily. + // Visual: 1 px line centered inside the hit zone, mirroring BuildDividers architecture. + // At rest: Separator color. On hover: teal tint + NS cursor. + ZUIBox* sash = ZUIPushBox(ctx, key, (uint32_t) strlen(key), ZUI_Clickable); + sash->Size[0] = ZFill(); + sash->Size[1] = ZPx(4.f); + sash->EdgeSoftness = 0.f; + + bool hot = (ctx->HotKey == sash->Key) || (ctx->ActiveKey == sash->Key); + if (hot) + ctx->ResizeCursor = 2; // NS cursor + + // 1 px visual line centered vertically in the 4 px hit zone + { + char vk[264]; + snprintf(vk, sizeof(vk), "##sv_%s", key); + ZUIBox* vis = ZUIPushBox(ctx, vk, (uint32_t) strlen(vk), ZUI_DrawBackground | ZUI_FloatX | ZUI_FloatY); + vis->Size[0] = ZFill(); + vis->Size[1] = ZPx(1.f); + vis->FloatPos[0] = 0.f; + vis->FloatPos[1] = 1.5f; // 1.5 px from sash top → centered in 4 px + vis->EdgeSoftness = 0.f; + if (hot) + { + ZUIBoxSetColor(vis, ctx->Theme.TabActiveBorder[0], ctx->Theme.TabActiveBorder[1], ctx->Theme.TabActiveBorder[2], ctx->ActiveKey == sash->Key ? 0.50f : 0.35f); + } + else + { + ZUIBoxSetColor(vis, ctx->Theme.Separator[0], ctx->Theme.Separator[1], ctx->Theme.Separator[2], ctx->Theme.Separator[3]); + } + ZUIPopBox(ctx); + } + + ZUISignal sig = ZUISignalFromBox(ctx, sash); + ZUIPopBox(ctx); + + if ((sig.Flags & ZUI_SignalHeld) && fabsf(sig.DragDelta[1]) > 0.05f) + PaneSashResize(heights, opens, n, boundary, sig.DragDelta[1], min_h); + } + + void ZUIDropZoneFill(ZUIContext* ctx, const char* key, float float_x, float float_y, float w, float h) + { + const float* ac = ctx->Theme.TabActiveBorder; + ZUIBox* prev = ZUIPushBox(ctx, key, (uint32_t) strlen(key), ZUI_DrawBackground | ZUI_DrawBorder | ZUI_FloatX | ZUI_FloatY); + prev->Size[0] = ZPx(w); + prev->Size[1] = ZPx(h); + prev->FloatPos[0] = float_x; + prev->FloatPos[1] = float_y; + prev->EdgeSoftness = 0.f; + ZUIBoxSetColor(prev, ac[0], ac[1], ac[2], ctx->Style.DockingDropPreviewAlpha); + prev->BorderColor[0] = ac[0]; + prev->BorderColor[1] = ac[1]; + prev->BorderColor[2] = ac[2]; + prev->BorderColor[3] = 0.80f; + prev->BorderThickness = 2.f; + ZUIPopBox(ctx); + } + + void ZUIDockDividerH(ZUIContext* ctx, const char* key) + { + const float* ac = ctx->Theme.TabActiveBorder; + ZUIBox* d = ZUIPushBox(ctx, key, (uint32_t) strlen(key), ZUI_DrawBackground); + d->Size[0] = ZFill(); + d->Size[1] = ZPx(2.f); + d->EdgeSoftness = 0.f; + ZUIBoxSetColor(d, ac[0], ac[1], ac[2], 1.f); + ZUIPopBox(ctx); + } + + void ZUIDockGhostHeader(ZUIContext* ctx, const char* key, const char* label, float cursor_x, float cursor_y) + { + float gh = ZUIGetFrameHeight(ctx); + float cnt_h = ctx->Style.TabGhostContentH; + + float text_w = 80.f; + if (ctx->GetFont(ZUIFontSize::Body) && label) + { + float ts[2] = {0.f, 0.f}; + ZUIMeasureText(ctx->GetFont(ZUIFontSize::Body), label, (uint32_t) strlen(label), ts); + text_w = ts[0]; + } + float gw = fmaxf(text_w + ZUIGetFramePadX(ctx) * 4.f, 120.f); + float px = cursor_x - gw * 0.3f; + float py = cursor_y - gh * 0.5f; + const float* ac = ctx->Theme.TabActiveBorder; + float off = ctx->Style.DropShadowOffset; + + // Drop shadow + char sk[272]; + snprintf(sk, sizeof(sk), "##%s_s", key); + ZUIBox* shad = ZUIPushBox(ctx, sk, (uint32_t) strlen(sk), ZUI_DrawBackground | ZUI_FloatX | ZUI_FloatY); + shad->Size[0] = ZPx(gw); + shad->Size[1] = ZPx(gh + cnt_h); + shad->FloatPos[0] = px + off; + shad->FloatPos[1] = py + off; + shad->EdgeSoftness = 4.f; + ZUIBoxSetColor(shad, 0.f, 0.f, 0.f, ctx->Style.DropShadowAlpha); + ZUIPopBox(ctx); + + // Content body stub + char ck[272]; + snprintf(ck, sizeof(ck), "##%s_c", key); + ZUIBox* cnt = ZUIPushBox(ctx, ck, (uint32_t) strlen(ck), ZUI_DrawBackground | ZUI_DrawBorder | ZUI_FloatX | ZUI_FloatY); + cnt->Size[0] = ZPx(gw); + cnt->Size[1] = ZPx(cnt_h); + cnt->FloatPos[0] = px; + cnt->FloatPos[1] = py + gh; + cnt->EdgeSoftness = 0.f; + ZUIBoxSetColorArr(cnt, ctx->Theme.PanelBg); + cnt->BorderColor[0] = ac[0]; + cnt->BorderColor[1] = ac[1]; + cnt->BorderColor[2] = ac[2]; + cnt->BorderColor[3] = 0.70f; + cnt->BorderThickness = 1.f; + ZUIPopBox(ctx); + + // Header row + char hk[272]; + snprintf(hk, sizeof(hk), "##%s_h", key); + ZUIBox* ghost = ZUIBeginRow(ctx, hk, ZPx(gw), ZPx(gh)); + ghost->Flags = ghost->Flags | ZUI_DrawBackground | ZUI_DrawBorder | ZUI_FloatX | ZUI_FloatY; + ghost->FloatPos[0] = px; + ghost->FloatPos[1] = py; + ghost->EdgeSoftness = 0.f; + ZUIBoxSetColorArr(ghost, ctx->Theme.TitleBgActive); + ZUIBoxSetTopRadius(ghost, ctx->Style.TabRounding); + ghost->BorderColor[0] = ac[0]; + ghost->BorderColor[1] = ac[1]; + ghost->BorderColor[2] = ac[2]; + ghost->BorderColor[3] = 0.90f; + ghost->BorderThickness = 1.f; + ZUISpacer(ctx, ZUIGetFramePadX(ctx)); + ZUILabel(ctx, label, ctx->Theme.TextDefault); + ZUIEndRow(ctx); + } + + bool ZUISelectable(ZUIContext* ctx, const char* label, bool* selected, ZUISize h) + { + char key[256]; + snprintf(key, sizeof(key), "##sel_%s", label); + + ZUIBox* row = ZUIBeginRow(ctx, key, ZFill(), h); + row->Flags = row->Flags | ZUI_DrawBackground | ZUI_Clickable; + + // Use RowSelectedBg for selected, transparent for rest — hover lerps via HotT + static const float kTransparent[4] = {0.f, 0.f, 0.f, 0.f}; + if (selected && *selected) + ZUIBoxSetColorArr(row, ctx->Theme.RowSelectedBg); + else + ZUIBoxSetColorArr(row, kTransparent); + + ZUISpacer(ctx, 6.f); + ZUILabel(ctx, label, ctx->Theme.TextDefault); + + bool is_focused = (ctx->FocusKey == row->Key); + + ZUISignal sig = ZUISignalFromBox(ctx, row); + // Lerp transparent → RowHoverBg → RowSelectedBg + if (!(selected && *selected)) + ApplyHotActive(row, ctx, kTransparent, ctx->Theme.HeaderHoveredBg, ctx->Theme.HeaderActiveBg); + ZUIEndRow(ctx); + + bool activated = (sig.Flags & ZUI_SignalClicked) || (is_focused && (ctx->SpacePressed || ctx->EnterPressed)); + if (activated && selected) + { + *selected = !(*selected); + return true; + } + return activated; // return true even when selected == nullptr (e.g. ZUIMenuItem) + } + + void ZUISeparatorText(ZUIContext* ctx, const char* text) + { + ZUIBeginRow(ctx, "##septext", ZFill(), ZPx(ZUIGetFrameHeight(ctx))); + ZUIBox* line1 = ZUIPushBox(ctx, "##sl1", 5, ZUI_DrawBackground); + line1->Size[0] = ZPx(8.f); + line1->Size[1] = ZPx(1.f); + SetBgArr(line1, ctx->Theme.Separator); + ZUIPopBox(ctx); + + ZUISpacer(ctx, 4.f); + ZUILabel(ctx, text, ctx->Theme.TextDim); + ZUISpacer(ctx, 4.f); + + ZUIBox* line2 = ZUIPushBox(ctx, "##sl2", 5, ZUI_DrawBackground); + line2->Size[0] = ZFill(); + line2->Size[1] = ZPx(1.f); + SetBgArr(line2, ctx->Theme.Separator); + ZUIPopBox(ctx); + ZUIEndRow(ctx); + } + + // Popup-based widgets + + bool ZUIBeginContextMenu(ZUIContext* ctx, const char* key) + { + // Opens on right-click anywhere (not on a specific item) + if (ctx->MousePressed[1]) + ZUIOpenPopup(ctx, key); + return ZUIBeginPopup(ctx, key); + } + void ZUIEndContextMenu(ZUIContext* ctx) + { + ZUIEndPopup(ctx); + } + + bool ZUIBeginCombo(ZUIContext* ctx, const char* key, const char* preview_label, ZUISize w) + { + uint32_t len = (uint32_t) strlen(key); + char btn_key[80]; + snprintf(btn_key, sizeof(btn_key), "##combo_btn_%s", key); + + // Preview row: label fills available space, arrow is a fixed-width flow child at the end. + const float kArrowW = ctx->Style.FontSize + ZUIGetFramePadX(ctx); + ZUIBox* row = ZUIBeginRow(ctx, btn_key, w, ZPx(ZUIGetFrameHeight(ctx))); + row->Flags = row->Flags | ZUI_DrawBackground | ZUI_DrawBorder | ZUI_Clickable; + row->Padding[0] = ZUIGetFramePadX(ctx); + row->Padding[2] = ZUIGetFramePadX(ctx); + ZUIBoxSetCornerRadius(row, ctx->Style.FrameRounding); + SetBgArr(row, ctx->Theme.InputBg); + SetBdrArr(row, ctx->Theme.InputBorder); + row->BorderThickness = 1.f; + + // Preview label: ZFill so it expands and pushes the arrow to the right + { + char lk[96]; + snprintf(lk, sizeof(lk), "##cbl_%s", key); + ZUIBox* lbl = ZUIPushBox(ctx, lk, (uint32_t) strlen(lk), ZUI_DrawText); + lbl->Size[0] = ZFill(); + lbl->Size[1] = ZFill(); + lbl->Label = ZUIPushStr(&ctx->FrameArena, preview_label ? preview_label : "", preview_label ? (uint32_t) strlen(preview_label) : 0u); + SetTextColor(lbl, ctx->Theme.TextDefault); + ZUIPopBox(ctx); + } + + // Dropdown arrow — flow child, always at the right edge + char arrow_key[96]; + snprintf(arrow_key, sizeof(arrow_key), "##carrow_%s", key); + ZUIBox* arrow = ZUIPushBox(ctx, arrow_key, (uint32_t) strlen(arrow_key), ZUI_DrawTriArrow); + arrow->Size[0] = ZPx(kArrowW); + arrow->Size[1] = ZFill(); + SetTextColor(arrow, ctx->Theme.TextDim); + { + auto* ps = ZUIStateGetOrInsert(&ctx->StateStore, arrow->Key); + if (ps) + ps->UserData = 2.f; // 2 = VS Code chevron ∨ (1 = filled ▼, 0 = filled ►) + } + ZUIPopBox(ctx); + + bool is_focused = (ctx->FocusKey == row->Key); + if (is_focused) + { + SetBdrArr(row, ctx->Theme.InputFocusBorder); + } + + ZUISignal sig = ZUISignalFromBox(ctx, row); + ZUIEndRow(ctx); + + if (sig.Flags & ZUI_SignalClicked) + { + ctx->FocusKey = row->Key; + } + + // Open popup on click OR Space/Enter/ArrowDown when focused — but only if + // not already open. ArrowDown while open is handled by ZUIBeginFrame popup nav; + // re-calling ZUIOpenPopup every frame resets the popup position and causes flicker. + uint64_t popup_hash = ZUIHashStr(key, (uint32_t) strlen(key)); + bool already_open = (ctx->PopupBuildDepth < ctx->PopupStackSize && ctx->PopupStack[ctx->PopupBuildDepth].Key == popup_hash); + bool open_popup = (sig.Flags & ZUI_SignalClicked) || (!already_open && is_focused && (ctx->SpacePressed || ctx->EnterPressed || ctx->ArrowDownPressed)); + if (open_popup) + { + ZUIPersistentState* ps = ZUIStateGetOrInsert(&ctx->StateStore, row->Key); + float py = (ps && ps->ScreenMaxY > 0.f) ? ps->ScreenMaxY : (ctx->MousePos[1] + 4.f); + float px = (ps && ps->ScreenMinX > 0.f) ? ps->ScreenMinX : (ctx->MousePos[0] - 8.f); + // Pass the button width so the dropdown popup matches the combo box width + float bw = (ps && ps->ScreenMaxX > ps->ScreenMinX) ? (ps->ScreenMaxX - ps->ScreenMinX) : 0.f; + ctx->PopupDesiredW = bw; + ZUIOpenPopup(ctx, key, px, py); + } + + return ZUIBeginPopup(ctx, key); + } + void ZUIEndCombo(ZUIContext* ctx) + { + ZUIEndPopup(ctx); + } + + // Internal: menu bar saved state so EndMenuBar can pop the right box + bool ZUIBeginMenuBar(ZUIContext* ctx) + { + ZUIBox* bar = ZUIBeginRow(ctx, "##menubar_zui", ZFill(), ZPx(ZUIGetFrameHeight(ctx))); + bar->Flags = bar->Flags | ZUI_DrawBackground | ZUI_DrawBorder; + bar->EdgeSoftness = 0.f; + bar->BorderThickness = 1.f; + SetBgArr(bar, ctx->Theme.MenuBarBg); + bar->BorderColor[0] = ctx->Theme.Separator[0]; + bar->BorderColor[1] = ctx->Theme.Separator[1]; + bar->BorderColor[2] = ctx->Theme.Separator[2]; + bar->BorderColor[3] = ctx->Theme.Separator[3]; + bar->LayoutAxis = ZUIAxis::X; + return true; + } + void ZUIEndMenuBar(ZUIContext* ctx) + { + ZUIEndRow(ctx); + } + + bool ZUIBeginMenu(ZUIContext* ctx, const char* label, bool enabled) + { + char key[80]; + snprintf(key, sizeof(key), "%s##menu_%s", label, label); + ZUIBoxFlags fl = ZUI_DrawText | ZUI_DrawBackground; + if (enabled) + fl = fl | ZUI_Clickable; + + ZUIBox* btn = ZUIPushBox(ctx, key, (uint32_t) strlen(key), fl); + btn->Size[0] = ZText(); + btn->Size[1] = ZFill(); // fill full bar height — VS Code style + btn->Padding[0] = 10.f; // left padding + btn->Padding[2] = 10.f; // right padding + btn->EdgeSoftness = 0.f; + ZUIBoxSetCornerRadius(btn, 0.f); + + // Check if this menu's popup is currently open + uint64_t popup_hash = ZUIHashStr(label, (uint32_t) strlen(label)); + bool is_open = (ctx->PopupBuildDepth < ctx->PopupStackSize && ctx->PopupStack[ctx->PopupBuildDepth].Key == popup_hash); + + // When open: teal highlight matching the VS Code Dark+ accent + if (is_open && enabled) + ZUIBoxSetColor(btn, ctx->Theme.TabAccent[0], ctx->Theme.TabAccent[1], ctx->Theme.TabAccent[2], 0.18f); + else + ZUIBoxSetColor(btn, 0.f, 0.f, 0.f, 0.f); + + SetTextColor(btn, enabled ? ctx->Theme.TextDefault : ctx->Theme.TextDim); + + ZUISignal sig = ZUISignalFromBox(ctx, btn); + if (enabled && !is_open) + { + // Smooth hover: transparent → teal tint (VS Code-style) + static const float kRest[4] = {0.f, 0.f, 0.f, 0.f}; + const float kHov[4] = {ctx->Theme.TabAccent[0], ctx->Theme.TabAccent[1], ctx->Theme.TabAccent[2], 0.12f}; + const float kAct[4] = {ctx->Theme.TabAccent[0], ctx->Theme.TabAccent[1], ctx->Theme.TabAccent[2], 0.22f}; + ApplyHotActive(btn, ctx, kRest, kHov, kAct); + } + ZUIPopBox(ctx); + + // Open on click; or on hover-switch when another menu popup is already open. + // Signal-based hover won't fire here because the interaction pass blocks non-popup + // boxes from receiving ctx->HotKey when a popup is open. Use a direct cursor-vs- + // ScreenRect check (prev-frame coords) instead — same pattern as BuildDividers. + ZUIPersistentState* ps = ZUIStateGetOrInsert(&ctx->StateStore, btn->Key); + bool cursor_over = ps && ps->ScreenMaxX > ps->ScreenMinX && ctx->MousePos[0] >= ps->ScreenMinX && ctx->MousePos[0] <= ps->ScreenMaxX && ctx->MousePos[1] >= ps->ScreenMinY && ctx->MousePos[1] <= ps->ScreenMaxY; + bool any_menu_open = (ctx->PopupStackSize > 0 && !is_open); + if (enabled && ((sig.Flags & ZUI_SignalClicked) || (any_menu_open && cursor_over))) + { + if (any_menu_open) + ctx->PopupStackSize = 0; // close current menu so new one opens cleanly next frame + // ImGui: menu popup opens at button's LEFT EDGE, BOTTOM of the menu bar. + float px = (ps && ps->ScreenMinX > 0.f) ? ps->ScreenMinX : ctx->MousePos[0]; + float py = (ps && ps->ScreenMaxY > 0.f) ? ps->ScreenMaxY : (ctx->MousePos[1] + 26.f); + ZUIOpenPopup(ctx, label, px, py); + } + + return ZUIBeginPopup(ctx, label); + } + void ZUIEndMenu(ZUIContext* ctx) + { + ZUIEndPopup(ctx); + } + + // Returns true when point (px, py) lies inside the triangle defined by apex A and + // base vertices B1/B2. Used to give the mouse a grace period while moving toward + // an open submenu popup (ImGui triangle-heuristic). + static bool MenuTriangleContains(float ax, float ay, float b1x, float b1y, float b2x, float b2y, float px, float py) + { + auto cross = [](float x1, float y1, float x2, float y2, float qx, float qy) -> float { return (qx - x1) * (y2 - y1) - (qy - y1) * (x2 - x1); }; + float d1 = cross(ax, ay, b1x, b1y, px, py); + float d2 = cross(b1x, b1y, b2x, b2y, px, py); + float d3 = cross(b2x, b2y, ax, ay, px, py); + bool has_neg = (d1 < 0.f) || (d2 < 0.f) || (d3 < 0.f); + bool has_pos = (d1 > 0.f) || (d2 > 0.f) || (d3 > 0.f); + return !(has_neg && has_pos); + } + + bool ZUIBeginSubMenu(ZUIContext* ctx, const char* label, bool enabled) + { + char row_key[128], popup_key[128]; + snprintf(row_key, sizeof(row_key), "##smrow_%s", label); + snprintf(popup_key, sizeof(popup_key), "##smpop_%s", label); + + uint64_t popup_hash = ZUIHashStr(popup_key, (uint32_t) strlen(popup_key)); + bool is_open = (ctx->PopupBuildDepth < ctx->PopupStackSize && ctx->PopupStack[ctx->PopupBuildDepth].Key == popup_hash); + + // Row: [label][fill][› chevron][right-pad] + ZUIBox* row = ZUIBeginRow(ctx, row_key, ZFill(), ZPx(ZUIGetFrameHeight(ctx))); + row->Flags = row->Flags | ZUI_DrawBackground | ZUI_Clickable; + row->Padding[0] = ZUIGetFramePadX(ctx) * 2.f; + row->EdgeSoftness = 0.f; + + bool kb_focus = enabled && (ctx->PopupNavIdx >= 0 && ctx->PopupBuildIdx == ctx->PopupNavIdx); + if (enabled) + ctx->PopupBuildIdx++; + + if (kb_focus || is_open) + ZUIBoxSetColorArr(row, ctx->Theme.HeaderHoveredBg); + else + ZUIBoxSetColor(row, 0.f, 0.f, 0.f, 0.f); + + ZUILabel(ctx, label, enabled ? ctx->Theme.TextDefault : ctx->Theme.TextDim); + + // Fill — pushes chevron to the right edge + { + char fk[64]; + snprintf(fk, sizeof(fk), "##smfill_%s", label); + ZUIBox* f = ZUIPushBox(ctx, fk, (uint32_t) strlen(fk), ZUI_None); + f->Size[0] = ZFill(); + f->Size[1] = ZFill(); + ZUIPopBox(ctx); + } + + // Right-pointing › chevron — always 3.f (submenu opens sideways, never ∨) + { + char ak[128]; + snprintf(ak, sizeof(ak), "##smarr_%s", label); + ZUIBox* arrow = ZUIPushBox(ctx, ak, (uint32_t) strlen(ak), ZUI_DrawTriArrow); + arrow->Size[0] = ZPx(ctx->Style.FontSize); + arrow->Size[1] = ZFill(); + SetTextColor(arrow, enabled ? ctx->Theme.TextDim : ctx->Theme.TextDim); + { + auto* ps = ZUIStateGetOrInsert(&ctx->StateStore, arrow->Key); + if (ps) + ps->UserData = 3.f; + } + ZUIPopBox(ctx); + } + ZUISpacer(ctx, ZUIGetFramePadX(ctx)); + + ZUISignal sig = ZUISignalFromBox(ctx, row); + + if (enabled && !kb_focus && !is_open) + { + bool hot = (ctx->HotKey == row->Key), act = (ctx->ActiveKey == row->Key); + if (act) + ZUIBoxSetColorArr(row, ctx->Theme.HeaderActiveBg); + else if (hot) + ZUIBoxSetColorArr(row, ctx->Theme.HeaderHoveredBg); + } + ZUIEndRow(ctx); + + // Close submenu when cursor leaves both the row and the popup. + // Triangle heuristic (ImGui style): if the cursor is moving toward the submenu + // popup (apex = prev cursor, base = left edge of popup ±8px), suppress the close. + // Guard: skip entirely on the first frame the popup is open (ScreenMaxX == 0) + // because the layout pass hasn't positioned the popup yet — coordinates are invalid + // and both the rect and triangle checks would incorrectly fire the close. + if (is_open) + { + ZUIPersistentState* sub_ps = ZUIStateGetOrInsert(&ctx->StateStore, popup_hash); + if (sub_ps && sub_ps->ScreenMaxX > 0.f) // popup has been laid out at least once + { + auto cursor_in = [&](uint64_t key) -> bool { + ZUIPersistentState* s = ZUIStateGetOrInsert(&ctx->StateStore, key); + return s && s->ScreenMaxX > s->ScreenMinX && ctx->MousePos[0] >= s->ScreenMinX && ctx->MousePos[0] <= s->ScreenMaxX && ctx->MousePos[1] >= s->ScreenMinY && ctx->MousePos[1] <= s->ScreenMaxY; + }; + bool in_tri = MenuTriangleContains(ctx->PrevMousePos[0], ctx->PrevMousePos[1], sub_ps->ScreenMinX, sub_ps->ScreenMinY - 8.f, sub_ps->ScreenMinX, sub_ps->ScreenMaxY + 8.f, ctx->MousePos[0], ctx->MousePos[1]); + if (!cursor_in(row->Key) && !cursor_in(popup_hash) && !in_tri) + ctx->PopupStackSize = ctx->PopupBuildDepth; + } + } + + // Open submenu popup on hover — to the right of the parent popup. + // Use the row's own ScreenMaxX once laid out; fall back to the parent + // popup box's right edge on the first frame (before layout runs). + if (enabled && (sig.Flags & (ZUI_SignalHovered | ZUI_SignalClicked)) && !is_open) + { + ZUIPersistentState* ps = ZUIStateGetOrInsert(&ctx->StateStore, row->Key); + float px = 0.f; + float py = ctx->MousePos[1]; + if (ps && ps->ScreenMaxX > 0.f) + { + px = ps->ScreenMaxX; + py = ps->ScreenMinY; + } + else + { + // First frame — use parent popup's right edge from its persistent state + uint32_t par_depth = (ctx->PopupBuildDepth > 0) ? ctx->PopupBuildDepth - 1 : 0; + ZUIBox* par_box = (par_depth < ctx->PopupStackSize) ? ctx->PopupStack[par_depth].Box : nullptr; + if (par_box) + { + ZUIPersistentState* pps = ZUIStateGetOrInsert(&ctx->StateStore, par_box->Key); + if (pps && pps->ScreenMaxX > 0.f) + px = pps->ScreenMaxX; + } + if (px == 0.f) + px = ctx->MousePos[0]; + } + ZUIOpenPopup(ctx, popup_key, px, py); + } + + // Arrow-Right on a focused or hovered submenu row opens the submenu immediately. + if (enabled && !is_open && (ctx->FocusKey == row->Key || ctx->HotKey == row->Key) && ctx->ArrowRightPressed) + { + ZUIPersistentState* ps = ZUIStateGetOrInsert(&ctx->StateStore, row->Key); + float px = 0.f; + float py = ctx->MousePos[1]; + if (ps && ps->ScreenMaxX > 0.f) + { + px = ps->ScreenMaxX; + py = ps->ScreenMinY; + } + else + { + uint32_t par_depth = (ctx->PopupBuildDepth > 0) ? ctx->PopupBuildDepth - 1 : 0; + ZUIBox* par_box = (par_depth < ctx->PopupStackSize) ? ctx->PopupStack[par_depth].Box : nullptr; + if (par_box) + { + ZUIPersistentState* pps = ZUIStateGetOrInsert(&ctx->StateStore, par_box->Key); + if (pps && pps->ScreenMaxX > 0.f) + px = pps->ScreenMaxX; + } + if (px == 0.f) + px = ctx->MousePos[0]; + } + ZUIOpenPopup(ctx, popup_key, px, py); + } + + return ZUIBeginPopup(ctx, popup_key); + } + + void ZUIEndSubMenu(ZUIContext* ctx) + { + ZUIEndPopup(ctx); + } + + void ZUIOpenModal(ZUIContext* ctx, const char* key) + { + ctx->ActiveModalKey = ZUIHashStr(key, (uint32_t) strlen(key)); + } + + bool ZUIBeginModal(ZUIContext* ctx, const char* key, const char* title) + { + uint64_t hash = ZUIHashStr(key, (uint32_t) strlen(key)); + if (ctx->ActiveModalKey != hash) + { + return false; + } + + float sw = (float) ctx->ScreenW; + float sh = (float) ctx->ScreenH; + float mw = 480.f, mh = 280.f; + + // Dim overlay — root level, covers full screen + ZUIBox* saved = ctx->Current; + ctx->Current = ctx->Root; + + ZUIBox* dim = ZUIPushBox(ctx, "##modal_dim", 12, ZUI_DrawBackground | ZUI_FloatX | ZUI_FloatY); + dim->Size[0] = ZPx(sw); + dim->Size[1] = ZPx(sh); + dim->FloatPos[0] = 0.f; + dim->FloatPos[1] = 0.f; + ZUIBoxSetColor(dim, 0.f, 0.f, 0.f, 0.55f); + ZUIPopBox(ctx); + + // Modal panel — centred + ZUIBox* panel = ZUIPushBox(ctx, key, (uint32_t) strlen(key), ZUI_DrawBackground | ZUI_DrawBorder | ZUI_DropShadow | ZUI_FloatX | ZUI_FloatY); + panel->Size[0] = ZPx(mw); + panel->Size[1] = ZPx(mh); + panel->FloatPos[0] = (sw - mw) * 0.5f; + panel->FloatPos[1] = (sh - mh) * 0.5f; + panel->LayoutAxis = ZUIAxis::Y; + panel->BorderThickness = 1.f; + SetBgArr(panel, ctx->Theme.PanelBg); + SetBdrArr(panel, ctx->Theme.PanelBorder); + + ctx->Current = panel; // children go inside modal + + // Title bar + if (title) + { + ZUIBox* hdr = ZUIBeginRow(ctx, "##modal_hdr", ZFill(), ZPx(ZUIGetFrameHeight(ctx))); + hdr->Flags = hdr->Flags | ZUI_DrawBackground; + SetBgArr(hdr, ctx->Theme.HeaderBg); + ZUISpacer(ctx, 8.f); + ZUILabel(ctx, title, ctx->Theme.TextDefault); + ZUIEndRow(ctx); + ZUISeparator(ctx); + } + + ctx->ModalSavedParent = saved; + return true; + } + + void ZUIEndModal(ZUIContext* ctx) + { + ZUIPopBox(ctx); // pop modal panel + ctx->Current = ctx->ModalSavedParent; + ctx->ModalSavedParent = nullptr; + } + + // Drag-and-drop helpers + + void ZUIBeginDragSource(ZUIContext* ctx, const ZUIBox* box, const char* payload, uint32_t payload_len) + { + if (!ctx || !box) + { + return; + } + // Activate drag when this box is the active (held) box and the mouse has moved. + // Reads ctx state directly to avoid a second ZUISignalFromBox call on the same box. + bool held = (ctx->ActiveKey == box->Key) && ctx->MouseDown[0]; + bool moving = held && (ctx->MousePos[0] != ctx->PrevMousePos[0] || ctx->MousePos[1] != ctx->PrevMousePos[1]); + if (moving && ctx->DragSourceKey == 0) + { + ctx->DragSourceKey = box->Key; + uint32_t copy_len = payload_len < 511u ? payload_len : 511u; + Helpers::secure_memcpy(ctx->DragPayload, sizeof(ctx->DragPayload), payload, copy_len); + ctx->DragPayload[copy_len] = '\0'; + ctx->DragPayloadLen = copy_len; + } + } + + bool ZUIAcceptDrop(ZUIContext* ctx, const ZUIBox* box, char* out_buf, uint32_t out_size) + { + if (!ctx || !box) + { + return false; + } + if (!ctx->DragDropFired || ctx->DragTargetKey != box->Key) + { + return false; + } + if (out_buf && out_size > 0) + { + uint32_t copy_len = ctx->DragPayloadLen < out_size - 1 ? ctx->DragPayloadLen : out_size - 1; + Helpers::secure_memcpy(out_buf, out_size, ctx->DragPayload, copy_len); + out_buf[copy_len] = '\0'; + } + return true; + } + + // ZUIImage + + void ZUIImage(ZUIContext* ctx, const char* key, uint32_t texture_index, ZUISize w, ZUISize h) + { + uint32_t len = (uint32_t) strlen(key); + ZUIBox* box = ZUIPushBox(ctx, key, len, ZUI_DrawBackground); + box->Size[0] = w; + box->Size[1] = h; + box->TextureIndex = texture_index; + // Colors must be non-transparent so the renderer draws this box + ZUIBoxSetColor(box, 1.f, 1.f, 1.f, 1.f); + ZUIPopBox(ctx); + } + + // ZUIDragFloat + + bool ZUIDragFloat(ZUIContext* ctx, const char* key, float* value, float speed, float width_px) + { + uint32_t key_len = (uint32_t) strlen(key); + uint64_t key_hash = ZUIHashStr(key, key_len); + + ZUIBox* field = ZUIPushBox(ctx, key, key_len, ZUI_DrawBackground | ZUI_DrawText | ZUI_Clickable | ZUI_DrawBorder); + field->Size[0] = ZPx(width_px); + field->Size[1] = ZPx(ZUIGetFrameHeight(ctx)); + field->Padding[0] = ZUIGetFramePadX(ctx); + field->Padding[2] = ZUIGetFramePadX(ctx); + ZUIBoxSetCornerRadius(field, ctx->Style.FrameRounding); + SetBgArr(field, ctx->Theme.InputBg); + SetTextColor(field, ctx->Theme.TextDefault); + SetBdrArr(field, ctx->Theme.InputBorder); + field->BorderThickness = 1.f; + + // Persistent state: UserData >= 0 → text-edit mode (cursor pos); -1 → drag mode + ZUIPersistentState* ps = ZUIStateGetOrInsert(&ctx->StateStore, key_hash); + bool is_focused = (ctx->FocusKey == key_hash); + bool text_mode = is_focused && ps && (ps->UserData >= 0.f); + + // Static edit buffer — shared, switched on focus change like ZUIInputInt + static char s_df_buf[64] = {}; + static uint64_t s_df_key = 0; + + if (text_mode) + { + if (s_df_key != key_hash) + { + snprintf(s_df_buf, sizeof(s_df_buf), "%.6g", (double) *value); + s_df_key = key_hash; + } + SetBdrArr(field, ctx->Theme.InputFocusBorder); + + // Accumulate typed characters (digits, '.', '-') + for (uint32_t i = 0; i < ctx->TextInputLen; ++i) + { + char c = ctx->TextInput[i]; + uint32_t l = (uint32_t) strlen(s_df_buf); + if (l < sizeof(s_df_buf) - 1 && ((c >= '0' && c <= '9') || c == '.' || c == '-' || c == 'e' || c == 'E')) + { + s_df_buf[l] = c; + s_df_buf[l + 1] = '\0'; + } + } + if (ctx->BackspacePressed) + { + uint32_t l = (uint32_t) strlen(s_df_buf); + if (l > 0) + s_df_buf[l - 1] = '\0'; + } + + // Display with blinking cursor + char display[72]; + bool show_pipe = (fmodf(ctx->Time, ctx->Style.CursorBlinkRate) < ctx->Style.CursorBlinkRate * 0.5f); + if (show_pipe) + snprintf(display, sizeof(display), "%s|", s_df_buf); + else + snprintf(display, sizeof(display), "%s", s_df_buf); + field->Label = ZUIPushStr(&ctx->FrameArena, display, (uint32_t) strlen(display)); + } + else + { + // Normal drag mode: show value + dim arrow hint when focused + char val_buf[40]; + if (is_focused) + snprintf(val_buf, sizeof(val_buf), "%.3f", (double) *value); + else + snprintf(val_buf, sizeof(val_buf), "%.3f", (double) *value); + if (is_focused) + SetBdrArr(field, ctx->Theme.InputFocusBorder); + uint32_t vlen = (uint32_t) Helpers::secure_strlen(val_buf); + field->Label = ZUIPushStr(&ctx->FrameArena, val_buf, vlen); + } + + ZUISignal sig = ZUISignalFromBox(ctx, field); + ZUIPopBox(ctx); + + bool changed = false; + + // Ctrl+click → enter text edit mode + if ((sig.Flags & ZUI_SignalClicked) && ctx->CtrlDown && !text_mode) + { + ctx->FocusKey = key_hash; + if (ps) + ps->UserData = 0.f; // text edit mode, cursor at 0 + snprintf(s_df_buf, sizeof(s_df_buf), "%.6g", (double) *value); + s_df_key = key_hash; + } + else if ((sig.Flags & ZUI_SignalClicked) && !ctx->CtrlDown) + { + ctx->FocusKey = key_hash; + // Normal click — stay in drag mode + } + + // Confirm text edit on Enter/Tab or when focus leaves + if (text_mode) + { + bool confirm = ctx->EnterPressed || (!is_focused); + bool cancel = ctx->EscapePressed; + if (confirm || cancel) + { + if (confirm && s_df_buf[0]) + { + float parsed = (float) atof(s_df_buf); + if (parsed != *value) + { + *value = parsed; + changed = true; + } + } + if (ps) + { + ps->UserData = -1.f; + } // exit text mode + s_df_key = 0; + s_df_buf[0] = '\0'; + } + } + else + { + // Normal drag mode + if ((sig.Flags & ZUI_SignalHeld) && sig.DragDelta[0] != 0.f) + { + *value += sig.DragDelta[0] * speed; + changed = true; + } + if (is_focused) + { + if (ctx->ArrowUpPressed) + { + *value += speed; + changed = true; + } + if (ctx->ArrowDownPressed) + { + *value -= speed; + changed = true; + } + } + } + return changed; + } + + // ZUIDragInt + + bool ZUIDragInt(ZUIContext* ctx, const char* key, int* value, float speed, float width_px) + { + uint32_t key_len = (uint32_t) strlen(key); + uint64_t key_hash = ZUIHashStr(key, key_len); + + ZUIBox* field = ZUIPushBox(ctx, key, key_len, ZUI_DrawBackground | ZUI_DrawText | ZUI_Clickable | ZUI_DrawBorder); + field->Size[0] = ZPx(width_px); + field->Size[1] = ZPx(ZUIGetFrameHeight(ctx)); + field->Padding[0] = ZUIGetFramePadX(ctx); + field->Padding[2] = ZUIGetFramePadX(ctx); + ZUIBoxSetCornerRadius(field, ctx->Style.FrameRounding); + SetBgArr(field, ctx->Theme.InputBg); + SetTextColor(field, ctx->Theme.TextDefault); + SetBdrArr(field, ctx->Theme.InputBorder); + field->BorderThickness = 1.f; + + ZUIPersistentState* ps = ZUIStateGetOrInsert(&ctx->StateStore, key_hash); + bool is_focused = (ctx->FocusKey == key_hash); + bool text_mode = is_focused && ps && (ps->UserData >= 0.f); + + static char s_di_buf[32] = {}; + static uint64_t s_di_key = 0; + + if (text_mode) + { + if (s_di_key != key_hash) + { + snprintf(s_di_buf, sizeof(s_di_buf), "%d", *value); + s_di_key = key_hash; + } + SetBdrArr(field, ctx->Theme.InputFocusBorder); + for (uint32_t i = 0; i < ctx->TextInputLen; ++i) + { + char c = ctx->TextInput[i]; + uint32_t l = (uint32_t) strlen(s_di_buf); + if (l < sizeof(s_di_buf) - 1 && ((c >= '0' && c <= '9') || (c == '-' && l == 0))) + { + s_di_buf[l] = c; + s_di_buf[l + 1] = '\0'; + } + } + if (ctx->BackspacePressed) + { + uint32_t l = (uint32_t) strlen(s_di_buf); + if (l > 0) + s_di_buf[l - 1] = '\0'; + } + + char display[36]; + if (fmodf(ctx->Time, ctx->Style.CursorBlinkRate) < ctx->Style.CursorBlinkRate * 0.5f) + snprintf(display, sizeof(display), "%s|", s_di_buf); + else + snprintf(display, sizeof(display), "%s", s_di_buf); + field->Label = ZUIPushStr(&ctx->FrameArena, display, (uint32_t) strlen(display)); + } + else + { + if (is_focused) + { + SetBdrArr(field, ctx->Theme.InputFocusBorder); + } + char buf[16]; + snprintf(buf, sizeof(buf), "%d", *value); + field->Label = ZUIPushStr(&ctx->FrameArena, buf, (uint32_t) strlen(buf)); + } + + ZUISignal sig = ZUISignalFromBox(ctx, field); + ZUIPopBox(ctx); + + bool changed = false; + + if ((sig.Flags & ZUI_SignalClicked) && ctx->CtrlDown && !text_mode) + { + ctx->FocusKey = key_hash; + if (ps) + ps->UserData = 0.f; + snprintf(s_di_buf, sizeof(s_di_buf), "%d", *value); + s_di_key = key_hash; + } + else if ((sig.Flags & ZUI_SignalClicked) && !ctx->CtrlDown) + { + ctx->FocusKey = key_hash; + } + + if (text_mode) + { + if (ctx->EnterPressed || !is_focused) + { + if (s_di_buf[0]) + { + *value = atoi(s_di_buf); + changed = true; + } + if (ps) + ps->UserData = -1.f; + s_di_key = 0; + s_di_buf[0] = '\0'; + } + else if (ctx->EscapePressed) + { + if (ps) + ps->UserData = -1.f; + s_di_key = 0; + s_di_buf[0] = '\0'; + } + } + else + { + if ((sig.Flags & ZUI_SignalHeld) && sig.DragDelta[0] != 0.f) + { + float fv = (float) *value + sig.DragDelta[0] * speed; + *value = (int) fv; + changed = true; + } + if (is_focused) + { + int step = (int) speed > 0 ? (int) speed : 1; + if (ctx->ArrowUpPressed) + { + *value += step; + changed = true; + } + if (ctx->ArrowDownPressed) + { + *value -= step; + changed = true; + } + } + } + return changed; + } + + // ZUIDragFloat3 + + bool ZUIDragFloat3(ZUIContext* ctx, const char* key, float v[3], float speed, float comp_w) + { + struct AxisStyle + { + const char* label; + float chip[4]; + }; + static const AxisStyle kAxes[3] = { + {"X", {0.70f, 0.20f, 0.20f, 1.f}}, + {"Y", {0.20f, 0.65f, 0.20f, 1.f}}, + {"Z", {0.20f, 0.40f, 0.80f, 1.f}}, + }; + + float fw = (comp_w > 0.f) ? comp_w : 66.f; + bool changed = false; + + for (int i = 0; i < 3; ++i) + { + if (i > 0) + ZUISpacer(ctx, 2.f); + + // Colored axis chip (X / Y / Z) + char lk[48]; + snprintf(lk, sizeof(lk), "##f3l%d%s", i, key); + uint32_t llen = (uint32_t) strlen(lk); + ZUIBox* chip = ZUIPushBox(ctx, lk, llen, ZUI_DrawBackground | ZUI_DrawText); + chip->Size[0] = ZPx(14.f); + chip->Size[1] = ZPx(ZUIGetFrameHeight(ctx)); + chip->TextAlign = ZUITextAlign::Center; + ZUIBoxSetColorArr(chip, kAxes[i].chip); + chip->TextColor[0] = 1.f; + chip->TextColor[1] = 1.f; + chip->TextColor[2] = 1.f; + chip->TextColor[3] = 1.f; + chip->Label = ZUIPushStr(&ctx->FrameArena, kAxes[i].label, 1); + ZUIBoxSetCornerRadius(chip, 2.f); + chip->EdgeSoftness = 0.f; + ZUIPopBox(ctx); + + // Drag field for this component + char dk[48]; + snprintf(dk, sizeof(dk), "##f3d%d%s", i, key); + changed |= ZUIDragFloat(ctx, dk, &v[i], speed, fw); + } + return changed; + } + + // ZUIInputFloat + + bool ZUIInputFloat(ZUIContext* ctx, const char* key, float* value, float width_px) + { + // Use persistent UserData to distinguish edit mode (1) vs display mode (0) + uint64_t hash = ZUIHashStr(key, (uint32_t) strlen(key)); + auto* state = ZUIStateGetOrInsert(&ctx->StateStore, hash); + bool editing = state && state->UserData > 0.5f; + + // Backing char buffer lives in persistent state via a side-channel. + // We use a static per-hash char buffer keyed approach: store the float + // as text in a small arena-free static buf of 32 chars. + // For simplicity we re-format from *value every non-editing frame. + char display[32]; + if (!editing) + snprintf(display, sizeof(display), "%.4f", (double) *value); + + uint32_t key_len = (uint32_t) strlen(key); + ZUIBox* field = ZUIPushBox(ctx, key, key_len, ZUI_DrawBackground | ZUI_DrawText | ZUI_Clickable | ZUI_DrawBorder); + field->Size[0] = ZPx(width_px); + field->Size[1] = ZPx(ZUIGetFrameHeight(ctx)); + SetBgArr(field, ctx->Theme.InputBg); + SetTextColor(field, ctx->Theme.TextDefault); + const float* bdr = editing ? ctx->Theme.InputFocusBorder : ctx->Theme.InputBorder; + SetBdrArr(field, bdr); + field->BorderThickness = 1.f; + + if (!editing) + { + uint32_t dlen = (uint32_t) strlen(display); + field->Label = ZUIPushStr(&ctx->FrameArena, display, dlen); + } + + ZUISignal sig = ZUISignalFromBox(ctx, field); + ZUIPopBox(ctx); + + bool changed = false; + + if (!editing && (sig.Flags & ZUI_SignalClicked)) + { + if (state) + state->UserData = 1.f; + ctx->TextInputLen = 0; + snprintf(ctx->TextInput, 32, "%.4f", (double) *value); + ctx->TextInputLen = (uint32_t) strlen(ctx->TextInput); + } + + if (editing) + { + // Show what the user is typing + uint32_t tlen = (uint32_t) strlen(ctx->TextInput); + field->Label = ZUIPushStr(&ctx->FrameArena, ctx->TextInput, tlen); + + // Commit on Enter (no Enter key tracking yet — commit on focus loss) + bool click_outside = ctx->MousePressed[0] && !(sig.Flags & ZUI_SignalHovered); + if (click_outside) + { + float parsed = (float) atof(ctx->TextInput); + if (parsed != *value) + { + *value = parsed; + changed = true; + } + if (state) + state->UserData = 0.f; + ctx->TextInputLen = 0; + ctx->TextInput[0] = '\0'; + } + } + return changed; + } + + // ZUIColorEdit4 + + bool ZUIColorEdit4(ZUIContext* ctx, const char* key, float color[4]) + { + bool changed = false; + + // Small swatch button + char swk[48]; + snprintf(swk, sizeof(swk), "##swatch_%s", key); + uint32_t swlen = (uint32_t) strlen(swk); + float swatch_sz = 22.f; + ZUIBox* swatch = ZUIPushBox(ctx, swk, swlen, ZUI_DrawBackground | ZUI_DrawBorder | ZUI_Clickable); + swatch->Size[0] = ZPx(swatch_sz); + swatch->Size[1] = ZPx(swatch_sz); + ZUIBoxSetColorArr(swatch, color); + swatch->BorderColor[0] = 0.4f; + swatch->BorderColor[1] = 0.4f; + swatch->BorderColor[2] = 0.4f; + swatch->BorderColor[3] = 0.9f; + swatch->BorderThickness = 1.f; + ZUIBoxSetCornerRadius(swatch, 3.f); + swatch->EdgeSoftness = 0.f; + ZUISignal sw_sig = ZUISignalFromBox(ctx, swatch); + ZUIPopBox(ctx); + + ZUISpacer(ctx, 6.f); + + // Hex label "#RRGGBBAA" + char hex[12]; + int r = (int) (color[0] * 255.f + 0.5f); + int g = (int) (color[1] * 255.f + 0.5f); + int b = (int) (color[2] * 255.f + 0.5f); + int a = (int) (color[3] * 255.f + 0.5f); + snprintf(hex, sizeof(hex), "#%02X%02X%02X%02X", r, g, b, a); + ZUILabel(ctx, hex, ctx->Theme.TextDim); + + // Open picker popup + if (sw_sig.Flags & ZUI_SignalClicked) + ZUIOpenPopup(ctx, key); + + if (ZUIBeginPopup(ctx, key)) + { + changed |= ZUIColorPicker4(ctx, key, color); + ZUIEndPopup(ctx); + } + + return changed; + } + + // ZUISpinner (3-dot pulse, arena-safe) + + void ZUISpinner(ZUIContext* ctx, const char* key, float radius_px, float speed) + { + float dot = radius_px * 0.65f; + float gap = dot * 0.6f; + + char rk[48]; + snprintf(rk, sizeof(rk), "##spn_%s", key); + ZUIBeginRow(ctx, rk, ZFit(), ZPx(dot)); + + for (int i = 0; i < 3; ++i) + { + if (i > 0) + ZUISpacer(ctx, gap); + + float phase = ctx->Time * speed - (float) i * 0.5f; + float t = 0.5f + 0.5f * sinf(phase); + float alpha = 0.25f + 0.75f * t; + + char dk[56]; + snprintf(dk, sizeof(dk), "##spd%d_%s", i, key); + ZUIBox* d = ZUIPushBox(ctx, dk, (uint32_t) strlen(dk), ZUI_DrawBackground); + d->Size[0] = ZPx(dot); + d->Size[1] = ZPx(dot); + float col[4] = {ctx->Theme.TabActiveBorder[0], ctx->Theme.TabActiveBorder[1], ctx->Theme.TabActiveBorder[2], alpha}; + ZUIBoxSetColorArr(d, col); + ZUIBoxSetCornerRadius(d, dot * 0.5f); + d->EdgeSoftness = 0.6f; + ZUIPopBox(ctx); + } + + ZUIEndRow(ctx); + } + + // ZUITextField + + bool ZUITextField(ZUIContext* ctx, const char* key, char* buf, uint32_t buf_size, float width_px) + { + uint32_t key_len = (uint32_t) strlen(key); + ZUIBox* field = ZUIPushBox(ctx, key, key_len, ZUI_DrawBackground | ZUI_DrawBorder | ZUI_Clickable | ZUI_DrawText); + field->Size[0] = ZPx(width_px); + field->Size[1] = ZPx(ZUIGetFrameHeight(ctx)); + field->Padding[0] = ZUIGetFramePadX(ctx); + field->Padding[2] = ZUIGetFramePadX(ctx); + ZUIBoxSetCornerRadius(field, ctx->Style.FrameRounding); + SetBgArr(field, ctx->Theme.InputBg); + SetTextColor(field, ctx->Theme.TextDefault); + + bool is_focused = (ctx->FocusKey == field->Key); + bool changed = false; + + ZUIPersistentState* ps = ZUIStateGetOrInsert(&ctx->StateStore, field->Key); + ZUIFont* font = ctx->GetFont(ZUIFontSize::Body); + + if (is_focused) + { + SetBdrArr(field, ctx->Theme.InputFocusBorder); + + uint32_t len = (uint32_t) Helpers::secure_strlen(buf); + int cpos = (ps && ps->UserData >= 0.f) ? (int) ps->UserData : (int) len; + if (cpos < 0) + cpos = 0; + if ((uint32_t) cpos > len) + cpos = (int) len; + + int sel_start = ps ? ps->SelectStart : -1; + + // Undo/Redo helpers + static constexpr int kUD = (int) ZUIContext::kUndoDepth; + + auto push_undo = [&]() { + if (ctx->UndoFieldKey != field->Key) + { + ctx->UndoFieldKey = field->Key; + ctx->UndoTop = ctx->RedoTop = 0; + } + ctx->RedoTop = 0; + if (ctx->UndoTop < kUD) + { + auto& e = ctx->UndoStack[ctx->UndoTop++]; + snprintf(e.Buf, sizeof(e.Buf), "%s", buf); + e.Cursor = cpos; + } + else + { + memmove(ctx->UndoStack, ctx->UndoStack + 1, (kUD - 1) * sizeof(ZUIContext::ZUIUndoEntry)); + auto& e = ctx->UndoStack[kUD - 1]; + snprintf(e.Buf, sizeof(e.Buf), "%s", buf); + e.Cursor = cpos; + } + }; + + // Ctrl+Z undo + if (ctx->CtrlZPressed && ctx->UndoFieldKey == field->Key && ctx->UndoTop > 0) + { + if (ctx->RedoTop < kUD) + { + auto& r = ctx->RedoStack[ctx->RedoTop++]; + snprintf(r.Buf, sizeof(r.Buf), "%s", buf); + r.Cursor = cpos; + } + auto& e = ctx->UndoStack[--ctx->UndoTop]; + snprintf(buf, buf_size, "%s", e.Buf); + cpos = e.Cursor; + if ((uint32_t) cpos > strlen(buf)) + cpos = (int) strlen(buf); + sel_start = -1; + changed = true; + } + // Ctrl+Y redo + if (ctx->CtrlYPressed && ctx->UndoFieldKey == field->Key && ctx->RedoTop > 0) + { + if (ctx->UndoTop < kUD) + { + auto& u = ctx->UndoStack[ctx->UndoTop++]; + snprintf(u.Buf, sizeof(u.Buf), "%s", buf); + u.Cursor = cpos; + } + auto& r = ctx->RedoStack[--ctx->RedoTop]; + snprintf(buf, buf_size, "%s", r.Buf); + cpos = r.Cursor; + if ((uint32_t) cpos > strlen(buf)) + cpos = (int) strlen(buf); + sel_start = -1; + changed = true; + } + + // Selection helpers + auto has_sel = [&] { return sel_start >= 0 && sel_start != cpos; }; + auto sel_lo = [&] { return sel_start >= 0 && sel_start < cpos ? sel_start : cpos; }; + auto sel_hi = [&] { return sel_start >= 0 && sel_start > cpos ? sel_start : cpos; }; + + auto delete_sel = [&]() { + if (!has_sel()) + return; + int lo = sel_lo(), hi = sel_hi(); + memmove(buf + lo, buf + hi, Helpers::secure_strlen(buf) - hi + 1); + cpos = lo; + sel_start = -1; + changed = true; + }; + + // Mouse click / drag to position and select + // prev-frame field X from state — used to map mouse pos → char index + { + float field_x = (ps && ps->ScreenMinX > 0.f) ? ps->ScreenMinX + ZUIGetFramePadX(ctx) : 0.f; + + auto char_at = [&](float offset) -> int { + if (!font || offset <= 0.f) + return 0; + uint32_t slen = (uint32_t) Helpers::secure_strlen(buf); + float cum = 0.f; + for (uint32_t i = 0; i < slen; ++i) + { + float ts[2] = {}; + ZUIMeasureText(font, buf + i, 1, ts); + if (cum + ts[0] * 0.5f > offset) + return (int) i; + cum += ts[0]; + } + return (int) slen; + }; + + if (ctx->MousePressed[0] && ctx->HotKey == field->Key) + { + // Click: set cursor to character under mouse, clear selection + cpos = char_at(ctx->MousePos[0] - field_x); + sel_start = -1; + } + else if (ctx->MouseDown[0] && !ctx->MousePressed[0] && ctx->ActiveKey == field->Key) + { + // Drag: extend selection from initial click position to current mouse + if (sel_start < 0) + sel_start = cpos; // anchor at click pos + cpos = char_at(ctx->MousePos[0] - field_x); + } + } + + // Text insertion + if (ctx->TextInputLen > 0) + { + push_undo(); + delete_sel(); + for (uint32_t i = 0; i < ctx->TextInputLen; ++i) + { + len = (uint32_t) Helpers::secure_strlen(buf); + if (len + 1 < buf_size) + { + memmove(buf + cpos + 1, buf + cpos, len - cpos + 1); + buf[cpos] = ctx->TextInput[i]; + cpos++; + changed = true; + } + } + } + + // Backspace + if (ctx->BackspacePressed) + { + push_undo(); + if (has_sel()) + delete_sel(); + else if (cpos > 0) + { + len = (uint32_t) Helpers::secure_strlen(buf); + memmove(buf + cpos - 1, buf + cpos, len - cpos + 1); + cpos--; + changed = true; + } + } + + // Forward delete + len = (uint32_t) Helpers::secure_strlen(buf); + if (ctx->DeletePressed) + { + push_undo(); + if (has_sel()) + delete_sel(); + else if ((uint32_t) cpos < len) + { + memmove(buf + cpos, buf + cpos + 1, len - cpos); + changed = true; + } + } + + // Arrow keys + Shift for selection + len = (uint32_t) Helpers::secure_strlen(buf); + if (ctx->ArrowLeftPressed) + { + if (ctx->ShiftDown) + { + if (sel_start < 0) + sel_start = cpos; + if (cpos > 0) + cpos--; + } + else + { + cpos = has_sel() ? sel_lo() : (cpos > 0 ? cpos - 1 : 0); + sel_start = -1; + } + } + if (ctx->ArrowRightPressed) + { + if (ctx->ShiftDown) + { + if (sel_start < 0) + sel_start = cpos; + if ((uint32_t) cpos < len) + cpos++; + } + else + { + cpos = has_sel() ? sel_hi() : ((uint32_t) cpos < len ? cpos + 1 : cpos); + sel_start = -1; + } + } + if (ctx->HomePressed) + { + if (ctx->ShiftDown) + { + if (sel_start < 0) + sel_start = cpos; + } + else + sel_start = -1; + cpos = 0; + } + if (ctx->EndPressed) + { + if (ctx->ShiftDown) + { + if (sel_start < 0) + sel_start = cpos; + } + else + sel_start = -1; + cpos = (int) len; + } + + // Ctrl+A — select all + if (ctx->CtrlAPressed) + { + sel_start = 0; + cpos = (int) len; + } + + // Clipboard + if (ctx->CtrlCPressed) + { + if (has_sel()) + { + int n = sel_hi() - sel_lo(); + if (n >= (int) sizeof(ctx->ClipboardWrite)) + n = (int) sizeof(ctx->ClipboardWrite) - 1; + memcpy(ctx->ClipboardWrite, buf + sel_lo(), n); + ctx->ClipboardWrite[n] = '\0'; + } + else + snprintf(ctx->ClipboardWrite, sizeof(ctx->ClipboardWrite), "%s", buf); + } + if (ctx->CtrlXPressed) + { + push_undo(); + if (has_sel()) + { + int n = sel_hi() - sel_lo(); + if (n >= (int) sizeof(ctx->ClipboardWrite)) + n = (int) sizeof(ctx->ClipboardWrite) - 1; + memcpy(ctx->ClipboardWrite, buf + sel_lo(), n); + ctx->ClipboardWrite[n] = '\0'; + delete_sel(); + } + else + { + snprintf(ctx->ClipboardWrite, sizeof(ctx->ClipboardWrite), "%s", buf); + buf[0] = '\0'; + cpos = 0; + sel_start = -1; + changed = true; + } + } + + // Ctrl+Backspace — delete word + if (ctx->CtrlBackspacePressed && cpos > 0) + { + push_undo(); + if (has_sel()) + delete_sel(); + else + { + len = (uint32_t) Helpers::secure_strlen(buf); + int start = cpos - 1; + while (start > 0 && buf[start - 1] != ' ' && buf[start - 1] != '/' && buf[start - 1] != '\\' && buf[start - 1] != '.') + start--; + memmove(buf + start, buf + cpos, len - cpos + 1); + cpos = start; + changed = true; + } + } + + // Clamp and save state + len = (uint32_t) Helpers::secure_strlen(buf); + if (cpos < 0) + cpos = 0; + if ((uint32_t) cpos > len) + cpos = (int) len; + if (ps) + { + ps->UserData = (float) cpos; + ps->SelectStart = sel_start; + } + + // Selection highlight (floated child — renders on top of text) + if (has_sel() && font) + { + float ts[2] = {}; + int lo = sel_lo(), hi = sel_hi(); + ZUIMeasureText(font, buf, lo, ts); + float before_w = ts[0]; + ZUIMeasureText(font, buf + lo, hi - lo, ts); + float sel_w = ts[0]; + if (sel_w > 0.5f) + { + float fpy = ZUIGetFramePadY(ctx); + char sk[80]; + snprintf(sk, sizeof(sk), "##fsel_%s", key); + ZUIBox* sb = ZUIPushBox(ctx, sk, (uint32_t) strlen(sk), ZUI_DrawBackground | ZUI_FloatX | ZUI_FloatY); + sb->Size[0] = ZPx(sel_w); + sb->Size[1] = ZPx(ZUIGetFrameHeight(ctx) - fpy * 2.f); + sb->FloatPos[0] = ZUIGetFramePadX(ctx) + before_w; + sb->FloatPos[1] = fpy; + ZUIBoxSetColorArr(sb, ctx->Theme.SelectionBg); + sb->EdgeSoftness = 0.f; + ZUIPopBox(ctx); + } + } + + // Cursor caret — 1.5 px drawn rect at the cursor X position (no | + // embedded in text, so character advance doesn't shift the layout) + bool show_caret = !has_sel() && (fmodf(ctx->Time, ctx->Style.CursorBlinkRate) < ctx->Style.CursorBlinkRate * 0.5f); + if (show_caret && font) + { + float ts[2] = {}; + ZUIMeasureText(font, buf, cpos, ts); + float fpy = ZUIGetFramePadY(ctx); + char ck[80]; + snprintf(ck, sizeof(ck), "##fcaret_%s", key); + ZUIBox* caret = ZUIPushBox(ctx, ck, (uint32_t) strlen(ck), ZUI_DrawBackground | ZUI_FloatX | ZUI_FloatY); + caret->Size[0] = ZPx(1.5f); + caret->Size[1] = ZPx(ZUIGetFrameHeight(ctx) - fpy * 2.f); + caret->FloatPos[0] = ZUIGetFramePadX(ctx) + ts[0]; // ts[0] already in logical px (ZUIMeasureText applies FontScale) + caret->FloatPos[1] = fpy; + ZUIBoxSetColorArr(caret, ctx->Theme.TextDefault); + caret->EdgeSoftness = 0.f; + ZUIPopBox(ctx); + } + + // Text label (cursor no longer embedded) + uint32_t dlen = (uint32_t) Helpers::secure_strlen(buf); + field->Label = ZUIPushStr(&ctx->FrameArena, buf, dlen); + } + else + { + SetBdrArr(field, ctx->Theme.InputBorder); + if (ps) + ps->SelectStart = -1; + uint32_t dlen = (uint32_t) Helpers::secure_strlen(buf); + field->Label = ZUIPushStr(&ctx->FrameArena, buf, dlen); + } + + ZUISignal sig = ZUISignalFromBox(ctx, field); + ZUIPopBox(ctx); + + if (sig.Flags & ZUI_SignalClicked) + { + ctx->FocusKey = field->Key; + // Cursor position already set by the press handler in the focused block. + // Only update here on first-click-to-focus (field was not focused yet). + if (!is_focused && ps) + { + if (font) + { + float field_x = (ps->ScreenMinX > 0.f) ? ps->ScreenMinX + ZUIGetFramePadX(ctx) : 0.f; + float offset = ctx->MousePos[0] - field_x; + float cum = 0.f; + int cp = (int) strlen(buf); + for (uint32_t i = 0; i < (uint32_t) cp; ++i) + { + float ts[2] = {}; + ZUIMeasureText(font, buf + i, 1, ts); + if (cum + ts[0] * 0.5f > offset) + { + cp = (int) i; + break; + } + cum += ts[0]; + } + ps->UserData = (float) cp; + } + else + ps->UserData = (float) strlen(buf); + ps->SelectStart = -1; + } + } + + return changed; + } + + // ZUIResizeHandle + + bool ZUIResizeHandle(ZUIContext* ctx, const char* key, float* value, float min_v, float max_v, bool horizontal) + { + uint32_t len = (uint32_t) strlen(key); + ZUIBox* box = ZUIPushBox(ctx, key, len, ZUI_Clickable); + if (horizontal) + { + box->Size[0] = ZFill(); + box->Size[1] = ZSPx(ctx, 4.f); + } + else + { + box->Size[0] = ZSPx(ctx, 4.f); + box->Size[1] = ZFill(); + } + + ZUISignal sig = ZUISignalFromBox(ctx, box); + ZUIPopBox(ctx); + + bool dragging = false; + if ((sig.Flags & ZUI_SignalHeld) && value) + { + float delta = horizontal ? sig.DragDelta[1] : sig.DragDelta[0]; + *value += delta; + if (*value < min_v) + *value = min_v; + if (*value > max_v) + *value = max_v; + dragging = (delta != 0.f); + } + return dragging; + } + + // ================================================================ + // Plot widgets + // ================================================================ + + static void PlotSetup(ZUIContext* ctx, const char* key, const float* values, int count, float v_min, float v_max, ZUIBoxFlags draw_flag, ZUISize w, ZUISize h) + { + if (!values || count <= 0) + return; + + // Auto-scale + if (v_min >= 3.0e+38f || v_max >= 3.0e+38f) + { + v_min = values[0]; + v_max = values[0]; + for (int i = 1; i < count; ++i) + { + if (values[i] < v_min) + v_min = values[i]; + if (values[i] > v_max) + v_max = values[i]; + } + if (v_min == v_max) + { + v_min -= 1.f; + v_max += 1.f; + } + } + + // Copy values to FrameArena so they survive until PreparePayload + float* data = ZPushArray(&ctx->FrameArena, float, (uint32_t) count); + for (int i = 0; i < count; ++i) + data[i] = values[i]; + + uint32_t klen = (uint32_t) strlen(key); + ZUIBox* box = ZUIPushBox(ctx, key, klen, ZUI_DrawBackground | draw_flag); + box->Size[0] = w; + box->Size[1] = h; + // Repurpose Label for data pointer + count (no DrawText flag, so text path skipped) + box->Label.Ptr = (const char*) data; + box->Label.Len = (uint32_t) count; + // Store range in Padding (normally {left,top,right,bottom} but unused for plots) + box->Padding[0] = v_min; + box->Padding[2] = v_max; + SetBgArr(box, ctx->Theme.InputBg); + box->EdgeSoftness = 0.f; + ZUIPopBox(ctx); + } + + void ZUIPlotLines(ZUIContext* ctx, const char* key, const float* values, int count, float v_scale_min, float v_scale_max, const char* /*overlay_text*/, ZUISize w, ZUISize h) + { + PlotSetup(ctx, key, values, count, v_scale_min, v_scale_max, ZUI_DrawPlotLines, w, h); + } + + void ZUIPlotHistogram(ZUIContext* ctx, const char* key, const float* values, int count, float v_scale_min, float v_scale_max, const char* /*overlay_text*/, ZUISize w, ZUISize h) + { + PlotSetup(ctx, key, values, count, v_scale_min, v_scale_max, ZUI_DrawPlotBars, w, h); + } + + // ================================================================ + // ZUIGridView + // ================================================================ + + ZUIBox* ZUIBeginGridView(ZUIContext* ctx, const char* key, float item_w, float item_h, ZUISize w, ZUISize h) + { + ctx->GV_ItemW = item_w; + ctx->GV_ItemH = item_h; + // items_per_row from container width approximation (ScreenW / item_w) + // This stabilises after frame 0; panels typically fill most of ScreenW. + int max_c = (item_w > 0.f) ? (int) ((float) ctx->ScreenW / item_w) : 1; + if (max_c < 1) + max_c = 1; + ctx->GV_MaxCols = max_c; + ctx->GV_CurCol = 0; + ctx->GV_CurRow = 0; + ctx->GV_RowOpen = false; + return ZUIBeginScrollRegion(ctx, key, w, h); + } + + bool ZUIGridViewNextItem(ZUIContext* ctx, const char* item_key, bool selected) + { + // Start a new row when needed + if (!ctx->GV_RowOpen || ctx->GV_CurCol >= ctx->GV_MaxCols) + { + if (ctx->GV_RowOpen) + { + ZUIEndRow(ctx); + } + char rk[72]; + snprintf(rk, sizeof(rk), "##gvrow_%s_%d", item_key, ctx->GV_CurRow); + ZUIBeginRow(ctx, rk, ZFill(), ZPx(ctx->GV_ItemH)); + ctx->GV_RowOpen = true; + ctx->GV_CurCol = 0; + ctx->GV_CurRow++; + } + + // Cell box + char ck[128]; + snprintf(ck, sizeof(ck), "##gvcell_%s_%d_%d", item_key, ctx->GV_CurRow, ctx->GV_CurCol); + ZUIBox* cell = ZUIPushBox(ctx, ck, (uint32_t) strlen(ck), ZUI_DrawBackground | ZUI_DrawBorder | ZUI_Clickable); + cell->Size[0] = ZPx(ctx->GV_ItemW); + cell->Size[1] = ZPx(ctx->GV_ItemH); + cell->LayoutAxis = ZUIAxis::Y; + + float cell_bg[4] = {ctx->Theme.PanelBgAlt[0], ctx->Theme.PanelBgAlt[1], ctx->Theme.PanelBgAlt[2], 0.8f}; + if (selected) + ZUIBoxSetColorArr(cell, ctx->Theme.RowSelectedBg); + else + ZUIBoxSetColorArr(cell, cell_bg); + + cell->BorderColor[0] = ctx->Theme.TabActiveBorder[0]; + cell->BorderColor[1] = ctx->Theme.TabActiveBorder[1]; + cell->BorderColor[2] = ctx->Theme.TabActiveBorder[2]; + cell->BorderColor[3] = 0.2f; + cell->BorderThickness = 1.f; + ZUIBoxSetCornerRadius(cell, 4.f); + cell->EdgeSoftness = 0.f; + + ZUISignal sig = ZUISignalFromBox(ctx, cell); + if (!selected) + ApplyHotActive(cell, ctx, cell_bg, ctx->Theme.RowHoverBg, ctx->Theme.RowSelectedBg); + // Note: cell stays open (NOT popped) — caller adds content, then calls EndItem + ctx->GV_CurCol++; + return (sig.Flags & ZUI_SignalClicked) != 0; + } + + void ZUIGridViewEndItem(ZUIContext* ctx) + { + ZUIPopBox(ctx); // close the cell box opened by NextItem + } + + void ZUIEndGridView(ZUIContext* ctx) + { + if (ctx->GV_RowOpen) + { + ZUIEndRow(ctx); + ctx->GV_RowOpen = false; + } + ZUIEndScrollRegion(ctx); + ctx->GV_ItemW = 0.f; + ctx->GV_ItemH = 0.f; + } + + // ================================================================ + // ZUITreeView + // ================================================================ + + // Each node's open state is stored as ZUIPersistentState.UserData + // (1.0=open, 0.0=closed), keyed by the node label hash. + + ZUIBox* ZUIBeginTreeView(ZUIContext* ctx, const char* key, ZUISize w, ZUISize h, const ZUITreeViewConfig* cfg) + { + if (cfg) + { + ctx->TV_RowH = cfg->RowH; + ctx->TV_IndentPx = cfg->IndentPx; + } + else + { + ctx->TV_RowH = ZUIGetFrameHeight(ctx); // ImGui GetFrameHeight + ctx->TV_IndentPx = ctx->Style.IndentSpacing; // ImGui IndentSpacing + } + ctx->TV_Depth = 0; + return ZUIBeginScrollRegion(ctx, key, w, h); + } + + void ZUIEndTreeView(ZUIContext* ctx) + { + ctx->TV_Depth = 0; + ZUIEndScrollRegion(ctx); + } + + // Shared row builder — matches ImGui TreeNodeBehavior exactly: + // • FramePadding.x (4px) left offset before indent + // • Arrow color = ImGuiCol_Text (TextDefault, not dim) + // • Label color = TextDefault always (selection shown via background only) + // • Hover/Active = HeaderHoveredBg / HeaderActiveBg (not subtle RowHoverBg) + // • Icon gap = ItemInnerSpacing.x = 4px + static ZUISignal TV_BuildRow(ZUIContext* ctx, const char* label, bool selected, bool has_arrow, bool is_open, const float icon_col[4]) + { + const float row_h = ctx->TV_RowH; + const float indent = (float) ctx->TV_Depth * ctx->TV_IndentPx; + const float arrow_w = ctx->Style.FontSize + 1.f; // FontSize + 1px (ImGui: g.FontSize) + const float icon_sz = ctx->Style.FontSize - 1.f; + const float kPadL = ctx->Style.FramePadding[0]; // ImGui FramePadding.x + + char rk[128]; + snprintf(rk, sizeof(rk), "##tvrow_%d_%s", ctx->TV_Depth, label); + + ZUIBox* row = ZUIPushBox(ctx, rk, (uint32_t) strlen(rk), ZUI_DrawBackground | ZUI_Clickable); + row->Size[0] = ZFill(); + row->Size[1] = ZPx(row_h); + row->Padding[0] = kPadL; // ImGui: FramePadding.x left margin before indent + row->LayoutAxis = ZUIAxis::X; + + // Background: transparent at rest, teal highlight on hover/select + // ImGui: HeaderHovered at 80% opacity, Header (selected) at 31% + static const float kRest[4] = {0.f, 0.f, 0.f, 0.f}; + ZUIBoxSetColorArr(row, selected ? ctx->Theme.RowSelectedBg : kRest); + + // Depth indent + if (indent > 0.f) + { + char sk[48]; + snprintf(sk, sizeof(sk), "##tvsp_%d_%s", ctx->TV_Depth, label); + ZUIBox* sp = ZUIPushBox(ctx, sk, (uint32_t) strlen(sk), ZUI_None); + sp->Size[0] = ZPx(indent); + sp->Size[1] = ZPx(row_h); + ZUIPopBox(ctx); + } + + // Disclosure arrow (ImGuiCol_Text color) or blank spacer for leaves + if (has_arrow) + { + char ak[128]; + snprintf(ak, sizeof(ak), "##tvarr_%d_%s", ctx->TV_Depth, label); + ZUIBox* ab = ZUIPushBox(ctx, ak, (uint32_t) strlen(ak), ZUI_DrawTriArrow); + ab->Size[0] = ZPx(arrow_w); + ab->Size[1] = ZPx(row_h); + // ImGui always uses ImGuiCol_Text for the arrow — never dimmed + SetTextColor(ab, ctx->Theme.TextDefault); + { + auto* ps = ZUIStateGetOrInsert(&ctx->StateStore, ab->Key); + if (ps) + ps->UserData = is_open ? 2.f : 3.f; // chevron: ∨ expanded, › collapsed + } + ZUIPopBox(ctx); + } + else + { + char lsk[48]; + snprintf(lsk, sizeof(lsk), "##tvlsp_%d_%s", ctx->TV_Depth, label); + ZUIBox* lsp = ZUIPushBox(ctx, lsk, (uint32_t) strlen(lsk), ZUI_None); + lsp->Size[0] = ZPx(arrow_w); + lsp->Size[1] = ZPx(row_h); + ZUIPopBox(ctx); + } + + // Icon dot (engine-specific — ImGui has no icon; gap = ItemInnerSpacing.x = 4px) + if (icon_col) + { + char ik[64]; + snprintf(ik, sizeof(ik), "##tvic_%d_%s", ctx->TV_Depth, label); + ZUIBox* ic = ZUIPushBox(ctx, ik, (uint32_t) strlen(ik), ZUI_DrawBackground); + ic->Size[0] = ZPx(icon_sz); + ic->Size[1] = ZPx(icon_sz); + ZUIBoxSetColorArr(ic, icon_col); + ZUIBoxSetCornerRadius(ic, icon_sz * 0.5f); + ic->EdgeSoftness = 0.5f; + ZUIPopBox(ctx); + + char gk[64]; + snprintf(gk, sizeof(gk), "##tvgap_%d_%s", ctx->TV_Depth, label); + ZUIBox* gap = ZUIPushBox(ctx, gk, (uint32_t) strlen(gk), ZUI_None); + gap->Size[0] = ZPx(ctx->Style.ItemInnerSpacing[0]); + gap->Size[1] = ZPx(row_h); // ItemInnerSpacing.x + ZUIPopBox(ctx); + } + + // Label — ImGui always renders with ImGuiCol_Text regardless of selection + uint32_t llen = (uint32_t) strlen(label); + ZUIBox* lbox = ZUIPushBox(ctx, label, llen, ZUI_DrawText); + lbox->Size[0] = ZText(); + lbox->Size[1] = ZPx(row_h); + SetTextColor(lbox, ctx->Theme.TextDefault); // ImGui: always ImGuiCol_Text + ZUIPopBox(ctx); + + bool is_focused = (ctx->FocusKey == row->Key); + + ZUISignal sig = ZUISignalFromBox(ctx, row); + if (sig.Flags & ZUI_SignalClicked) + { + ctx->FocusKey = row->Key; + } + + // Hover/active: ImGui uses HeaderHovered (strong) not subtle RowHoverBg + if (!selected) + ApplyHotActive( + row, + ctx, + kRest, + ctx->Theme.HeaderHoveredBg, // ImGui ImGuiCol_HeaderHovered + ctx->Theme.HeaderActiveBg); // ImGui ImGuiCol_HeaderActive + + if (is_focused && (ctx->SpacePressed || ctx->EnterPressed)) + sig.Flags = sig.Flags | ZUI_SignalClicked; + + ZUIPopBox(ctx); + return sig; + } + + bool ZUITreeViewBeginNode(ZUIContext* ctx, const char* label, bool selected, const float icon_col[4], bool initial_open) + { + uint64_t hash = ZUIHashStr(label, (uint32_t) strlen(label)) ^ (uint64_t) ctx->TV_Depth; + auto* state = ZUIStateGetOrInsert(&ctx->StateStore, hash); + // UserData < 0 means never explicitly set — apply initial_open on first use + if (state && state->UserData < 0.f) + state->UserData = initial_open ? 1.f : 0.f; + bool is_open = state && state->UserData > 0.5f; + + ZUISignal sig = TV_BuildRow(ctx, label, selected, true, is_open, icon_col); + + if (sig.Flags & ZUI_SignalClicked) + { + is_open = !is_open; + if (state) + state->UserData = is_open ? 1.f : 0.f; + } + // Arrow Right opens a closed node; Arrow Left closes an open one (when row has focus) + // The row key matches what TV_BuildRow built: "##tvrow_<depth>_<label>" + { + char tvk[128]; + snprintf(tvk, sizeof(tvk), "##tvrow_%d_%s", ctx->TV_Depth, label); + if (ctx->FocusKey == ZUIHashStr(tvk, (uint32_t) strlen(tvk))) + { + if (ctx->ArrowRightPressed && !is_open) + { + is_open = true; + if (state) + state->UserData = 1.f; + } + if (ctx->ArrowLeftPressed && is_open) + { + is_open = false; + if (state) + state->UserData = 0.f; + } + } + } + + if (is_open) + { + ctx->TV_Depth++; + } + return is_open; + } + + void ZUITreeViewEndNode(ZUIContext* ctx) + { + if (ctx->TV_Depth > 0) + ctx->TV_Depth--; + } + + bool ZUITreeViewLeaf(ZUIContext* ctx, const char* label, bool selected, const float icon_col[4]) + { + ZUISignal sig = TV_BuildRow(ctx, label, selected, false, false, icon_col); + return (sig.Flags & ZUI_SignalClicked) != 0; + } + + // ================================================================ + // ZUIDataTable + // ================================================================ + + // Persistent state layout for a data table: + // slot key = DT_Key ^ (col * 2654435761ULL) → UserData = column width + // slot key = DT_Key ^ 0xBAADF00DULL → UserData = sort encoding + + static constexpr uint64_t kDT_SortSuffix = 0xBAADF00DULL; + static constexpr float kDT_ColDefault = 100.f; // default logical width + static constexpr float kDT_HeaderH = 24.f; // logical header row height + static constexpr float kDT_RowH = 22.f; // logical data row height + static constexpr float kDT_ResizeW = 4.f; // resize grip logical width + + static uint64_t DT_ColKey(uint64_t table_key, int col) + { + return table_key ^ ((uint64_t) col * 2654435761ULL); + } + + bool ZUIBeginDataTable(ZUIContext* ctx, const char* key, int col_count, const ZUIDataTableColumn* cols, ZUISize h) + { + ctx->DT_Key = ZUIHashStr(key, (uint32_t) strlen(key)); + ctx->DT_ColCount = col_count; + ctx->DT_CurCol = -1; + ctx->DT_RowIndex = 0; + ctx->DT_InRow = false; + ctx->DT_RowBox = nullptr; + ctx->DT_SortChanged = false; + + // Load column widths from persistent state; apply InitWidth for new tables + ctx->DT_ColWidths = ZPushArray(&ctx->FrameArena, float, col_count); + for (int i = 0; i < col_count; ++i) + { + auto* s = ZUIStateGetOrInsert(&ctx->StateStore, DT_ColKey(ctx->DT_Key, i)); + float init = (cols && cols[i].InitWidth > 0.f) ? cols[i].InitWidth : kDT_ColDefault; + if (s && s->UserData > 1.f) // already set + ctx->DT_ColWidths[i] = s->UserData; + else + { + ctx->DT_ColWidths[i] = init; + if (s) + s->UserData = init; + } + } + + // Load sort state + { + auto* ss = ZUIStateGetOrInsert(&ctx->StateStore, ctx->DT_Key ^ kDT_SortSuffix); + if (ss && ss->UserData != 0.f && ss->UserData > -0.5f) // skip -1 sentinel + { + float enc = ss->UserData; + ctx->DT_SortAsc = (enc > 0.f); + ctx->DT_SortCol = (int) (enc > 0.f ? enc : -enc) - 1; + } + else + { + ctx->DT_SortCol = -1; + ctx->DT_SortAsc = true; + } + } + + // Outer container (full-width column that clips) + ZUIBeginColumn(ctx, key, ZFill(), h); + + // Store cols in FrameArena for HeadersRow + if (cols) + { + auto* copy = ZPushArray(&ctx->FrameArena, ZUIDataTableColumn, col_count); + for (int i = 0; i < col_count; ++i) + copy[i] = cols[i]; + ctx->DT_Cols = copy; + } + else + { + ctx->DT_Cols = nullptr; + } + + return true; + } + + void ZUIDataTableHeadersRow(ZUIContext* ctx) + { + const ZUIDataTableColumn* cols = (const ZUIDataTableColumn*) ctx->DT_Cols; + float header_h = kDT_HeaderH; + float resize_w = kDT_ResizeW; + + char hk[48]; + snprintf(hk, sizeof(hk), "##dthdr_%llu", (unsigned long long) ctx->DT_Key); + ZUIBox* hrow = ZUIBeginRow(ctx, hk, ZFill(), ZPx(header_h)); + hrow->Flags = hrow->Flags | ZUI_DrawBackground; + ZUIBoxSetColorArr(hrow, ctx->Theme.TableHeaderBg); + hrow->EdgeSoftness = 0.f; + + for (int i = 0; i < ctx->DT_ColCount; ++i) + { + float cw = ctx->DT_ColWidths[i]; + bool sortable = cols && cols[i].Sortable; + bool resizable = cols && cols[i].Resizable; + + // Outer header cell box (clickable when sortable) + char ck[56]; + snprintf(ck, sizeof(ck), "##dthc_%llu_%d", (unsigned long long) ctx->DT_Key, i); + ZUIBoxFlags cell_flags = ZUI_DrawBackground | ZUI_DrawBorder; + if (sortable) + cell_flags = cell_flags | ZUI_Clickable; + ZUIBox* cell = ZUIPushBox(ctx, ck, (uint32_t) strlen(ck), cell_flags); + cell->Size[0] = ZPx(cw - (resizable ? resize_w : 0.f)); + cell->Size[1] = ZPx(header_h); + cell->LayoutAxis = ZUIAxis::X; + + // Hover tint on sortable headers + bool cell_hot = (ctx->HotKey == cell->Key); + if (cell_hot && sortable) + ZUIBoxSetColorArr(cell, ctx->Theme.TableBorderLight); + else + ZUIBoxSetColorArr(cell, ctx->Theme.TableHeaderBg); + cell->BorderColor[0] = ctx->Theme.TableBorderStrong[0]; + cell->BorderColor[1] = ctx->Theme.TableBorderStrong[1]; + cell->BorderColor[2] = ctx->Theme.TableBorderStrong[2]; + cell->BorderColor[3] = ctx->Theme.TableBorderStrong[3]; + cell->BorderThickness = 1.f; + cell->EdgeSoftness = 0.f; + + // Left padding + char sp1k[32]; + snprintf(sp1k, sizeof(sp1k), "##thsp1_%d", i); + ZUIBox* sp1 = ZUIPushBox(ctx, sp1k, (uint32_t) strlen(sp1k), ZUI_None); + sp1->Size[0] = ZPx(ctx->Style.CellPadding[0]); + sp1->Size[1] = ZPx(header_h); + ZUIPopBox(ctx); + + // Column label + const char* lbl = (cols && cols[i].Label) ? cols[i].Label : "?"; + bool is_sort_col = (ctx->DT_SortCol == i); + uint32_t lblen = (uint32_t) strlen(lbl); + ZUIBox* lbox = ZUIPushBox(ctx, lbl, lblen, ZUI_DrawText); + lbox->Size[0] = ZText(); + lbox->Size[1] = ZPx(header_h); + const float* lc = is_sort_col ? ctx->Theme.TextDefault : ctx->Theme.TextDim; + lbox->TextColor[0] = lc[0]; + lbox->TextColor[1] = lc[1]; + lbox->TextColor[2] = lc[2]; + lbox->TextColor[3] = lc[3]; + ZUIPopBox(ctx); + + // Sort direction indicator + if (is_sort_col && sortable) + { + char sk[32]; + snprintf(sk, sizeof(sk), "##ths_%d", i); + const char* arrow = ctx->DT_SortAsc ? " ^" : " v"; + ZUIBox* arr = ZUIPushBox(ctx, sk, (uint32_t) strlen(sk), ZUI_DrawText); + arr->Size[0] = ZPx(14.f); + arr->Size[1] = ZPx(header_h); + arr->Label = ZUIPushStr(&ctx->FrameArena, arrow, (uint32_t) strlen(arrow)); + float ac[4] = {ctx->Theme.TabActiveBorder[0], ctx->Theme.TabActiveBorder[1], ctx->Theme.TabActiveBorder[2], 1.f}; + arr->TextColor[0] = ac[0]; + arr->TextColor[1] = ac[1]; + arr->TextColor[2] = ac[2]; + arr->TextColor[3] = ac[3]; + ZUIPopBox(ctx); + } + + ZUISignal cell_sig = ZUISignalFromBox(ctx, cell); + ZUIPopBox(ctx); // cell + + // Sort on click + if (sortable && (cell_sig.Flags & ZUI_SignalClicked)) + { + if (ctx->DT_SortCol == i) + { + ctx->DT_SortAsc = !ctx->DT_SortAsc; + } + else + { + ctx->DT_SortCol = i; + ctx->DT_SortAsc = true; + } + ctx->DT_SortChanged = true; + // Persist sort state + float enc = (float) (ctx->DT_SortCol + 1) * (ctx->DT_SortAsc ? 1.f : -1.f); + auto* ss = ZUIStateGetOrInsert(&ctx->StateStore, ctx->DT_Key ^ kDT_SortSuffix); + if (ss) + ss->UserData = enc; + } + + // Column resize grip (right edge of header cell) + if (resizable) + { + char rk[56]; + snprintf(rk, sizeof(rk), "##dtresize_%llu_%d", (unsigned long long) ctx->DT_Key, i); + ZUIBox* grip = ZUIPushBox(ctx, rk, (uint32_t) strlen(rk), ZUI_DrawBackground | ZUI_Clickable); + grip->Size[0] = ZPx(resize_w); + grip->Size[1] = ZPx(header_h); + bool grip_hot = (ctx->HotKey == grip->Key); + float gc[4] = {0.35f, 0.35f, 0.40f, grip_hot ? 0.9f : 0.3f}; + ZUIBoxSetColorArr(grip, gc); + grip->EdgeSoftness = 0.f; + ZUISignal gsig = ZUISignalFromBox(ctx, grip); + ZUIPopBox(ctx); + + // Drag to resize + if ((gsig.Flags & ZUI_SignalHeld) && gsig.DragDelta[0] != 0.f) + { + float new_w = ctx->DT_ColWidths[i] + gsig.DragDelta[0]; + if (new_w < 30.f) + new_w = 30.f; + ctx->DT_ColWidths[i] = new_w; + auto* cs = ZUIStateGetOrInsert(&ctx->StateStore, DT_ColKey(ctx->DT_Key, i)); + if (cs) + cs->UserData = new_w; + } + } + } + + ZUIEndRow(ctx); + } + + bool ZUIDataTableNextRow(ZUIContext* ctx, bool selected) + { + float row_h = kDT_RowH; + + // Close previous row if open + if (ctx->DT_InRow) + { + if (ctx->DT_CurCol >= 0) + { + ZUIEndColumn(ctx); + ctx->DT_CurCol = -1; + } + ZUIEndRow(ctx); + ctx->DT_InRow = false; + } + + // Alternating row background + bool alt = (ctx->DT_RowIndex % 2) == 1; + float bg[4]; + if (selected) + { + bg[0] = ctx->Theme.RowSelectedBg[0]; + bg[1] = ctx->Theme.RowSelectedBg[1]; + bg[2] = ctx->Theme.RowSelectedBg[2]; + bg[3] = ctx->Theme.RowSelectedBg[3]; + } + else if (alt) + { + bg[0] = ctx->Theme.PanelBgAlt[0]; + bg[1] = ctx->Theme.PanelBgAlt[1]; + bg[2] = ctx->Theme.PanelBgAlt[2]; + bg[3] = 0.5f; + } + else + { + bg[0] = 0.f; + bg[1] = 0.f; + bg[2] = 0.f; + bg[3] = 0.f; + } + + char rk[48]; + snprintf(rk, sizeof(rk), "##dtrow_%llu_%d", (unsigned long long) ctx->DT_Key, ctx->DT_RowIndex); + ZUIBox* row = ZUIPushBox(ctx, rk, (uint32_t) strlen(rk), ZUI_DrawBackground | ZUI_Clickable); + row->Size[0] = ZFill(); + row->Size[1] = ZPx(row_h); + row->LayoutAxis = ZUIAxis::X; + ZUIBoxSetColorArr(row, bg); + row->EdgeSoftness = 0.f; + + ZUISignal rsig = ZUISignalFromBox(ctx, row); + // Lerped hover via HotT + if (!selected) + { + static const float kDTRest[4] = {0.f, 0.f, 0.f, 0.f}; + ApplyHotActive(row, ctx, kDTRest, ctx->Theme.RowHoverBg, ctx->Theme.RowSelectedBg); + } + + ctx->DT_RowBox = row; + ctx->DT_InRow = true; + ctx->DT_CurCol = -1; + ctx->DT_RowIndex++; + + return (rsig.Flags & ZUI_SignalClicked) != 0; + } + + void ZUIDataTableSetColumn(ZUIContext* ctx, int col) + { + if (!ctx->DT_InRow) + { + return; + } + // Close previous cell + if (ctx->DT_CurCol >= 0) + { + ZUIEndColumn(ctx); + } + + ctx->DT_CurCol = col; + float cw = (col < ctx->DT_ColCount && ctx->DT_ColWidths) ? ctx->DT_ColWidths[col] : ctx->Style.DataTableDefaultColumnW; + + char ck[48]; + snprintf(ck, sizeof(ck), "##dtcell_%llu_%d_%d", (unsigned long long) ctx->DT_Key, ctx->DT_RowIndex, col); + ZUIBeginColumn(ctx, ck, ZPx(cw), ZFill()); + ZUISpacer(ctx, ctx->Style.CellPadding[0]); // left padding + } + + void ZUIEndDataTable(ZUIContext* ctx) + { + // Close open cell + row + if (ctx->DT_InRow) + { + if (ctx->DT_CurCol >= 0) + { + ZUIEndColumn(ctx); + ctx->DT_CurCol = -1; + } + ZUIEndRow(ctx); + ctx->DT_InRow = false; + } + ZUIEndColumn(ctx); // outer container + ctx->DT_ColCount = 0; + } + + ZUITableSortSpec ZUIDataTableGetSortSpecs(ZUIContext* ctx) + { + ZUITableSortSpec spec; + spec.ColumnIndex = ctx->DT_SortCol; + spec.Ascending = ctx->DT_SortAsc; + spec.Changed = ctx->DT_SortChanged; + ctx->DT_SortChanged = false; // consume + return spec; + } + + // ZUISearchBox + + bool ZUISearchBox(ZUIContext* ctx, const char* key, char* buf, uint32_t buf_size, const char* placeholder, ZUISize w) + { + // Full bordered row: [Q icon] [editable text field] + char rk[80]; + snprintf(rk, sizeof(rk), "##sb_%s", key); + ZUIBox* row = ZUIBeginRow(ctx, rk, w, ZPx(ZUIGetFrameHeight(ctx))); + row->Flags = row->Flags | ZUI_DrawBackground | ZUI_DrawBorder | ZUI_Clickable; + SetBgArr(row, ctx->Theme.InputBg); + SetBdrArr(row, ctx->Theme.InputBorder); + row->BorderThickness = 1.f; + ZUIBoxSetCornerRadius(row, ctx->Style.FrameRounding); + row->EdgeSoftness = 0.f; + + // Q — search icon, dim + { + char ik[48]; + snprintf(ik, sizeof(ik), "Q##si_%s", key); + ZUIBox* ic = ZUIPushBox(ctx, ik, (uint32_t) strlen(ik), ZUI_DrawText); + ic->Size[0] = ZPx(ctx->Style.FontSize + 5.f); + ic->Size[1] = ZFill(); + ic->TextAlign = ZUITextAlign::Center; + SetTextColor(ic, ctx->Theme.TextDim); + ZUIPopBox(ctx); + } + + // Text field (no border — border is on the outer row) + uint32_t klen = (uint32_t) strlen(key); + uint32_t field_key_hash = ZUIHashStr(key, klen); + bool is_focused = (ctx->FocusKey == field_key_hash); + + ZUIPersistentState* ps = ZUIStateGetOrInsert(&ctx->StateStore, field_key_hash); + + // Build display string — cursor position from persistent state + char display[512]; + if (!is_focused && buf[0] == '\0') + { + snprintf(display, sizeof(display), "%s", placeholder); + } + else if (is_focused) + { + uint32_t len = (uint32_t) strlen(buf); + int cpos = (ps && ps->UserData >= 0.f) ? (int) ps->UserData : (int) len; + if (cpos < 0) + cpos = 0; + if ((uint32_t) cpos > len) + cpos = (int) len; + bool show_pipe = (fmodf(ctx->Time, ctx->Style.CursorBlinkRate) < ctx->Style.CursorBlinkRate * 0.5f); + if (show_pipe) + snprintf(display, sizeof(display), "%.*s|%s", cpos, buf, buf + cpos); + else + snprintf(display, sizeof(display), "%s", buf); + } + else + { + snprintf(display, sizeof(display), "%s", buf); + } + + ZUIBox* field = ZUIPushBox(ctx, key, klen, ZUI_DrawText | ZUI_Clickable); + field->Size[0] = ZFill(); + field->Size[1] = ZFill(); + field->Padding[0] = 2.f; + uint32_t dlen = (uint32_t) Helpers::secure_strlen(display); + field->Label = ZUIPushStr(&ctx->FrameArena, display, dlen); + if (!is_focused && buf[0] == '\0') + { + SetTextColor(field, ctx->Theme.TextDim); + } + else + { + SetTextColor(field, ctx->Theme.TextDefault); + } + if (is_focused) + { + SetBdrArr(row, ctx->Theme.InputFocusBorder); + } + + ZUISignal sig = ZUISignalFromBox(ctx, field); + ZUIPopBox(ctx); + ZUIEndRow(ctx); + + bool changed = false; + if (sig.Flags & ZUI_SignalClicked) + { + ctx->FocusKey = field_key_hash; + if (ps) + ps->UserData = (float) strlen(buf); // cursor at end on click + } + + // Accept text/cursor operations when focused + if (is_focused) + { + uint32_t len = (uint32_t) strlen(buf); + int cpos = (ps && ps->UserData >= 0.f) ? (int) ps->UserData : (int) len; + if (cpos < 0) + cpos = 0; + if ((uint32_t) cpos > len) + cpos = (int) len; + + for (uint32_t i = 0; i < ctx->TextInputLen && len + 1 < buf_size; ++i) + { + memmove(buf + cpos + 1, buf + cpos, len - cpos + 1); + buf[cpos] = ctx->TextInput[i]; + cpos++; + len++; + changed = true; + } + if (ctx->BackspacePressed && cpos > 0) + { + len = (uint32_t) strlen(buf); + memmove(buf + cpos - 1, buf + cpos, len - cpos + 1); + cpos--; + changed = true; + } + len = (uint32_t) strlen(buf); + if (ctx->DeletePressed && (uint32_t) cpos < len) + { + memmove(buf + cpos, buf + cpos + 1, len - cpos); + changed = true; + } + if (ctx->ArrowLeftPressed && cpos > 0) + cpos--; + if (ctx->ArrowRightPressed && (uint32_t) cpos < len) + cpos++; + if (ctx->HomePressed) + cpos = 0; + if (ctx->EndPressed) + cpos = (int) (uint32_t) strlen(buf); + if (ctx->CtrlXPressed || ctx->CtrlAPressed) + { + snprintf(ctx->ClipboardWrite, sizeof(ctx->ClipboardWrite), "%s", buf); + buf[0] = '\0'; + cpos = 0; + changed = true; + } + if (ctx->CtrlCPressed) + snprintf(ctx->ClipboardWrite, sizeof(ctx->ClipboardWrite), "%s", buf); + if (ps) + ps->UserData = (float) cpos; + } + + return changed; + } + +} // namespace ZEngine::UI diff --git a/ZEngine/ZEngine/UI/ZUIWidgets.h b/ZEngine/ZEngine/UI/ZUIWidgets.h new file mode 100644 index 000000000..8a288b0a0 --- /dev/null +++ b/ZEngine/ZEngine/UI/ZUIWidgets.h @@ -0,0 +1,510 @@ +#pragma once +#include <ZEngine/UI/ZUIBox.h> +#include <ZEngine/UI/ZUIContext.h> +#include <ZEngine/UI/ZUIInteraction.h> + +namespace ZEngine::UI +{ + // Size constructors — wrap ZUISize for use as function arguments. + + /// @brief Fixed pixel size (logical coordinates; no UIScale multiplication). + inline ZUISize ZPx(float v) + { + return {ZUISizeKind::Pixels, v, 1.f}; + } + /// @brief Alias of ZPx — kept for call-site clarity. + inline ZUISize ZSPx(const ZUIContext* /*ctx*/, float v) + { + return ZPx(v); + } + /// @brief Fill all available space along the parent's layout axis. + inline ZUISize ZFill() + { + return {ZUISizeKind::Fill, 0.f, 1.f}; + } + /// @brief Size to the rendered text width (includes FramePadding.x on each side). + inline ZUISize ZText() + { + return {ZUISizeKind::Text, 0.f, 1.f}; + } + /// @brief Percentage of the parent's dimension. + /// @param v Fraction in [0, 1]. + inline ZUISize ZPct(float v) + { + return {ZUISizeKind::ParentPercent, v, 1.f}; + } + /// @brief Size to the sum of children (intrinsic fit). + inline ZUISize ZFit() + { + return {ZUISizeKind::ChildrenSum, 0.f, 1.f}; + } + + // Padding helpers — call immediately after ZUIBeginColumn / ZUIBeginRow. + + /// @brief Apply uniform padding on all four sides of @p box. + inline void ZUIPadding(ZUIBox* box, float all) + { + box->Padding[0] = box->Padding[1] = box->Padding[2] = box->Padding[3] = all; + } + /// @brief Apply separate horizontal and vertical padding to @p box. + inline void ZUIPaddingXY(ZUIBox* box, float horiz, float vert) + { + box->Padding[0] = box->Padding[2] = horiz; + box->Padding[1] = box->Padding[3] = vert; + } + + // Layout containers — always pair Begin with the matching End. + + /// @brief Push a vertical stack; children are laid out along the Y axis. + /// @return The container box — override size/color before adding children. + ZUIBox* ZUIBeginColumn(ZUIContext* ctx, const char* key, ZUISize w = ZFill(), ZUISize h = ZFit()); + void ZUIEndColumn(ZUIContext* ctx); + + /// @brief Push a horizontal stack; children are laid out along the X axis. + /// @return The container box. + ZUIBox* ZUIBeginRow(ZUIContext* ctx, const char* key, ZUISize w = ZFill(), ZUISize h = ZFit()); + void ZUIEndRow(ZUIContext* ctx); + + /// @brief Push a vertically-scrollable clipped region. + /// + /// Children that overflow the height are scissored; the user scrolls with + /// the mouse wheel. Horizontal scroll is supported when LayoutAxis==X. + /// @return The container box — set Size / BgColor before adding children. + ZUIBox* ZUIBeginScrollRegion(ZUIContext* ctx, const char* key, ZUISize w = ZFill(), ZUISize h = ZFill()); + void ZUIEndScrollRegion(ZUIContext* ctx); + + /// @brief Request the named scroll region to jump to its bottom on the next frame. + /// @note Call once whenever new content is appended (e.g. a new log entry). + void ZUIScrollToBottom(ZUIContext* ctx, const char* key); + + /// @brief Read the current vertical scroll offset of the named region. + /// @return Scroll offset in logical pixels, or 0 if key not found. + float ZUIGetScrollY(ZUIContext* ctx, const char* key); + + // Leaf widgets + + /// @brief Non-interactive text label. + /// @param text Null-terminated string to display. + /// @param color RGBA color, or nullptr to use Theme.TextDefault. + /// @param size Font variant (Body/Small/Header). + void ZUILabel(ZUIContext* ctx, const char* text, const float color[4] = nullptr, ZUIFontSize size = ZUIFontSize::Body); + + /// @brief Standard push button. + /// @param w Width (default ZText() = label width + FramePadding.x × 2). + /// @param h Height (default = FrameHeight ≈ 19 px). + /// @return ZUISignal — check ZUI_SignalClicked for activation. + ZUISignal ZUIButton(ZUIContext* ctx, const char* label, ZUISize w = ZText(), ZUISize h = ZPx(19.f)); + + /// @brief Compact borderless button — safe inside rows and toolbars. + ZUISignal ZUISmallButton(ZUIContext* ctx, const char* label); + + /// @brief Invisible hit-area box — no drawing; use for custom-drawn clickable regions. + ZUISignal ZUIInvisibleButton(ZUIContext* ctx, const char* key, ZUISize w = ZText(), ZUISize h = ZPx(28.f)); + + /// @brief Stateful toggle button — background brightens when @p *active is true. + /// @param active Toggled in-place on click. + /// @return true the frame @p *active changes. + bool ZUIToggleButton(ZUIContext* ctx, const char* label, bool* active, ZUISize w = ZText(), ZUISize h = ZPx(28.f)); + + /// @brief Clickable image drawn from the bindless texture array. + /// @param texture_index Bindless slot (e.g. TextureHandle::Index). + ZUISignal ZUIImageButton(ZUIContext* ctx, const char* key, uint32_t texture_index, ZUISize w = ZPx(28.f), ZUISize h = ZPx(28.f)); + + /// @brief Begin a disabled scope — nested widgets skip Clickable and are visually dimmed. + void ZUIBeginDisabled(ZUIContext* ctx); + void ZUIEndDisabled(ZUIContext* ctx); + + /// @brief 1 px horizontal divider line. + void ZUISeparator(ZUIContext* ctx); + + /// @brief Empty gap of @p px pixels along the parent's layout axis. + void ZUISpacer(ZUIContext* ctx, float px); + + /// @brief Collapsible tree row with a disclosure triangle. + /// @param open Toggled on click. + /// @return Signal from the row box (check ZUI_SignalClicked for external handling). + ZUISignal ZUITreeNode(ZUIContext* ctx, const char* label, bool* open); + + /// @brief Checkbox with a text label. + /// @param checked Toggled in-place on click. + /// @return true the frame @p *checked changes. + bool ZUICheckbox(ZUIContext* ctx, const char* label, bool* checked); + + /// @brief Radio button — sets @p *selected = index when clicked. + /// @return true when the value changes. + bool ZUIRadioButton(ZUIContext* ctx, const char* label, int* selected, int index); + + /// @brief Filled horizontal progress bar. + /// @param fraction Progress in [0, 1]. + /// @param overlay_text Optional label drawn centered on the bar (may be nullptr). + void ZUIProgressBar(ZUIContext* ctx, const char* key, float fraction, ZUISize w = ZFill(), ZUISize h = ZPx(18.f), const char* overlay_text = nullptr); + + /// @brief Show a tooltip near the cursor while @p sig contains ZUI_SignalHovered. + /// @note Call immediately after the relevant ZUISignalFromBox call. + void ZUISetTooltip(ZUIContext* ctx, const ZUISignal& sig, const char* text); + + /// @brief Full-width collapsible section header — behaves as a mini-panel in a vertical stack. + /// + /// Click toggles open/close internally (no close button). The returned ZUISignal exposes + /// the full interaction state so the owning stack can detect drag for section reorder: + /// ZUI_SignalClicked → toggle already applied to *open + /// ZUI_SignalHeld → header held; DragDelta[1] available for threshold accumulation + /// + /// @param bg_color Optional RGBA background override; nullptr = transparent. + /// @param show_focus_border Draw a 1px teal focus ring when focused (default true). + /// Pass false to suppress it — e.g. mini-panel section stacks. + /// @return Interaction signal from the header box. + ZUISignal ZUICollapsingHeader(ZUIContext* ctx, const char* label, bool* open, const float* bg_color = nullptr, bool show_focus_border = true); + + /// @brief 4 px horizontal drag sash between two adjacent collapsible sections. + /// + /// Positioned in the layout immediately between the bottom of section[boundary] + /// and the header of section[boundary+1]. On drag applies VS Code's greedy + /// cascade resize: + /// delta > 0 (drag DOWN) → sections above boundary grow, sections below shrink + /// delta < 0 (drag UP) → sections above shrink, sections below grow + /// Each side absorbs as much delta as it can (respecting min_h) before passing + /// the remainder to the next section in that direction. + /// + /// @param heights Content-height array for all N sections (in/out). + /// @param opens Open-state array (collapsed sections are skipped in resize). + /// @param n Total number of sections. + /// @param boundary Index of the section ABOVE this sash (sash sits after section[boundary]). + /// @param min_h Minimum content height per section (default 30 px). + void ZUIPaneSash(ZUIContext* ctx, const char* key, float* heights, const bool* opens, int n, int boundary, float min_h = 30.f); + + // ── Shared drag / drop-zone helpers (used by panel docking and section stack) ────── + + /// @brief Teal drop-zone fill with border — matches panel docking `DockingDropPreviewAlpha`. + /// + /// Rendered as ZUI_FloatX | ZUI_FloatY; @p float_x / @p float_y are relative to the + /// current parent box (##pm_bg → absolute screen coords; panel content → panel-relative). + void ZUIDropZoneFill(ZUIContext* ctx, const char* key, float float_x, float float_y, float w, float h); + + /// @brief 2px solid teal horizontal divider rendered in flow. + /// Marks the exact insertion boundary between sections or panels. + void ZUIDockDividerH(ZUIContext* ctx, const char* key); + + /// @brief Full drag ghost: drop-shadow + content-body stub + labeled header row. + /// + /// @p cursor_x / @p cursor_y are in the current parent's coordinate space. + /// The ghost is offset so the cursor sits at ~30 % of the ghost width horizontally + /// and is vertically centered on cursor_y. + void ZUIDockGhostHeader(ZUIContext* ctx, const char* key, const char* label, float cursor_x, float cursor_y); + + // ────────────────────────────────────────────────────────────────────────────────── + + /// @brief Full-width selectable row. + /// @param selected Toggled in-place on click. + /// @return true the frame @p *selected changes. + bool ZUISelectable(ZUIContext* ctx, const char* label, bool* selected, ZUISize h = ZPx(24.f)); + + /// @brief Horizontal separator with a centered label. + void ZUISeparatorText(ZUIContext* ctx, const char* text); + + /// @brief Drag-to-edit a float value. + /// + /// Horizontal mouse drag changes @p *value by delta * speed. + /// Click to enter text-edit mode. + /// @return true if @p *value changed this frame. + bool ZUIDragFloat(ZUIContext* ctx, const char* key, float* value, float speed = 0.05f, float width_px = 60.f); + + /// @brief Drag-to-edit an integer value. Same mechanics as ZUIDragFloat. + /// @return true if @p *value changed this frame. + bool ZUIDragInt(ZUIContext* ctx, const char* key, int* value, float speed = 1.f, float width_px = 60.f); + + /// @brief Three-component XYZ drag in a single compact row. + /// + /// Renders [X][Y][Z] drag boxes with colored axis labels. + /// @param component_w Per-component box width; 0 = equal distribution. + /// @return true if any component changed this frame. + bool ZUIDragFloat3(ZUIContext* ctx, const char* key, float v[3], float speed = 0.05f, float component_w = 0.f); + + /// @brief Text field that edits a float — click to focus, type, press Enter. + /// @return true when the value changes (on Enter or focus loss). + bool ZUIInputFloat(ZUIContext* ctx, const char* key, float* value, float width_px = 80.f); + + /// @brief Inline color editor: swatch + hex label; clicking opens ZUIColorPicker4. + /// @param color RGBA in linear [0, 1]. + /// @return true when changed. + bool ZUIColorEdit4(ZUIContext* ctx, const char* key, float color[4]); + + /// @brief Animated loading arc driven by ctx->Time. + /// @param radius_px Visual radius in logical pixels. + /// @param speed Angular velocity in radians per second. + void ZUISpinner(ZUIContext* ctx, const char* key, float radius_px = 10.f, float speed = 5.f); + + // Popup / overlay system + + /// @brief Request a popup to open at the given screen position. + /// @param pos_x X position; -1 = current mouse X. + /// @param pos_y Y position; -1 = current mouse Y. + void ZUIOpenPopup(ZUIContext* ctx, const char* key, float pos_x = -1.f, float pos_y = -1.f); + + /// @brief Begin building a popup. + /// + /// Pushes a floated root-level column. Always pair with ZUIEndPopup when + /// this returns true. + /// @return true while the popup is active. + bool ZUIBeginPopup(ZUIContext* ctx, const char* key); + void ZUIEndPopup(ZUIContext* ctx); + + /// @brief Close whichever popup is currently active. + void ZUIClosePopup(ZUIContext* ctx); + + /// @brief Open a popup on right-click over the previous signal's box. + /// @param item_signal Signal obtained from ZUISignalFromBox immediately before. + /// @return true if the popup is now active. + bool ZUIBeginPopupContextItem(ZUIContext* ctx, const char* key, const ZUISignal& item_signal); + + /// @brief Menu item inside a popup — returns true on click (also closes the popup). + bool ZUIMenuItem(ZUIContext* ctx, const char* label, bool enabled = true); + + /// @brief Extended menu item — checkmark, keyboard shortcut, enabled flag. + bool ZUIMenuItemEx(ZUIContext* ctx, const char* label, const char* shortcut = nullptr, bool selected = false, bool enabled = true); + + /// @brief Selectable item inside a ZUIBeginCombo popup. + /// @param selected true tints the item with RowSelectedBg. + /// @return true when clicked; also closes the combo. + bool ZUIComboItem(ZUIContext* ctx, const char* label, bool selected = false); + + // Layout helpers + + /// @brief Place the next item on the same line as the previous one. + /// @note Use ZUISpacer() for explicit gaps rather than @p spacing. + void ZUISameLine(ZUIContext* ctx, float spacing = 0.f); + + /// @brief Begin a simple fixed-column table. + /// @param col_count Number of columns. + /// @param widths Per-column pixel widths; nullptr = equal distribution. + void ZUIBeginTable(ZUIContext* ctx, const char* key, int columns, const float* widths = nullptr, ZUISize h = ZFit()); + void ZUITableNextRow(ZUIContext* ctx); + void ZUITableSetColumn(ZUIContext* ctx, int col_index); + void ZUIEndTable(ZUIContext* ctx); + + /// @brief Set the text alignment on any ZUIBox. + inline void ZUISetTextAlign(ZUIBox* box, ZUITextAlign align) + { + box->TextAlign = align; + } + + /// @brief Apply a vertical gradient to @p box (top → bottom). + inline void ZUISetGradient(ZUIBox* box, const float top[4], const float bot[4]) + { + ZUIBoxSetGradientV(box, top, bot); + } + /// @brief Convenience gradient: solid color at top, transparent at bottom. + inline void ZUISetGradientFade(ZUIBox* box, float r, float g, float b, float a) + { + const float top[4] = {r, g, b, a}; + const float bot[4] = {r, g, b, 0.f}; + ZUIBoxSetGradientV(box, top, bot); + } + + // Complex widgets + + /// @brief Begin a tab bar. + /// + /// @code + /// ZUIBeginTabBar(ctx, "##tabs"); + /// if (ZUIBeginTabItem(ctx, "Tab A")) { /* content */ ZUIEndTabItem(ctx); } + /// if (ZUIBeginTabItem(ctx, "Tab B")) { /* content */ ZUIEndTabItem(ctx); } + /// ZUIEndTabBar(ctx); + /// @endcode + void ZUIBeginTabBar(ZUIContext* ctx, const char* key); + bool ZUIBeginTabItem(ZUIContext* ctx, const char* label); + void ZUIEndTabItem(ZUIContext* ctx); + void ZUIEndTabBar(ZUIContext* ctx); + + /// @brief Scrollable list box wrapping a scroll region with Selectable items. + /// @return The container box. + ZUIBox* ZUIBeginListBox(ZUIContext* ctx, const char* key, ZUISize w = ZFill(), ZUISize h = ZPx(120.f)); + void ZUIEndListBox(ZUIContext* ctx); + + /// @brief Horizontal slider — maps thumb position linearly to [v_min, v_max]. + /// @return true while the value changes. + bool ZUISliderFloat(ZUIContext* ctx, const char* key, float* value, float v_min, float v_max, ZUISize w = ZFill(), ZUISize h = ZPx(24.f)); + + /// @brief Integer text field clamped to [v_min, v_max]. + /// @return true when value changes. + bool ZUIInputInt(ZUIContext* ctx, const char* key, int* value, int v_min = -0x7FFFFFFF, int v_max = 0x7FFFFFFF, ZUISize w = ZFill()); + + /// @brief Multi-line text input inside a scroll region. + /// @return true when @p buf changes. + bool ZUIInputTextMultiline(ZUIContext* ctx, const char* key, char* buf, uint32_t buf_size, ZUISize w = ZFill(), ZUISize h = ZPx(120.f)); + + /// @brief RGBA colour picker (hue bar + SV square + alpha bar). + /// @param color RGBA in linear [0, 1]. Modified in-place. + /// @return true when changed. + bool ZUIColorPicker4(ZUIContext* ctx, const char* key, float color[4]); + + /// @brief Context menu — opens on right-click anywhere in the caller's region. + bool ZUIBeginContextMenu(ZUIContext* ctx, const char* key); + void ZUIEndContextMenu(ZUIContext* ctx); + + /// @brief Dropdown combo box. + /// + /// @p preview_label is shown in the collapsed button. + /// Add ZUIComboItem / ZUISelectable items inside, then call ZUIEndCombo. + /// @return true while the dropdown is open. + bool ZUIBeginCombo(ZUIContext* ctx, const char* key, const char* preview_label, ZUISize w = ZFill()); + void ZUIEndCombo(ZUIContext* ctx); + + /// @brief Horizontal menu bar. Pair with ZUIEndMenuBar. + bool ZUIBeginMenuBar(ZUIContext* ctx); + void ZUIEndMenuBar(ZUIContext* ctx); + + /// @brief Menu button inside a menu bar — opens a popup column on click. + bool ZUIBeginMenu(ZUIContext* ctx, const char* label, bool enabled = true); + void ZUIEndMenu(ZUIContext* ctx); + + /// @brief Submenu item — renders like ZUIMenuItem but with a right-aligned › chevron. + /// Opens a popup to the right on hover, positioned at the item's right edge. + /// Pair with ZUIEndSubMenu. + bool ZUIBeginSubMenu(ZUIContext* ctx, const char* label, bool enabled = true); + void ZUIEndSubMenu(ZUIContext* ctx); + + /// @brief Open a modal dialog (dims background, cannot be dismissed by outside click). + void ZUIOpenModal(ZUIContext* ctx, const char* key); + /// @return true while the modal is active. + bool ZUIBeginModal(ZUIContext* ctx, const char* key, const char* title); + void ZUIEndModal(ZUIContext* ctx); + + // Plot widgets + + /// @brief Line chart over @p count samples. + /// @param v_scale_min Lower bound; FLT_MAX = auto-scale. + /// @param v_scale_max Upper bound; FLT_MAX = auto-scale. + void ZUIPlotLines(ZUIContext* ctx, const char* key, const float* values, int count, float v_scale_min = 3.402823e+38f, float v_scale_max = 3.402823e+38f, const char* overlay_text = nullptr, ZUISize w = ZFill(), ZUISize h = ZPx(40.f)); + + /// @brief Histogram over @p count samples. Same scale semantics as ZUIPlotLines. + void ZUIPlotHistogram(ZUIContext* ctx, const char* key, const float* values, int count, float v_scale_min = 3.402823e+38f, float v_scale_max = 3.402823e+38f, const char* overlay_text = nullptr, ZUISize w = ZFill(), ZUISize h = ZPx(40.f)); + + // ZUITreeView — recursive tree widget + + /// @brief Per-instance configuration for ZUIBeginTreeView. + struct ZUITreeViewConfig + { + float RowH = 19.f; ///< Row height in logical px (FrameHeight) + float IndentPx = 21.f; ///< Indent per depth level (IndentSpacing) + }; + + /// @brief Push a tree view scroll region. + /// @param cfg Layout configuration; nullptr uses defaults. + /// @return The scroll container box. + ZUIBox* ZUIBeginTreeView(ZUIContext* ctx, const char* key, ZUISize w = ZFill(), ZUISize h = ZFill(), const ZUITreeViewConfig* cfg = nullptr); + void ZUIEndTreeView(ZUIContext* ctx); + + /// @brief Push an expandable tree node. + /// + /// If this returns true the node is expanded — add children, then + /// always call ZUITreeViewEndNode. + /// @param selected Tints the row background. + /// @param icon_col RGBA icon color; nullptr = no icon dot. + /// @param initial_open Whether the node starts expanded. + /// @return true when the node is expanded (content should be added). + bool ZUITreeViewBeginNode(ZUIContext* ctx, const char* label, bool selected, const float icon_col[4] = nullptr, bool initial_open = false); + void ZUITreeViewEndNode(ZUIContext* ctx); + + /// @brief Leaf row (no expand arrow). + /// @return true when clicked. + bool ZUITreeViewLeaf(ZUIContext* ctx, const char* label, bool selected, const float icon_col[4] = nullptr); + + // ZUIDataTable — sortable, resizable data table + + /// @brief Column descriptor for ZUIBeginDataTable. + struct ZUIDataTableColumn + { + const char* Label; + float InitWidth; ///< 0 = 100 px default + bool Sortable; + bool Resizable; + }; + + /// @brief Sort state returned by ZUIDataTableGetSortSpecs. + struct ZUITableSortSpec + { + int ColumnIndex; ///< -1 = unsorted + bool Ascending; + bool Changed; ///< true the frame the sort spec changed + }; + + /// @brief Begin a data table. + /// @param col_count Number of columns. + /// @param cols Column descriptors (array of length col_count). + /// @return false if the table is off-screen (still call ZUIEndDataTable). + bool ZUIBeginDataTable(ZUIContext* ctx, const char* key, int col_count, const ZUIDataTableColumn* cols, ZUISize h = ZFill()); + + /// @brief Render the sticky header row with labels and sort arrows. + /// @note Must be called once before any ZUIDataTableNextRow calls. + void ZUIDataTableHeadersRow(ZUIContext* ctx); + + /// @brief Advance to the next data row. + /// @param selected Tints the row with RowSelectedBg when true. + /// @return true if the row was clicked. + bool ZUIDataTableNextRow(ZUIContext* ctx, bool selected = false); + + /// @brief Set the active column cell for the current row. + void ZUIDataTableSetColumn(ZUIContext* ctx, int col); + void ZUIEndDataTable(ZUIContext* ctx); + + /// @return Current sort specification; Changed==true the frame a header was clicked. + ZUITableSortSpec ZUIDataTableGetSortSpecs(ZUIContext* ctx); + + // ZUIGridView — icon grid for content browsers + + /// @brief Push an auto-wrapping icon grid. + /// @param item_w Cell width in logical px. + /// @param item_h Cell height in logical px. + ZUIBox* ZUIBeginGridView(ZUIContext* ctx, const char* key, float item_w, float item_h, ZUISize w = ZFill(), ZUISize h = ZFill()); + + /// @brief Advance to the next grid cell. + /// @param selected Tints the cell background. + /// @return true when the cell is clicked. + bool ZUIGridViewNextItem(ZUIContext* ctx, const char* item_key, bool selected = false); + void ZUIGridViewEndItem(ZUIContext* ctx); + void ZUIEndGridView(ZUIContext* ctx); + + // Drag-and-drop + + /// @brief Begin a drag source on @p box. + /// + /// When the box is held and the mouse has moved, records + /// ctx->DragSourceKey and copies @p payload so the next ZUIAcceptDrop + /// call can retrieve it. + void ZUIBeginDragSource(ZUIContext* ctx, const ZUIBox* box, const char* payload, uint32_t payload_len); + + /// @brief Accept a drop on @p box. + /// + /// Returns true exactly once — on the frame the drop fires. + /// @param out_buf Receives the payload (null-terminated); may be nullptr. + /// @param out_size Capacity of @p out_buf. + bool ZUIAcceptDrop(ZUIContext* ctx, const ZUIBox* box, char* out_buf, uint32_t out_size); + + /// @brief Display a texture in a box. + /// @param texture_index Bindless array slot (e.g. TextureHandle::Index). + void ZUIImage(ZUIContext* ctx, const char* key, uint32_t texture_index, ZUISize w = ZFill(), ZUISize h = ZFill()); + + /// @brief Single-line editable text field with full selection and undo/redo. + /// + /// Keyboard shortcuts: Shift+Arrow to select, Ctrl+A to select all, + /// Ctrl+C/X/V for clipboard, Ctrl+Z/Y for undo/redo (8 levels). + /// Mouse: click to place cursor, drag to select. + /// @param buf Editable buffer. + /// @param buf_size Capacity including the null terminator. + /// @return true if @p buf changed this frame. + bool ZUITextField(ZUIContext* ctx, const char* key, char* buf, uint32_t buf_size, float width_px = 160.f); + + /// @brief Search box — ZUITextField with a dim icon and placeholder text. + /// @param placeholder Shown when @p buf is empty. + bool ZUISearchBox(ZUIContext* ctx, const char* key, char* buf, uint32_t buf_size, const char* placeholder = "Search...", ZUISize w = ZFill()); + + /// @brief Thin invisible resize strip for manual splitter controls. + /// + /// @p horizontal = true → full-width 4 px tall (top/bottom split). + /// @p horizontal = false → full-height 4 px wide (left/right split). + /// While held, updates @p *value by DragDelta clamped to [min_v, max_v]. + /// @return true when actively dragging. + bool ZUIResizeHandle(ZUIContext* ctx, const char* key, float* value, float min_v, float max_v, bool horizontal); + +} // namespace ZEngine::UI diff --git a/ZEngine/docs/future-plan/ui-docking-floating-windows.md b/ZEngine/docs/future-plan/ui-docking-floating-windows.md new file mode 100644 index 000000000..e270b4442 --- /dev/null +++ b/ZEngine/docs/future-plan/ui-docking-floating-windows.md @@ -0,0 +1,783 @@ +# ZUI — ImGui Floating-Window Docking Model + +**Status:** Planning +**Branch target:** `feature/zui-floating-dock` +**Priority:** P2 — Major editor UX improvement +**Depends on:** +- ZUI Style System (`ZUIStyle` struct) — **done** on `feature/zui` +- ZUI Docking v3 (split tree, tab metrics, central node) — **done** on `feature/zui` +- ZUI Panel close deferred queue (`PendingCloseKeys`) — **done** on `feature/zui` + +**Estimated effort:** 6–7 engineering days +**Author:** (assign when work begins) +**Last updated:** 2026-08-27 + +--- + +## 1. Motivation + +The current ZUI docking system is VS Code / RAD Debugger-style: panels live +permanently in a binary split tree. There is no concept of a "floating" panel. +The user cannot detach a panel to a free-floating window, rearrange it freely, +or have it overlap other panels. + +ImGui's model is the opposite: every window starts floating and can be *optionally* +docked into a `DockSpace`. This gives the user full freedom to: + +- Float any panel anywhere on screen +- Resize floating panels independently +- Stack floating panels (z-order) +- Re-dock floating panels into the split tree at any time +- Have a clean "3D viewport" central node that the floating layer never obscures + +--- + +## 2. Architecture Overview + +### 2.1 Conceptual Mapping + +| ZUI concept | ImGui equivalent | Notes | +|---|---|---| +| `ZUIPanel` | `ImGuiWindow` | Holds views (tabs), can be docked or floating | +| `ZUIDockNode` | `ImGuiDockNode` | Node in split tree; leaf = one panel slot | +| `ZUIPanelView` | ImGui window content | The actual rendered content | +| `ZUIBeginDockSpace` (new) | `ImGui::DockSpace()` | Background area accepting docked panels | +| Central node (`IsCentral`) | `ImGuiDockNodeFlags_CentralNode` | Viewport passthrough | +| `BuildFloatingPanel` (new) | ImGui window render | Free-floating panel chrome + content | + +### 2.2 Render Pass Model (new: 3 passes) + +``` +Frame N: +┌──────────────────────────────────────────────────────┐ +│ Pass 1: Docked panels (split-tree order) │ +│ └─ BuildDockedPanel for each non-hidden docked p │ +│ │ +│ Pass 2: Floating panels (sorted by ZOrder asc) │ +│ └─ BuildFloatingPanel for each IsFloating panel │ +│ Highest ZOrder rendered last = visually on top │ +│ │ +│ Pass 3: Popups (menus, combos, tooltips) │ +│ └─ Root-level float boxes added by ZUIBeginPopup │ +│ Must be LAST or floating windows cover menus │ +└──────────────────────────────────────────────────────┘ +``` + +### 2.3 Panel State Machine + +``` + ┌──────────────┐ + │ HIDDEN │◄────────── Close (× button) + └──────────────┘ + │ Restore (Window menu) + ▼ + ┌──────────────┐ Undock gesture + Initial ───────►│ DOCKED │──────────────────►┌─────────────┐ + │ (split tree)│◄──────────────────│ FLOATING │ + └──────────────┘ Drop on dockspace └─────────────┘ + │ + Close (× button) + │ + ▼ + ┌──────────────┐ + │ HIDDEN │ + └──────────────┘ +``` + +--- + +## 3. Data Model Changes + +### 3.1 `ZUIPanel` additions (ZUIPanel.h) + +```cpp +struct ZUIPanel +{ + // ── existing fields (unchanged) ────────────────────────────── + uint64_t DockKey = 0; + ZUIPanelView* Views[kMaxTabsPerPanel] = {}; + uint32_t ViewCount = 0; + uint32_t ActiveTab = 0; + bool Hidden = false; + bool ReorderActive = false; + uint32_t ReorderTabIdx = 0; + float ReorderAccumX = 0.f; + + // ── NEW: floating window state ──────────────────────────────── + bool IsFloating = false; // true = not in split tree + + // Position and size in screen-space logical pixels + float FloatX = 60.f; + float FloatY = 60.f; + float FloatW = 400.f; + float FloatH = 300.f; + + // Z-ordering: higher value = rendered on top; updated on click + uint32_t ZOrder = 0; + + // Title-bar drag (move the floating window) + bool FloatDragging = false; + float FloatDragOffX = 0.f; // offset from FloatX at drag start + float FloatDragOffY = 0.f; + + // Bottom-right resize grip + bool FloatResizing = false; + float FloatResizeOrigW = 0.f; // FloatW at resize start + float FloatResizeOrigH = 0.f; +}; +``` + +### 3.2 `ZUIPanelManager` additions (ZUIPanel.h) + +```cpp +struct ZUIPanelManager +{ + // ... existing fields ... + + // NEW: z-order counter — increment on every click-to-raise + uint32_t NextZOrder = 0; + + // NEW: helper — true if panel p can be undocked without leaving + // the split tree empty (at least one non-floating non-hidden + // non-central panel must remain docked). + bool CanUndock(const ZUIPanel* p) const; +}; +``` + +### 3.3 `ZUIStyle` additions (ZUIContext.h) + +```cpp +// Add to ZUIStyle struct: + +// ── Floating windows ──────────────────────────────────────────── +float FloatWindowMinSize[2] = {120.f, 80.f}; // ImGui: WindowMinSize +float ResizeGripSize = 14.f; // corner grip pixel size +float ResizeGripRounding = 4.f; // corner grip rounding +``` + +Also update the `WindowRounding` default from `0.f` to `4.f` — floating windows +should have rounded corners (like popups) to visually distinguish them from the +flat docked panels. + +--- + +## 4. Three-Pass Render (BuildUI) + +### 4.1 Full BuildUI skeleton after changes + +```cpp +void ZUIPanelManager::BuildUI(ZUIContext* ctx, float menu_h, float status_h) +{ + // ── Deferred closes (before layout) ───────────────────────── + for (uint32_t ci = 0; ci < PendingCloseCount; ++ci) + { + ZUIPanel* cp = FindPanel(PendingCloseKeys[ci]); + if (cp) { cp->Hidden = true; } + if (!cp || !cp->IsFloating) // only collapse if was docked + { + ZUIDockNode* leaf = ZUIDockFindLeaf(DockTree, PendingCloseKeys[ci]); + if (leaf) ZUIDockCollapseLeaf(DockTree, leaf); + } + LayoutDirty = true; + } + PendingCloseCount = 0; + + // ── Layout ─────────────────────────────────────────────────── + if (DockTree) + { + float root_rect[4] = { 0.f, menu_h, sw, sh - status_h }; + ZUIDockLayout(DockTree, root_rect); + SyncSplitDividers(); + } + + ZUIBox* bg = ZUIBeginColumn(ctx, "##pm_bg", ...); // WindowBg + + BuildMenuBar(ctx, sw, menu_h); + + // ── PASS 1: Docked panels ──────────────────────────────────── + Drag.HoverNode = nullptr; + Drag.DropZone = ZUIDropZone::None; + if (DockTree) + { + for (uint32_t i = 0; i < PanelCount; ++i) + { + ZUIPanel* p = &Panels[i]; + if (p->Hidden || p->IsFloating) { continue; } // ← skip floats + float r[4]; + if (!ZUIDockRectForKey(DockTree, p->DockKey, r)) { continue; } + // hover detection for drop zones + BuildDockedPanel(ctx, p, r); + if (ctx->MousePressed[0] && hit_test(ctx, r)) + FocusPanel(i); + } + } + + BuildDividers(ctx); + + // ── Status bar ─────────────────────────────────────────────── + BuildStatusBar(ctx, sw, sh, status_h); + + // ── PASS 2: Floating panels (sorted by ZOrder) ─────────────── + { + ZUIPanel* sorted[kMaxPanels]; uint32_t fc = 0; + for (uint32_t i = 0; i < PanelCount; ++i) + if (!Panels[i].Hidden && Panels[i].IsFloating) + sorted[fc++] = &Panels[i]; + + // Insertion-sort ascending by ZOrder (highest last = on top) + for (uint32_t i = 1; i < fc; ++i) + for (uint32_t j = i; j > 0 && sorted[j-1]->ZOrder > sorted[j]->ZOrder; --j) + { auto* t = sorted[j]; sorted[j] = sorted[j-1]; sorted[j-1] = t; } + + for (uint32_t i = 0; i < fc; ++i) + BuildFloatingPanel(ctx, sorted[i]); + } + + // ── Drag ghost (both docked and floating drags) ────────────── + BuildDragGhost(ctx); + + ZUIEndColumn(ctx); // end bg + + // NOTE: Pass 3 (popups) happens automatically — ZUIBeginPopup + // adds boxes as the last children of ctx->Root, so they are + // rendered after bg and after all floating windows. No code + // change needed here IF floating windows are built inside bg. + // If floating windows escape to ctx->Root level, popup ordering + // must be explicitly managed. See Gap #1. +} +``` + +### 4.2 Popup z-order issue (Gap #1) + +`BuildFloatingPanel` uses `ZUI_FloatX | ZUI_FloatY` boxes. If these are added +as children of `##pm_bg`, they render in bg's subtree. Popups use +`ctx->Current = ctx->Root` → added to Root directly → rendered AFTER bg's +entire subtree → popups are always on top. ✓ + +**Constraint:** floating panel boxes MUST be built inside `##pm_bg` (current +`ZUIBeginColumn`), NOT escaped to `ctx->Root`. Verify this in `BuildFloatingPanel`. + +--- + +## 5. `BuildFloatingPanel` (new function) + +### 5.1 Full implementation spec + +```cpp +void ZUIPanelManager::BuildFloatingPanel(ZUIContext* ctx, ZUIPanel* p) +{ + const float header_h = ZUIGetFrameHeight(ctx); + const float grip = ctx->Style.ResizeGripSize; + const float grip_r = ctx->Style.ResizeGripRounding; + + // ── Clamp to screen ────────────────────────────────────────── + // Allow partially off-screen but keep title bar always on-screen + const float min_visible = header_h + 4.f; + p->FloatW = fmaxf(p->FloatW, ctx->Style.FloatWindowMinSize[0]); + p->FloatH = fmaxf(p->FloatH, ctx->Style.FloatWindowMinSize[1]); + p->FloatX = fmaxf(-(p->FloatW - min_visible), + fminf(p->FloatX, ctx->ScreenW - min_visible)); + p->FloatY = fmaxf(0.f, + fminf(p->FloatY, ctx->ScreenH - min_visible)); + + bool focused = (&Panels[FocusedPanelIdx] == p); + float rect[4] = { p->FloatX, p->FloatY, + p->FloatX + p->FloatW, p->FloatY + p->FloatH }; + + // ── Outer window box ───────────────────────────────────────── + char wk[48]; snprintf(wk, sizeof(wk), "##fw_%llx", (ull)p->DockKey); + ZUIBox* win = ZUIPushBox(ctx, wk, (uint32_t)strlen(wk), + ZUI_DrawBackground | ZUI_DrawBorder | ZUI_DropShadow | + ZUI_FloatX | ZUI_FloatY | ZUI_Clickable); + win->Size[0] = ZPx(p->FloatW); + win->Size[1] = ZPx(p->FloatH); + win->FloatPos[0] = p->FloatX; + win->FloatPos[1] = p->FloatY; + win->LayoutAxis = ZUIAxis::Y; + win->BorderThickness = ctx->Style.WindowBorderSize; + win->EdgeSoftness = 0.5f; + ZUIBoxSetCornerRadius(win, ctx->Style.WindowRounding); + ZUIBoxSetColorArr(win, ctx->Theme.PanelBg); + SetBdrArr(win, focused ? ctx->Theme.PanelFocusBorder + : ctx->Theme.PanelBorder); + + // ── Tab bar or title strip (same as docked) ────────────────── + // NOTE: ZUIDockFindLeaf returns nullptr for floating panels. + // BuildTabBar must handle nullptr gracefully everywhere. + bool show_tabs = p->ViewCount > 1; // floating: always auto-hide for single + if (show_tabs) + { + float tab_rect[4] = { rect[0], rect[1], rect[2], rect[1] + header_h }; + BuildTabBar(ctx, p, tab_rect); // uses rect, not split-tree rect + } + else + { + BuildFloatingTitleStrip(ctx, p, rect, header_h); // dedicated function + } + + // ── Content ────────────────────────────────────────────────── + ZUIPanelView* view = p->ActiveTab < p->ViewCount + ? p->Views[p->ActiveTab] : nullptr; + if (view) + { + ZUIBox* content = ZUIBeginColumn(ctx, "##fwc", ZFill(), ZFill()); + content->Flags = content->Flags | ZUI_ClipChildren; + content->EdgeSoftness = 0.f; + float cr[4] = { + rect[0] + ctx->Style.WindowPadding[0], + rect[1] + header_h + ctx->Style.WindowPadding[1], + rect[2] - ctx->Style.WindowPadding[0], + rect[3] - grip - ctx->Style.WindowPadding[1] + }; + view->BuildContent(ctx, cr); + ZUIEndColumn(ctx); + } + + // ── Resize grip ─────────────────────────────────────────────── + { + char gk[48]; snprintf(gk, sizeof(gk), "##fwg_%llx", (ull)p->DockKey); + ZUIBox* grp = ZUIPushBox(ctx, gk, (uint32_t)strlen(gk), + ZUI_DrawBackground | ZUI_Clickable | ZUI_FloatX | ZUI_FloatY); + grp->Size[0] = ZPx(grip * 2.f); + grp->Size[1] = ZPx(grip * 2.f); + grp->FloatPos[0] = rect[2] - grip * 2.f; + grp->FloatPos[1] = rect[3] - grip * 2.f; + ZUIBoxSetCornerRadius(grp, grip_r); + ZUIBoxSetColor(grp, + ctx->Theme.ScrollbarGrab[0], ctx->Theme.ScrollbarGrab[1], + ctx->Theme.ScrollbarGrab[2], 0.60f); + ZUISignal gs = ZUISignalFromBox(ctx, grp); + ZUIPopBox(ctx); + + if (gs.Flags & ZUI_SignalPressed) + { + p->FloatResizing = true; + p->FloatResizeOrigW = p->FloatW; + p->FloatResizeOrigH = p->FloatH; + } + if (ctx->MouseReleased[0]) p->FloatResizing = false; + if (p->FloatResizing && (gs.Flags & ZUI_SignalHeld)) + { + p->FloatW = fmaxf(ctx->Style.FloatWindowMinSize[0], + p->FloatResizeOrigW + gs.DragDelta[0]); + p->FloatH = fmaxf(ctx->Style.FloatWindowMinSize[1], + p->FloatResizeOrigH + gs.DragDelta[1]); + } + } + + // ── Window-level signal (click to raise) ───────────────────── + ZUISignal win_sig = ZUISignalFromBox(ctx, win); + ZUIPopBox(ctx); + + if (win_sig.Flags & ZUI_SignalClicked) + { + p->ZOrder = ++NextZOrder; + for (uint32_t pi = 0; pi < PanelCount; ++pi) + if (&Panels[pi] == p) { FocusPanel(pi); break; } + } + + // ── Drop zones when this floating panel is being dragged ────── + if (p->FloatDragging && Drag.HoverNode) + { + ZUIPanel* dst = FindPanel(Drag.HoverNode->ContentKey); + if (dst && dst != p) + { + float dr[4]; + if (ZUIDockRectForKey(DockTree, dst->DockKey, dr)) + BuildDropZones(ctx, dst, dr); + } + } +} +``` + +### 5.2 `BuildFloatingTitleStrip` (new helper) + +A single-view floating panel shows a title strip with: +- Drag handle (moves window when held) +- Icon dot +- Title text +- Close button (always visible, not hover-only like docked) + +```cpp +void ZUIPanelManager::BuildFloatingTitleStrip(ZUIContext* ctx, + ZUIPanel* p, float rect[4], float header_h) +{ + // ... similar to existing single-view strip but: + // - drag updates FloatX/Y (not Drag.Active) + // - close button always visible (not hover-only) + // - NO drag-to-dock trigger (floating move is different from docked drag) +} +``` + +--- + +## 6. Undock Gesture (docked → floating) + +### 6.1 Gesture state machine in `BuildTabBar` + +``` +Tab held + mouse moved: +│ +├─ |total_drag| < DockingTabReorderThreshold +│ → nothing (wait) +│ +├─ dx > dy * 1.5 AND |total_drag| > DockingTabReorderThreshold +│ → tab REORDER within bar (existing behavior, unchanged) +│ +├─ dy > DockingUndockVertical (e.g. 24px downward) +│ → UNDOCK: create floating panel +│ +└─ |total_drag| > DockingDragThreshold AND !mostly_horizontal + → whole-panel drag / dock-tree drag (existing Drag.Active path) +``` + +Note: `DockingUndockVertical` is increased from `12.f` to `24.f` to prevent +accidental undocks during diagonal reorder drags. + +### 6.2 Undock implementation + +```cpp +// In BuildTabBar, inside tab drag handling: +if (dy > ctx->Style.DockingUndockVertical && !Drag.Active && CanUndock(p)) +{ + ZUIPanelView* view = p->Views[ti]; + + // Determine the panel that will float: + // If this is the only tab, float the existing panel (retain DockKey). + // If this is one of multiple tabs, create a new floating panel. + ZUIPanel* float_p = nullptr; + if (p->ViewCount == 1) + { + // Float the panel itself — retain DockKey, leaf will be collapsed + p->IsFloating = true; + p->FloatX = ctx->MousePos[0] - ZUIGetFramePadX(ctx) * 2.f; + p->FloatY = ctx->MousePos[1] - ZUIGetFrameHeight(ctx) * 0.5f; + p->FloatW = fmaxf(ctx->Style.FloatWindowMinSize[0], 320.f); + p->FloatH = fmaxf(ctx->Style.FloatWindowMinSize[1], 240.f); + p->ZOrder = ++NextZOrder; + p->FloatDragging = true; + p->FloatDragOffX = ctx->MousePos[0] - p->FloatX; + p->FloatDragOffY = ctx->MousePos[1] - p->FloatY; + float_p = p; + // Collapse the leaf (panel leaves the split tree) + if (PendingCloseCount < kMaxPanels) + PendingCloseKeys[PendingCloseCount++] = p->DockKey; + } + else + { + // Create a new floating panel with a derived key + // Use original DockKey XOR'd with view index for uniqueness + uint64_t new_key = p->DockKey ^ ZUIDockHashName(view->Title ? view->Title : "panel"); + ZUIPanel* new_p = AddPanel(new_key); + if (new_p) + { + AddView(new_p, view); + // Remove view from source + for (uint32_t j = ti; j+1 < p->ViewCount; ++j) p->Views[j] = p->Views[j+1]; + --p->ViewCount; + if (p->ActiveTab >= p->ViewCount && p->ViewCount > 0) + p->ActiveTab = p->ViewCount - 1; + + new_p->IsFloating = true; + new_p->FloatX = ctx->MousePos[0] - ZUIGetFramePadX(ctx) * 2.f; + new_p->FloatY = ctx->MousePos[1] - ZUIGetFrameHeight(ctx) * 0.5f; + new_p->FloatW = 320.f; + new_p->FloatH = 240.f; + new_p->ZOrder = ++NextZOrder; + new_p->FloatDragging = true; + new_p->FloatDragOffX = ctx->MousePos[0] - new_p->FloatX; + new_p->FloatDragOffY = ctx->MousePos[1] - new_p->FloatY; + float_p = new_p; + } + } + LayoutDirty = true; + break; // exit tab loop +} +``` + +### 6.3 `CanUndock()` implementation + +```cpp +bool ZUIPanelManager::CanUndock(const ZUIPanel* p) const +{ + // Must leave at least one docked, non-hidden, non-central panel + uint32_t remaining_docked = 0; + for (uint32_t i = 0; i < PanelCount; ++i) + { + const ZUIPanel* q = &Panels[i]; + if (q == p || q->Hidden || q->IsFloating) continue; + ZUIDockNode* leaf = ZUIDockFindLeaf(DockTree, q->DockKey); + if (leaf && !leaf->IsCentral) remaining_docked++; + } + return remaining_docked >= 1; +} +``` + +--- + +## 7. Floating Window Move (title-bar drag) + +### 7.1 Move via title strip drag + +In `BuildFloatingTitleStrip`: + +```cpp +ZUISignal strip_sig = ZUISignalFromBox(ctx, strip); +ZUIEndRow(ctx); + +if (strip_sig.Flags & ZUI_SignalPressed) +{ + p->FloatDragging = true; + p->FloatDragOffX = ctx->MousePos[0] - p->FloatX; + p->FloatDragOffY = ctx->MousePos[1] - p->FloatY; + p->ZOrder = ++NextZOrder; +} + +if (ctx->MouseReleased[0]) p->FloatDragging = false; + +if (p->FloatDragging && ctx->MouseDown[0]) +{ + p->FloatX = ctx->MousePos[0] - p->FloatDragOffX; + p->FloatY = ctx->MousePos[1] - p->FloatDragOffY; + + // Check if hovering over a docked panel → enable drop zones + for (uint32_t i = 0; i < PanelCount; ++i) + { + ZUIPanel* q = &Panels[i]; + if (q->Hidden || q->IsFloating) continue; + float r[4]; + if (!ZUIDockRectForKey(DockTree, q->DockKey, r)) continue; + if (ctx->MousePos[0] >= r[0] && ctx->MousePos[0] <= r[2] && + ctx->MousePos[1] >= r[1] && ctx->MousePos[1] <= r[3]) + { + // Set Drag state so BuildDropZones fires in BuildFloatingPanel + Drag.HoverNode = ZUIDockFindLeaf(DockTree, q->DockKey); + Drag.SrcPanel = p; + Drag.SrcTabIdx = kWholePanel; + Drag.GhostX = ctx->MousePos[0]; + Drag.GhostY = ctx->MousePos[1]; + // Note: Drag.Active intentionally NOT set here — + // floating move and docked-tab drag must not conflict + break; + } + } + if (!ctx->MouseDown[0]) Drag.HoverNode = nullptr; +} +``` + +--- + +## 8. Redock Gesture (floating → docked) + +### 8.1 Trigger in BuildFloatingPanel + +Drop zones are shown when `p->FloatDragging && Drag.HoverNode`. On +`ctx->MouseReleased[0]` with a valid `Drag.DropZone`: + +```cpp +// At bottom of BuildFloatingPanel: +if (ctx->MouseReleased[0] && p->FloatDragging && Drag.HoverNode + && Drag.DropZone != ZUIDropZone::None) +{ + CommitDrop(p, kWholePanel, Drag.HoverNode, Drag.DropZone); + p->FloatDragging = false; + Drag.HoverNode = nullptr; + Drag.DropZone = ZUIDropZone::None; +} +``` + +### 8.2 `CommitDrop` changes for floating source + +```cpp +// In CommitDrop, at the top — handle floating source: +if (src->IsFloating) +{ + // Floating panel re-enters the split tree + src->IsFloating = false; + // No ZUIDockCollapseLeaf needed — wasn't in tree + // Insert into tree at dst via existing split logic + // ... (existing Left/Right/Top/Bottom/Center code) ... + LayoutDirty = true; + return; +} +// ... existing docked-source logic follows ... +``` + +### 8.3 Docked tab dropped onto floating panel (Gap #13 fix) + +```cpp +// In CommitDrop, when dst node belongs to a floating panel: +ZUIPanel* dst_panel = FindPanel(dst->ContentKey); +if (dst_panel && dst_panel->IsFloating && zone == ZUIDropZone::Center) +{ + // Merge source views into floating panel's tab list + for (uint32_t i = 0; i < src->ViewCount; ++i) + AddView(dst_panel, src->Views[i]); + src->ViewCount = 0; + if (PendingCloseCount < kMaxPanels) + PendingCloseKeys[PendingCloseCount++] = src->DockKey; + dst_panel->ZOrder = ++NextZOrder; + return; +} +// Edge drops onto floating panel: not supported in v1 — ignore +if (dst_panel && dst_panel->IsFloating) + return; +``` + +--- + +## 9. Central Node (Viewport passthrough) + +Re-enable central node for Viewport in `ZUIPanelManagerComponent.h`: + +```cpp +// After all panels registered and layout loaded: +ZUIDockMarkCentral(Manager.DockTree, ZUIDockHashName("Viewport")); +``` + +The Viewport panel is docked (not floating) and marked central. Its +`BuildDockedPanel` path skips chrome and passes the full rect to +`view->BuildContent`. Mouse events in the central area that are not +captured by any floating window reach the 3D scene (`ctx->ViewportHovered`). + +--- + +## 10. `ZUIDockSerial` v4 + +### 10.1 Save format additions + +``` +# ZUI Layout v4 +node ... (unchanged from v3 — only docked panels have nodes) +panel <key_hex> <active_tab> <hidden> <view_count> +view <title> +floating <is_floating> <float_x> <float_y> <float_w> <float_h> <zorder> +``` + +`floating` line is only written when `p->IsFloating == true`. Floating panels +have NO corresponding `node` line. + +### 10.2 Load changes + +```cpp +// When parsing a panel record: +if (is_floating) +{ + p->IsFloating = true; + p->FloatX = float_x; p->FloatY = float_y; + p->FloatW = float_w; p->FloatH = float_h; + p->ZOrder = zorder; + // Do NOT try to find a leaf or collapse anything +} +else +{ + // Docked: find leaf in rebuilt tree, collapse if hidden (existing logic) +} +``` + +--- + +## 11. Known Gaps + +Each gap has a severity, phase, and resolution strategy. + +| # | Gap | Severity | Phase | Resolution | +|---|---|---|---|---| +| 1 | Popup z-ordering: floating windows could cover popups | **Critical** | Phase 2 | Build floating panels inside `##pm_bg`, not escaped to Root. Popups use Root → always rendered last. Verify and add a note/assert. | +| 2 | `ZUIDockFindLeaf` nullptr in `BuildTabBar` | **Critical** | Phase 3 | Audit all 5+ `ZUIDockFindLeaf` calls in `BuildTabBar`/`BuildDockedPanel`. Add `if (!leaf) { /* fallback */ }` at each site. | +| 3 | `PendingCloseKeys` calls `ZUIDockCollapseLeaf` for floating panels | **Critical** | Phase 1 | Add `if (!cp || !cp->IsFloating)` guard before `ZUIDockCollapseLeaf` in the flush loop. | +| 4 | `Drag.Active` not set during floating window move | **Critical** | Phase 4 | Explicitly set `Drag.HoverNode` (but NOT `Drag.Active`) when floating panel hovers over docked panel. `BuildDropZones` check updated to also fire when `p->FloatDragging && Drag.HoverNode`. | +| 5 | Stable `DockKey` for new floating panels | **Critical** | Phase 5 | Single-tab undock: retain original `DockKey`. Multi-tab undock: XOR original key with `ZUIDockHashName(view->Title)`. | +| 6 | `rect[4]` for floating `BuildTabBar` | **Critical** | Phase 3 | `BuildFloatingPanel` passes `{ FloatX, FloatY, FloatX+FloatW, FloatY+header_h }` explicitly. Document this clearly in the function contract. | +| 7 | Gesture disambiguation: reorder vs undock | Important | Phase 5 | Full state machine documented in §6.1. `DockingUndockVertical` raised to 24px. | +| 8 | 1-frame undock visual flash | Important | Phase 5 | Use immediate float creation (not deferred). Floating panel appears frame N; source collapse is deferred to frame N+1. Net: 1-frame overlap acceptable at 60fps. | +| 9 | `CanUndock()` guard implementation | Important | Phase 5 | Implementation in §6.3. Count non-floating non-hidden non-central docked panels ≥ 1. | +| 10 | `FloatWindowMinSize` / `ResizeGripSize` in `ZUIStyle` | Important | Phase 1 | Added to §3.3. | +| 11 | Serial v4: floating panels have no node entry | Important | Phase 8 | Loader handles `floating` line before `view` lines. Skip tree insertion for floating panels. | +| 12 | Window menu does not list floating panels | Important | Phase 9 | Show `"Panel (floating)##wm_i"` entries; add "Dock all" menu item. | +| 13 | Docked tab dropped onto floating panel | Important | Phase 6 | Handled in `CommitDrop` — Center drop merges into float; Edge drops ignored in v1. | +| 14 | `WindowRounding = 0` makes floating windows look like docked panels | Style | Phase 1 | Change `ZUIStyle.WindowRounding` default to `4.f`. | +| 15 | Off-screen recovery: floating panel fully off-screen | Style | Phase 9 | Clamp in `BuildFloatingPanel`: allow partial off-screen but title bar always accessible. | + +--- + +## 12. Verification Checklist + +### Floating window basics +- [ ] Hierarchy, Inspector, Output can each be undocked to float independently +- [ ] Floating panel can be dragged to any on-screen position via title bar +- [ ] Resize via bottom-right grip: width and height change, minimum enforced +- [ ] Click on floating panel raises it to the top (z-order) +- [ ] Two overlapping floating panels: click correct one raises it +- [ ] Floating panel partially dragged off-screen: title bar still accessible +- [ ] Floating panel dragged to bottom of screen: clamp fires correctly + +### Dock / undock gestures +- [ ] Horizontal tab drag → tab reorder (existing, not broken) +- [ ] Downward tab drag > 24px → undock, panel floats at cursor +- [ ] Single-tab panel undocked: panel retains original DockKey +- [ ] Multi-tab panel: one tab undocked creates new floating panel +- [ ] Source panel collapses correctly after undock (sibling fills space) +- [ ] Last docked panel cannot be undocked (guard fires, drag ignored) +- [ ] Floating panel dragged over docked panel → teal drop zones appear +- [ ] Drop on Left/Right/Top/Bottom/Center → redocks with correct split +- [ ] Docked tab dragged onto floating panel (Center) → merges into float + +### Central node + viewport +- [ ] Viewport panel in central node: no chrome visible +- [ ] Mouse over viewport with no floating windows: camera controller active +- [ ] Mouse over floating window covering viewport: camera inactive (float captures) +- [ ] Float dragged off viewport: camera re-activates + +### Popup z-ordering (critical) +- [ ] Open combo inside floating panel → dropdown appears on top of all windows +- [ ] Open menu bar → menu appears on top of all floating panels +- [ ] Click floating panel while menu open → menu closes, panel raised + +### Persistence (v4 serial) +- [ ] Layout saved with floating state in `# ZUI Layout v4` format +- [ ] Relaunch: floating panels restore at correct positions and sizes +- [ ] Relaunch: floating z-order higher than docked panels +- [ ] Relaunch: mix of docked and floating panels restores correctly +- [ ] Empty dockspace (all floating): `ZUIDockLayout` no crash, layout recovers + +### Edge cases +- [ ] Close a floating panel: disappears (no tree collapse needed) +- [ ] Reopen hidden floating panel from Window menu: appears floating +- [ ] "Dock all" menu item: all floating panels re-inserted into split tree +- [ ] All panels floating: Window menu "Dock all" restores to default layout +- [ ] `FloatW/H` minimum enforced after resize attempts to go below minimum + +--- + +## 13. Sequencing + Time Estimate + +| Step | File(s) | Est. time | +|---|---|---| +| 1. `ZUIStyle` + `ZUIPanel` floating fields | `ZUIContext.h`, `ZUIPanel.h` | 2 h | +| 2. Fix `PendingCloseKeys` + `CanUndock()` | `ZUIPanel.cpp` | 1 h | +| 3. Audit + fix nullptr for floating in `BuildTabBar` | `ZUIPanel.cpp` | 2 h | +| 4. `BuildFloatingPanel` (chrome, tabs, resize) | `ZUIPanel.cpp` | 6 h | +| 5. Three-pass render in `BuildUI` | `ZUIPanel.cpp` | 2 h | +| 6. Floating title strip drag (move window) | `ZUIPanel.cpp` | 2 h | +| 7. Undock gesture + `CanUndock` | `ZUIPanel.cpp` | 4 h | +| 8. `CommitDrop` for floating sources + float→float merge | `ZUIPanel.cpp` | 3 h | +| 9. `ZUIDockSerial` v4 | `ZUIDockSerial.cpp` | 3 h | +| 10. Central node reinstatement | `ZUIPanelManagerComponent.h` | 0.5 h | +| 11. Window menu updates + "Dock all" | `ZUIPanel.cpp` | 2 h | +| 12. Edge cases + clamping + off-screen | `ZUIPanel.cpp` | 2 h | +| 13. Full verification pass | — | 4 h | +| **Total** | | **~33 h (~7 days)** | + +--- + +## 14. Deferred (post-v1) + +These are intentionally out of scope for the first implementation: + +- **Remember original split percentage** before undock — ImGui does this with `SavedDocksizeVec2`. Adds ~1 day complexity. +- **Ctrl+drag to force-float** — keyboard modifier to undock via drag even from within tab bar. +- **Double-click title bar** to toggle float/dock instantly. +- **Snap to screen edges** and "magnetism" near screen corners. +- **Multi-monitor / OS-level windows** — requires per-window Vulkan swapchain. Major effort (2–3 weeks). +- **Floating window animations** — fade-in on undock, spring physics on resize. diff --git a/ZEngine/docs/future-plan/ui-system.md b/ZEngine/docs/future-plan/ui-system.md index bc06daee1..08dfb9f7f 100644 --- a/ZEngine/docs/future-plan/ui-system.md +++ b/ZEngine/docs/future-plan/ui-system.md @@ -1,1278 +1,162 @@ -# ZEngine — UI System (RAD Debugger-Inspired) +# ZEngine — UI System (ZUI) -**Priority:** P2 — Required for DebugOverlay, DebugConsole, in-game HUD, menus, and eventual editor migration -**Status:** Design -**Depends on:** `ArenaAllocator` (done), `Array<T>` (done), `UnorderedHashMap` (done), `VulkanDevice` (done), `RenderGraph` (done), stb (vendored), rapidhash (vendored) -**Blocks:** `profiling.md` (DebugOverlay, DebugConsole), in-game HUD, main menu, settings screen +**Priority:** P1 — Editor shell, in-game HUD, menus, debug overlay +**Status:** Implementation in progress on `feature/zui` +**Depends on:** `ArenaAllocator` (done), `Array<T>` (done), `UnorderedHashMap` (done), `VulkanDevice` (done), `RenderGraph` (done), `InputManager` (done), stb_truetype (vendored via stb FetchContent), rapidhash (vendored via FetchContent) +**Blocks:** DebugOverlay, DebugConsole, in-game HUD, main menu, settings screen --- -## 1. Architecture Overview +## Architecture Overview -Every widget — button, label, panel, scroll area, slider — is the **same struct: `Box`**. -No widget class hierarchy. No virtual dispatch. No manual rect arithmetic. -The system is a direct adaptation of the RAD Debugger UI architecture (Ryan Fleury / Epic Games) -translated onto ZEngine's existing primitives (`ArenaAllocator`, `Array<T>`, `UnorderedHashMap`, -rapidhash, stb_truetype). +Every widget is the same struct: `ZUIBox`. No widget class hierarchy. No virtual dispatch. +The system follows the RAD Debugger UI architecture (Ryan Fleury) adapted to ZEngine primitives. -**Why this over the previous typed-union approach:** +Key design choices made during implementation that diverged from the original plan: -| Concern | Typed-union (previous) | Box (this doc) | -|---|---|---| -| Layout | Manual `UIRect` per call site | Constraint solver (5 `SizeKind`s) | -| Widget identity | None — stateless | Key hash — persistent across frames | -| Hover/press animation | None | Implicit via `HotTransition` / `ActiveTransition` | -| Adding a new widget type | New struct + union slot + renderer branch | Compose existing `BoxFlags` | -| Style management | Args per call | Push/pop stacks on `UIContext` | -| Scroll areas | Manual offset math | `BoxFlag_Scrollable` + scroll state in anim map | - -**Critical path — must be implemented in order:** - -``` -Step 1 → StringHash ~0.5 days box identity -Step 2 → InputFrame ~1.5 days per-frame input cache (replaces window-arg queries) -Step 3 → UIInput ~1 day hit-test + hot/active routing into UIContext -Step 4 → BitmapFontAtlas ~6 days THE blocker — no text = no widgets -Step 5 → Box + UIContext ~5 days core API, style stacks, persistent anim map -Step 6 → Layout pass ~4 days 5-phase constraint solver -Step 7 → UIRenderer ~4 days RenderGraph node, quad batcher, ui.vert/frag -Step 8 → Widgets layer ~3 days named helpers (Button, Slider, Panel, etc.) -────────────────────────────────────────── -Total ~4–5 weeks -``` - -ImGui and ImGuizmo stay alive throughout. This system is additive. `SceneViewportUIComponent` -keeps ImGui + ImGuizmo permanently (gizmos depend on ImGui draw lists). Other Tetragrama panels -migrate one-by-one after the Widgets layer is stable. First milestone: DebugOverlay and -DebugConsole running via UIContext. - ---- - -## 2. Coordinate System - -All UI coordinates are **screen-space pixels**, top-left origin, Y increasing downward. -This matches GLFW cursor coordinates directly; no conversion needed in `InputFrame`. - -``` -(0,0) ──────────────────► X - │ - │ pos = {10, 10}, size = {200, 40} - │ - ▼ Y -``` - -The `UIRenderer` orthographic projection maps `[0, W] × [0, H]` to NDC: - -``` -P = ortho(0, W, H, 0, -1, 1) -``` - -Pushed as a 64-byte push constant each frame. - ---- - -## 3. Step 1 — StringHash - -### Why - -`Box` is identified across frames by a `uint64_t` key hashed from its tag string. Without -a stable hash, hover/active state and animation cannot persist from frame N to frame N+1. - -### Files - -``` -ZEngine/ZEngine/Core/Containers/StringHash.h (header-only) -``` - -### Implementation - -rapidhash is already vendored at `__externals/rapidhash/src/rapidhash.h`. - -```cpp -// ZEngine/ZEngine/Core/Containers/StringHash.h -#pragma once -#include <cstdint> -#include <cstring> -#include <rapidhash/src/rapidhash.h> - -namespace ZEngine::Core::Containers -{ - // 64-bit hash of a null-terminated string. Deterministic within a process. - inline uint64_t StringHash(const char* str) noexcept - { - if (!str) return 0; - return rapidhash(str, strlen(str)); - } - - // Hash a string + integer suffix — unique keys for list items. - // StringHashN("item", 3) produces a stable key for the 3rd "item". - inline uint64_t StringHashN(const char* str, uint32_t index) noexcept - { - return StringHash(str) ^ (uint64_t(index) * 0x9E3779B97F4A7C15ULL); - } -} -``` - -### Deliverable - -- [ ] `Core/Containers/StringHash.h` — `StringHash(const char*)` + `StringHashN` - ---- - -## 4. Step 2 — InputFrame - -### Why - -The current input layer (`Keyboard`, `Mouse`) requires `CoreWindow*` at every query site and -has no per-frame delta accumulation, no edge-detect (just-pressed / just-released), and no -retained scroll state. `UIContext` needs all of these as zero-argument queries. - -`InputFrame` implements `IMouseEventCallback`, `IKeyboardEventCallback`, and -`ITextInputEventCallback` and self-registers alongside the window's existing listeners. It is -the single source of per-frame input truth for the UI system and for game input consumers. - -### Files - -``` -ZEngine/ZEngine/Windows/Inputs/InputFrame.h -ZEngine/ZEngine/Windows/Inputs/InputFrame.cpp -``` - -### Header - -```cpp -// ZEngine/ZEngine/Windows/Inputs/InputFrame.h -#pragma once -#include <ZEngine/Windows/Inputs/IInputEventCallback.h> -#include <ZEngine/Windows/Inputs/KeyCode.h> -#include <ZEngine/Core/Maths/Vec.h> - -namespace ZEngine::Windows::Inputs -{ - // Per-frame snapshot of all input state. - // Call BeginFrame() at the top of each main-loop tick before event polling. - // Events are fed automatically via the IXxxEventCallback interfaces. - struct InputFrame - : public IMouseEventCallback - , public IKeyboardEventCallback - , public ITextInputEventCallback - { - // ── Frame lifecycle ────────────────────────────────────────────────── - // Rotate cur→prev, zero scroll delta and text buffer. - // Called at the top of Engine::MainThreadRun, before PollEvents(). - void BeginFrame(); - - // ── Mouse position ─────────────────────────────────────────────────── - Core::Maths::Vec2f MousePos() const noexcept { return m_mouse_pos; } - Core::Maths::Vec2f MouseDelta() const noexcept { return m_mouse_delta; } - - // ── Mouse buttons ──────────────────────────────────────────────────── - bool IsMouseDown(ZENGINE_KEYCODE btn) const noexcept; - bool IsMouseJustPressed(ZENGINE_KEYCODE btn) const noexcept; - bool IsMouseJustReleased(ZENGINE_KEYCODE btn) const noexcept; - - // ── Scroll ──────────────────────────────────────────────────────────── - // Accumulated across all wheel events in this frame. Cleared by BeginFrame. - Core::Maths::Vec2f ScrollDelta() const noexcept { return m_scroll_delta; } - - // ── Keyboard ───────────────────────────────────────────────────────── - bool IsKeyDown(ZENGINE_KEYCODE key) const noexcept; - bool IsKeyJustPressed(ZENGINE_KEYCODE key) const noexcept; - bool IsKeyJustReleased(ZENGINE_KEYCODE key) const noexcept; - - // ── Text input ──────────────────────────────────────────────────────── - // UTF-8 codepoints typed this frame. Cleared by BeginFrame. - const char* TextInput() const noexcept { return m_text_buf; } - uint32_t TextInputLen() const noexcept { return m_text_len; } - - // ── IMouseEventCallback ─────────────────────────────────────────────── - bool OnMouseButtonPressed(Events::MouseButtonPressedEvent&) override; - bool OnMouseButtonReleased(Events::MouseButtonReleasedEvent&) override; - bool OnMouseButtonMoved(Events::MouseButtonMovedEvent&) override; - bool OnMouseButtonWheelMoved(Events::MouseButtonWheelEvent&) override; - - // ── IKeyboardEventCallback ──────────────────────────────────────────── - bool OnKeyPressed(Events::KeyPressedEvent&) override; - bool OnKeyReleased(Events::KeyReleasedEvent&) override; - - // ── ITextInputEventCallback ─────────────────────────────────────────── - bool OnTextInputRaised(Events::TextInputEvent&) override; - - // Process-global singleton. Initialised in Engine::Initialize(). - static InputFrame& Get(); - - private: - static constexpr uint32_t k_MaxKeys = 512; - static constexpr uint32_t k_MaxButtons = 8; - static constexpr uint32_t k_TextBufLen = 64; - - Core::Maths::Vec2f m_mouse_pos = {}; - Core::Maths::Vec2f m_mouse_prev_pos = {}; - Core::Maths::Vec2f m_mouse_delta = {}; - Core::Maths::Vec2f m_scroll_delta = {}; - - bool m_keys_cur[k_MaxKeys] = {}; - bool m_keys_prev[k_MaxKeys] = {}; - bool m_buttons_cur[k_MaxButtons] = {}; - bool m_buttons_prev[k_MaxButtons] = {}; - - char m_text_buf[k_TextBufLen] = {}; - uint32_t m_text_len = 0; - }; -} // namespace ZEngine::Windows::Inputs -``` - -### Implementation notes - -- `BeginFrame()`: `memcpy(m_keys_prev, m_keys_cur, sizeof m_keys_cur)`, - `memcpy(m_buttons_prev, m_buttons_cur, sizeof m_buttons_cur)`, - `m_mouse_prev_pos = m_mouse_pos`, `m_mouse_delta = m_mouse_pos - m_mouse_prev_pos`, - zero `m_scroll_delta`, zero `m_text_buf`, `m_text_len = 0`. -- `IsKeyJustPressed(k)` = `m_keys_cur[k] && !m_keys_prev[k]` -- `IsKeyJustReleased(k)` = `!m_keys_cur[k] && m_keys_prev[k]` -- `OnMouseButtonWheelMoved` accumulates into `m_scroll_delta` (trackpads send multiple - events per frame). -- `OnTextInputRaised` appends the UTF-8 string into `m_text_buf` up to `k_TextBufLen - 1`. - -### Registration - -`CoreWindow` registers `InputFrame::Get()` alongside its existing listeners: - -```cpp -// CoreWindow (after existing callback registration): -RegisterInputCallback(&InputFrame::Get()); -``` - -`InputFrame::BeginFrame()` is called at the top of `Engine::MainThreadRun` before -`PollEvents()`. - -### Input consumption contract - -When `UIContext` captures the mouse (a `Clickable` box is hot or active), it sets a -`m_input_consumed` flag. Game systems check `UIContext::IsInputConsumed()` before reading -mouse state — prevents clicks "passing through" UI panels into the 3D scene. - -### Deliverables - -- [ ] `Windows/Inputs/InputFrame.h/.cpp` — `BeginFrame`, all query methods, all callback implementations -- [ ] `CoreWindow` registers `InputFrame::Get()` as a listener -- [ ] `Engine::MainThreadRun` calls `InputFrame::Get().BeginFrame()` before `PollEvents()` - ---- - -## 5. Step 3 — UIInput - -### Why - -Translates `InputFrame` state into `Box` hot (hovered) and active (held) state on the -laid-out box tree. Owns hit-testing. Called once per frame in `UIContext::EndFrame()`. - -### Files - -``` -ZEngine/ZEngine/UI/UIInput.h -ZEngine/ZEngine/UI/UIInput.cpp -``` - -### Header - -```cpp -// ZEngine/ZEngine/UI/UIInput.h -#pragma once -#include <ZEngine/Core/Maths/Vec.h> - -namespace ZEngine::UI -{ - struct Box; - - // Depth-first search returning the deepest Clickable box whose - // ComputedAbsPos / ComputedSize rect contains `point`. - // Children are tested in reverse order (last child is visually on top). - Box* HitTest(Box* root, Core::Maths::Vec2f point); - - // UpdateInteraction — call once per frame after the layout pass, before render. - // Reads InputFrame::Get() and updates Hot/Active/Focused on boxes. - // hot_box: deepest box under the cursor (updated every frame). - // active_box: box that owns mouse capture (set on press, cleared on release). - // focused_box: box with keyboard focus (Tab cycles through Focusable boxes). - // input_consumed_out: set true if any Clickable box is hot or active. - void UpdateInteraction(Box* root, - Box** hot_box, - Box** active_box, - Box** focused_box, - bool* input_consumed_out); -} -``` - -### Interaction rules - -``` -new_hot = HitTest(root, InputFrame::Get().MousePos()) - -if new_hot != hot_box: - old hot_box → Hot = false - new_hot → Hot = true - hot_box = new_hot - -if IsMouseJustPressed(LeftButton): - active_box = hot_box - if active_box: active_box->Active = true - -if IsMouseJustReleased(LeftButton): - if active_box: active_box->Active = false - active_box = nullptr - -if IsKeyJustPressed(Tab): - AdvanceFocus(root, focused_box) - -*input_consumed_out = (hot_box != nullptr || active_box != nullptr) -``` - -`UIContext::Clicked(box)` = `box == active_box && IsMouseJustReleased(LeftButton)` - -### Deliverables - -- [ ] `UI/UIInput.h/.cpp` — `HitTest`, `UpdateInteraction`, `AdvanceFocus` - ---- - -## 6. Step 4 — BitmapFontAtlas - -### Why - -This is the critical-path item. No text means no widget captions, no debug values, no console -output. The entire UI is blocked here. stb_truetype is already vendored in `__externals/stb`. - -### Approach: bitmap atlas (not MSDF) - -MSDF gives better quality at arbitrary sizes but requires a distance-field generation step and -a custom shader. A **bitmap atlas** (stb_truetype `stbtt_BakeFontBitmap`) is sufficient for -debug/overlay/console use at fixed sizes and is buildable in 5–6 days. MSDF can replace it -later without changing the calling API — the `GlyphInfo` struct and `BitmapFontAtlas` interface -are stable. - -### Files - -``` -ZEngine/ZEngine/UI/FontAtlas.h -ZEngine/ZEngine/UI/FontAtlas.cpp -Resources/Engine/Fonts/Inter-Regular.ttf (OFL licensed, redistributable) -``` - -### Header - -```cpp -// ZEngine/ZEngine/UI/FontAtlas.h -#pragma once -#include <ZEngine/Core/Memory/Allocator.h> -#include <ZEngine/Core/Maths/Vec.h> -#include <ZEngine/Hardwares/VulkanDevice.h> -#include <cstdint> - -namespace ZEngine::UI -{ - // UV region and metrics for one glyph in the atlas texture. - struct GlyphInfo - { - Core::Maths::Vec2f UV0; // top-left UV (0..1) - Core::Maths::Vec2f UV1; // bottom-right UV (0..1) - Core::Maths::Vec2f Size; // pixel size of the glyph rect - Core::Maths::Vec2f Offset; // left-bearing + ascent offset (pixels) - float Advance; // horizontal advance (pixels) - }; - - using FontHandle = uint32_t; - static constexpr FontHandle k_InvalidFont = UINT32_MAX; - - // Single-channel R8 GPU texture atlas for one typeface at one size. - // Baked at startup from a .ttf file. Covers printable ASCII (codepoints 32–126). - struct BitmapFontAtlas - { - // Load .ttf, rasterize glyphs, upload VK_FORMAT_R8_UNORM texture. - // ttf_path — VFS path to the .ttf file - // font_size — pixel height (e.g. 16.f) - // atlas_dim — texture dimension (power-of-2; 512 recommended for 16px) - void Initialize(Core::Memory::ArenaAllocator* arena, - Hardwares::VulkanDevice* device, - const char* ttf_path, - float font_size, - uint32_t atlas_dim = 512); - - void Destroy(Hardwares::VulkanDevice* device); - - // Query glyph info for a UTF-32 codepoint. - // Writes a '?' fallback rect for unknown codepoints. - bool GetGlyph(uint32_t codepoint, GlyphInfo* out) const noexcept; - - // Measure the pixel width of a null-terminated UTF-8 string. - float MeasureText(const char* text) const noexcept; - - // Height of a line of text (ascent + descent + line gap). - float LineHeight() const noexcept { return m_line_height; } - - // VkImageView handle — passed to UIRenderer for descriptor binding. - VkImageView AtlasView() const noexcept { return m_atlas_view; } - - float FontSize() const noexcept { return m_font_size; } - - private: - static constexpr uint32_t k_FirstChar = 32; - static constexpr uint32_t k_CharCount = 95; // codepoints 32–126 - - // One entry per codepoint (stbtt_bakedchar layout). - struct BakedChar { uint16_t x0,y0,x1,y1; float xoff,yoff,xadvance; }; - - BakedChar* m_glyphs = nullptr; // arena-allocated - VkImage m_atlas_img = VK_NULL_HANDLE; - VkImageView m_atlas_view = VK_NULL_HANDLE; - VkDeviceMemory m_atlas_mem = VK_NULL_HANDLE; - float m_font_size = 0.f; - float m_line_height = 0.f; - uint32_t m_atlas_dim = 0; - }; - - // Registry: up to 8 named fonts loaded at startup. - struct FontRegistry - { - static void Register(const char* name, BitmapFontAtlas* atlas); - static BitmapFontAtlas* Get(FontHandle handle) noexcept; - static FontHandle Find(const char* name) noexcept; - static FontHandle Default() noexcept; // handle 0 = first registered - }; -} -``` - -### Implementation steps - -1. Read .ttf bytes into a temp arena scratch buffer via `VFSContext::Open(ttf_path)`. -2. Call `stbtt_BakeFontBitmap(ttf_data, 0, font_size, bitmap, atlas_dim, atlas_dim, k_FirstChar, k_CharCount, baked_chars)`. -3. Upload the single-channel R8 bitmap to a `VK_FORMAT_R8_UNORM` texture via the existing - `VulkanDevice` staging upload path. -4. Free the CPU bitmap (it was in a scratch arena that is cleared after upload). -5. `GetGlyph(cp)`: index into `m_glyphs[cp - k_FirstChar]` and compute `UV0/UV1` by - dividing pixel coords by `m_atlas_dim`. For `cp` outside `[32, 126]`, return the `?` glyph. -6. `MeasureText`: iterate UTF-8 bytes, decode codepoints, sum `xadvance` values. - -### Registration at engine startup - -```cpp -// Engine::Initialize() -auto* default_font = ZPushStructCtor(m_ui_arena, UI::BitmapFontAtlas); -default_font->Initialize(m_ui_arena, m_vulkan_device, - "Engine/Fonts/Inter-Regular.ttf", 16.f, 512); -UI::FontRegistry::Register("default", default_font); -``` - -### Deliverables - -- [ ] `UI/FontAtlas.h/.cpp` — `BitmapFontAtlas`, `GlyphInfo`, `FontRegistry` -- [ ] `Inter-Regular.ttf` in `Resources/Engine/Fonts/` -- [ ] Default font registered in `Engine::Initialize()` - ---- - -## 7. Step 5 — Box and UIContext - -### Files - -``` -ZEngine/ZEngine/UI/Box.h -ZEngine/ZEngine/UI/UIContext.h -ZEngine/ZEngine/UI/UIContext.cpp -``` - -### Box.h - -The single widget primitive. Every visible element — label, button, scroll area, separator — -is a `Box`. No subclasses. Type is implied by `BoxFlags`. - -```cpp -// ZEngine/ZEngine/UI/Box.h -#pragma once -#include <ZEngine/Core/Maths/Vec.h> -#include <ZEngine/UI/FontAtlas.h> -#include <cstdint> - -namespace ZEngine::UI -{ - // ── Size kinds ──────────────────────────────────────────────────────────── - enum SizeKind : uint8_t - { - SizeKind_Null, // 0 — no preference; stays 0 unless parent forces - SizeKind_Pixels, // exact pixel count - SizeKind_TextContent, // fit to DisplayString extent + padding - SizeKind_PercentOfParent, // Value is 0..1 fraction of parent's resolved size - SizeKind_ChildrenSum, // grow to contain all children along the layout axis - }; - - struct Size - { - SizeKind Kind = SizeKind_Null; - float Value = 0.f; - float Strictness = 1.f; // 1 = hard, 0 = freely violated by violation pass - }; - - enum Axis2 : uint8_t { Axis2_X = 0, Axis2_Y = 1, Axis2_COUNT = 2 }; - - // ── BoxFlags ─────────────────────────────────────────────────────────────── - using BoxFlags = uint32_t; - enum BoxFlag : BoxFlags - { - BoxFlag_DrawBackground = 1 << 0, - BoxFlag_DrawBorder = 1 << 1, - BoxFlag_DrawText = 1 << 2, - BoxFlag_Clip = 1 << 3, // scissor to ComputedSize - BoxFlag_Clickable = 1 << 4, - BoxFlag_Scrollable = 1 << 5, - BoxFlag_Focusable = 1 << 6, - BoxFlag_FloatingX = 1 << 7, // exempt from parent layout cursor (X axis) - BoxFlag_FloatingY = 1 << 8, // exempt from parent layout cursor (Y axis) - BoxFlag_LayoutAxisX = 1 << 9, // children laid out horizontally (default: vertical) - BoxFlag_AnimateHot = 1 << 10, - BoxFlag_AnimateActive = 1 << 11, - BoxFlag_NoInput = 1 << 12, // invisible to hit-testing; passes through - BoxFlag_DrawShadow = 1 << 13, - }; - - // ── Box ─────────────────────────────────────────────────────────────────── - struct Box - { - // Identity - uint64_t Key = 0; // StringHash(tag); persistent across frames - const char* Tag = nullptr; - - // Tree (within the current frame's arena) - Box* Parent = nullptr; - Box* FirstChild = nullptr; - Box* LastChild = nullptr; - Box* NextSibling = nullptr; - Box* PrevSibling = nullptr; - - // Layout input — set by caller each frame - BoxFlags Flags = 0; - Size SemanticSize[Axis2_COUNT] = {}; - - // Layout output — written by Layout pass - Core::Maths::Vec2f ComputedRelPos = {}; // relative to parent's top-left - Core::Maths::Vec2f ComputedAbsPos = {}; // absolute screen position - Core::Maths::Vec2f ComputedSize = {}; - - // Style — snapshotted from UIContext stacks at BoxMake time - Core::Maths::Vec4f BackgroundColor = { 0.f, 0.f, 0.f, 0.f }; - Core::Maths::Vec4f BorderColor = { 1.f, 1.f, 1.f, 1.f }; - Core::Maths::Vec4f TextColor = { 1.f, 1.f, 1.f, 1.f }; - float BorderThickness = 1.f; - float CornerRadius = 0.f; - Core::Maths::Vec4f Padding = {}; // left, right, top, bottom - FontHandle Font = k_InvalidFont; - float FontSizePx = 16.f; - - // Content - const char* DisplayString = nullptr; // frame-arena copy; null if no text - - // Interaction (read by caller after BoxMake) - bool Hot = false; - bool Active = false; - bool Focused = false; - - // Animation — lerped each frame via the persistent anim map - float HotTransition = 0.f; // 0 = cold, 1 = fully hot - float ActiveTransition = 0.f; - - // Frame stamp — box is stale if LastFrameTouched != UIContext::CurrentFrame() - uint64_t LastFrameTouched = 0; - - // Scroll state (Scrollable boxes only) - float ScrollOffsetY = 0.f; - }; -} -``` - -### UIContext.h - -```cpp -// ZEngine/ZEngine/UI/UIContext.h -#pragma once -#include <ZEngine/Core/Memory/Allocator.h> -#include <ZEngine/Core/Containers/Array.h> -#include <ZEngine/Core/Containers/UnorderedHashMap.h> -#include <ZEngine/UI/Box.h> - -namespace ZEngine::UI -{ - class UIContext - { - public: - // persistent_arena: owns the animation state map (lives for UIContext lifetime). - // per_frame_arena_size: frame arena reset each BeginFrame (default 4 MiB). - void Initialize(Core::Memory::ArenaAllocator* persistent_arena, - uint32_t per_frame_arena_size = 4 * 1024 * 1024); - void Destroy(); - - // ── Frame lifecycle ─────────────────────────────────────────────────── - void BeginFrame(float dt, Core::Maths::Vec2f viewport_size); - // EndFrame: runs layout, hit-testing, animation update, stale box pruning. - void EndFrame(); - - // ── Box construction ────────────────────────────────────────────────── - // Retrieve-or-create a box for this frame. Style stacks are snapshotted here. - // `tag` must be unique among siblings (or use ## suffix to disambiguate). - Box* BoxMake(BoxFlags flags, const char* tag); - - // BoxMake with a printf-style display string (copied into frame arena). - Box* BoxMakeF(BoxFlags flags, const char* tag, const char* fmt, ...); - - // ── Interaction queries ─────────────────────────────────────────────── - // Call after BoxMake and before the next PopParent. - bool Clicked(const Box* box) const noexcept; // just-released while active - bool Hovered(const Box* box) const noexcept; // is the hot box - bool IsActive(const Box* box) const noexcept; // owns mouse capture - bool IsFocused(const Box* box)const noexcept; // has keyboard focus - bool IsInputConsumed() const noexcept { return m_input_consumed; } - - // ── Style stacks ────────────────────────────────────────────────────── - void PushBackgroundColor(Core::Maths::Vec4f c); void PopBackgroundColor(); - void PushBorderColor(Core::Maths::Vec4f c); void PopBorderColor(); - void PushTextColor(Core::Maths::Vec4f c); void PopTextColor(); - void PushBorderThickness(float t); void PopBorderThickness(); - void PushCornerRadius(float r); void PopCornerRadius(); - void PushPadding(Core::Maths::Vec4f p); void PopPadding(); - void PushFont(FontHandle f, float size_px); void PopFont(); - // PushParent is implicit: BoxMake appends to the current parent. - // Call these to create an explicit parent scope. - void PushParent(Box* b); void PopParent(); - - // ── Accessors ───────────────────────────────────────────────────────── - Box* Root() const noexcept { return m_root; } - uint64_t CurrentFrame() const noexcept { return m_frame_index; } - float DeltaTime() const noexcept { return m_dt; } - Core::Maths::Vec2f ViewportSize() const noexcept { return m_viewport_size; } - - private: - // Persistent animation state, keyed by Box::Key. - struct AnimState { float Hot = 0.f; float Active = 0.f; float ScrollY = 0.f; }; - Core::Containers::UnorderedHashMap<uint64_t, AnimState> m_anim_states; - - Core::Memory::ArenaAllocator m_frame_arena; - Core::Memory::ArenaAllocator* m_persistent_arena = nullptr; - - Box* m_root = nullptr; - Box* m_hot_box = nullptr; - Box* m_active_box = nullptr; - Box* m_focused_box = nullptr; - bool m_input_consumed = false; - - float m_dt = 0.f; - uint64_t m_frame_index = 0; - Core::Maths::Vec2f m_viewport_size = {}; - - // Style stacks (arrays used as stacks — Top() returns last element) - Core::Containers::Array<Core::Maths::Vec4f> m_stack_bg; - Core::Containers::Array<Core::Maths::Vec4f> m_stack_border_color; - Core::Containers::Array<Core::Maths::Vec4f> m_stack_text_color; - Core::Containers::Array<float> m_stack_border_thickness; - Core::Containers::Array<float> m_stack_corner_radius; - Core::Containers::Array<Core::Maths::Vec4f> m_stack_padding; - Core::Containers::Array<FontHandle> m_stack_font; - Core::Containers::Array<float> m_stack_font_size; - Core::Containers::Array<Box*> m_stack_parent; - - void SnapshotStyle(Box* box); - void PruneStaleBoxes(); - void UpdateAnimations(); - }; -} -``` - -### UIContext::BoxMake internals - -``` -1. key = StringHash(tag) -2. Lookup key in m_anim_states (persistent map) -3. Allocate Box from m_frame_arena (ZPushStructCtor) -4. Fill Box::Key, Box::Tag, Box::LastFrameTouched = m_frame_index -5. SnapshotStyle(box) — copies top of every style stack into box fields -6. Link box under m_stack_parent.Top() as new last child -7. If anim entry found: copy old Hot/Active/ScrollY transition values into box -8. Lerp animation: - box->HotTransition = Lerp(old.Hot, box->Hot ? 1.f : 0.f, m_dt * 10.f) - box->ActiveTransition = Lerp(old.Active, box->Active ? 1.f : 0.f, m_dt * 20.f) -9. Return box -``` - -### UIContext::BeginFrame / EndFrame - -``` -BeginFrame: - m_frame_arena.Clear() - m_frame_index++ - Create root box (full viewport, no flags) from m_frame_arena - Push root onto m_stack_parent - Push default style values onto all stacks (bg=transparent, text=white, etc.) - -EndFrame: - Pop root from m_stack_parent - ZENGINE_VALIDATE_ASSERT(m_stack_parent.IsEmpty(), "Mismatched PushParent/PopParent") - Layout::Solve(m_root) - UIInput::UpdateInteraction(m_root, &m_hot_box, &m_active_box, &m_focused_box, &m_input_consumed) - PruneStaleBoxes() — remove anim_states entries not touched this frame - UpdateAnimations() — write back HotTransition/ActiveTransition to m_anim_states -``` - -### Deliverables - -- [ ] `UI/Box.h` — `Box`, `Size`, `SizeKind`, `BoxFlags`, `Axis2` -- [ ] `UI/UIContext.h/.cpp` — `BeginFrame`, `EndFrame`, `BoxMake`, `BoxMakeF`, style stacks, interaction queries +- **Input**: feed-based model (`ZUIFeedMousePos`, `ZUIFeedKey`, etc.) rather than a snapshot `InputFrame`. The `ZUILayer` translates engine events to feed calls directly. +- **Renderer**: `ZUIDrawList` (CPU-side draw list mirroring ImDrawList) consumed by `ZUIRenderer` (standalone Vulkan pass), rather than writing geometry directly into an ImGui draw list. +- **Layout**: 2-pass solver (post-order intrinsic sizes, pre-order extrinsic + positions) rather than the planned 5-phase pass. Simpler and sufficient for current needs. +- **Docking**: a full panel docking system (`ZUIDockspace`, `ZUIDockSerial`) was built on top of the base layer — not in the original spec. --- -## 8. Step 6 — Layout Pass - -### Files - -``` -ZEngine/ZEngine/UI/Layout.h -ZEngine/ZEngine/UI/Layout.cpp -``` - -### API - -```cpp -// ZEngine/ZEngine/UI/Layout.h -#pragma once -namespace ZEngine::UI { struct Box; } - -namespace ZEngine::UI::Layout -{ - // Run all 5 phases on the tree rooted at `root`. - // Writes ComputedSize, ComputedRelPos, and ComputedAbsPos on every box. - // All allocations use the box's owning arena (passed via root). - void Solve(Box* root); -} -``` - -### Five phases (all tree walks on the frame-arena box tree, zero heap allocation) +## What Was Built -**Phase 1 — Standalone sizes (post-order)** +### Core layer — `ZEngine/ZEngine/UI/` -For each box, for each axis: -- `SizeKind_Pixels` → `ComputedSize[axis] = Value` -- `SizeKind_TextContent` → X: `FontAtlas::MeasureText(DisplayString) + Padding.left + Padding.right`; - Y: `FontAtlas::LineHeight() + Padding.top + Padding.bottom` -- `SizeKind_Null` → `ComputedSize[axis] = 0` (may be updated later) - -**Phase 2 — ChildrenSum (post-order)** +| Component | File | What it does | +|---|---|---| +| Box | `ZUIBox.h` (170 lines) | Single widget primitive — flags, size spec, layout output, style fields | +| Context | `ZUIContext.h/.cpp` (940 lines) | Frame lifecycle, box tree, persistent state hash table, font/theme state | +| Input feed | `ZUIInput.h/.cpp` (147 lines) | `ZUIFeedMousePos/Button/Scroll/Text/Key` — engine-agnostic event intake | +| Key codes | `ZUIKey.h` (110 lines) | Engine-independent key enum | +| Interaction | `ZUIInteraction.h/.cpp` (298 lines) | `ZUIInteractionPass` (hit-test, hot/active routing), `ZUISignalFromBox` | +| Font atlas | `ZUIFont.h/.cpp` (334 lines) | `ZUIFontBake` via stb_truetype, `ZUIMeasureText`, `TextureHandle`-backed atlas | +| Layout | `ZUILayout.h/.cpp` (347 lines) | `ZUILayoutSolve` — 2-pass constraint solver (intrinsic post-order, extrinsic pre-order) | +| Draw list | `ZUIDrawList.h/.cpp` (858 lines) | CPU-side vector draw list: `ZUIDrawVtx/ZUIDrawListCmd`; lines, rects, triangles, text, images with AA fringe | +| Widgets | `ZUIWidgets.h/.cpp` (4,434 lines) | Full widget library (see below) | +| Panel | `ZUIPanel.h/.cpp` (1,679 lines) | Panel management, VS Code-style collapsing sections, drag-to-reorder | +| Dockspace | `ZUIDockspace.h/.cpp` (432 lines) | Panel docking — split, merge, pane sash resize | +| Dock serial | `ZUIDockSerial.h/.cpp` (502 lines) | Serialize and restore dock layout across sessions | -For each box where `SemanticSize[axis].Kind == SizeKind_ChildrenSum`: -- Along the layout axis: sum all children's `ComputedSize[axis]`. -- Along the cross axis: take the max of all children's `ComputedSize[cross]`. +### Renderer — `ZEngine/ZEngine/Rendering/Renderers/ZUIRenderer` -Post-order ensures children are resolved before their parent reads their sizes. +Standalone Vulkan pass registered in `AppRenderPipeline`: +- `ZUICtx` and `ZUIRenderPayload` on `AppRenderPipeline` +- Shaders: `Resources/Shaders/zui_draw.vert` + `zui_draw.frag` (compiled SPV in `Cache/`) +- Vertex layout: `ZUIDrawVtx` (20 bytes — pos xy, uv, RGBA8 col) -**Phase 3 — PercentOfParent (pre-order)** +### Widget inventory (`ZUIWidgets.h`) -For each box where `SemanticSize[axis].Kind == SizeKind_PercentOfParent`: -- `ComputedSize[axis] = parent->ComputedSize[axis] * Value` +Layout containers: `ZUIBeginColumn`, `ZUIBeginRow`, `ZUIBeginScrollRegion` -Pre-order ensures the parent's size is resolved before children read it. +Display: `ZUILabel`, `ZUISeparator`, `ZUISpacing`, `ZUIImage` -**Phase 4 — Violation fixing** +Interactive: `ZUIButton`, `ZUISmallButton`, `ZUIInvisibleButton`, `ZUIImageButton`, `ZUICheckbox`, `ZUIToggle`, `ZUIRadioButton` -For each box on the layout axis, if the sum of children sizes exceeds the parent's size: -- Collect children with `Strictness < 1.0` on that axis. -- Reduce each such child proportionally: - `child->ComputedSize[axis] -= overflow * child->SemanticSize[axis].Strictness_complement` -- Children with `Strictness == 1.0` are never shrunk. +Input: `ZUIDragFloat`, `ZUIDragFloat3`, `ZUISliderFloat`, `ZUIInputText`, `ZUIColorEdit3`, `ZUIColorEdit4` -**Phase 5 — Position (pre-order)** +Selection: `ZUISelectable`, `ZUITreeNode`, `ZUICollapsingHeader`, `ZUIComboBox`, `ZUIBeginListBox`, `ZUIBeginTreeView`, `ZUIBeginGridView` -For each box, walk children maintaining a layout cursor: -- Cursor starts at `(Padding.left, Padding.top)` for the parent. -- `BoxFlag_LayoutAxisX`: advance cursor in X after each non-floating child. -- Default (vertical): advance cursor in Y. -- `BoxFlag_FloatingX` / `BoxFlag_FloatingY`: skip cursor for that axis (absolute positioning). -- Set `ComputedRelPos = cursor_position`. -- Set `ComputedAbsPos = parent->ComputedAbsPos + ComputedRelPos`. +Table/data: `ZUIBeginDataTable`, `ZUIDataTableHeader`, `ZUIDataTableGetSortSpecs` -### Deliverables +Popup/menu: `ZUIBeginPopup`, `ZUIOpenPopup`, `ZUIClosePopup`, `ZUIBeginMenu`, `ZUIMenuItem`, `ZUIBeginContextMenu` -- [ ] `UI/Layout.h/.cpp` — `Layout::Solve(Box*)` implementing all 5 phases with no heap allocation +Progress/misc: `ZUIProgressBar`, `ZUITooltip`, `ZUIBeginDisabled`, `ZUIEndDisabled` --- -## 9. Step 7 — UIRenderer - -### Files - -``` -ZEngine/ZEngine/UI/UIRenderer.h -ZEngine/ZEngine/UI/UIRenderer.cpp -Resources/Shaders/ui.vert -Resources/Shaders/ui.frag -``` - -### Header - -```cpp -// ZEngine/ZEngine/UI/UIRenderer.h -#pragma once -#include <ZEngine/Rendering/Renderers/RenderGraph.h> -#include <ZEngine/Hardwares/VulkanDevice.h> -#include <ZEngine/UI/UIContext.h> -#include <ZEngine/Core/Maths/Vec.h> - -namespace ZEngine::UI -{ - // One vertex emitted by BuildDrawList. - struct UIVertex - { - Core::Maths::Vec2f Pos; // screen-space pixels - Core::Maths::Vec2f UV; // atlas UV; (0,0) = use solid color - Core::Maths::Vec4f Color; // pre-multiplied alpha - uint32_t TexID; // 0 = white 1x1 (solid), 1 = font atlas - }; +## Migrated Editor Panels — `Tetragrama/Components/ZUI/` - // IRenderGraphCallbackPass registered as the last node in the RenderGraph. - class UIRenderer : public Rendering::Renderers::IRenderGraphCallbackPass - { - public: - void Initialize(Core::Memory::ArenaAllocator* arena, - Hardwares::VulkanDevice* device, - uint32_t max_vertices = 65536); - void Destroy(); +All panels below use `ZUIContext` directly. `ZUILayer` wires engine events to the context. - void SetContext(UIContext* ctx) { m_ctx = ctx; } - - // IRenderGraphCallbackPass - void Setup(Rendering::Renderers::RenderGraphResourceBuilder&) override; - void Compile(Rendering::Renderers::RenderGraphResourceInspector&) override; - void Execute(VkCommandBuffer cmd, - const Rendering::Renderers::RenderGraph& rg) override; - const char* GetName() const override { return "UIPass"; } - - private: - void BuildDrawList(Box* box); - void FlushBatch(VkCommandBuffer cmd); - void EmitQuad(Core::Maths::Vec2f pos, Core::Maths::Vec2f size, - Core::Maths::Vec2f uv0, Core::Maths::Vec2f uv1, - Core::Maths::Vec4f color, uint32_t tex_id); - void EmitText(const char* text, Core::Maths::Vec2f origin, - Core::Maths::Vec4f color, BitmapFontAtlas* font); - void EmitRoundedRect(Core::Maths::Vec2f pos, Core::Maths::Vec2f size, - float radius, Core::Maths::Vec4f color); - - UIContext* m_ctx = nullptr; - Hardwares::VulkanDevice* m_device = nullptr; - Core::Memory::ArenaAllocator* m_arena = nullptr; - - UIVertex* m_vertices = nullptr; // frame-arena; reset each Execute - uint32_t m_vert_count = 0; - uint32_t m_max_verts = 0; - - VkBuffer m_vb = VK_NULL_HANDLE; - VkDeviceMemory m_vb_mem = VK_NULL_HANDLE; - - VkPipeline m_pipeline = VK_NULL_HANDLE; - VkPipelineLayout m_pipeline_layout = VK_NULL_HANDLE; - VkDescriptorSetLayout m_desc_layout = VK_NULL_HANDLE; - VkDescriptorSet m_desc_set = VK_NULL_HANDLE; - - VkImage m_white_img = VK_NULL_HANDLE; // 1x1 white RGBA - VkImageView m_white_view = VK_NULL_HANDLE; - VkSampler m_sampler = VK_NULL_HANDLE; - }; -} -``` - -### Shaders - -**ui.vert** -```glsl -layout(push_constant) uniform PC { vec2 inv_viewport; } pc; -layout(location=0) in vec2 aPos; -layout(location=1) in vec2 aUV; -layout(location=2) in vec4 aColor; -layout(location=3) in uint aTexID; -layout(location=0) out vec2 vUV; -layout(location=1) out vec4 vColor; -layout(location=2) out flat uint vTexID; -void main() { - gl_Position = vec4(aPos * pc.inv_viewport * 2.0 - 1.0, 0.0, 1.0); - vUV = aUV; - vColor = aColor; - vTexID = aTexID; -} -``` - -**ui.frag** -```glsl -layout(binding=0) uniform sampler2D uWhite; // 1x1 white -layout(binding=1) uniform sampler2D uFont; // font atlas (R8_UNORM) -layout(location=0) in vec2 vUV; -layout(location=1) in vec4 vColor; -layout(location=2) in flat uint vTexID; -layout(location=0) out vec4 outColor; -void main() { - float alpha = (vTexID == 1u) ? texture(uFont, vUV).r : 1.0; - outColor = vColor * alpha; - if (outColor.a < 0.01) discard; -} -``` - -Pipeline blend state: `VK_BLEND_FACTOR_ONE` / `VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA` -(pre-multiplied alpha). No depth test. No face culling. - -### BuildDrawList traversal (pre-order) - -``` -For each box (pre-order): - if BoxFlag_Clip: vkCmdSetScissor to ComputedAbsPos / ComputedSize - if BoxFlag_DrawShadow: EmitQuad (offset +2,+4; dark semi-transparent) - if BoxFlag_DrawBackground: - if CornerRadius > 0: EmitRoundedRect (approximate with fan triangles) - else: EmitQuad (solid) - if BoxFlag_DrawBorder: 4 thin quads (top/bottom/left/right edges) - if BoxFlag_DrawText && DisplayString: - EmitText(DisplayString, ComputedAbsPos + padding, TextColor, font) - recurse children - if BoxFlag_Clip: restore scissor -``` - -### Deliverables +| Panel | File | Notes | +|---|---|---| +| Dockspace | `ZUIDockspaceComponent.h/.cpp` | Main editor layout | +| Hierarchy view | `ZUIHierarchyViewComponent.h/.cpp` | Actor outliner | +| Inspector | `ZUIInspectorViewComponent.h/.cpp` | Property editor | +| Project view | `ZUIProjectViewComponent.h/.cpp` | Content browser | +| Log | `ZUILogComponent.h/.cpp` | Log output | +| Status bar | `ZUIStatusBarComponent.h/.cpp` | Bottom bar | +| Scene viewport | `ZUISceneViewportComponent.h/.cpp` | 3D view (ImGuizmo still in ImGui) | -- [ ] `UI/UIRenderer.h/.cpp` — `UIRenderer` implementing `IRenderGraphCallbackPass` -- [ ] `Resources/Shaders/ui.vert` + `ui.frag` -- [ ] Compiled `.spv` cached in `Resources/Shaders/Cache/` -- [ ] UIRenderer registered as the last pass in `RenderGraph` in `Engine::Initialize()` +`ZUILayer` (`Tetragrama/Layers/ZUILayer.h/.cpp`) — engine layer; implements `IMouseEventCallback`, `IKeyboardEventCallback`, `ITextInputEventCallback`; routes to `ZUIFeed*` calls; owns the `ZUIContext*`. --- -## 10. Step 8 — Widgets Layer - -### Files - -``` -ZEngine/ZEngine/UI/Widgets.h -ZEngine/ZEngine/UI/Widgets.cpp -``` - -### API - -```cpp -// ZEngine/ZEngine/UI/Widgets.h -#pragma once -#include <ZEngine/UI/UIContext.h> +## What Remains -namespace ZEngine::UI::Widgets -{ - // ── Text ────────────────────────────────────────────────────────────────── - Box* Label(UIContext* ctx, const char* text); - Box* LabelF(UIContext* ctx, const char* tag, const char* fmt, ...); +### DebugOverlay - // ── Buttons ─────────────────────────────────────────────────────────────── - bool Button(UIContext* ctx, const char* label); - bool IconButton(UIContext* ctx, const char* tag, - Core::Maths::Vec2f icon_uv0, Core::Maths::Vec2f icon_uv1); +A runtime overlay (F3 toggle) showing FPS, frame time, memory arena bars. Should be a `ZUIComponent` that reads from `MemoryProfiler`. First milestone after current panel migration is complete. - // ── Controls ────────────────────────────────────────────────────────────── - bool Checkbox(UIContext* ctx, const char* label, bool* value); - bool SliderFloat(UIContext* ctx, const char* label, - float* v, float v_min, float v_max); - bool InputText(UIContext* ctx, const char* label, - char* buf, uint32_t buf_len); - bool ColorPicker(UIContext* ctx, const char* label, Core::Maths::Vec4f* color); +Target files: +- `Tetragrama/Components/ZUI/ZUIDebugOverlayComponent.h/.cpp` +- Toggle wired through `ZUIFeedKey` in `ZUILayer` - // ── Layout helpers ───────────────────────────────────────────────────────── - void Spacer(UIContext* ctx, float pixels); // fixed-size empty box - void Separator(UIContext* ctx); // 1px horizontal rule +### DebugConsole - // Horizontal row scope: children are laid out left-to-right. - // BeginRow pushes a LayoutAxisX parent; EndRow pops it. - void BeginRow(UIContext* ctx, const char* tag); - void EndRow(UIContext* ctx); +Tilde-toggle console (Debug builds only). Input field + scrollable log. A `ZUIComponent` with `ZUIInputText` + `ZUIBeginScrollRegion`. - // ── Containers ──────────────────────────────────────────────────────────── - Box* BeginPanel(UIContext* ctx, const char* tag, Core::Maths::Vec2f size, - Core::Maths::Vec4f bg = {0.12f, 0.12f, 0.12f, 0.95f}); - void EndPanel(UIContext* ctx); +Target files: +- `Tetragrama/Components/ZUI/ZUIDebugConsoleComponent.h/.cpp` - Box* BeginScrollArea(UIContext* ctx, const char* tag, - Core::Maths::Vec2f size, float* scroll_y); - void EndScrollArea(UIContext* ctx); - - bool BeginCollapsible(UIContext* ctx, const char* label, bool* open); - void EndCollapsible(UIContext* ctx); -} -``` - -### Example: Button - -```cpp -bool Widgets::Button(UIContext* ctx, const char* label) -{ - ctx->PushBackgroundColor({0.20f, 0.40f, 0.80f, 1.f}); - ctx->PushCornerRadius(4.f); - ctx->PushPadding({8.f, 8.f, 4.f, 4.f}); - - Box* btn = ctx->BoxMakeF( - BoxFlag_DrawBackground | BoxFlag_DrawBorder | - BoxFlag_DrawText | BoxFlag_Clickable | - BoxFlag_AnimateHot | BoxFlag_AnimateActive, - label, "%s", label); - - btn->SemanticSize[Axis2_X] = { SizeKind_TextContent, 0.f, 1.f }; - btn->SemanticSize[Axis2_Y] = { SizeKind_TextContent, 0.f, 1.f }; - - // Brighten on hover using the smooth HotTransition value - float h = btn->HotTransition; - btn->BackgroundColor = { 0.20f + h * 0.15f, 0.40f + h * 0.10f, 0.80f + h * 0.10f, 1.f }; - - bool clicked = ctx->Clicked(btn); - - ctx->PopPadding(); - ctx->PopCornerRadius(); - ctx->PopBackgroundColor(); - return clicked; -} -``` - -### Example: SliderFloat - -```cpp -bool Widgets::SliderFloat(UIContext* ctx, const char* label, - float* v, float v_min, float v_max) -{ - BeginRow(ctx, label); - Label(ctx, label); - - Box* track = ctx->BoxMakeF( - BoxFlag_DrawBackground | BoxFlag_Clickable | BoxFlag_AnimateHot, - label, "##track"); - track->SemanticSize[Axis2_X] = { SizeKind_PercentOfParent, 0.6f, 0.8f }; - track->SemanticSize[Axis2_Y] = { SizeKind_Pixels, 6.f, 1.f }; - - if (ctx->IsActive(track)) { - // Map drag X into [v_min, v_max] - Core::Maths::Vec2f delta = InputFrame::Get().MouseDelta(); - float range = v_max - v_min; - *v = Core::Maths::Clamp(*v + delta.X / track->ComputedSize.X * range, - v_min, v_max); - } - - float t = (*v - v_min) / (v_max - v_min); - // Emit fill as a child floating box - ctx->PushParent(track); - Box* fill = ctx->BoxMake(BoxFlag_DrawBackground | BoxFlag_FloatingY, "##fill"); - fill->SemanticSize[Axis2_X] = { SizeKind_PercentOfParent, t, 1.f }; - fill->SemanticSize[Axis2_Y] = { SizeKind_PercentOfParent, 1.f, 1.f }; - fill->BackgroundColor = { 0.3f, 0.6f, 1.f, 1.f }; - ctx->PopParent(); - - EndRow(ctx); - return ctx->IsActive(track); -} -``` +### UIScreenStack (in-game menus) -### Deliverables +For shipping a game: main menu, pause menu, settings screen. Not needed for the editor. Design is unchanged from the original spec — a stack of `ZUIScreen` objects each calling `ZUIBeginFrame` / `ZUIEndFrame`. -- [ ] `UI/Widgets.h/.cpp` — Label, LabelF, Button, IconButton, Checkbox, SliderFloat, InputText, ColorPicker, Spacer, Separator, BeginRow/EndRow, BeginPanel/EndPanel, BeginScrollArea/EndScrollArea, BeginCollapsible/EndCollapsible +Target files when needed: +- `ZEngine/ZEngine/UI/ZUIScreen.h` +- `ZEngine/ZEngine/UI/ZUIScreenStack.h/.cpp` --- -## 11. DebugOverlay and DebugConsole Integration +## ImGui Coexistence -Once all 8 steps are done, `DebugOverlay` and `DebugConsole` (specified in `profiling.md`) -use `UIContext` directly: - -```cpp -// DebugOverlay::Render -void DebugOverlay::Render() { - if (!m_visible) return; - using namespace UI::Widgets; - - BeginPanel(m_ui_ctx, "debug_overlay", {320.f, 0.f}); // height = ChildrenSum - m_ui_ctx->Root()->SemanticSize[Axis2_Y] = { SizeKind_ChildrenSum, 0.f, 1.f }; - - LabelF(m_ui_ctx, "fps", "FPS: %.0f", m_fps); - LabelF(m_ui_ctx, "cpu_ms", "CPU: %.2f ms", m_cpu_ms); - LabelF(m_ui_ctx, "gpu_ms", "GPU: %.2f ms", m_gpu_ms); - LabelF(m_ui_ctx, "draws", "Draws: %u", m_draw_calls); - LabelF(m_ui_ctx, "ents", "Entities: %u", m_entity_count); - // Memory bars (arena stats from MemoryProfiler) - for (uint32_t i = 0; i < m_arena_stats.Size(); ++i) { - auto& a = m_arena_stats[i]; - float t = float(a.CurrentOffset) / float(a.Capacity); - Core::Maths::Vec4f fill = t < 0.5f ? Vec4f{0.2f,0.8f,0.2f,1.f} - : t < 0.8f ? Vec4f{0.9f,0.8f,0.1f,1.f} - : Vec4f{0.9f,0.2f,0.1f,1.f}; - Widgets::SliderFloat(m_ui_ctx, a.Name, - (float*)&a.CurrentOffset, // read-only display - 0.f, float(a.Capacity)); - } - EndPanel(m_ui_ctx); -} -``` - -F3 toggle wired to `InputFrame::Get().IsKeyJustPressed(ZENGINE_KEY_F3)`. -Tilde toggle for DebugConsole: `ZENGINE_KEY_GRAVE_ACCENT`. +ImGui and ImGuizmo remain in the editor permanently. Both renderers register as separate passes: +- `ImGuiPass` runs before `ZUIPass` +- `SceneViewportUIComponent` keeps ImGui + ImGuizmo (gizmos depend on ImGui draw lists) +- The ZUI system is additive — it does not replace ImGui in the editor --- -## 12. UIScreenStack (Menus) - -Game menus use `UIScreenStack` — screens push/pop onto a stack; each draws via `UIContext` -each frame. - -```cpp -// ZEngine/ZEngine/UI/UIScreen.h -namespace ZEngine::UI { - class UIScreen { - public: - virtual ~UIScreen() = default; - virtual void Draw(UIContext& ctx, float dt) = 0; - virtual void OnEnter() {} - virtual void OnExit() {} - virtual bool IsOpaque() const { return true; } // false = draw screen below too - }; -} -``` - -```cpp -// ZEngine/ZEngine/UI/UIScreenStack.h -namespace ZEngine::UI { - class UIScreenStack { - public: - static constexpr int k_MaxDepth = 16; - void Push(Helpers::Ref<UIScreen> screen); - void Pop(); - UIScreen* Peek() const noexcept; - bool IsEmpty() const noexcept; - void Draw(UIContext& ctx, float dt); - private: - Core::Containers::Array<Helpers::Ref<UIScreen>> m_stack; - }; -} -``` - -Main loop integration: +## File Layout -```cpp -// Engine::MainThreadRun — between BeginFrame and EndFrame: -m_ui_ctx->BeginFrame(dt, viewport_size); -m_screen_stack->Draw(*m_ui_ctx, dt); // menus -m_hud_system->Draw(*m_ui_ctx, dt); // always-on HUD -m_debug_overlay->Render(); // F3 toggle -m_debug_console->Render(); // tilde toggle (debug only) -m_ui_ctx->EndFrame(); ``` +ZEngine/ZEngine/UI/ +├── ZUIBox.h Box struct, flags, size kinds +├── ZUIContext.h/.cpp Frame lifecycle, box tree, persistent state +├── ZUIInput.h/.cpp Feed-based input (engine-agnostic) +├── ZUIKey.h Key code enum +├── ZUIInteraction.h/.cpp Hit-test, hot/active routing, ZUISignal +├── ZUIFont.h/.cpp Font atlas baking (stb_truetype) +├── ZUILayout.h/.cpp 2-pass constraint solver +├── ZUIDrawList.h/.cpp CPU-side draw list → ZUIRenderer +├── ZUIWidgets.h/.cpp Full widget library +├── ZUIPanel.h/.cpp Panel management, section drag-to-reorder +├── ZUIDockspace.h/.cpp Panel docking +└── ZUIDockSerial.h/.cpp Dock layout persistence ---- +ZEngine/ZEngine/Rendering/Renderers/ +└── ZUIRenderer.h/.cpp Vulkan pass consuming ZUIDrawList output -## 13. File Layout - -``` -ZEngine/ZEngine/ -├── Core/Containers/ -│ └── StringHash.h Step 1 -│ -├── Windows/Inputs/ -│ ├── InputFrame.h Step 2 -│ └── InputFrame.cpp -│ -└── UI/ - ├── Box.h Step 5 - ├── UIContext.h Step 5 - ├── UIContext.cpp - ├── UIInput.h Step 3 - ├── UIInput.cpp - ├── FontAtlas.h Step 4 - ├── FontAtlas.cpp - ├── Layout.h Step 6 - ├── Layout.cpp - ├── UIRenderer.h Step 7 - ├── UIRenderer.cpp - ├── Widgets.h Step 8 - ├── Widgets.cpp - ├── UIScreen.h §12 - ├── UIScreenStack.h - └── UIScreenStack.cpp +Resources/Shaders/ +├── zui_draw.vert Screen-space vertex transform +├── zui_draw.frag Texture + solid color fragment +└── Cache/ + ├── zui_draw_vertex.spv + └── zui_draw_fragment.spv -Resources/ -├── Shaders/ -│ ├── ui.vert Step 7 -│ └── ui.frag -│ └── Cache/ (ui.vert.spv, ui.frag.spv) -└── Engine/Fonts/ - └── Inter-Regular.ttf Step 4 +Tetragrama/ +├── Layers/ZUILayer.h/.cpp Engine layer — event routing, context ownership +└── Components/ZUI/ + ├── ZUIComponent.h Base class + ├── ZUIDockspaceComponent.h/.cpp + ├── ZUIHierarchyViewComponent.h/.cpp + ├── ZUIInspectorViewComponent.h/.cpp + ├── ZUIProjectViewComponent.h/.cpp + ├── ZUILogComponent.h/.cpp + ├── ZUIStatusBarComponent.h/.cpp + └── ZUISceneViewportComponent.h/.cpp ``` - ---- - -## 14. ImGui Coexistence - -- `ImGUIRenderer` stays untouched. Both renderers register as separate `IRenderGraphCallbackPass` nodes. -- UIPass is the final node; ImGuiPass runs before it. -- `SceneViewportUIComponent` keeps ImGui + ImGuizmo permanently — gizmos depend on ImGui draw lists. -- Other Tetragrama panels migrate one-by-one after Widgets layer is stable: `LogUIComponent` first (simplest), then `InspectorViewUIComponent`, `HierarchyViewUIComponent`, `ProjectViewUIComponent`. -- No migration timeline pressure. First milestone is DebugOverlay + DebugConsole working via UIContext. - ---- - -## 15. Deliverables Checklist - -### Step 1 — StringHash -- [ ] `Core/Containers/StringHash.h` — `StringHash(const char*)` + `StringHashN` - -### Step 2 — InputFrame -- [ ] `Windows/Inputs/InputFrame.h/.cpp` — `BeginFrame`, all query methods, all callbacks -- [ ] `CoreWindow` registers `InputFrame::Get()` as a listener -- [ ] `Engine::MainThreadRun` calls `InputFrame::Get().BeginFrame()` before `PollEvents()` - -### Step 3 — UIInput -- [ ] `UI/UIInput.h/.cpp` — `HitTest`, `UpdateInteraction`, `AdvanceFocus` - -### Step 4 — BitmapFontAtlas -- [ ] `UI/FontAtlas.h/.cpp` — `BitmapFontAtlas`, `GlyphInfo`, `FontRegistry` -- [ ] `Resources/Engine/Fonts/Inter-Regular.ttf` present -- [ ] Default font registered in `Engine::Initialize()` - -### Step 5 — Box + UIContext -- [ ] `UI/Box.h` — `Box`, `Size`, `SizeKind`, `BoxFlags` -- [ ] `UI/UIContext.h/.cpp` — `BeginFrame`, `EndFrame`, `BoxMake`, `BoxMakeF`, all stacks, all queries - -### Step 6 — Layout -- [ ] `UI/Layout.h/.cpp` — `Layout::Solve(Box*)` with all 5 phases; zero heap allocation - -### Step 7 — UIRenderer -- [ ] `UI/UIRenderer.h/.cpp` — `IRenderGraphCallbackPass`; quad batcher; text glyph emitter -- [ ] `Resources/Shaders/ui.vert` + `ui.frag` (plus compiled `.spv` cache) -- [ ] UIRenderer registered as last RenderGraph pass in `Engine::Initialize()` - -### Step 8 — Widgets -- [ ] `UI/Widgets.h/.cpp` — Label, LabelF, Button, IconButton, Checkbox, SliderFloat, InputText, ColorPicker, Spacer, Separator, BeginRow/EndRow, BeginPanel/EndPanel, BeginScrollArea/EndScrollArea, BeginCollapsible/EndCollapsible - -### Screen stack -- [ ] `UI/UIScreen.h` — abstract base -- [ ] `UI/UIScreenStack.h/.cpp` — Push/Pop/Peek/Draw (max depth 16) - -### Integration -- [ ] `DebugOverlay` reads from `UIContext` instead of ImGui stubs -- [ ] `DebugConsole` reads from `UIContext` (Debug builds only) -- [ ] F3 and tilde toggle wired through `InputFrame::Get()` - ---- - -## 16. Verification - -- [ ] `StringHash("hello") == StringHash("hello")` across multiple calls (deterministic) -- [ ] `InputFrame::IsKeyJustPressed` true only on the one frame of key-down transition -- [ ] `InputFrame::MouseDelta()` is zero when cursor does not move -- [ ] `BitmapFontAtlas::MeasureText("Hello")` returns a positive non-zero float -- [ ] Layout test (no GPU): parent 400px wide, two children `SizeKind_PercentOfParent = 0.5` each → `ComputedSize.X == 200` for both -- [ ] `Button` returns `true` exactly once on click, not on hold -- [ ] `SliderFloat` responds to drag; clamps to [min, max] -- [ ] `BeginScrollArea` clips children and scrolls on mouse wheel -- [ ] F3 shows DebugOverlay with non-zero FPS and frame time -- [ ] Tilde shows DebugConsole (Debug build); `help` outputs command list -- [ ] UIRenderer draws correctly with 0 boxes (empty frame = no crash) -- [ ] `ZENGINE_PROFILE_SCOPE("UIContext::EndFrame")` reads < 0.2 ms for 500 boxes in Debug diff --git a/dependencies.cmake b/dependencies.cmake index a370b9a09..d288c6827 100644 --- a/dependencies.cmake +++ b/dependencies.cmake @@ -7,22 +7,6 @@ FetchContent_Declare( GIT_TAG main ) -FetchContent_Declare( - imgui - GIT_REPOSITORY https://github.com/ocornut/imgui.git - GIT_SHALLOW TRUE - GIT_TAG v1.92.9b-docking - SOURCE_DIR "${FETCHCONTENT_BASE_DIR}/imgui" - ) - -FetchContent_Declare( - imguizmo - GIT_REPOSITORY https://github.com/CedricGuillemet/ImGuizmo.git - GIT_SHALLOW TRUE - GIT_TAG 1.83 - SOURCE_DIR "${FETCHCONTENT_BASE_DIR}/ImGuizmo" - PATCH_COMMAND python3 "${CMAKE_SOURCE_DIR}/patches/imguizmo_imgui192_compat.py" "${FETCHCONTENT_BASE_DIR}/ImGuizmo/ImGuizmo.cpp" - ) FetchContent_Declare( stb @@ -198,6 +182,18 @@ FetchContent_Declare(simdjson ) set(SIMDJSON_DEVELOPER_MODE OFF CACHE BOOL "" FORCE) +FetchContent_Declare( + freetype + GIT_REPOSITORY https://gitlab.freedesktop.org/freetype/freetype.git + GIT_SHALLOW TRUE + GIT_TAG VER-2-13-3 +) +set(FT_DISABLE_ZLIB ON CACHE BOOL "" FORCE) +set(FT_DISABLE_BZIP2 ON CACHE BOOL "" FORCE) +set(FT_DISABLE_PNG ON CACHE BOOL "" FORCE) +set(FT_DISABLE_HARFBUZZ ON CACHE BOOL "" FORCE) +set(FT_DISABLE_BROTLI ON CACHE BOOL "" FORCE) + FetchContent_Declare(fastgltf GIT_REPOSITORY https://github.com/spnda/fastgltf.git GIT_SHALLOW TRUE @@ -216,11 +212,10 @@ if(ZENGINE_TRACY) endif() FetchContent_MakeAvailable( + freetype fmt Vulkan-Headers Vulkan-Loader - imgui - ImGuizmo stb glfw3 spdlog @@ -258,39 +253,6 @@ foreach(_spirv_target IN ITEMS endif() endforeach() -set(IMGUIDIR ${FETCHCONTENT_BASE_DIR}/imgui) - -add_library(imgui STATIC) - -target_sources( - imgui - PRIVATE ${IMGUIDIR}/imgui.cpp - ${IMGUIDIR}/imgui_demo.cpp - ${IMGUIDIR}/imgui_draw.cpp - ${IMGUIDIR}/imgui_tables.cpp - ${IMGUIDIR}/imgui_widgets.cpp - ${IMGUIDIR}/misc/cpp/imgui_stdlib.cpp - ${IMGUIDIR}/backends/imgui_impl_glfw.cpp - ${IMGUIDIR}/backends/imgui_impl_vulkan.cpp) - - target_include_directories(imgui - PUBLIC ${FETCHCONTENT_BASE_DIR} - PUBLIC ${FETCHCONTENT_BASE_DIR}/imgui - ) - -target_compile_definitions(imgui PUBLIC GLFW_INCLUDE_VULKAN IMGUI_DEFINE_MATH_OPERATORS) - -target_link_libraries(imgui PRIVATE glfw Vulkan::Headers Vulkan::Loader) - -add_library(imguizmo STATIC) - -target_sources(imguizmo - PRIVATE ${FETCHCONTENT_BASE_DIR}/ImGuizmo/ImGuizmo.cpp) - -target_include_directories(imguizmo - PUBLIC ${FETCHCONTENT_BASE_DIR}/imguizmo-src) - -target_link_libraries(imguizmo PUBLIC imgui) add_library(External_libs INTERFACE) @@ -320,7 +282,6 @@ target_link_libraries(External_libs Vulkan::Loader glfw fmt::fmt - imguizmo spdlog::spdlog assimp::assimp stduuid @@ -336,6 +297,7 @@ target_link_libraries(External_libs fastgltf::fastgltf ufbx meshoptimizer + freetype ) if(ZENGINE_TRACY)