diff --git a/.gitignore b/.gitignore index d4224e5..8dcc3d5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ -/zig-cache -/zig-out +.zig-cache/ +zig-cache/ +zig-out/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5052cbe --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2022 Michal Ziulek +Copyright (c) 2024 zig-gamedev contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 7c75bcb..aedb21d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# zgui v0.1.0 - dear imgui bindings +# zgui v0.2.0 - dear imgui bindings Easy to use, hand-crafted API with default arguments, named parameters and Zig style text formatting. [Here](https://github.com/michal-z/zig-gamedev/tree/main/samples/minimal_zgpu_zgui) is a simple sample application, and [here](https://github.com/michal-z/zig-gamedev/tree/main/samples/gui_test_wgpu) is a full one. @@ -8,6 +8,13 @@ Easy to use, hand-crafted API with default arguments, named parameters and Zig s * All memory allocations go through user provided Zig allocator * [DrawList API](#drawlist-api) for vector graphics, text rendering and custom widgets * [Plot API](#plot-api) for advanced data visualizations +* [Test engine API](#test-engine-api) for automatic testing + +## Versions + +* [ImGui](https://github.com/ocornut/imgui/tree/v1.90.4-docking) `1.90.4-docking` +* [ImGui test engine](https://github.com/ocornut/imgui_test_engine/tree/v1.90.4) `1.90.4` +* [ImPlot](https://github.com/epezent/implot) `O.17` ## Getting started @@ -37,11 +44,11 @@ pub fn build(b: *std.Build) void { exe.linkLibrary(zglfw.artifact("glfw")); const zpool = b.dependency("zpool", .{}); - exe.root_module.addImport("zpool", zglfw.module("root")); + exe.root_module.addImport("zpool", zpool.module("root")); const zgpu = b.dependency("zgpu", .{}); - exe.root_module.addImport("zgpu", zglfw.module("root")); - exe.linkLibrary(zglfw.artifact("wgpu")); + exe.root_module.addImport("zgpu", zgpu.module("root")); + exe.linkLibrary(zgpu.artifact("zdawn")); } } ``` @@ -147,3 +154,48 @@ if (zgui.plot.beginPlot("Line Plot", .{ .h = -1.0 })) { zgui.plot.endPlot(); } ``` + +### Test Engine API +Zig wraper for [ImGUI test engine](https://github.com/ocornut/imgui_test_engine). + +```zig +var check_b = false; +var _te: *zgui.te.TestEngine = zgui.te.getTestEngine().?; +fn registerTests() void { + _ = _te.registerTest( + "Awesome", + "should_do_some_another_magic", + @src(), + struct { + pub fn gui(ctx: *zgui.te.TestContext) !void { + _ = ctx; // autofix + _ = zgui.begin("Test Window", .{ .flags = .{ .no_saved_settings = true } }); + defer zgui.end(); + + zgui.text("Hello, automation world", .{}); + _ = zgui.button("Click Me", .{}); + if (zgui.treeNode("Node")) { + defer zgui.treePop(); + + _ = zgui.checkbox("Checkbox", .{ .v = &check_b }); + } + } + + pub fn run(ctx: *zgui.te.TestContext) !void { + ctx.setRef("/Test Window"); + ctx.windowFocus(""); + + ctx.itemAction(.click, "Click Me", .{}, null); + ctx.itemAction(.open, "Node", .{}, null); + ctx.itemAction(.check, "Node/Checkbox", .{}, null); + ctx.itemAction(.uncheck, "Node/Checkbox", .{}, null); + + std.testing.expect(true) catch |err| { + zgui.te.checkTestError(@src(), err); + return; + }; + } + }, + ); +} +``` diff --git a/build.zig b/build.zig index 8cdc49f..b401699 100644 --- a/build.zig +++ b/build.zig @@ -6,6 +6,7 @@ pub const Backend = enum { glfw_opengl3, glfw_dx12, win32_dx12, + glfw, }; pub fn build(b: *std.Build) void { @@ -24,6 +25,16 @@ pub fn build(b: *std.Build) void { "with_implot", "Build with bundled implot source", ) orelse true, + .with_te = b.option( + bool, + "with_te", + "Build with bundled test engine support", + ) orelse false, + .use_wchar32 = b.option( + bool, + "use_wchar32", + "Extended unicode support", + ) orelse false, }; const options_step = b.addOptions(); @@ -34,7 +45,7 @@ pub fn build(b: *std.Build) void { const options_module = options_step.createModule(); _ = b.addModule("root", .{ - .root_source_file = .{ .path = "src/gui.zig" }, + .root_source_file = b.path("src/gui.zig"), .imports = &.{ .{ .name = "zgui_options", .module = options_module }, }, @@ -49,7 +60,6 @@ pub fn build(b: *std.Build) void { .optimize = optimize, }); - b.installArtifact(lib); if (target.result.os.tag == .windows) { lib.defineCMacro("IMGUI_API", "__declspec(dllexport)"); lib.defineCMacro("IMPLOT_API", "__declspec(dllexport)"); @@ -69,15 +79,15 @@ pub fn build(b: *std.Build) void { b.installArtifact(imgui); - imgui.addIncludePath(.{ .path = "libs" }); - imgui.addIncludePath(.{ .path = "libs/imgui" }); + imgui.addIncludePath(b.path("libs")); + imgui.addIncludePath(b.path("libs/imgui")); imgui.linkLibC(); if (target.result.abi != .msvc) imgui.linkLibCpp(); imgui.addCSourceFile(.{ - .file = .{ .path = "src/zgui.cpp" }, + .file = b.path("src/zgui.cpp"), .flags = cflags, }); @@ -106,12 +116,79 @@ pub fn build(b: *std.Build) void { imgui.defineCMacro("ZGUI_IMPLOT", "0"); } + if (options.use_wchar32) { + imgui.defineCMacro("IMGUI_USE_WCHAR32", "1"); + } + + if (options.with_te) { + imgui.defineCMacro("ZGUI_TE", "1"); + + imgui.defineCMacro("IMGUI_ENABLE_TEST_ENGINE", null); + imgui.defineCMacro("IMGUI_TEST_ENGINE_ENABLE_COROUTINE_STDTHREAD_IMPL", "1"); + + imgui.addIncludePath(b.path("libs/imgui_test_engine/")); + + imgui.addCSourceFile(.{ .file = b.path("libs/imgui_test_engine/imgui_capture_tool.cpp"), .flags = cflags }); + imgui.addCSourceFile(.{ .file = b.path("libs/imgui_test_engine/imgui_te_context.cpp"), .flags = cflags }); + imgui.addCSourceFile(.{ .file = b.path("libs/imgui_test_engine/imgui_te_coroutine.cpp"), .flags = cflags }); + imgui.addCSourceFile(.{ .file = b.path("libs/imgui_test_engine/imgui_te_engine.cpp"), .flags = cflags }); + imgui.addCSourceFile(.{ .file = b.path("libs/imgui_test_engine/imgui_te_exporters.cpp"), .flags = cflags }); + imgui.addCSourceFile(.{ .file = b.path("libs/imgui_test_engine/imgui_te_perftool.cpp"), .flags = cflags }); + imgui.addCSourceFile(.{ .file = b.path("libs/imgui_test_engine/imgui_te_ui.cpp"), .flags = cflags }); + imgui.addCSourceFile(.{ .file = b.path("libs/imgui_test_engine/imgui_te_utils.cpp"), .flags = cflags }); + + // TODO: Workaround because zig on win64 doesn have phtreads + // TODO: Implement corutine in zig can solve this + if (target.result.os.tag == .windows) { + const src: []const []const u8 = &.{ + "libs/winpthreads/src/nanosleep.c", + "libs/winpthreads/src/cond.c", + "libs/winpthreads/src/barrier.c", + "libs/winpthreads/src/misc.c", + "libs/winpthreads/src/clock.c", + "libs/winpthreads/src/libgcc/dll_math.c", + "libs/winpthreads/src/spinlock.c", + "libs/winpthreads/src/thread.c", + "libs/winpthreads/src/mutex.c", + "libs/winpthreads/src/sem.c", + "libs/winpthreads/src/sched.c", + "libs/winpthreads/src/ref.c", + "libs/winpthreads/src/rwlock.c", + }; + + const winpthreads = b.addStaticLibrary(.{ + .name = "winpthreads", + .optimize = optimize, + .target = target, + }); + winpthreads.want_lto = false; + winpthreads.root_module.sanitize_c = false; + if (optimize == .Debug or optimize == .ReleaseSafe) + winpthreads.bundle_compiler_rt = true + else + winpthreads.root_module.strip = true; + winpthreads.addCSourceFiles(.{ .files = src, .flags = &.{ + "-Wall", + "-Wextra", + } }); + winpthreads.defineCMacro("__USE_MINGW_ANSI_STDIO", "1"); + winpthreads.addIncludePath(b.path("libs/winpthreads/include")); + winpthreads.addIncludePath(b.path("libs/winpthreads/src")); + winpthreads.linkLibC(); + b.installArtifact(winpthreads); + imgui.linkLibrary(winpthreads); + imgui.addSystemIncludePath(b.path("libs/winpthreads/include")); + } + } else { + imgui.defineCMacro("ZGUI_TE", "0"); + } + switch (options.backend) { .glfw_wgpu => { const zglfw = b.dependency("zglfw", .{}); const zgpu = b.dependency("zgpu", .{}); - imgui.addIncludePath(.{ .path = zglfw.path("libs/glfw/include").getPath(b) }); - imgui.addIncludePath(.{ .path = zgpu.path("libs/dawn/include").getPath(b) }); + imgui.addIncludePath(zglfw.path("libs/glfw/include")); + imgui.addIncludePath(zgpu.path("libs/dawn/include")); imgui.addCSourceFiles(.{ .files = &.{ "libs/imgui/backends/imgui_impl_glfw.cpp", @@ -122,7 +199,7 @@ pub fn build(b: *std.Build) void { }, .glfw_opengl3 => { const zglfw = b.dependency("zglfw", .{}); - imgui.addIncludePath(.{ .path = zglfw.path("libs/glfw/include").getPath(b) }); + imgui.addIncludePath(zglfw.path("libs/glfw/include")); imgui.addCSourceFiles(.{ .files = &.{ "libs/imgui/backends/imgui_impl_glfw.cpp", @@ -133,7 +210,7 @@ pub fn build(b: *std.Build) void { }, .glfw_dx12 => { const zglfw = b.dependency("zglfw", .{}); - imgui.addIncludePath(.{ .path = zglfw.path("libs/glfw/include").getPath(b) }); + imgui.addIncludePath(zglfw.path("libs/glfw/include")); imgui.addCSourceFiles(.{ .files = &.{ "libs/imgui/backends/imgui_impl_glfw.cpp", @@ -153,6 +230,21 @@ pub fn build(b: *std.Build) void { }); imgui.linkSystemLibrary("d3dcompiler_47"); imgui.linkSystemLibrary("dwmapi"); + switch (target.result.abi) { + .msvc => imgui.linkSystemLibrary("Gdi32"), + .gnu => imgui.linkSystemLibrary("gdi32"), + else => {}, + } + }, + .glfw => { + const zglfw = b.dependency("zglfw", .{}); + imgui.addIncludePath(zglfw.path("libs/glfw/include")); + imgui.addCSourceFiles(.{ + .files = &.{ + "libs/imgui/backends/imgui_impl_glfw.cpp", + }, + .flags = cflags, + }); }, .no_backend => {}, } @@ -161,7 +253,7 @@ pub fn build(b: *std.Build) void { const tests = b.addTest(.{ .name = "zgui-tests", - .root_source_file = .{ .path = "src/gui.zig" }, + .root_source_file = b.path("src/gui.zig"), .target = target, .optimize = optimize, }); diff --git a/build.zig.zon b/build.zig.zon index 00a295a..2a766c5 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,6 +1,6 @@ .{ .name = "zgui", - .version = "0.1.0", + .version = "0.2.0", .paths = .{ "build.zig", "build.zig.zon", diff --git a/libs/imgui/backends/imgui_impl_dx12.cpp b/libs/imgui/backends/imgui_impl_dx12.cpp index 6f445d7..86cb043 100644 --- a/libs/imgui/backends/imgui_impl_dx12.cpp +++ b/libs/imgui/backends/imgui_impl_dx12.cpp @@ -3,7 +3,9 @@ // Implemented features: // [X] Renderer: User texture binding. Use 'D3D12_GPU_DESCRIPTOR_HANDLE' as ImTextureID. Read the FAQ about ImTextureID! -// [X] Renderer: Support for large meshes (64k+ vertices) with 16-bit indices. +// [X] Renderer: Large meshes support (64k+ vertices) with 16-bit indices. +// [X] Renderer: Multi-viewport support (multiple windows). Enable with 'io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable'. +// FIXME: The transition from removing a viewport and moving the window in an existing hosted viewport tends to flicker. // Important: to compile on 32-bit systems, this backend requires code to be compiled with '#define ImTextureID ImU64'. // This is because we need ImTextureID to carry a 64-bit value and by default ImTextureID is defined as void*. @@ -15,11 +17,15 @@ // You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this. // Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need. -// If you are new to Dear ImGui, read documentation from the docs/ folder + read the top of imgui.cpp. -// Read online: https://github.com/ocornut/imgui/tree/master/docs +// Learn about Dear ImGui: +// - FAQ https://dearimgui.com/faq +// - Getting Started https://dearimgui.com/getting-started +// - Documentation https://dearimgui.com/docs (same as your local docs/ folder). +// - Introduction, links and more at the top of imgui.cpp // CHANGELOG // (minor and older changes stripped away, please see git history for details) +// 2024-XX-XX: Platform: Added support for multiple windows via the ImGuiPlatformIO interface. // 2022-10-11: Using 'nullptr' instead of 'NULL' as per our switch to C++11. // 2021-06-29: Reorganized backend to pull data from a single structure to facilitate usage with multiple-contexts (all g_XXXX access changed to bd->XXXX). // 2021-05-19: DirectX12: Replaced direct access to ImDrawCmd::TextureId with a call to ImDrawCmd::GetTexID(). (will become a requirement) @@ -39,6 +45,7 @@ // 2018-02-22: Merged into master with all Win32 code synchronized to other examples. #include "imgui.h" +#ifndef IMGUI_DISABLE #include "imgui_impl_dx12.h" // DirectX @@ -47,43 +54,22 @@ #include #ifdef _MSC_VER #pragma comment(lib, "d3dcompiler") // Automatically link with d3dcompiler.lib as we are using D3DCompile() below. -#else // mziulek: PFN_D3D12_SERIALIZE_ROOT_SIGNATURE is not defined when using MinGW -typedef HRESULT (WINAPI* PFN_D3D12_SERIALIZE_ROOT_SIGNATURE)( - const D3D12_ROOT_SIGNATURE_DESC* pRootSignature, - D3D_ROOT_SIGNATURE_VERSION Version, - ID3DBlob** ppBlob, - ID3DBlob** ppErrorBlob); #endif // DirectX data -struct ImGui_ImplDX12_RenderBuffers -{ - ID3D12Resource* IndexBuffer; - ID3D12Resource* VertexBuffer; - int IndexBufferSize; - int VertexBufferSize; -}; - struct ImGui_ImplDX12_Data { - ID3D12Device* pd3dDevice; - ID3D12RootSignature* pRootSignature; - ID3D12PipelineState* pPipelineState; - DXGI_FORMAT RTVFormat; - ID3D12Resource* pFontTextureResource; - D3D12_CPU_DESCRIPTOR_HANDLE hFontSrvCpuDescHandle; - D3D12_GPU_DESCRIPTOR_HANDLE hFontSrvGpuDescHandle; + ID3D12Device* pd3dDevice; + ID3D12RootSignature* pRootSignature; + ID3D12PipelineState* pPipelineState; + DXGI_FORMAT RTVFormat; + ID3D12Resource* pFontTextureResource; + D3D12_CPU_DESCRIPTOR_HANDLE hFontSrvCpuDescHandle; + D3D12_GPU_DESCRIPTOR_HANDLE hFontSrvGpuDescHandle; + ID3D12DescriptorHeap* pd3dSrvDescHeap; + UINT numFramesInFlight; - ImGui_ImplDX12_RenderBuffers* pFrameResources; - UINT numFramesInFlight; - UINT frameIndex; - - ImGui_ImplDX12_Data() { memset((void*)this, 0, sizeof(*this)); frameIndex = UINT_MAX; } -}; - -struct VERTEX_CONSTANT_BUFFER_DX12 -{ - float mvp[4][4]; + ImGui_ImplDX12_Data() { memset((void*)this, 0, sizeof(*this)); } }; // Backend data stored in io.BackendRendererUserData to allow support for multiple Dear ImGui contexts @@ -93,6 +79,97 @@ static ImGui_ImplDX12_Data* ImGui_ImplDX12_GetBackendData() return ImGui::GetCurrentContext() ? (ImGui_ImplDX12_Data*)ImGui::GetIO().BackendRendererUserData : nullptr; } +// Buffers used during the rendering of a frame +struct ImGui_ImplDX12_RenderBuffers +{ + ID3D12Resource* IndexBuffer; + ID3D12Resource* VertexBuffer; + int IndexBufferSize; + int VertexBufferSize; +}; + +// Buffers used for secondary viewports created by the multi-viewports systems +struct ImGui_ImplDX12_FrameContext +{ + ID3D12CommandAllocator* CommandAllocator; + ID3D12Resource* RenderTarget; + D3D12_CPU_DESCRIPTOR_HANDLE RenderTargetCpuDescriptors; +}; + +// Helper structure we store in the void* RendererUserData field of each ImGuiViewport to easily retrieve our backend data. +// Main viewport created by application will only use the Resources field. +// Secondary viewports created by this backend will use all the fields (including Window fields), +struct ImGui_ImplDX12_ViewportData +{ + // Window + ID3D12CommandQueue* CommandQueue; + ID3D12GraphicsCommandList* CommandList; + ID3D12DescriptorHeap* RtvDescHeap; + IDXGISwapChain3* SwapChain; + ID3D12Fence* Fence; + UINT64 FenceSignaledValue; + HANDLE FenceEvent; + UINT NumFramesInFlight; + ImGui_ImplDX12_FrameContext* FrameCtx; + + // Render buffers + UINT FrameIndex; + ImGui_ImplDX12_RenderBuffers* FrameRenderBuffers; + + ImGui_ImplDX12_ViewportData(UINT num_frames_in_flight) + { + CommandQueue = nullptr; + CommandList = nullptr; + RtvDescHeap = nullptr; + SwapChain = nullptr; + Fence = nullptr; + FenceSignaledValue = 0; + FenceEvent = nullptr; + NumFramesInFlight = num_frames_in_flight; + FrameCtx = new ImGui_ImplDX12_FrameContext[NumFramesInFlight]; + FrameIndex = UINT_MAX; + FrameRenderBuffers = new ImGui_ImplDX12_RenderBuffers[NumFramesInFlight]; + + for (UINT i = 0; i < NumFramesInFlight; ++i) + { + FrameCtx[i].CommandAllocator = nullptr; + FrameCtx[i].RenderTarget = nullptr; + + // Create buffers with a default size (they will later be grown as needed) + FrameRenderBuffers[i].IndexBuffer = nullptr; + FrameRenderBuffers[i].VertexBuffer = nullptr; + FrameRenderBuffers[i].VertexBufferSize = 5000; + FrameRenderBuffers[i].IndexBufferSize = 10000; + } + } + ~ImGui_ImplDX12_ViewportData() + { + IM_ASSERT(CommandQueue == nullptr && CommandList == nullptr); + IM_ASSERT(RtvDescHeap == nullptr); + IM_ASSERT(SwapChain == nullptr); + IM_ASSERT(Fence == nullptr); + IM_ASSERT(FenceEvent == nullptr); + + for (UINT i = 0; i < NumFramesInFlight; ++i) + { + IM_ASSERT(FrameCtx[i].CommandAllocator == nullptr && FrameCtx[i].RenderTarget == nullptr); + IM_ASSERT(FrameRenderBuffers[i].IndexBuffer == nullptr && FrameRenderBuffers[i].VertexBuffer == nullptr); + } + + delete[] FrameCtx; FrameCtx = nullptr; + delete[] FrameRenderBuffers; FrameRenderBuffers = nullptr; + } +}; + +struct VERTEX_CONSTANT_BUFFER_DX12 +{ + float mvp[4][4]; +}; + +// Forward Declarations +static void ImGui_ImplDX12_InitPlatformInterface(); +static void ImGui_ImplDX12_ShutdownPlatformInterface(); + // Functions static void ImGui_ImplDX12_SetupRenderState(ImDrawData* draw_data, ID3D12GraphicsCommandList* ctx, ImGui_ImplDX12_RenderBuffers* fr) { @@ -166,11 +243,10 @@ void ImGui_ImplDX12_RenderDrawData(ImDrawData* draw_data, ID3D12GraphicsCommandL if (draw_data->DisplaySize.x <= 0.0f || draw_data->DisplaySize.y <= 0.0f) return; - // FIXME: I'm assuming that this only gets called once per frame! - // If not, we can't just re-allocate the IB or VB, we'll have to do a proper allocator. ImGui_ImplDX12_Data* bd = ImGui_ImplDX12_GetBackendData(); - bd->frameIndex = bd->frameIndex + 1; - ImGui_ImplDX12_RenderBuffers* fr = &bd->pFrameResources[bd->frameIndex % bd->numFramesInFlight]; + ImGui_ImplDX12_ViewportData* vd = (ImGui_ImplDX12_ViewportData*)draw_data->OwnerViewport->RendererUserData; + vd->FrameIndex++; + ImGui_ImplDX12_RenderBuffers* fr = &vd->FrameRenderBuffers[vd->FrameIndex % bd->numFramesInFlight]; // Create and grow vertex/index buffers if needed if (fr->VertexBuffer == nullptr || fr->VertexBufferSize < draw_data->TotalVtxCount) @@ -522,6 +598,19 @@ bool ImGui_ImplDX12_CreateDeviceObjects() return false; } +// fix(zig-gamedev): TODO workaround from https://github.com/ocornut/imgui/pull/4604/files +#ifdef __MINGW32__ + PFN_D3D12_SERIALIZE_VERSIONED_ROOT_SIGNATURE D3D12SerializeVersionedRootSignatureFn = + (PFN_D3D12_SERIALIZE_VERSIONED_ROOT_SIGNATURE)::GetProcAddress(d3d12_dll, "D3D12SerializeVersionedRootSignature"); + if (D3D12SerializeVersionedRootSignatureFn == nullptr) + return false; + + ID3DBlob* blob = nullptr; + D3D12_VERSIONED_ROOT_SIGNATURE_DESC versioned_desc = {.Version = D3D_ROOT_SIGNATURE_VERSION_1_0, .Desc_1_0 = desc}; + if (D3D12SerializeVersionedRootSignatureFn(&versioned_desc, &blob, nullptr) != S_OK) + return false; +#else + PFN_D3D12_SERIALIZE_ROOT_SIGNATURE D3D12SerializeRootSignatureFn = (PFN_D3D12_SERIALIZE_ROOT_SIGNATURE)::GetProcAddress(d3d12_dll, "D3D12SerializeRootSignature"); if (D3D12SerializeRootSignatureFn == nullptr) return false; @@ -529,6 +618,7 @@ bool ImGui_ImplDX12_CreateDeviceObjects() ID3DBlob* blob = nullptr; if (D3D12SerializeRootSignatureFn(&desc, D3D_ROOT_SIGNATURE_VERSION_1, &blob, nullptr) != S_OK) return false; +#endif bd->pd3dDevice->CreateRootSignature(0, blob->GetBufferPointer(), blob->GetBufferSize(), IID_PPV_ARGS(&bd->pRootSignature)); blob->Release(); @@ -591,9 +681,9 @@ bool ImGui_ImplDX12_CreateDeviceObjects() // Create the input layout static D3D12_INPUT_ELEMENT_DESC local_layout[] = { - { "POSITION", 0, DXGI_FORMAT_R32G32_FLOAT, 0, (UINT)IM_OFFSETOF(ImDrawVert, pos), D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }, - { "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, (UINT)IM_OFFSETOF(ImDrawVert, uv), D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }, - { "COLOR", 0, DXGI_FORMAT_R8G8B8A8_UNORM, 0, (UINT)IM_OFFSETOF(ImDrawVert, col), D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }, + { "POSITION", 0, DXGI_FORMAT_R32G32_FLOAT, 0, (UINT)offsetof(ImDrawVert, pos), D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }, + { "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, (UINT)offsetof(ImDrawVert, uv), D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }, + { "COLOR", 0, DXGI_FORMAT_R8G8B8A8_UNORM, 0, (UINT)offsetof(ImDrawVert, col), D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }, }; psoDesc.InputLayout = { local_layout, 3 }; } @@ -677,24 +767,24 @@ bool ImGui_ImplDX12_CreateDeviceObjects() return true; } +static void ImGui_ImplDX12_DestroyRenderBuffers(ImGui_ImplDX12_RenderBuffers* render_buffers) +{ + SafeRelease(render_buffers->IndexBuffer); + SafeRelease(render_buffers->VertexBuffer); + render_buffers->IndexBufferSize = render_buffers->VertexBufferSize = 0; +} + void ImGui_ImplDX12_InvalidateDeviceObjects() { ImGui_ImplDX12_Data* bd = ImGui_ImplDX12_GetBackendData(); if (!bd || !bd->pd3dDevice) return; - ImGuiIO& io = ImGui::GetIO(); + ImGuiIO& io = ImGui::GetIO(); SafeRelease(bd->pRootSignature); SafeRelease(bd->pPipelineState); SafeRelease(bd->pFontTextureResource); io.Fonts->SetTexID(0); // We copied bd->pFontTextureView to io.Fonts->TexID so let's clear that as well. - - for (UINT i = 0; i < bd->numFramesInFlight; i++) - { - ImGui_ImplDX12_RenderBuffers* fr = &bd->pFrameResources[i]; - SafeRelease(fr->IndexBuffer); - SafeRelease(fr->VertexBuffer); - } } bool ImGui_ImplDX12_Init(ID3D12Device* device, int num_frames_in_flight, DXGI_FORMAT rtv_format, ID3D12DescriptorHeap* cbv_srv_heap, @@ -708,25 +798,21 @@ bool ImGui_ImplDX12_Init(ID3D12Device* device, int num_frames_in_flight, DXGI_FO io.BackendRendererUserData = (void*)bd; io.BackendRendererName = "imgui_impl_dx12"; io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset; // We can honor the ImDrawCmd::VtxOffset field, allowing for large meshes. + io.BackendFlags |= ImGuiBackendFlags_RendererHasViewports; // We can create multi-viewports on the Renderer side (optional) + if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) + ImGui_ImplDX12_InitPlatformInterface(); bd->pd3dDevice = device; bd->RTVFormat = rtv_format; bd->hFontSrvCpuDescHandle = font_srv_cpu_desc_handle; bd->hFontSrvGpuDescHandle = font_srv_gpu_desc_handle; - bd->pFrameResources = new ImGui_ImplDX12_RenderBuffers[num_frames_in_flight]; bd->numFramesInFlight = num_frames_in_flight; - bd->frameIndex = UINT_MAX; - IM_UNUSED(cbv_srv_heap); // Unused in master branch (will be used by multi-viewports) + bd->pd3dSrvDescHeap = cbv_srv_heap; - // Create buffers with a default size (they will later be grown as needed) - for (int i = 0; i < num_frames_in_flight; i++) - { - ImGui_ImplDX12_RenderBuffers* fr = &bd->pFrameResources[i]; - fr->IndexBuffer = nullptr; - fr->VertexBuffer = nullptr; - fr->IndexBufferSize = 10000; - fr->VertexBufferSize = 5000; - } + // Create a dummy ImGui_ImplDX12_ViewportData holder for the main viewport, + // Since this is created and managed by the application, we will only use the ->Resources[] fields. + ImGuiViewport* main_viewport = ImGui::GetMainViewport(); + main_viewport->RendererUserData = IM_NEW(ImGui_ImplDX12_ViewportData)(bd->numFramesInFlight); return true; } @@ -737,10 +823,24 @@ void ImGui_ImplDX12_Shutdown() IM_ASSERT(bd != nullptr && "No renderer backend to shutdown, or already shutdown?"); ImGuiIO& io = ImGui::GetIO(); + // Manually delete main viewport render resources in-case we haven't initialized for viewports + ImGuiViewport* main_viewport = ImGui::GetMainViewport(); + if (ImGui_ImplDX12_ViewportData* vd = (ImGui_ImplDX12_ViewportData*)main_viewport->RendererUserData) + { + // We could just call ImGui_ImplDX12_DestroyWindow(main_viewport) as a convenience but that would be misleading since we only use data->Resources[] + for (UINT i = 0; i < bd->numFramesInFlight; i++) + ImGui_ImplDX12_DestroyRenderBuffers(&vd->FrameRenderBuffers[i]); + IM_DELETE(vd); + main_viewport->RendererUserData = nullptr; + } + + // Clean up windows and device objects + ImGui_ImplDX12_ShutdownPlatformInterface(); ImGui_ImplDX12_InvalidateDeviceObjects(); - delete[] bd->pFrameResources; + io.BackendRendererName = nullptr; io.BackendRendererUserData = nullptr; + io.BackendFlags &= ~(ImGuiBackendFlags_RendererHasVtxOffset | ImGuiBackendFlags_RendererHasViewports); IM_DELETE(bd); } @@ -752,3 +852,247 @@ void ImGui_ImplDX12_NewFrame() if (!bd->pPipelineState) ImGui_ImplDX12_CreateDeviceObjects(); } + +//-------------------------------------------------------------------------------------------------------- +// MULTI-VIEWPORT / PLATFORM INTERFACE SUPPORT +// This is an _advanced_ and _optional_ feature, allowing the backend to create and handle multiple viewports simultaneously. +// If you are new to dear imgui or creating a new binding for dear imgui, it is recommended that you completely ignore this section first.. +//-------------------------------------------------------------------------------------------------------- + +static void ImGui_ImplDX12_CreateWindow(ImGuiViewport* viewport) +{ + ImGui_ImplDX12_Data* bd = ImGui_ImplDX12_GetBackendData(); + ImGui_ImplDX12_ViewportData* vd = IM_NEW(ImGui_ImplDX12_ViewportData)(bd->numFramesInFlight); + viewport->RendererUserData = vd; + + // PlatformHandleRaw should always be a HWND, whereas PlatformHandle might be a higher-level handle (e.g. GLFWWindow*, SDL_Window*). + // Some backends will leave PlatformHandleRaw == 0, in which case we assume PlatformHandle will contain the HWND. + HWND hwnd = viewport->PlatformHandleRaw ? (HWND)viewport->PlatformHandleRaw : (HWND)viewport->PlatformHandle; + IM_ASSERT(hwnd != 0); + + vd->FrameIndex = UINT_MAX; + + // Create command queue. + D3D12_COMMAND_QUEUE_DESC queue_desc = {}; + queue_desc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE; + queue_desc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT; + + HRESULT res = S_OK; + res = bd->pd3dDevice->CreateCommandQueue(&queue_desc, IID_PPV_ARGS(&vd->CommandQueue)); + IM_ASSERT(res == S_OK); + + // Create command allocator. + for (UINT i = 0; i < bd->numFramesInFlight; ++i) + { + res = bd->pd3dDevice->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&vd->FrameCtx[i].CommandAllocator)); + IM_ASSERT(res == S_OK); + } + + // Create command list. + res = bd->pd3dDevice->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, vd->FrameCtx[0].CommandAllocator, nullptr, IID_PPV_ARGS(&vd->CommandList)); + IM_ASSERT(res == S_OK); + vd->CommandList->Close(); + + // Create fence. + res = bd->pd3dDevice->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&vd->Fence)); + IM_ASSERT(res == S_OK); + + vd->FenceEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr); + IM_ASSERT(vd->FenceEvent != nullptr); + + // Create swap chain + // FIXME-VIEWPORT: May want to copy/inherit swap chain settings from the user/application. + DXGI_SWAP_CHAIN_DESC1 sd1; + ZeroMemory(&sd1, sizeof(sd1)); + sd1.BufferCount = bd->numFramesInFlight; + sd1.Width = (UINT)viewport->Size.x; + sd1.Height = (UINT)viewport->Size.y; + sd1.Format = bd->RTVFormat; + sd1.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; + sd1.SampleDesc.Count = 1; + sd1.SampleDesc.Quality = 0; + sd1.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD; + sd1.AlphaMode = DXGI_ALPHA_MODE_UNSPECIFIED; + sd1.Scaling = DXGI_SCALING_NONE; + sd1.Stereo = FALSE; + + IDXGIFactory4* dxgi_factory = nullptr; + res = ::CreateDXGIFactory1(IID_PPV_ARGS(&dxgi_factory)); + IM_ASSERT(res == S_OK); + + IDXGISwapChain1* swap_chain = nullptr; + res = dxgi_factory->CreateSwapChainForHwnd(vd->CommandQueue, hwnd, &sd1, nullptr, nullptr, &swap_chain); + IM_ASSERT(res == S_OK); + + dxgi_factory->Release(); + + // Or swapChain.As(&mSwapChain) + IM_ASSERT(vd->SwapChain == nullptr); + swap_chain->QueryInterface(IID_PPV_ARGS(&vd->SwapChain)); + swap_chain->Release(); + + // Create the render targets + if (vd->SwapChain) + { + D3D12_DESCRIPTOR_HEAP_DESC desc = {}; + desc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV; + desc.NumDescriptors = bd->numFramesInFlight; + desc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE; + desc.NodeMask = 1; + + HRESULT hr = bd->pd3dDevice->CreateDescriptorHeap(&desc, IID_PPV_ARGS(&vd->RtvDescHeap)); + IM_ASSERT(hr == S_OK); + + SIZE_T rtv_descriptor_size = bd->pd3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV); + D3D12_CPU_DESCRIPTOR_HANDLE rtv_handle = vd->RtvDescHeap->GetCPUDescriptorHandleForHeapStart(); + for (UINT i = 0; i < bd->numFramesInFlight; i++) + { + vd->FrameCtx[i].RenderTargetCpuDescriptors = rtv_handle; + rtv_handle.ptr += rtv_descriptor_size; + } + + ID3D12Resource* back_buffer; + for (UINT i = 0; i < bd->numFramesInFlight; i++) + { + IM_ASSERT(vd->FrameCtx[i].RenderTarget == nullptr); + vd->SwapChain->GetBuffer(i, IID_PPV_ARGS(&back_buffer)); + bd->pd3dDevice->CreateRenderTargetView(back_buffer, nullptr, vd->FrameCtx[i].RenderTargetCpuDescriptors); + vd->FrameCtx[i].RenderTarget = back_buffer; + } + } + + for (UINT i = 0; i < bd->numFramesInFlight; i++) + ImGui_ImplDX12_DestroyRenderBuffers(&vd->FrameRenderBuffers[i]); +} + +static void ImGui_WaitForPendingOperations(ImGui_ImplDX12_ViewportData* vd) +{ + HRESULT hr = S_FALSE; + if (vd && vd->CommandQueue && vd->Fence && vd->FenceEvent) + { + hr = vd->CommandQueue->Signal(vd->Fence, ++vd->FenceSignaledValue); + IM_ASSERT(hr == S_OK); + ::WaitForSingleObject(vd->FenceEvent, 0); // Reset any forgotten waits + hr = vd->Fence->SetEventOnCompletion(vd->FenceSignaledValue, vd->FenceEvent); + IM_ASSERT(hr == S_OK); + ::WaitForSingleObject(vd->FenceEvent, INFINITE); + } +} + +static void ImGui_ImplDX12_DestroyWindow(ImGuiViewport* viewport) +{ + // The main viewport (owned by the application) will always have RendererUserData == 0 since we didn't create the data for it. + ImGui_ImplDX12_Data* bd = ImGui_ImplDX12_GetBackendData(); + if (ImGui_ImplDX12_ViewportData* vd = (ImGui_ImplDX12_ViewportData*)viewport->RendererUserData) + { + ImGui_WaitForPendingOperations(vd); + + SafeRelease(vd->CommandQueue); + SafeRelease(vd->CommandList); + SafeRelease(vd->SwapChain); + SafeRelease(vd->RtvDescHeap); + SafeRelease(vd->Fence); + ::CloseHandle(vd->FenceEvent); + vd->FenceEvent = nullptr; + + for (UINT i = 0; i < bd->numFramesInFlight; i++) + { + SafeRelease(vd->FrameCtx[i].RenderTarget); + SafeRelease(vd->FrameCtx[i].CommandAllocator); + ImGui_ImplDX12_DestroyRenderBuffers(&vd->FrameRenderBuffers[i]); + } + IM_DELETE(vd); + } + viewport->RendererUserData = nullptr; +} + +static void ImGui_ImplDX12_SetWindowSize(ImGuiViewport* viewport, ImVec2 size) +{ + ImGui_ImplDX12_Data* bd = ImGui_ImplDX12_GetBackendData(); + ImGui_ImplDX12_ViewportData* vd = (ImGui_ImplDX12_ViewportData*)viewport->RendererUserData; + + ImGui_WaitForPendingOperations(vd); + + for (UINT i = 0; i < bd->numFramesInFlight; i++) + SafeRelease(vd->FrameCtx[i].RenderTarget); + + if (vd->SwapChain) + { + ID3D12Resource* back_buffer = nullptr; + vd->SwapChain->ResizeBuffers(0, (UINT)size.x, (UINT)size.y, DXGI_FORMAT_UNKNOWN, 0); + for (UINT i = 0; i < bd->numFramesInFlight; i++) + { + vd->SwapChain->GetBuffer(i, IID_PPV_ARGS(&back_buffer)); + bd->pd3dDevice->CreateRenderTargetView(back_buffer, nullptr, vd->FrameCtx[i].RenderTargetCpuDescriptors); + vd->FrameCtx[i].RenderTarget = back_buffer; + } + } +} + +static void ImGui_ImplDX12_RenderWindow(ImGuiViewport* viewport, void*) +{ + ImGui_ImplDX12_Data* bd = ImGui_ImplDX12_GetBackendData(); + ImGui_ImplDX12_ViewportData* vd = (ImGui_ImplDX12_ViewportData*)viewport->RendererUserData; + + ImGui_ImplDX12_FrameContext* frame_context = &vd->FrameCtx[vd->FrameIndex % bd->numFramesInFlight]; + UINT back_buffer_idx = vd->SwapChain->GetCurrentBackBufferIndex(); + + const ImVec4 clear_color = ImVec4(0.0f, 0.0f, 0.0f, 1.0f); + D3D12_RESOURCE_BARRIER barrier = {}; + barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; + barrier.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE; + barrier.Transition.pResource = vd->FrameCtx[back_buffer_idx].RenderTarget; + barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; + barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_PRESENT; + barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_RENDER_TARGET; + + // Draw + ID3D12GraphicsCommandList* cmd_list = vd->CommandList; + + frame_context->CommandAllocator->Reset(); + cmd_list->Reset(frame_context->CommandAllocator, nullptr); + cmd_list->ResourceBarrier(1, &barrier); + cmd_list->OMSetRenderTargets(1, &vd->FrameCtx[back_buffer_idx].RenderTargetCpuDescriptors, FALSE, nullptr); + if (!(viewport->Flags & ImGuiViewportFlags_NoRendererClear)) + cmd_list->ClearRenderTargetView(vd->FrameCtx[back_buffer_idx].RenderTargetCpuDescriptors, (float*)&clear_color, 0, nullptr); + cmd_list->SetDescriptorHeaps(1, &bd->pd3dSrvDescHeap); + + ImGui_ImplDX12_RenderDrawData(viewport->DrawData, cmd_list); + + barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_RENDER_TARGET; + barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_PRESENT; + cmd_list->ResourceBarrier(1, &barrier); + cmd_list->Close(); + + vd->CommandQueue->Wait(vd->Fence, vd->FenceSignaledValue); + vd->CommandQueue->ExecuteCommandLists(1, (ID3D12CommandList* const*)&cmd_list); + vd->CommandQueue->Signal(vd->Fence, ++vd->FenceSignaledValue); +} + +static void ImGui_ImplDX12_SwapBuffers(ImGuiViewport* viewport, void*) +{ + ImGui_ImplDX12_ViewportData* vd = (ImGui_ImplDX12_ViewportData*)viewport->RendererUserData; + + vd->SwapChain->Present(0, 0); + while (vd->Fence->GetCompletedValue() < vd->FenceSignaledValue) + ::SwitchToThread(); +} + +void ImGui_ImplDX12_InitPlatformInterface() +{ + ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO(); + platform_io.Renderer_CreateWindow = ImGui_ImplDX12_CreateWindow; + platform_io.Renderer_DestroyWindow = ImGui_ImplDX12_DestroyWindow; + platform_io.Renderer_SetWindowSize = ImGui_ImplDX12_SetWindowSize; + platform_io.Renderer_RenderWindow = ImGui_ImplDX12_RenderWindow; + platform_io.Renderer_SwapBuffers = ImGui_ImplDX12_SwapBuffers; +} + +void ImGui_ImplDX12_ShutdownPlatformInterface() +{ + ImGui::DestroyPlatformWindows(); +} + +//----------------------------------------------------------------------------- + +#endif // #ifndef IMGUI_DISABLE diff --git a/libs/imgui/backends/imgui_impl_dx12.h b/libs/imgui/backends/imgui_impl_dx12.h index da62dd6..1db5d49 100644 --- a/libs/imgui/backends/imgui_impl_dx12.h +++ b/libs/imgui/backends/imgui_impl_dx12.h @@ -3,18 +3,23 @@ // Implemented features: // [X] Renderer: User texture binding. Use 'D3D12_GPU_DESCRIPTOR_HANDLE' as ImTextureID. Read the FAQ about ImTextureID! -// [X] Renderer: Support for large meshes (64k+ vertices) with 16-bit indices. +// [X] Renderer: Large meshes support (64k+ vertices) with 16-bit indices. +// [X] Renderer: Multi-viewport support (multiple windows). Enable with 'io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable'. // Important: to compile on 32-bit systems, this backend requires code to be compiled with '#define ImTextureID ImU64'. // See imgui_impl_dx12.cpp file for details. // You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this. // Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need. -// If you are new to Dear ImGui, read documentation from the docs/ folder + read the top of imgui.cpp. -// Read online: https://github.com/ocornut/imgui/tree/master/docs +// Learn about Dear ImGui: +// - FAQ https://dearimgui.com/faq +// - Getting Started https://dearimgui.com/getting-started +// - Documentation https://dearimgui.com/docs (same as your local docs/ folder). +// - Introduction, links and more at the top of imgui.cpp #pragma once #include "imgui.h" // IMGUI_IMPL_API +#ifndef IMGUI_DISABLE #include // DXGI_FORMAT struct ID3D12Device; @@ -23,7 +28,8 @@ struct ID3D12GraphicsCommandList; struct D3D12_CPU_DESCRIPTOR_HANDLE; struct D3D12_GPU_DESCRIPTOR_HANDLE; -extern "C" { // mziulek +// FIX(zig-gamedev) +extern "C" { // cmd_list is the command list that the implementation will use to render imgui draw lists. // Before calling the render function, caller must prepare cmd_list by resetting it and setting the appropriate @@ -40,3 +46,5 @@ IMGUI_IMPL_API void ImGui_ImplDX12_InvalidateDeviceObjects(); IMGUI_IMPL_API bool ImGui_ImplDX12_CreateDeviceObjects(); } + +#endif // #ifndef IMGUI_DISABLE diff --git a/libs/imgui/backends/imgui_impl_glfw.cpp b/libs/imgui/backends/imgui_impl_glfw.cpp index 9e2fd65..c859946 100644 --- a/libs/imgui/backends/imgui_impl_glfw.cpp +++ b/libs/imgui/backends/imgui_impl_glfw.cpp @@ -1,31 +1,49 @@ // dear imgui: Platform Backend for GLFW // This needs to be used along with a Renderer (e.g. OpenGL3, Vulkan, WebGPU..) // (Info: GLFW is a cross-platform general purpose library for handling windows, inputs, OpenGL/Vulkan graphics context creation, etc.) -// (Requires: GLFW 3.1+) +// (Requires: GLFW 3.1+. Prefer GLFW 3.3+ or GLFW 3.4+ for full feature support.) // Implemented features: // [X] Platform: Clipboard support. +// [X] Platform: Mouse support. Can discriminate Mouse/TouchScreen/Pen (Windows only). // [X] Platform: Keyboard support. Since 1.87 we are using the io.AddKeyEvent() function. Pass ImGuiKey values to all key functions e.g. ImGui::IsKeyPressed(ImGuiKey_Space). [Legacy GLFW_KEY_* values will also be supported unless IMGUI_DISABLE_OBSOLETE_KEYIO is set] // [X] Platform: Gamepad support. Enable with 'io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad'. // [X] Platform: Mouse cursor shape and visibility. Disable with 'io.ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange' (note: the resizing cursors requires GLFW 3.4+). +// [X] Platform: Multi-viewport support (multiple windows). Enable with 'io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable'. + +// Issues: +// [ ] Platform: Multi-viewport support: ParentViewportID not honored, and so io.ConfigViewportsNoDefaultParent has no effect (minor). // You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this. // Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need. -// If you are new to Dear ImGui, read documentation from the docs/ folder + read the top of imgui.cpp. -// Read online: https://github.com/ocornut/imgui/tree/master/docs +// Learn about Dear ImGui: +// - FAQ https://dearimgui.com/faq +// - Getting Started https://dearimgui.com/getting-started +// - Documentation https://dearimgui.com/docs (same as your local docs/ folder). +// - Introduction, links and more at the top of imgui.cpp // CHANGELOG // (minor and older changes stripped away, please see git history for details) +// 2024-XX-XX: Platform: Added support for multiple windows via the ImGuiPlatformIO interface. +// 2023-12-19: Emscripten: Added ImGui_ImplGlfw_InstallEmscriptenCanvasResizeCallback() to register canvas selector and auto-resize GLFW window. +// 2023-10-05: Inputs: Added support for extra ImGuiKey values: F13 to F24 function keys. +// 2023-07-18: Inputs: Revert ignoring mouse data on GLFW_CURSOR_DISABLED as it can be used differently. User may set ImGuiConfigFLags_NoMouse if desired. (#5625, #6609) +// 2023-06-12: Accept glfwGetTime() not returning a monotonically increasing value. This seems to happens on some Windows setup when peripherals disconnect, and is likely to also happen on browser + Emscripten. (#6491) +// 2023-04-04: Inputs: Added support for io.AddMouseSourceEvent() to discriminate ImGuiMouseSource_Mouse/ImGuiMouseSource_TouchScreen/ImGuiMouseSource_Pen on Windows ONLY, using a custom WndProc hook. (#2702) +// 2023-03-16: Inputs: Fixed key modifiers handling on secondary viewports (docking branch). Broken on 2023/01/04. (#6248, #6034) +// 2023-03-14: Emscripten: Avoid using glfwGetError() and glfwGetGamepadState() which are not correctly implemented in Emscripten emulation. (#6240) +// 2023-02-03: Emscripten: Registering custom low-level mouse wheel handler to get more accurate scrolling impulses on Emscripten. (#4019, #6096) +// 2023-01-18: Handle unsupported glfwGetVideoMode() call on e.g. Emscripten. // 2023-01-04: Inputs: Fixed mods state on Linux when using Alt-GR text input (e.g. German keyboard layout), could lead to broken text input. Revert a 2022/01/17 change were we resumed using mods provided by GLFW, turns out they were faulty. // 2022-11-22: Perform a dummy glfwGetError() read to cancel missing names with glfwGetKeyName(). (#5908) // 2022-10-18: Perform a dummy glfwGetError() read to cancel missing mouse cursors errors. Using GLFW_VERSION_COMBINED directly. (#5785) // 2022-10-11: Using 'nullptr' instead of 'NULL' as per our switch to C++11. // 2022-09-26: Inputs: Renamed ImGuiKey_ModXXX introduced in 1.87 to ImGuiMod_XXX (old names still supported). -// 2022-09-01: Inputs: Honor GLFW_CURSOR_DISABLED by not setting mouse position. +// 2022-09-01: Inputs: Honor GLFW_CURSOR_DISABLED by not setting mouse position *EDIT* Reverted 2023-07-18. // 2022-04-30: Inputs: Fixed ImGui_ImplGlfw_TranslateUntranslatedKey() for lower case letters on OSX. // 2022-03-23: Inputs: Fixed a regression in 1.87 which resulted in keyboard modifiers events being reported incorrectly on Linux/X11. // 2022-02-07: Added ImGui_ImplGlfw_InstallCallbacks()/ImGui_ImplGlfw_RestoreCallbacks() helpers to facilitate user installing callbacks after initializing backend. -// 2022-01-26: Inputs: replaced short-lived io.AddKeyModsEvent() (added two weeks ago)with io.AddKeyEvent() using ImGuiKey_ModXXX flags. Sorry for the confusion. +// 2022-01-26: Inputs: replaced short-lived io.AddKeyModsEvent() (added two weeks ago) with io.AddKeyEvent() using ImGuiKey_ModXXX flags. Sorry for the confusion. // 2021-01-20: Inputs: calling new io.AddKeyAnalogEvent() for gamepad support, instead of writing directly to io.NavInputs[]. // 2022-01-17: Inputs: calling new io.AddMousePosEvent(), io.AddMouseButtonEvent(), io.AddMouseWheelEvent() API (1.87+). // 2022-01-17: Inputs: always update key mods next and before key event (not in NewFrame) to fix input queue with very low framerates. @@ -58,6 +76,9 @@ // 2016-10-15: Misc: Added a void* user_data parameter to Clipboard function handlers. #include "imgui.h" +#ifndef IMGUI_DISABLE +// FIX(zig-gamedev): +//#include "imgui_impl_glfw.h" // Clang warnings with -Weverything #if defined(__clang__) @@ -69,24 +90,53 @@ // GLFW #define GLFW_INCLUDE_NONE #include + #ifdef _WIN32 #undef APIENTRY #define GLFW_EXPOSE_NATIVE_WIN32 -#include // for glfwGetWin32Window +#include // for glfwGetWin32Window() +#endif +#ifdef __APPLE__ +#define GLFW_EXPOSE_NATIVE_COCOA +#include // for glfwGetCocoaWindow() +#endif + +#ifdef __EMSCRIPTEN__ +#include +#include #endif // We gather version tests as define in order to easily see which features are version-dependent. #define GLFW_VERSION_COMBINED (GLFW_VERSION_MAJOR * 1000 + GLFW_VERSION_MINOR * 100 + GLFW_VERSION_REVISION) +#define GLFW_HAS_WINDOW_TOPMOST (GLFW_VERSION_COMBINED >= 3200) // 3.2+ GLFW_FLOATING +#define GLFW_HAS_WINDOW_HOVERED (GLFW_VERSION_COMBINED >= 3300) // 3.3+ GLFW_HOVERED +#define GLFW_HAS_WINDOW_ALPHA (GLFW_VERSION_COMBINED >= 3300) // 3.3+ glfwSetWindowOpacity +#define GLFW_HAS_PER_MONITOR_DPI (GLFW_VERSION_COMBINED >= 3300) // 3.3+ glfwGetMonitorContentScale +#if defined(__EMSCRIPTEN__) || defined(__SWITCH__) // no Vulkan support in GLFW for Emscripten or homebrew Nintendo Switch +#define GLFW_HAS_VULKAN (0) +#else +#define GLFW_HAS_VULKAN (GLFW_VERSION_COMBINED >= 3200) // 3.2+ glfwCreateWindowSurface +#endif +#define GLFW_HAS_FOCUS_WINDOW (GLFW_VERSION_COMBINED >= 3200) // 3.2+ glfwFocusWindow +#define GLFW_HAS_FOCUS_ON_SHOW (GLFW_VERSION_COMBINED >= 3300) // 3.3+ GLFW_FOCUS_ON_SHOW +#define GLFW_HAS_MONITOR_WORK_AREA (GLFW_VERSION_COMBINED >= 3300) // 3.3+ glfwGetMonitorWorkarea +#define GLFW_HAS_OSX_WINDOW_POS_FIX (GLFW_VERSION_COMBINED >= 3301) // 3.3.1+ Fixed: Resizing window repositions it on MacOS #1553 #ifdef GLFW_RESIZE_NESW_CURSOR // Let's be nice to people who pulled GLFW between 2019-04-16 (3.4 define) and 2019-11-29 (cursors defines) // FIXME: Remove when GLFW 3.4 is released? #define GLFW_HAS_NEW_CURSORS (GLFW_VERSION_COMBINED >= 3400) // 3.4+ GLFW_RESIZE_ALL_CURSOR, GLFW_RESIZE_NESW_CURSOR, GLFW_RESIZE_NWSE_CURSOR, GLFW_NOT_ALLOWED_CURSOR #else #define GLFW_HAS_NEW_CURSORS (0) #endif +#ifdef GLFW_MOUSE_PASSTHROUGH // Let's be nice to people who pulled GLFW between 2019-04-16 (3.4 define) and 2020-07-17 (passthrough) +#define GLFW_HAS_MOUSE_PASSTHROUGH (GLFW_VERSION_COMBINED >= 3400) // 3.4+ GLFW_MOUSE_PASSTHROUGH +#else +#define GLFW_HAS_MOUSE_PASSTHROUGH (0) +#endif #define GLFW_HAS_GAMEPAD_API (GLFW_VERSION_COMBINED >= 3300) // 3.3+ glfwGetGamepadState() new api -#define GLFW_HAS_GETKEYNAME (GLFW_VERSION_COMBINED >= 3200) // 3.2+ glfwGetKeyName() // - +#define GLFW_HAS_GETKEYNAME (GLFW_VERSION_COMBINED >= 3200) // 3.2+ glfwGetKeyName() +#define GLFW_HAS_GETERROR (GLFW_VERSION_COMBINED >= 3300) // 3.3+ glfwGetError() #include +// FIX(zig-gamedev): extern "C" { bool ImGui_ImplGlfw_InitForOpenGL(GLFWwindow* window, bool install_callbacks); @@ -118,7 +168,7 @@ enum GlfwClientApi { GlfwClientApi_Unknown, GlfwClientApi_OpenGL, - GlfwClientApi_Vulkan + GlfwClientApi_Vulkan, }; struct ImGui_ImplGlfw_Data @@ -129,11 +179,16 @@ struct ImGui_ImplGlfw_Data GLFWwindow* MouseWindow; GLFWcursor* MouseCursors[ImGuiMouseCursor_COUNT]; ImVec2 LastValidMousePos; + GLFWwindow* KeyOwnerWindows[GLFW_KEY_LAST]; bool InstalledCallbacks; + bool CallbacksChainForAllWindows; + bool WantUpdateMonitors; + + ImVec2 DpiScale; // fix(zig-gamedev) - ImVec2 DpiScale; // mziulek: We scale mouse coords so that we have pixel coords on all OSes. - // This also lets us set `FramebufferScale` to 1.0 and always work with pixel coords - // even on macOS Retina displays. +#ifdef __EMSCRIPTEN__ + const char* CanvasSelector; +#endif // Chain GLFW callbacks: our callbacks will call the user's previously installed callbacks, if any. GLFWwindowfocusfun PrevUserCallbackWindowFocus; @@ -144,6 +199,9 @@ struct ImGui_ImplGlfw_Data GLFWkeyfun PrevUserCallbackKey; GLFWcharfun PrevUserCallbackChar; GLFWmonitorfun PrevUserCallbackMonitor; +#ifdef _WIN32 + WNDPROC PrevWndProc; +#endif ImGui_ImplGlfw_Data() { memset((void*)this, 0, sizeof(*this)); } }; @@ -160,6 +218,11 @@ static ImGui_ImplGlfw_Data* ImGui_ImplGlfw_GetBackendData() return ImGui::GetCurrentContext() ? (ImGui_ImplGlfw_Data*)ImGui::GetIO().BackendPlatformUserData : nullptr; } +// Forward Declarations +static void ImGui_ImplGlfw_UpdateMonitors(); +static void ImGui_ImplGlfw_InitPlatformInterface(); +static void ImGui_ImplGlfw_ShutdownPlatformInterface(); + // Functions static const char* ImGui_ImplGlfw_GetClipboardText(void* user_data) { @@ -280,29 +343,46 @@ static ImGuiKey ImGui_ImplGlfw_KeyToImGuiKey(int key) case GLFW_KEY_F10: return ImGuiKey_F10; case GLFW_KEY_F11: return ImGuiKey_F11; case GLFW_KEY_F12: return ImGuiKey_F12; + case GLFW_KEY_F13: return ImGuiKey_F13; + case GLFW_KEY_F14: return ImGuiKey_F14; + case GLFW_KEY_F15: return ImGuiKey_F15; + case GLFW_KEY_F16: return ImGuiKey_F16; + case GLFW_KEY_F17: return ImGuiKey_F17; + case GLFW_KEY_F18: return ImGuiKey_F18; + case GLFW_KEY_F19: return ImGuiKey_F19; + case GLFW_KEY_F20: return ImGuiKey_F20; + case GLFW_KEY_F21: return ImGuiKey_F21; + case GLFW_KEY_F22: return ImGuiKey_F22; + case GLFW_KEY_F23: return ImGuiKey_F23; + case GLFW_KEY_F24: return ImGuiKey_F24; default: return ImGuiKey_None; } } // X11 does not include current pressed/released modifier key in 'mods' flags submitted by GLFW // See https://github.com/ocornut/imgui/issues/6034 and https://github.com/glfw/glfw/issues/1630 -static void ImGui_ImplGlfw_UpdateKeyModifiers() +static void ImGui_ImplGlfw_UpdateKeyModifiers(GLFWwindow* window) { ImGuiIO& io = ImGui::GetIO(); + io.AddKeyEvent(ImGuiMod_Ctrl, (glfwGetKey(window, GLFW_KEY_LEFT_CONTROL) == GLFW_PRESS) || (glfwGetKey(window, GLFW_KEY_RIGHT_CONTROL) == GLFW_PRESS)); + io.AddKeyEvent(ImGuiMod_Shift, (glfwGetKey(window, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS) || (glfwGetKey(window, GLFW_KEY_RIGHT_SHIFT) == GLFW_PRESS)); + io.AddKeyEvent(ImGuiMod_Alt, (glfwGetKey(window, GLFW_KEY_LEFT_ALT) == GLFW_PRESS) || (glfwGetKey(window, GLFW_KEY_RIGHT_ALT) == GLFW_PRESS)); + io.AddKeyEvent(ImGuiMod_Super, (glfwGetKey(window, GLFW_KEY_LEFT_SUPER) == GLFW_PRESS) || (glfwGetKey(window, GLFW_KEY_RIGHT_SUPER) == GLFW_PRESS)); +} + +static bool ImGui_ImplGlfw_ShouldChainCallback(GLFWwindow* window) +{ ImGui_ImplGlfw_Data* bd = ImGui_ImplGlfw_GetBackendData(); - io.AddKeyEvent(ImGuiMod_Ctrl, (glfwGetKey(bd->Window, GLFW_KEY_LEFT_CONTROL) == GLFW_PRESS) || (glfwGetKey(bd->Window, GLFW_KEY_RIGHT_CONTROL) == GLFW_PRESS)); - io.AddKeyEvent(ImGuiMod_Shift, (glfwGetKey(bd->Window, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS) || (glfwGetKey(bd->Window, GLFW_KEY_RIGHT_SHIFT) == GLFW_PRESS)); - io.AddKeyEvent(ImGuiMod_Alt, (glfwGetKey(bd->Window, GLFW_KEY_LEFT_ALT) == GLFW_PRESS) || (glfwGetKey(bd->Window, GLFW_KEY_RIGHT_ALT) == GLFW_PRESS)); - io.AddKeyEvent(ImGuiMod_Super, (glfwGetKey(bd->Window, GLFW_KEY_LEFT_SUPER) == GLFW_PRESS) || (glfwGetKey(bd->Window, GLFW_KEY_RIGHT_SUPER) == GLFW_PRESS)); + return bd->CallbacksChainForAllWindows ? true : (window == bd->Window); } void ImGui_ImplGlfw_MouseButtonCallback(GLFWwindow* window, int button, int action, int mods) { ImGui_ImplGlfw_Data* bd = ImGui_ImplGlfw_GetBackendData(); - if (bd->PrevUserCallbackMousebutton != nullptr && window == bd->Window) + if (bd->PrevUserCallbackMousebutton != nullptr && ImGui_ImplGlfw_ShouldChainCallback(window)) bd->PrevUserCallbackMousebutton(window, button, action, mods); - ImGui_ImplGlfw_UpdateKeyModifiers(); + ImGui_ImplGlfw_UpdateKeyModifiers(window); ImGuiIO& io = ImGui::GetIO(); if (button >= 0 && button < ImGuiMouseButton_COUNT) @@ -312,9 +392,14 @@ void ImGui_ImplGlfw_MouseButtonCallback(GLFWwindow* window, int button, int acti void ImGui_ImplGlfw_ScrollCallback(GLFWwindow* window, double xoffset, double yoffset) { ImGui_ImplGlfw_Data* bd = ImGui_ImplGlfw_GetBackendData(); - if (bd->PrevUserCallbackScroll != nullptr && window == bd->Window) + if (bd->PrevUserCallbackScroll != nullptr && ImGui_ImplGlfw_ShouldChainCallback(window)) bd->PrevUserCallbackScroll(window, xoffset, yoffset); +#ifdef __EMSCRIPTEN__ + // Ignore GLFW events: will be processed in ImGui_ImplEmscripten_WheelCallback(). + return; +#endif + ImGuiIO& io = ImGui::GetIO(); io.AddMouseWheelEvent((float)xoffset, (float)yoffset); } @@ -332,8 +417,8 @@ static int ImGui_ImplGlfw_TranslateUntranslatedKey(int key, int scancode) GLFWerrorfun prev_error_callback = glfwSetErrorCallback(nullptr); const char* key_name = glfwGetKeyName(key, scancode); glfwSetErrorCallback(prev_error_callback); -#if (GLFW_VERSION_COMBINED >= 3300) // Eat errors (see #5908) - (void)glfwGetError(NULL); +#if GLFW_HAS_GETERROR && !defined(__EMSCRIPTEN__) // Eat errors (see #5908) + (void)glfwGetError(nullptr); #endif if (key_name && key_name[0] != 0 && key_name[1] == 0) { @@ -355,13 +440,16 @@ static int ImGui_ImplGlfw_TranslateUntranslatedKey(int key, int scancode) void ImGui_ImplGlfw_KeyCallback(GLFWwindow* window, int keycode, int scancode, int action, int mods) { ImGui_ImplGlfw_Data* bd = ImGui_ImplGlfw_GetBackendData(); - if (bd->PrevUserCallbackKey != nullptr && window == bd->Window) + if (bd->PrevUserCallbackKey != nullptr && ImGui_ImplGlfw_ShouldChainCallback(window)) bd->PrevUserCallbackKey(window, keycode, scancode, action, mods); if (action != GLFW_PRESS && action != GLFW_RELEASE) return; - ImGui_ImplGlfw_UpdateKeyModifiers(); + ImGui_ImplGlfw_UpdateKeyModifiers(window); + + if (keycode >= 0 && keycode < IM_ARRAYSIZE(bd->KeyOwnerWindows)) + bd->KeyOwnerWindows[keycode] = (action == GLFW_PRESS) ? window : nullptr; keycode = ImGui_ImplGlfw_TranslateUntranslatedKey(keycode, scancode); @@ -374,7 +462,7 @@ void ImGui_ImplGlfw_KeyCallback(GLFWwindow* window, int keycode, int scancode, i void ImGui_ImplGlfw_WindowFocusCallback(GLFWwindow* window, int focused) { ImGui_ImplGlfw_Data* bd = ImGui_ImplGlfw_GetBackendData(); - if (bd->PrevUserCallbackWindowFocus != nullptr && window == bd->Window) + if (bd->PrevUserCallbackWindowFocus != nullptr && ImGui_ImplGlfw_ShouldChainCallback(window)) bd->PrevUserCallbackWindowFocus(window, focused); ImGuiIO& io = ImGui::GetIO(); @@ -384,15 +472,19 @@ void ImGui_ImplGlfw_WindowFocusCallback(GLFWwindow* window, int focused) void ImGui_ImplGlfw_CursorPosCallback(GLFWwindow* window, double x, double y) { ImGui_ImplGlfw_Data* bd = ImGui_ImplGlfw_GetBackendData(); - if (bd->PrevUserCallbackCursorPos != nullptr && window == bd->Window) - bd->PrevUserCallbackCursorPos(window, x * bd->DpiScale.x, y * bd->DpiScale.y); - - if (glfwGetInputMode(window, GLFW_CURSOR) == GLFW_CURSOR_DISABLED) - return; + if (bd->PrevUserCallbackCursorPos != nullptr && ImGui_ImplGlfw_ShouldChainCallback(window)) + bd->PrevUserCallbackCursorPos(window, x * bd->DpiScale.x, y * bd->DpiScale.y); // fix(zig-gamedev) ImGuiIO& io = ImGui::GetIO(); - io.AddMousePosEvent((float)x * bd->DpiScale.x, (float)y * bd->DpiScale.y); - bd->LastValidMousePos = ImVec2((float)x * bd->DpiScale.x, (float)y * bd->DpiScale.y); + if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) + { + int window_x, window_y; + glfwGetWindowPos(window, &window_x, &window_y); + x += window_x; + y += window_y; + } + io.AddMousePosEvent((float)x * bd->DpiScale.x, (float)y * bd->DpiScale.y); // fix(zig-gamedev) + bd->LastValidMousePos = ImVec2((float)x * bd->DpiScale.x, (float)y * bd->DpiScale.y); // fix(zig-gamedev) } // Workaround: X11 seems to send spurious Leave/Enter events which would make us lose our position, @@ -400,12 +492,9 @@ void ImGui_ImplGlfw_CursorPosCallback(GLFWwindow* window, double x, double y) void ImGui_ImplGlfw_CursorEnterCallback(GLFWwindow* window, int entered) { ImGui_ImplGlfw_Data* bd = ImGui_ImplGlfw_GetBackendData(); - if (bd->PrevUserCallbackCursorEnter != nullptr && window == bd->Window) + if (bd->PrevUserCallbackCursorEnter != nullptr && ImGui_ImplGlfw_ShouldChainCallback(window)) bd->PrevUserCallbackCursorEnter(window, entered); - if (glfwGetInputMode(window, GLFW_CURSOR) == GLFW_CURSOR_DISABLED) - return; - ImGuiIO& io = ImGui::GetIO(); if (entered) { @@ -423,7 +512,7 @@ void ImGui_ImplGlfw_CursorEnterCallback(GLFWwindow* window, int entered) void ImGui_ImplGlfw_CharCallback(GLFWwindow* window, unsigned int c) { ImGui_ImplGlfw_Data* bd = ImGui_ImplGlfw_GetBackendData(); - if (bd->PrevUserCallbackChar != nullptr && window == bd->Window) + if (bd->PrevUserCallbackChar != nullptr && ImGui_ImplGlfw_ShouldChainCallback(window)) bd->PrevUserCallbackChar(window, c); ImGuiIO& io = ImGui::GetIO(); @@ -432,9 +521,32 @@ void ImGui_ImplGlfw_CharCallback(GLFWwindow* window, unsigned int c) void ImGui_ImplGlfw_MonitorCallback(GLFWmonitor*, int) { - // Unused in 'master' branch but 'docking' branch will use this, so we declare it ahead of it so if you have to install callbacks you can install this one too. + ImGui_ImplGlfw_Data* bd = ImGui_ImplGlfw_GetBackendData(); + bd->WantUpdateMonitors = true; } +#ifdef __EMSCRIPTEN__ +static EM_BOOL ImGui_ImplEmscripten_WheelCallback(int, const EmscriptenWheelEvent* ev, void*) +{ + // Mimic Emscripten_HandleWheel() in SDL. + // Corresponding equivalent in GLFW JS emulation layer has incorrect quantizing preventing small values. See #6096 + float multiplier = 0.0f; + if (ev->deltaMode == DOM_DELTA_PIXEL) { multiplier = 1.0f / 100.0f; } // 100 pixels make up a step. + else if (ev->deltaMode == DOM_DELTA_LINE) { multiplier = 1.0f / 3.0f; } // 3 lines make up a step. + else if (ev->deltaMode == DOM_DELTA_PAGE) { multiplier = 80.0f; } // A page makes up 80 steps. + float wheel_x = ev->deltaX * -multiplier; + float wheel_y = ev->deltaY * -multiplier; + ImGuiIO& io = ImGui::GetIO(); + io.AddMouseWheelEvent(wheel_x, wheel_y); + //IMGUI_DEBUG_LOG("[Emsc] mode %d dx: %.2f, dy: %.2f, dz: %.2f --> feed %.2f %.2f\n", (int)ev->deltaMode, ev->deltaX, ev->deltaY, ev->deltaZ, wheel_x, wheel_y); + return EM_TRUE; +} +#endif + +#ifdef _WIN32 +static LRESULT CALLBACK ImGui_ImplGlfw_WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); +#endif + void ImGui_ImplGlfw_InstallCallbacks(GLFWwindow* window) { ImGui_ImplGlfw_Data* bd = ImGui_ImplGlfw_GetBackendData(); @@ -477,6 +589,16 @@ void ImGui_ImplGlfw_RestoreCallbacks(GLFWwindow* window) bd->PrevUserCallbackMonitor = nullptr; } +// Set to 'true' to enable chaining installed callbacks for all windows (including secondary viewports created by backends or by user. +// This is 'false' by default meaning we only chain callbacks for the main viewport. +// We cannot set this to 'true' by default because user callbacks code may be not testing the 'window' parameter of their callback. +// If you set this to 'true' your user callback code will need to make sure you are testing the 'window' parameter. +void ImGui_ImplGlfw_SetCallbacksChainForAllWindows(bool chain_for_all_windows) +{ + ImGui_ImplGlfw_Data* bd = ImGui_ImplGlfw_GetBackendData(); + bd->CallbacksChainForAllWindows = chain_for_all_windows; +} + static bool ImGui_ImplGlfw_Init(GLFWwindow* window, bool install_callbacks, GlfwClientApi client_api) { ImGuiIO& io = ImGui::GetIO(); @@ -489,20 +611,23 @@ static bool ImGui_ImplGlfw_Init(GLFWwindow* window, bool install_callbacks, Glfw io.BackendPlatformName = "imgui_impl_glfw"; io.BackendFlags |= ImGuiBackendFlags_HasMouseCursors; // We can honor GetMouseCursor() values (optional) io.BackendFlags |= ImGuiBackendFlags_HasSetMousePos; // We can honor io.WantSetMousePos requests (optional, rarely used) +#ifndef __EMSCRIPTEN__ + io.BackendFlags |= ImGuiBackendFlags_PlatformHasViewports; // We can create multi-viewports on the Platform side (optional) +#endif +#if GLFW_HAS_MOUSE_PASSTHROUGH || GLFW_HAS_WINDOW_HOVERED + io.BackendFlags |= ImGuiBackendFlags_HasMouseHoveredViewport; // We can call io.AddMouseViewportEvent() with correct data (optional) +#endif bd->Window = window; bd->Time = 0.0; - bd->DpiScale = ImVec2{ 1.0, 1.0 }; + bd->WantUpdateMonitors = true; + + bd->DpiScale = ImVec2{ 1.0, 1.0 }; // fix(zig-gamedev) io.SetClipboardTextFn = ImGui_ImplGlfw_SetClipboardText; io.GetClipboardTextFn = ImGui_ImplGlfw_GetClipboardText; io.ClipboardUserData = bd->Window; - // Set platform dependent data in viewport -#if defined(_WIN32) - ImGui::GetMainViewport()->PlatformHandleRaw = (void*)glfwGetWin32Window(bd->Window); -#endif - // Create mouse cursors // (By design, on X11 cursors are user configurable and some cursors may be missing. When a cursor doesn't exist, // GLFW will emit an error which will often be printed by the app, so we temporarily disable error reporting. @@ -525,13 +650,43 @@ static bool ImGui_ImplGlfw_Init(GLFWwindow* window, bool install_callbacks, Glfw bd->MouseCursors[ImGuiMouseCursor_NotAllowed] = glfwCreateStandardCursor(GLFW_ARROW_CURSOR); #endif glfwSetErrorCallback(prev_error_callback); -#if (GLFW_VERSION_COMBINED >= 3300) // Eat errors (see #5785) - (void)glfwGetError(NULL); +#if GLFW_HAS_GETERROR && !defined(__EMSCRIPTEN__) // Eat errors (see #5908) + (void)glfwGetError(nullptr); #endif // Chain GLFW callbacks: our callbacks will call the user's previously installed callbacks, if any. if (install_callbacks) ImGui_ImplGlfw_InstallCallbacks(window); + // Register Emscripten Wheel callback to workaround issue in Emscripten GLFW Emulation (#6096) + // We intentionally do not check 'if (install_callbacks)' here, as some users may set it to false and call GLFW callback themselves. + // FIXME: May break chaining in case user registered their own Emscripten callback? +#ifdef __EMSCRIPTEN__ + emscripten_set_wheel_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, nullptr, false, ImGui_ImplEmscripten_WheelCallback); +#endif + + // Update monitors the first time (note: monitor callback are broken in GLFW 3.2 and earlier, see github.com/glfw/glfw/issues/784) + ImGui_ImplGlfw_UpdateMonitors(); + glfwSetMonitorCallback(ImGui_ImplGlfw_MonitorCallback); + + // Set platform dependent data in viewport + ImGuiViewport* main_viewport = ImGui::GetMainViewport(); + main_viewport->PlatformHandle = (void*)bd->Window; +#ifdef _WIN32 + main_viewport->PlatformHandleRaw = glfwGetWin32Window(bd->Window); +#elif defined(__APPLE__) + main_viewport->PlatformHandleRaw = (void*)glfwGetCocoaWindow(bd->Window); +#else + IM_UNUSED(main_viewport); +#endif + if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) + ImGui_ImplGlfw_InitPlatformInterface(); + + // Windows: register a WndProc hook so we can intercept some messages. +#ifdef _WIN32 + bd->PrevWndProc = (WNDPROC)::GetWindowLongPtrW((HWND)main_viewport->PlatformHandleRaw, GWLP_WNDPROC); + IM_ASSERT(bd->PrevWndProc != nullptr); + ::SetWindowLongPtrW((HWND)main_viewport->PlatformHandleRaw, GWLP_WNDPROC, (LONG_PTR)ImGui_ImplGlfw_WndProc); +#endif bd->ClientApi = client_api; return true; @@ -558,14 +713,27 @@ void ImGui_ImplGlfw_Shutdown() IM_ASSERT(bd != nullptr && "No platform backend to shutdown, or already shutdown?"); ImGuiIO& io = ImGui::GetIO(); + ImGui_ImplGlfw_ShutdownPlatformInterface(); + if (bd->InstalledCallbacks) ImGui_ImplGlfw_RestoreCallbacks(bd->Window); +#ifdef __EMSCRIPTEN__ + emscripten_set_wheel_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, nullptr, false, nullptr); +#endif for (ImGuiMouseCursor cursor_n = 0; cursor_n < ImGuiMouseCursor_COUNT; cursor_n++) glfwDestroyCursor(bd->MouseCursors[cursor_n]); + // Windows: restore our WndProc hook +#ifdef _WIN32 + ImGuiViewport* main_viewport = ImGui::GetMainViewport(); + ::SetWindowLongPtrW((HWND)main_viewport->PlatformHandleRaw, GWLP_WNDPROC, (LONG_PTR)bd->PrevWndProc); + bd->PrevWndProc = nullptr; +#endif + io.BackendPlatformName = nullptr; io.BackendPlatformUserData = nullptr; + io.BackendFlags &= ~(ImGuiBackendFlags_HasMouseCursors | ImGuiBackendFlags_HasSetMousePos | ImGuiBackendFlags_HasGamepad | ImGuiBackendFlags_PlatformHasViewports | ImGuiBackendFlags_HasMouseHoveredViewport); IM_DELETE(bd); } @@ -573,33 +741,72 @@ static void ImGui_ImplGlfw_UpdateMouseData() { ImGui_ImplGlfw_Data* bd = ImGui_ImplGlfw_GetBackendData(); ImGuiIO& io = ImGui::GetIO(); + ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO(); - if (glfwGetInputMode(bd->Window, GLFW_CURSOR) == GLFW_CURSOR_DISABLED) + ImGuiID mouse_viewport_id = 0; + const ImVec2 mouse_pos_prev = io.MousePos; + for (int n = 0; n < platform_io.Viewports.Size; n++) { - io.AddMousePosEvent(-FLT_MAX, -FLT_MAX); - return; - } + ImGuiViewport* viewport = platform_io.Viewports[n]; + GLFWwindow* window = (GLFWwindow*)viewport->PlatformHandle; #ifdef __EMSCRIPTEN__ - const bool is_app_focused = true; + const bool is_window_focused = true; #else - const bool is_app_focused = glfwGetWindowAttrib(bd->Window, GLFW_FOCUSED) != 0; + const bool is_window_focused = glfwGetWindowAttrib(window, GLFW_FOCUSED) != 0; #endif - if (is_app_focused) - { - // (Optional) Set OS mouse position from Dear ImGui if requested (rarely used, only when ImGuiConfigFlags_NavEnableSetMousePos is enabled by user) - if (io.WantSetMousePos) - glfwSetCursorPos(bd->Window, (double)io.MousePos.x, (double)io.MousePos.y); - - // (Optional) Fallback to provide mouse position when focused (ImGui_ImplGlfw_CursorPosCallback already provides this when hovered or captured) - if (is_app_focused && bd->MouseWindow == nullptr) + if (is_window_focused) { - double mouse_x, mouse_y; - glfwGetCursorPos(bd->Window, &mouse_x, &mouse_y); - io.AddMousePosEvent((float)mouse_x * bd->DpiScale.x, (float)mouse_y * bd->DpiScale.y); - bd->LastValidMousePos = ImVec2((float)mouse_x * bd->DpiScale.x, (float)mouse_y * bd->DpiScale.y); + // (Optional) Set OS mouse position from Dear ImGui if requested (rarely used, only when ImGuiConfigFlags_NavEnableSetMousePos is enabled by user) + // When multi-viewports are enabled, all Dear ImGui positions are same as OS positions. + if (io.WantSetMousePos) + glfwSetCursorPos(window, (double)(mouse_pos_prev.x - viewport->Pos.x), (double)(mouse_pos_prev.y - viewport->Pos.y)); + + // (Optional) Fallback to provide mouse position when focused (ImGui_ImplGlfw_CursorPosCallback already provides this when hovered or captured) + if (bd->MouseWindow == nullptr) + { + double mouse_x, mouse_y; + glfwGetCursorPos(window, &mouse_x, &mouse_y); + if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) + { + // Single viewport mode: mouse position in client window coordinates (io.MousePos is (0,0) when the mouse is on the upper-left corner of the app window) + // Multi-viewport mode: mouse position in OS absolute coordinates (io.MousePos is (0,0) when the mouse is on the upper-left of the primary monitor) + int window_x, window_y; + glfwGetWindowPos(window, &window_x, &window_y); + mouse_x += window_x; + mouse_y += window_y; + } + + bd->LastValidMousePos = ImVec2((float)mouse_x * bd->DpiScale.x, (float)mouse_y * bd->DpiScale.y); // fix(zig-gamedev) + io.AddMousePosEvent((float)mouse_x * bd->DpiScale.x, (float)mouse_y * bd->DpiScale.y); // fix(zig-gamedev) + } } + + // (Optional) When using multiple viewports: call io.AddMouseViewportEvent() with the viewport the OS mouse cursor is hovering. + // If ImGuiBackendFlags_HasMouseHoveredViewport is not set by the backend, Dear imGui will ignore this field and infer the information using its flawed heuristic. + // - [X] GLFW >= 3.3 backend ON WINDOWS ONLY does correctly ignore viewports with the _NoInputs flag (since we implement hit via our WndProc hook) + // On other platforms we rely on the library fallbacking to its own search when reporting a viewport with _NoInputs flag. + // - [!] GLFW <= 3.2 backend CANNOT correctly ignore viewports with the _NoInputs flag, and CANNOT reported Hovered Viewport because of mouse capture. + // Some backend are not able to handle that correctly. If a backend report an hovered viewport that has the _NoInputs flag (e.g. when dragging a window + // for docking, the viewport has the _NoInputs flag in order to allow us to find the viewport under), then Dear ImGui is forced to ignore the value reported + // by the backend, and use its flawed heuristic to guess the viewport behind. + // - [X] GLFW backend correctly reports this regardless of another viewport behind focused and dragged from (we need this to find a useful drag and drop target). + // FIXME: This is currently only correct on Win32. See what we do below with the WM_NCHITTEST, missing an equivalent for other systems. + // See https://github.com/glfw/glfw/issues/1236 if you want to help in making this a GLFW feature. +#if GLFW_HAS_MOUSE_PASSTHROUGH + const bool window_no_input = (viewport->Flags & ImGuiViewportFlags_NoInputs) != 0; + glfwSetWindowAttrib(window, GLFW_MOUSE_PASSTHROUGH, window_no_input); +#endif +#if GLFW_HAS_MOUSE_PASSTHROUGH || GLFW_HAS_WINDOW_HOVERED + if (glfwGetWindowAttrib(window, GLFW_HOVERED)) + mouse_viewport_id = viewport->ID; +#else + // We cannot use bd->MouseWindow maintained from CursorEnter/Leave callbacks, because it is locked to the window capturing mouse. +#endif } + + if (io.BackendFlags & ImGuiBackendFlags_HasMouseHoveredViewport) + io.AddMouseViewportEvent(mouse_viewport_id); } static void ImGui_ImplGlfw_UpdateMouseCursor() @@ -610,17 +817,22 @@ static void ImGui_ImplGlfw_UpdateMouseCursor() return; ImGuiMouseCursor imgui_cursor = ImGui::GetMouseCursor(); - if (imgui_cursor == ImGuiMouseCursor_None || io.MouseDrawCursor) + ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO(); + for (int n = 0; n < platform_io.Viewports.Size; n++) { - // Hide OS mouse cursor if imgui is drawing it or if it wants no cursor - glfwSetInputMode(bd->Window, GLFW_CURSOR, GLFW_CURSOR_HIDDEN); - } - else - { - // Show OS mouse cursor - // FIXME-PLATFORM: Unfocused windows seems to fail changing the mouse cursor with GLFW 3.2, but 3.3 works here. - glfwSetCursor(bd->Window, bd->MouseCursors[imgui_cursor] ? bd->MouseCursors[imgui_cursor] : bd->MouseCursors[ImGuiMouseCursor_Arrow]); - glfwSetInputMode(bd->Window, GLFW_CURSOR, GLFW_CURSOR_NORMAL); + GLFWwindow* window = (GLFWwindow*)platform_io.Viewports[n]->PlatformHandle; + if (imgui_cursor == ImGuiMouseCursor_None || io.MouseDrawCursor) + { + // Hide OS mouse cursor if imgui is drawing it or if it wants no cursor + glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_HIDDEN); + } + else + { + // Show OS mouse cursor + // FIXME-PLATFORM: Unfocused windows seems to fail changing the mouse cursor with GLFW 3.2, but 3.3 works here. + glfwSetCursor(window, bd->MouseCursors[imgui_cursor] ? bd->MouseCursors[imgui_cursor] : bd->MouseCursors[ImGuiMouseCursor_Arrow]); + glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_NORMAL); + } } } @@ -629,11 +841,11 @@ static inline float Saturate(float v) { return v < 0.0f ? 0.0f : v > 1.0f ? 1.0 static void ImGui_ImplGlfw_UpdateGamepads() { ImGuiIO& io = ImGui::GetIO(); - if ((io.ConfigFlags & ImGuiConfigFlags_NavEnableGamepad) == 0) + if ((io.ConfigFlags & ImGuiConfigFlags_NavEnableGamepad) == 0) // FIXME: Technically feeding gamepad shouldn't depend on this now that they are regular inputs. return; io.BackendFlags &= ~ImGuiBackendFlags_HasGamepad; -#if GLFW_HAS_GAMEPAD_API +#if GLFW_HAS_GAMEPAD_API && !defined(__EMSCRIPTEN__) GLFWgamepadstate gamepad; if (!glfwGetGamepadState(GLFW_JOYSTICK_1, &gamepad)) return; @@ -651,10 +863,10 @@ static void ImGui_ImplGlfw_UpdateGamepads() io.BackendFlags |= ImGuiBackendFlags_HasGamepad; MAP_BUTTON(ImGuiKey_GamepadStart, GLFW_GAMEPAD_BUTTON_START, 7); MAP_BUTTON(ImGuiKey_GamepadBack, GLFW_GAMEPAD_BUTTON_BACK, 6); - MAP_BUTTON(ImGuiKey_GamepadFaceDown, GLFW_GAMEPAD_BUTTON_A, 0); // Xbox A, PS Cross - MAP_BUTTON(ImGuiKey_GamepadFaceRight, GLFW_GAMEPAD_BUTTON_B, 1); // Xbox B, PS Circle MAP_BUTTON(ImGuiKey_GamepadFaceLeft, GLFW_GAMEPAD_BUTTON_X, 2); // Xbox X, PS Square + MAP_BUTTON(ImGuiKey_GamepadFaceRight, GLFW_GAMEPAD_BUTTON_B, 1); // Xbox B, PS Circle MAP_BUTTON(ImGuiKey_GamepadFaceUp, GLFW_GAMEPAD_BUTTON_Y, 3); // Xbox Y, PS Triangle + MAP_BUTTON(ImGuiKey_GamepadFaceDown, GLFW_GAMEPAD_BUTTON_A, 0); // Xbox A, PS Cross MAP_BUTTON(ImGuiKey_GamepadDpadLeft, GLFW_GAMEPAD_BUTTON_DPAD_LEFT, 13); MAP_BUTTON(ImGuiKey_GamepadDpadRight, GLFW_GAMEPAD_BUTTON_DPAD_RIGHT, 11); MAP_BUTTON(ImGuiKey_GamepadDpadUp, GLFW_GAMEPAD_BUTTON_DPAD_UP, 10); @@ -677,6 +889,48 @@ static void ImGui_ImplGlfw_UpdateGamepads() #undef MAP_ANALOG } +static void ImGui_ImplGlfw_UpdateMonitors() +{ + ImGui_ImplGlfw_Data* bd = ImGui_ImplGlfw_GetBackendData(); + ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO(); + bd->WantUpdateMonitors = false; + + int monitors_count = 0; + GLFWmonitor** glfw_monitors = glfwGetMonitors(&monitors_count); + if (monitors_count == 0) // Preserve existing monitor list if there are none. Happens on macOS sleeping (#5683) + return; + + platform_io.Monitors.resize(0); + for (int n = 0; n < monitors_count; n++) + { + ImGuiPlatformMonitor monitor; + int x, y; + glfwGetMonitorPos(glfw_monitors[n], &x, &y); + const GLFWvidmode* vid_mode = glfwGetVideoMode(glfw_monitors[n]); + if (vid_mode == nullptr) + continue; // Failed to get Video mode (e.g. Emscripten does not support this function) + monitor.MainPos = monitor.WorkPos = ImVec2((float)x, (float)y); + monitor.MainSize = monitor.WorkSize = ImVec2((float)vid_mode->width, (float)vid_mode->height); +#if GLFW_HAS_MONITOR_WORK_AREA + int w, h; + glfwGetMonitorWorkarea(glfw_monitors[n], &x, &y, &w, &h); + if (w > 0 && h > 0) // Workaround a small GLFW issue reporting zero on monitor changes: https://github.com/glfw/glfw/pull/1761 + { + monitor.WorkPos = ImVec2((float)x, (float)y); + monitor.WorkSize = ImVec2((float)w, (float)h); + } +#endif +#if GLFW_HAS_PER_MONITOR_DPI + // Warning: the validity of monitor DPI information on Windows depends on the application DPI awareness settings, which generally needs to be set in the manifest or at runtime. + float x_scale, y_scale; + glfwGetMonitorContentScale(glfw_monitors[n], &x_scale, &y_scale); + monitor.DpiScale = x_scale; +#endif + monitor.PlatformHandle = (void*)glfw_monitors[n]; // [...] GLFW doc states: "guaranteed to be valid only until the monitor configuration changes" + platform_io.Monitors.push_back(monitor); + } +} + void ImGui_ImplGlfw_NewFrame() { ImGuiIO& io = ImGui::GetIO(); @@ -691,12 +945,19 @@ void ImGui_ImplGlfw_NewFrame() io.DisplaySize = ImVec2((float)w, (float)h); if (w > 0 && h > 0) io.DisplayFramebufferScale = ImVec2((float)display_w / (float)w, (float)display_h / (float)h); - + + // fix(zig-gamedev) bd->DpiScale.x = ceil(io.DisplayFramebufferScale.x); bd->DpiScale.y = ceil(io.DisplayFramebufferScale.y); + if (bd->WantUpdateMonitors) + ImGui_ImplGlfw_UpdateMonitors(); + // Setup time step + // (Accept glfwGetTime() not returning a monotonically increasing value. Seems to happens on disconnecting peripherals and probably on VMs and Emscripten, see #6491, #6189, #6114, #3644) double current_time = glfwGetTime(); + if (current_time <= bd->Time) + current_time = bd->Time + 0.00001f; io.DeltaTime = bd->Time > 0.0 ? (float)(current_time - bd->Time) : (float)(1.0f / 60.0f); bd->Time = current_time; @@ -707,6 +968,439 @@ void ImGui_ImplGlfw_NewFrame() ImGui_ImplGlfw_UpdateGamepads(); } +#ifdef __EMSCRIPTEN__ +static EM_BOOL ImGui_ImplGlfw_OnCanvasSizeChange(int event_type, const EmscriptenUiEvent* event, void* user_data) +{ + ImGui_ImplGlfw_Data* bd = (ImGui_ImplGlfw_Data*)user_data; + double canvas_width, canvas_height; + emscripten_get_element_css_size(bd->CanvasSelector, &canvas_width, &canvas_height); + glfwSetWindowSize(bd->Window, (int)canvas_width, (int)canvas_height); + return true; +} + +static EM_BOOL ImGui_ImplEmscripten_FullscreenChangeCallback(int event_type, const EmscriptenFullscreenChangeEvent* event, void* user_data) +{ + ImGui_ImplGlfw_Data* bd = (ImGui_ImplGlfw_Data*)user_data; + double canvas_width, canvas_height; + emscripten_get_element_css_size(bd->CanvasSelector, &canvas_width, &canvas_height); + glfwSetWindowSize(bd->Window, (int)canvas_width, (int)canvas_height); + return true; +} + +// 'canvas_selector' is a CSS selector. The event listener is applied to the first element that matches the query. +// STRING MUST PERSIST FOR THE APPLICATION DURATION. PLEASE USE A STRING LITERAL OR ENSURE POINTER WILL STAY VALID. +void ImGui_ImplGlfw_InstallEmscriptenCanvasResizeCallback(const char* canvas_selector) +{ + IM_ASSERT(canvas_selector != nullptr); + ImGui_ImplGlfw_Data* bd = ImGui_ImplGlfw_GetBackendData(); + IM_ASSERT(bd != nullptr && "Did you call ImGui_ImplGlfw_InitForXXX()?"); + + bd->CanvasSelector = canvas_selector; + emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, bd, false, ImGui_ImplGlfw_OnCanvasSizeChange); + emscripten_set_fullscreenchange_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, bd, false, ImGui_ImplEmscripten_FullscreenChangeCallback); + + // Change the size of the GLFW window according to the size of the canvas + ImGui_ImplGlfw_OnCanvasSizeChange(EMSCRIPTEN_EVENT_RESIZE, {}, bd); +} +#endif + + +//-------------------------------------------------------------------------------------------------------- +// MULTI-VIEWPORT / PLATFORM INTERFACE SUPPORT +// This is an _advanced_ and _optional_ feature, allowing the backend to create and handle multiple viewports simultaneously. +// If you are new to dear imgui or creating a new binding for dear imgui, it is recommended that you completely ignore this section first.. +//-------------------------------------------------------------------------------------------------------- + +// Helper structure we store in the void* RendererUserData field of each ImGuiViewport to easily retrieve our backend data. +struct ImGui_ImplGlfw_ViewportData +{ + GLFWwindow* Window; + bool WindowOwned; + int IgnoreWindowPosEventFrame; + int IgnoreWindowSizeEventFrame; +#ifdef _WIN32 + WNDPROC PrevWndProc; +#endif + + ImGui_ImplGlfw_ViewportData() { memset(this, 0, sizeof(*this)); IgnoreWindowSizeEventFrame = IgnoreWindowPosEventFrame = -1; } + ~ImGui_ImplGlfw_ViewportData() { IM_ASSERT(Window == nullptr); } +}; + +static void ImGui_ImplGlfw_WindowCloseCallback(GLFWwindow* window) +{ + if (ImGuiViewport* viewport = ImGui::FindViewportByPlatformHandle(window)) + viewport->PlatformRequestClose = true; +} + +// GLFW may dispatch window pos/size events after calling glfwSetWindowPos()/glfwSetWindowSize(). +// However: depending on the platform the callback may be invoked at different time: +// - on Windows it appears to be called within the glfwSetWindowPos()/glfwSetWindowSize() call +// - on Linux it is queued and invoked during glfwPollEvents() +// Because the event doesn't always fire on glfwSetWindowXXX() we use a frame counter tag to only +// ignore recent glfwSetWindowXXX() calls. +static void ImGui_ImplGlfw_WindowPosCallback(GLFWwindow* window, int, int) +{ + if (ImGuiViewport* viewport = ImGui::FindViewportByPlatformHandle(window)) + { + if (ImGui_ImplGlfw_ViewportData* vd = (ImGui_ImplGlfw_ViewportData*)viewport->PlatformUserData) + { + bool ignore_event = (ImGui::GetFrameCount() <= vd->IgnoreWindowPosEventFrame + 1); + //data->IgnoreWindowPosEventFrame = -1; + if (ignore_event) + return; + } + viewport->PlatformRequestMove = true; + } +} + +static void ImGui_ImplGlfw_WindowSizeCallback(GLFWwindow* window, int, int) +{ + if (ImGuiViewport* viewport = ImGui::FindViewportByPlatformHandle(window)) + { + if (ImGui_ImplGlfw_ViewportData* vd = (ImGui_ImplGlfw_ViewportData*)viewport->PlatformUserData) + { + bool ignore_event = (ImGui::GetFrameCount() <= vd->IgnoreWindowSizeEventFrame + 1); + //data->IgnoreWindowSizeEventFrame = -1; + if (ignore_event) + return; + } + viewport->PlatformRequestResize = true; + } +} + +static void ImGui_ImplGlfw_CreateWindow(ImGuiViewport* viewport) +{ + ImGui_ImplGlfw_Data* bd = ImGui_ImplGlfw_GetBackendData(); + ImGui_ImplGlfw_ViewportData* vd = IM_NEW(ImGui_ImplGlfw_ViewportData)(); + viewport->PlatformUserData = vd; + + // GLFW 3.2 unfortunately always set focus on glfwCreateWindow() if GLFW_VISIBLE is set, regardless of GLFW_FOCUSED + // With GLFW 3.3, the hint GLFW_FOCUS_ON_SHOW fixes this problem + glfwWindowHint(GLFW_VISIBLE, false); + glfwWindowHint(GLFW_FOCUSED, false); +#if GLFW_HAS_FOCUS_ON_SHOW + glfwWindowHint(GLFW_FOCUS_ON_SHOW, false); + #endif + glfwWindowHint(GLFW_DECORATED, (viewport->Flags & ImGuiViewportFlags_NoDecoration) ? false : true); +#if GLFW_HAS_WINDOW_TOPMOST + glfwWindowHint(GLFW_FLOATING, (viewport->Flags & ImGuiViewportFlags_TopMost) ? true : false); +#endif + GLFWwindow* share_window = (bd->ClientApi == GlfwClientApi_OpenGL) ? bd->Window : nullptr; + vd->Window = glfwCreateWindow((int)viewport->Size.x, (int)viewport->Size.y, "No Title Yet", nullptr, share_window); + vd->WindowOwned = true; + viewport->PlatformHandle = (void*)vd->Window; +#ifdef _WIN32 + viewport->PlatformHandleRaw = glfwGetWin32Window(vd->Window); +#elif defined(__APPLE__) + viewport->PlatformHandleRaw = (void*)glfwGetCocoaWindow(vd->Window); +#endif + glfwSetWindowPos(vd->Window, (int)viewport->Pos.x, (int)viewport->Pos.y); + + // Install GLFW callbacks for secondary viewports + glfwSetWindowFocusCallback(vd->Window, ImGui_ImplGlfw_WindowFocusCallback); + glfwSetCursorEnterCallback(vd->Window, ImGui_ImplGlfw_CursorEnterCallback); + glfwSetCursorPosCallback(vd->Window, ImGui_ImplGlfw_CursorPosCallback); + glfwSetMouseButtonCallback(vd->Window, ImGui_ImplGlfw_MouseButtonCallback); + glfwSetScrollCallback(vd->Window, ImGui_ImplGlfw_ScrollCallback); + glfwSetKeyCallback(vd->Window, ImGui_ImplGlfw_KeyCallback); + glfwSetCharCallback(vd->Window, ImGui_ImplGlfw_CharCallback); + glfwSetWindowCloseCallback(vd->Window, ImGui_ImplGlfw_WindowCloseCallback); + glfwSetWindowPosCallback(vd->Window, ImGui_ImplGlfw_WindowPosCallback); + glfwSetWindowSizeCallback(vd->Window, ImGui_ImplGlfw_WindowSizeCallback); + if (bd->ClientApi == GlfwClientApi_OpenGL) + { + glfwMakeContextCurrent(vd->Window); + glfwSwapInterval(0); + } +} + +static void ImGui_ImplGlfw_DestroyWindow(ImGuiViewport* viewport) +{ + ImGui_ImplGlfw_Data* bd = ImGui_ImplGlfw_GetBackendData(); + if (ImGui_ImplGlfw_ViewportData* vd = (ImGui_ImplGlfw_ViewportData*)viewport->PlatformUserData) + { + if (vd->WindowOwned) + { +#if !GLFW_HAS_MOUSE_PASSTHROUGH && GLFW_HAS_WINDOW_HOVERED && defined(_WIN32) + HWND hwnd = (HWND)viewport->PlatformHandleRaw; + ::RemovePropA(hwnd, "IMGUI_VIEWPORT"); +#endif + + // Release any keys that were pressed in the window being destroyed and are still held down, + // because we will not receive any release events after window is destroyed. + for (int i = 0; i < IM_ARRAYSIZE(bd->KeyOwnerWindows); i++) + if (bd->KeyOwnerWindows[i] == vd->Window) + ImGui_ImplGlfw_KeyCallback(vd->Window, i, 0, GLFW_RELEASE, 0); // Later params are only used for main viewport, on which this function is never called. + + glfwDestroyWindow(vd->Window); + } + vd->Window = nullptr; + IM_DELETE(vd); + } + viewport->PlatformUserData = viewport->PlatformHandle = nullptr; +} + +static void ImGui_ImplGlfw_ShowWindow(ImGuiViewport* viewport) +{ + ImGui_ImplGlfw_ViewportData* vd = (ImGui_ImplGlfw_ViewportData*)viewport->PlatformUserData; + +#if defined(_WIN32) + // GLFW hack: Hide icon from task bar + HWND hwnd = (HWND)viewport->PlatformHandleRaw; + if (viewport->Flags & ImGuiViewportFlags_NoTaskBarIcon) + { + LONG ex_style = ::GetWindowLong(hwnd, GWL_EXSTYLE); + ex_style &= ~WS_EX_APPWINDOW; + ex_style |= WS_EX_TOOLWINDOW; + ::SetWindowLong(hwnd, GWL_EXSTYLE, ex_style); + } + + // GLFW hack: install hook for WM_NCHITTEST message handler +#if !GLFW_HAS_MOUSE_PASSTHROUGH && GLFW_HAS_WINDOW_HOVERED && defined(_WIN32) + ::SetPropA(hwnd, "IMGUI_VIEWPORT", viewport); + vd->PrevWndProc = (WNDPROC)::GetWindowLongPtrW(hwnd, GWLP_WNDPROC); + ::SetWindowLongPtrW(hwnd, GWLP_WNDPROC, (LONG_PTR)ImGui_ImplGlfw_WndProc); +#endif + +#if !GLFW_HAS_FOCUS_ON_SHOW + // GLFW hack: GLFW 3.2 has a bug where glfwShowWindow() also activates/focus the window. + // The fix was pushed to GLFW repository on 2018/01/09 and should be included in GLFW 3.3 via a GLFW_FOCUS_ON_SHOW window attribute. + // See https://github.com/glfw/glfw/issues/1189 + // FIXME-VIEWPORT: Implement same work-around for Linux/OSX in the meanwhile. + if (viewport->Flags & ImGuiViewportFlags_NoFocusOnAppearing) + { + ::ShowWindow(hwnd, SW_SHOWNA); + return; + } +#endif +#endif + + glfwShowWindow(vd->Window); +} + +static ImVec2 ImGui_ImplGlfw_GetWindowPos(ImGuiViewport* viewport) +{ + ImGui_ImplGlfw_ViewportData* vd = (ImGui_ImplGlfw_ViewportData*)viewport->PlatformUserData; + int x = 0, y = 0; + glfwGetWindowPos(vd->Window, &x, &y); + return ImVec2((float)x, (float)y); +} + +static void ImGui_ImplGlfw_SetWindowPos(ImGuiViewport* viewport, ImVec2 pos) +{ + ImGui_ImplGlfw_ViewportData* vd = (ImGui_ImplGlfw_ViewportData*)viewport->PlatformUserData; + vd->IgnoreWindowPosEventFrame = ImGui::GetFrameCount(); + glfwSetWindowPos(vd->Window, (int)pos.x, (int)pos.y); +} + +static ImVec2 ImGui_ImplGlfw_GetWindowSize(ImGuiViewport* viewport) +{ + ImGui_ImplGlfw_ViewportData* vd = (ImGui_ImplGlfw_ViewportData*)viewport->PlatformUserData; + int w = 0, h = 0; + glfwGetWindowSize(vd->Window, &w, &h); + return ImVec2((float)w, (float)h); +} + +static void ImGui_ImplGlfw_SetWindowSize(ImGuiViewport* viewport, ImVec2 size) +{ + ImGui_ImplGlfw_ViewportData* vd = (ImGui_ImplGlfw_ViewportData*)viewport->PlatformUserData; +#if __APPLE__ && !GLFW_HAS_OSX_WINDOW_POS_FIX + // Native OS windows are positioned from the bottom-left corner on macOS, whereas on other platforms they are + // positioned from the upper-left corner. GLFW makes an effort to convert macOS style coordinates, however it + // doesn't handle it when changing size. We are manually moving the window in order for changes of size to be based + // on the upper-left corner. + int x, y, width, height; + glfwGetWindowPos(vd->Window, &x, &y); + glfwGetWindowSize(vd->Window, &width, &height); + glfwSetWindowPos(vd->Window, x, y - height + size.y); +#endif + vd->IgnoreWindowSizeEventFrame = ImGui::GetFrameCount(); + glfwSetWindowSize(vd->Window, (int)size.x, (int)size.y); +} + +static void ImGui_ImplGlfw_SetWindowTitle(ImGuiViewport* viewport, const char* title) +{ + ImGui_ImplGlfw_ViewportData* vd = (ImGui_ImplGlfw_ViewportData*)viewport->PlatformUserData; + glfwSetWindowTitle(vd->Window, title); +} + +static void ImGui_ImplGlfw_SetWindowFocus(ImGuiViewport* viewport) +{ +#if GLFW_HAS_FOCUS_WINDOW + ImGui_ImplGlfw_ViewportData* vd = (ImGui_ImplGlfw_ViewportData*)viewport->PlatformUserData; + glfwFocusWindow(vd->Window); +#else + // FIXME: What are the effect of not having this function? At the moment imgui doesn't actually call SetWindowFocus - we set that up ahead, will answer that question later. + (void)viewport; +#endif +} + +static bool ImGui_ImplGlfw_GetWindowFocus(ImGuiViewport* viewport) +{ + ImGui_ImplGlfw_ViewportData* vd = (ImGui_ImplGlfw_ViewportData*)viewport->PlatformUserData; + return glfwGetWindowAttrib(vd->Window, GLFW_FOCUSED) != 0; +} + +static bool ImGui_ImplGlfw_GetWindowMinimized(ImGuiViewport* viewport) +{ + ImGui_ImplGlfw_ViewportData* vd = (ImGui_ImplGlfw_ViewportData*)viewport->PlatformUserData; + return glfwGetWindowAttrib(vd->Window, GLFW_ICONIFIED) != 0; +} + +#if GLFW_HAS_WINDOW_ALPHA +static void ImGui_ImplGlfw_SetWindowAlpha(ImGuiViewport* viewport, float alpha) +{ + ImGui_ImplGlfw_ViewportData* vd = (ImGui_ImplGlfw_ViewportData*)viewport->PlatformUserData; + glfwSetWindowOpacity(vd->Window, alpha); +} +#endif + +static void ImGui_ImplGlfw_RenderWindow(ImGuiViewport* viewport, void*) +{ + ImGui_ImplGlfw_Data* bd = ImGui_ImplGlfw_GetBackendData(); + ImGui_ImplGlfw_ViewportData* vd = (ImGui_ImplGlfw_ViewportData*)viewport->PlatformUserData; + if (bd->ClientApi == GlfwClientApi_OpenGL) + glfwMakeContextCurrent(vd->Window); +} + +static void ImGui_ImplGlfw_SwapBuffers(ImGuiViewport* viewport, void*) +{ + ImGui_ImplGlfw_Data* bd = ImGui_ImplGlfw_GetBackendData(); + ImGui_ImplGlfw_ViewportData* vd = (ImGui_ImplGlfw_ViewportData*)viewport->PlatformUserData; + if (bd->ClientApi == GlfwClientApi_OpenGL) + { + glfwMakeContextCurrent(vd->Window); + glfwSwapBuffers(vd->Window); + } +} + +//-------------------------------------------------------------------------------------------------------- +// Vulkan support (the Vulkan renderer needs to call a platform-side support function to create the surface) +//-------------------------------------------------------------------------------------------------------- + +// Avoid including so we can build without it +#if GLFW_HAS_VULKAN +#ifndef VULKAN_H_ +#define VK_DEFINE_HANDLE(object) typedef struct object##_T* object; +#if defined(__LP64__) || defined(_WIN64) || defined(__x86_64__) || defined(_M_X64) || defined(__ia64) || defined (_M_IA64) || defined(__aarch64__) || defined(__powerpc64__) +#define VK_DEFINE_NON_DISPATCHABLE_HANDLE(object) typedef struct object##_T *object; +#else +#define VK_DEFINE_NON_DISPATCHABLE_HANDLE(object) typedef uint64_t object; +#endif +VK_DEFINE_HANDLE(VkInstance) +VK_DEFINE_NON_DISPATCHABLE_HANDLE(VkSurfaceKHR) +struct VkAllocationCallbacks; +enum VkResult { VK_RESULT_MAX_ENUM = 0x7FFFFFFF }; +#endif // VULKAN_H_ +extern "C" { extern GLFWAPI VkResult glfwCreateWindowSurface(VkInstance instance, GLFWwindow* window, const VkAllocationCallbacks* allocator, VkSurfaceKHR* surface); } +static int ImGui_ImplGlfw_CreateVkSurface(ImGuiViewport* viewport, ImU64 vk_instance, const void* vk_allocator, ImU64* out_vk_surface) +{ + ImGui_ImplGlfw_Data* bd = ImGui_ImplGlfw_GetBackendData(); + ImGui_ImplGlfw_ViewportData* vd = (ImGui_ImplGlfw_ViewportData*)viewport->PlatformUserData; + IM_UNUSED(bd); + IM_ASSERT(bd->ClientApi == GlfwClientApi_Vulkan); + VkResult err = glfwCreateWindowSurface((VkInstance)vk_instance, vd->Window, (const VkAllocationCallbacks*)vk_allocator, (VkSurfaceKHR*)out_vk_surface); + return (int)err; +} +#endif // GLFW_HAS_VULKAN + +static void ImGui_ImplGlfw_InitPlatformInterface() +{ + // Register platform interface (will be coupled with a renderer interface) + ImGui_ImplGlfw_Data* bd = ImGui_ImplGlfw_GetBackendData(); + ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO(); + platform_io.Platform_CreateWindow = ImGui_ImplGlfw_CreateWindow; + platform_io.Platform_DestroyWindow = ImGui_ImplGlfw_DestroyWindow; + platform_io.Platform_ShowWindow = ImGui_ImplGlfw_ShowWindow; + platform_io.Platform_SetWindowPos = ImGui_ImplGlfw_SetWindowPos; + platform_io.Platform_GetWindowPos = ImGui_ImplGlfw_GetWindowPos; + platform_io.Platform_SetWindowSize = ImGui_ImplGlfw_SetWindowSize; + platform_io.Platform_GetWindowSize = ImGui_ImplGlfw_GetWindowSize; + platform_io.Platform_SetWindowFocus = ImGui_ImplGlfw_SetWindowFocus; + platform_io.Platform_GetWindowFocus = ImGui_ImplGlfw_GetWindowFocus; + platform_io.Platform_GetWindowMinimized = ImGui_ImplGlfw_GetWindowMinimized; + platform_io.Platform_SetWindowTitle = ImGui_ImplGlfw_SetWindowTitle; + platform_io.Platform_RenderWindow = ImGui_ImplGlfw_RenderWindow; + platform_io.Platform_SwapBuffers = ImGui_ImplGlfw_SwapBuffers; +#if GLFW_HAS_WINDOW_ALPHA + platform_io.Platform_SetWindowAlpha = ImGui_ImplGlfw_SetWindowAlpha; +#endif +#if GLFW_HAS_VULKAN + platform_io.Platform_CreateVkSurface = ImGui_ImplGlfw_CreateVkSurface; +#endif + + // Register main window handle (which is owned by the main application, not by us) + // This is mostly for simplicity and consistency, so that our code (e.g. mouse handling etc.) can use same logic for main and secondary viewports. + ImGuiViewport* main_viewport = ImGui::GetMainViewport(); + ImGui_ImplGlfw_ViewportData* vd = IM_NEW(ImGui_ImplGlfw_ViewportData)(); + vd->Window = bd->Window; + vd->WindowOwned = false; + main_viewport->PlatformUserData = vd; + main_viewport->PlatformHandle = (void*)bd->Window; +} + +static void ImGui_ImplGlfw_ShutdownPlatformInterface() +{ + ImGui::DestroyPlatformWindows(); +} + +//----------------------------------------------------------------------------- + +// WndProc hook (declared here because we will need access to ImGui_ImplGlfw_ViewportData) +#ifdef _WIN32 +static ImGuiMouseSource GetMouseSourceFromMessageExtraInfo() +{ + LPARAM extra_info = ::GetMessageExtraInfo(); + if ((extra_info & 0xFFFFFF80) == 0xFF515700) + return ImGuiMouseSource_Pen; + if ((extra_info & 0xFFFFFF80) == 0xFF515780) + return ImGuiMouseSource_TouchScreen; + return ImGuiMouseSource_Mouse; +} +static LRESULT CALLBACK ImGui_ImplGlfw_WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) +{ + ImGui_ImplGlfw_Data* bd = ImGui_ImplGlfw_GetBackendData(); + WNDPROC prev_wndproc = bd->PrevWndProc; + ImGuiViewport* viewport = (ImGuiViewport*)::GetPropA(hWnd, "IMGUI_VIEWPORT"); + if (viewport != NULL) + if (ImGui_ImplGlfw_ViewportData* vd = (ImGui_ImplGlfw_ViewportData*)viewport->PlatformUserData) + prev_wndproc = vd->PrevWndProc; + + switch (msg) + { + // GLFW doesn't allow to distinguish Mouse vs TouchScreen vs Pen. + // Add support for Win32 (based on imgui_impl_win32), because we rely on _TouchScreen info to trickle inputs differently. + case WM_MOUSEMOVE: case WM_NCMOUSEMOVE: + case WM_LBUTTONDOWN: case WM_LBUTTONDBLCLK: case WM_LBUTTONUP: + case WM_RBUTTONDOWN: case WM_RBUTTONDBLCLK: case WM_RBUTTONUP: + case WM_MBUTTONDOWN: case WM_MBUTTONDBLCLK: case WM_MBUTTONUP: + case WM_XBUTTONDOWN: case WM_XBUTTONDBLCLK: case WM_XBUTTONUP: + ImGui::GetIO().AddMouseSourceEvent(GetMouseSourceFromMessageExtraInfo()); + break; + + // We have submitted https://github.com/glfw/glfw/pull/1568 to allow GLFW to support "transparent inputs". + // In the meanwhile we implement custom per-platform workarounds here (FIXME-VIEWPORT: Implement same work-around for Linux/OSX!) +#if !GLFW_HAS_MOUSE_PASSTHROUGH && GLFW_HAS_WINDOW_HOVERED + case WM_NCHITTEST: + { + // Let mouse pass-through the window. This will allow the backend to call io.AddMouseViewportEvent() properly (which is OPTIONAL). + // The ImGuiViewportFlags_NoInputs flag is set while dragging a viewport, as want to detect the window behind the one we are dragging. + // If you cannot easily access those viewport flags from your windowing/event code: you may manually synchronize its state e.g. in + // your main loop after calling UpdatePlatformWindows(). Iterate all viewports/platform windows and pass the flag to your windowing system. + if (viewport && (viewport->Flags & ImGuiViewportFlags_NoInputs)) + return HTTRANSPARENT; + break; + } +#endif + } + return ::CallWindowProcW(prev_wndproc, hWnd, msg, wParam, lParam); +} +#endif // #ifdef _WIN32 + +//----------------------------------------------------------------------------- + #if defined(__clang__) #pragma clang diagnostic pop #endif + +#endif // #ifndef IMGUI_DISABLE diff --git a/libs/imgui/backends/imgui_impl_glfw.h b/libs/imgui/backends/imgui_impl_glfw.h new file mode 100644 index 0000000..14e6ee6 --- /dev/null +++ b/libs/imgui/backends/imgui_impl_glfw.h @@ -0,0 +1,63 @@ +// dear imgui: Platform Backend for GLFW +// This needs to be used along with a Renderer (e.g. OpenGL3, Vulkan, WebGPU..) +// (Info: GLFW is a cross-platform general purpose library for handling windows, inputs, OpenGL/Vulkan graphics context creation, etc.) +// (Requires: GLFW 3.1+. Prefer GLFW 3.3+ for full feature support.) + +// Implemented features: +// [X] Platform: Clipboard support. +// [X] Platform: Mouse support. Can discriminate Mouse/TouchScreen/Pen (Windows only). +// [X] Platform: Keyboard support. Since 1.87 we are using the io.AddKeyEvent() function. Pass ImGuiKey values to all key functions e.g. ImGui::IsKeyPressed(ImGuiKey_Space). [Legacy GLFW_KEY_* values will also be supported unless IMGUI_DISABLE_OBSOLETE_KEYIO is set] +// [X] Platform: Gamepad support. Enable with 'io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad'. +// [x] Platform: Mouse cursor shape and visibility. Disable with 'io.ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange' (note: the resizing cursors requires GLFW 3.4+). +// [X] Platform: Multi-viewport support (multiple windows). Enable with 'io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable'. + +// Issues: +// [ ] Platform: Multi-viewport support: ParentViewportID not honored, and so io.ConfigViewportsNoDefaultParent has no effect (minor). + +// You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this. +// Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need. +// Learn about Dear ImGui: +// - FAQ https://dearimgui.com/faq +// - Getting Started https://dearimgui.com/getting-started +// - Documentation https://dearimgui.com/docs (same as your local docs/ folder). +// - Introduction, links and more at the top of imgui.cpp + +#pragma once +#include "imgui.h" // IMGUI_IMPL_API +#ifndef IMGUI_DISABLE + +struct GLFWwindow; +struct GLFWmonitor; + +IMGUI_IMPL_API bool ImGui_ImplGlfw_InitForOpenGL(GLFWwindow* window, bool install_callbacks); +IMGUI_IMPL_API bool ImGui_ImplGlfw_InitForVulkan(GLFWwindow* window, bool install_callbacks); +IMGUI_IMPL_API bool ImGui_ImplGlfw_InitForOther(GLFWwindow* window, bool install_callbacks); +IMGUI_IMPL_API void ImGui_ImplGlfw_Shutdown(); +IMGUI_IMPL_API void ImGui_ImplGlfw_NewFrame(); + +// Emscripten related initialization phase methods +#ifdef __EMSCRIPTEN__ +IMGUI_IMPL_API void ImGui_ImplGlfw_InstallEmscriptenCanvasResizeCallback(const char* canvas_selector); +#endif + +// GLFW callbacks install +// - When calling Init with 'install_callbacks=true': ImGui_ImplGlfw_InstallCallbacks() is called. GLFW callbacks will be installed for you. They will chain-call user's previously installed callbacks, if any. +// - When calling Init with 'install_callbacks=false': GLFW callbacks won't be installed. You will need to call individual function yourself from your own GLFW callbacks. +IMGUI_IMPL_API void ImGui_ImplGlfw_InstallCallbacks(GLFWwindow* window); +IMGUI_IMPL_API void ImGui_ImplGlfw_RestoreCallbacks(GLFWwindow* window); + +// GFLW callbacks options: +// - Set 'chain_for_all_windows=true' to enable chaining callbacks for all windows (including secondary viewports created by backends or by user) +IMGUI_IMPL_API void ImGui_ImplGlfw_SetCallbacksChainForAllWindows(bool chain_for_all_windows); + +// GLFW callbacks (individual callbacks to call yourself if you didn't install callbacks) +IMGUI_IMPL_API void ImGui_ImplGlfw_WindowFocusCallback(GLFWwindow* window, int focused); // Since 1.84 +IMGUI_IMPL_API void ImGui_ImplGlfw_CursorEnterCallback(GLFWwindow* window, int entered); // Since 1.84 +IMGUI_IMPL_API void ImGui_ImplGlfw_CursorPosCallback(GLFWwindow* window, double x, double y); // Since 1.87 +IMGUI_IMPL_API void ImGui_ImplGlfw_MouseButtonCallback(GLFWwindow* window, int button, int action, int mods); +IMGUI_IMPL_API void ImGui_ImplGlfw_ScrollCallback(GLFWwindow* window, double xoffset, double yoffset); +IMGUI_IMPL_API void ImGui_ImplGlfw_KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods); +IMGUI_IMPL_API void ImGui_ImplGlfw_CharCallback(GLFWwindow* window, unsigned int c); +IMGUI_IMPL_API void ImGui_ImplGlfw_MonitorCallback(GLFWmonitor* monitor, int event); + +#endif // #ifndef IMGUI_DISABLE diff --git a/libs/imgui/backends/imgui_impl_opengl3.cpp b/libs/imgui/backends/imgui_impl_opengl3.cpp index a62e95d..586219d 100644 --- a/libs/imgui/backends/imgui_impl_opengl3.cpp +++ b/libs/imgui/backends/imgui_impl_opengl3.cpp @@ -14,11 +14,17 @@ // You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this. // Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need. -// If you are new to Dear ImGui, read documentation from the docs/ folder + read the top of imgui.cpp. -// Read online: https://github.com/ocornut/imgui/tree/master/docs +// Learn about Dear ImGui: +// - FAQ https://dearimgui.com/faq +// - Getting Started https://dearimgui.com/getting-started +// - Documentation https://dearimgui.com/docs (same as your local docs/ folder). +// - Introduction, links and more at the top of imgui.cpp // CHANGELOG // (minor and older changes stripped away, please see git history for details) +// 2024-01-09: OpenGL: Update GL3W based imgui_impl_opengl3_loader.h to load "libGL.so" and variants, fixing regression on distros missing a symlink. +// 2023-11-08: OpenGL: Update GL3W based imgui_impl_opengl3_loader.h to load "libGL.so" instead of "libGL.so.1", accommodating for NetBSD systems having only "libGL.so.3" available. (#6983) +// 2023-10-05: OpenGL: Rename symbols in our internal loader so that LTO compilation with another copy of gl3w is possible. (#6875, #6668, #4445) // 2023-06-20: OpenGL: Fixed erroneous use glGetIntegerv(GL_CONTEXT_PROFILE_MASK) on contexts lower than 3.2. (#6539, #6333) // 2023-05-09: OpenGL: Support for glBindSampler() backup/restore on ES3. (#6375) // 2023-04-18: OpenGL: Restore front and back polygon mode separately when supported by context. (#6333) @@ -103,9 +109,8 @@ #define _CRT_SECURE_NO_WARNINGS #endif -#ifndef IMGUI_DISABLE - #include "imgui.h" +#ifndef IMGUI_DISABLE #include "imgui_impl_opengl3.h" #include #include // intptr_t @@ -175,9 +180,20 @@ extern "C" { #define GL_VERTEX_ARRAY_BINDING GL_VERTEX_ARRAY_BINDING_OES #endif -// Desktop GL 2.0+ has glPolygonMode() which GL ES and WebGL don't have. -#ifdef GL_POLYGON_MODE -#define IMGUI_IMPL_HAS_POLYGON_MODE +// Desktop GL 2.0+ has extension and glPolygonMode() which GL ES and WebGL don't have.. +#if !defined(IMGUI_IMPL_OPENGL_ES2) && !defined(IMGUI_IMPL_OPENGL_ES3) +#define IMGUI_IMPL_OPENGL_HAS_EXTENSIONS // has glGetIntegerv(GL_NUM_EXTENSIONS) +#define IMGUI_IMPL_OPENGL_HAS_POLYGON_MODE // has glPolygonMode() +#endif + +// Desktop GL 2.1+ and GL ES 3.0+ have glBindBuffer() with GL_PIXEL_UNPACK_BUFFER target. +#if !defined(IMGUI_IMPL_OPENGL_ES2) +#define IMGUI_IMPL_OPENGL_MAY_HAVE_BIND_BUFFER_PIXEL_UNPACK +#endif + +// Desktop GL 3.1+ has GL_PRIMITIVE_RESTART state +#if !defined(IMGUI_IMPL_OPENGL_ES2) && !defined(IMGUI_IMPL_OPENGL_ES3) && defined(GL_VERSION_3_1) +#define IMGUI_IMPL_OPENGL_MAY_HAVE_PRIMITIVE_RESTART #endif // Desktop GL 3.2+ has glDrawElementsBaseVertex() which GL ES and WebGL don't have. @@ -190,16 +206,6 @@ extern "C" { #define IMGUI_IMPL_OPENGL_MAY_HAVE_BIND_SAMPLER #endif -// Desktop GL 3.1+ has GL_PRIMITIVE_RESTART state -#if !defined(IMGUI_IMPL_OPENGL_ES2) && !defined(IMGUI_IMPL_OPENGL_ES3) && defined(GL_VERSION_3_1) -#define IMGUI_IMPL_OPENGL_MAY_HAVE_PRIMITIVE_RESTART -#endif - -// Desktop GL use extension detection -#if !defined(IMGUI_IMPL_OPENGL_ES2) && !defined(IMGUI_IMPL_OPENGL_ES3) -#define IMGUI_IMPL_OPENGL_MAY_HAVE_EXTENSIONS -#endif - // [Debugging] //#define IMGUI_IMPL_OPENGL_DEBUG #ifdef IMGUI_IMPL_OPENGL_DEBUG @@ -272,13 +278,13 @@ bool ImGui_ImplOpenGL3_Init(const char* glsl_version) IM_ASSERT(io.BackendRendererUserData == nullptr && "Already initialized a renderer backend!"); // Initialize our loader - #if !defined(IMGUI_IMPL_OPENGL_ES2) && !defined(IMGUI_IMPL_OPENGL_ES3) && !defined(IMGUI_IMPL_OPENGL_LOADER_CUSTOM) - if (imgl3wInit() != 0) - { - fprintf(stderr, "Failed to initialize OpenGL loader!\n"); - return false; - } - #endif +#if !defined(IMGUI_IMPL_OPENGL_ES2) && !defined(IMGUI_IMPL_OPENGL_ES3) && !defined(IMGUI_IMPL_OPENGL_LOADER_CUSTOM) + if (imgl3wInit() != 0) + { + fprintf(stderr, "Failed to initialize OpenGL loader!\n"); + return false; + } +#endif // Setup backend capabilities flags ImGui_ImplOpenGL3_Data* bd = IM_NEW(ImGui_ImplOpenGL3_Data)(); @@ -358,7 +364,7 @@ bool ImGui_ImplOpenGL3_Init(const char* glsl_version) // Detect extensions we support bd->HasClipOrigin = (bd->GlVersion >= 450); -#ifdef IMGUI_IMPL_OPENGL_MAY_HAVE_EXTENSIONS +#ifdef IMGUI_IMPL_OPENGL_HAS_EXTENSIONS GLint num_extensions = 0; glGetIntegerv(GL_NUM_EXTENSIONS, &num_extensions); for (GLint i = 0; i < num_extensions; i++) @@ -410,7 +416,7 @@ static void ImGui_ImplOpenGL3_SetupRenderState(ImDrawData* draw_data, int fb_wid if (bd->GlVersion >= 310) glDisable(GL_PRIMITIVE_RESTART); #endif -#ifdef IMGUI_IMPL_HAS_POLYGON_MODE +#ifdef IMGUI_IMPL_OPENGL_HAS_POLYGON_MODE glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); #endif @@ -462,9 +468,9 @@ static void ImGui_ImplOpenGL3_SetupRenderState(ImDrawData* draw_data, int fb_wid GL_CALL(glEnableVertexAttribArray(bd->AttribLocationVtxPos)); GL_CALL(glEnableVertexAttribArray(bd->AttribLocationVtxUV)); GL_CALL(glEnableVertexAttribArray(bd->AttribLocationVtxColor)); - GL_CALL(glVertexAttribPointer(bd->AttribLocationVtxPos, 2, GL_FLOAT, GL_FALSE, sizeof(ImDrawVert), (GLvoid*)IM_OFFSETOF(ImDrawVert, pos))); - GL_CALL(glVertexAttribPointer(bd->AttribLocationVtxUV, 2, GL_FLOAT, GL_FALSE, sizeof(ImDrawVert), (GLvoid*)IM_OFFSETOF(ImDrawVert, uv))); - GL_CALL(glVertexAttribPointer(bd->AttribLocationVtxColor, 4, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(ImDrawVert), (GLvoid*)IM_OFFSETOF(ImDrawVert, col))); + GL_CALL(glVertexAttribPointer(bd->AttribLocationVtxPos, 2, GL_FLOAT, GL_FALSE, sizeof(ImDrawVert), (GLvoid*)offsetof(ImDrawVert, pos))); + GL_CALL(glVertexAttribPointer(bd->AttribLocationVtxUV, 2, GL_FLOAT, GL_FALSE, sizeof(ImDrawVert), (GLvoid*)offsetof(ImDrawVert, uv))); + GL_CALL(glVertexAttribPointer(bd->AttribLocationVtxColor, 4, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(ImDrawVert), (GLvoid*)offsetof(ImDrawVert, col))); } // OpenGL3 Render function. @@ -499,7 +505,7 @@ void ImGui_ImplOpenGL3_RenderDrawData(ImDrawData* draw_data) #ifdef IMGUI_IMPL_OPENGL_USE_VERTEX_ARRAY GLuint last_vertex_array_object; glGetIntegerv(GL_VERTEX_ARRAY_BINDING, (GLint*)&last_vertex_array_object); #endif -#ifdef IMGUI_IMPL_HAS_POLYGON_MODE +#ifdef IMGUI_IMPL_OPENGL_HAS_POLYGON_MODE GLint last_polygon_mode[2]; glGetIntegerv(GL_POLYGON_MODE, last_polygon_mode); #endif GLint last_viewport[4]; glGetIntegerv(GL_VIEWPORT, last_viewport); @@ -638,7 +644,7 @@ void ImGui_ImplOpenGL3_RenderDrawData(ImDrawData* draw_data) if (bd->GlVersion >= 310) { if (last_enable_primitive_restart) glEnable(GL_PRIMITIVE_RESTART); else glDisable(GL_PRIMITIVE_RESTART); } #endif -#ifdef IMGUI_IMPL_HAS_POLYGON_MODE +#ifdef IMGUI_IMPL_OPENGL_HAS_POLYGON_MODE // Desktop OpenGL 3.0 and OpenGL 3.1 had separate polygon draw modes for front-facing and back-facing faces of polygons if (bd->GlVersion <= 310 || bd->GlProfileIsCompat) { @@ -649,7 +655,7 @@ void ImGui_ImplOpenGL3_RenderDrawData(ImDrawData* draw_data) { glPolygonMode(GL_FRONT_AND_BACK, (GLenum)last_polygon_mode[0]); } -#endif // IMGUI_IMPL_HAS_POLYGON_MODE +#endif // IMGUI_IMPL_OPENGL_HAS_POLYGON_MODE glViewport(last_viewport[0], last_viewport[1], (GLsizei)last_viewport[2], (GLsizei)last_viewport[3]); glScissor(last_scissor_box[0], last_scissor_box[1], (GLsizei)last_scissor_box[2], (GLsizei)last_scissor_box[3]); @@ -746,6 +752,10 @@ bool ImGui_ImplOpenGL3_CreateDeviceObjects() GLint last_texture, last_array_buffer; glGetIntegerv(GL_TEXTURE_BINDING_2D, &last_texture); glGetIntegerv(GL_ARRAY_BUFFER_BINDING, &last_array_buffer); +#ifdef IMGUI_IMPL_OPENGL_MAY_HAVE_BIND_BUFFER_PIXEL_UNPACK + GLint last_pixel_unpack_buffer; + if (bd->GlVersion >= 210) { glGetIntegerv(GL_PIXEL_UNPACK_BUFFER_BINDING, &last_pixel_unpack_buffer); glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); } +#endif #ifdef IMGUI_IMPL_OPENGL_USE_VERTEX_ARRAY GLint last_vertex_array; glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &last_vertex_array); @@ -919,6 +929,9 @@ bool ImGui_ImplOpenGL3_CreateDeviceObjects() // Restore modified GL state glBindTexture(GL_TEXTURE_2D, last_texture); glBindBuffer(GL_ARRAY_BUFFER, last_array_buffer); +#ifdef IMGUI_IMPL_OPENGL_MAY_HAVE_BIND_BUFFER_PIXEL_UNPACK + if (bd->GlVersion >= 210) { glBindBuffer(GL_PIXEL_UNPACK_BUFFER, last_pixel_unpack_buffer); } +#endif #ifdef IMGUI_IMPL_OPENGL_USE_VERTEX_ARRAY glBindVertexArray(last_vertex_array); #endif @@ -944,4 +957,4 @@ void ImGui_ImplOpenGL3_DestroyDeviceObjects() #pragma clang diagnostic pop #endif -#endif // #ifndef IMGUI_DISABLE \ No newline at end of file +#endif // #ifndef IMGUI_DISABLE diff --git a/libs/imgui/backends/imgui_impl_opengl3.h b/libs/imgui/backends/imgui_impl_opengl3.h index 42e1d84..a99d307 100644 --- a/libs/imgui/backends/imgui_impl_opengl3.h +++ b/libs/imgui/backends/imgui_impl_opengl3.h @@ -14,8 +14,11 @@ // You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this. // Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need. -// If you are new to Dear ImGui, read documentation from the docs/ folder + read the top of imgui.cpp. -// Read online: https://github.com/ocornut/imgui/tree/master/docs +// Learn about Dear ImGui: +// - FAQ https://dearimgui.com/faq +// - Getting Started https://dearimgui.com/getting-started +// - Documentation https://dearimgui.com/docs (same as your local docs/ folder). +// - Introduction, links and more at the top of imgui.cpp // About GLSL version: // The 'glsl_version' initialization parameter should be nullptr (default) or a "#version XXX" string. @@ -26,20 +29,19 @@ #include "imgui.h" // IMGUI_IMPL_API #ifndef IMGUI_DISABLE +// FIX(zig-gamedev) extern "C" { + // Backend API + IMGUI_IMPL_API bool ImGui_ImplOpenGL3_Init(const char* glsl_version = nullptr); + IMGUI_IMPL_API void ImGui_ImplOpenGL3_Shutdown(); + IMGUI_IMPL_API void ImGui_ImplOpenGL3_NewFrame(); + IMGUI_IMPL_API void ImGui_ImplOpenGL3_RenderDrawData(ImDrawData* draw_data); -// Backend API -IMGUI_IMPL_API bool ImGui_ImplOpenGL3_Init(const char* glsl_version = nullptr); -IMGUI_IMPL_API void ImGui_ImplOpenGL3_Shutdown(); -IMGUI_IMPL_API void ImGui_ImplOpenGL3_NewFrame(); -IMGUI_IMPL_API void ImGui_ImplOpenGL3_RenderDrawData(ImDrawData* draw_data); - -// (Optional) Called by Init/NewFrame/Shutdown -IMGUI_IMPL_API bool ImGui_ImplOpenGL3_CreateFontsTexture(); -IMGUI_IMPL_API void ImGui_ImplOpenGL3_DestroyFontsTexture(); -IMGUI_IMPL_API bool ImGui_ImplOpenGL3_CreateDeviceObjects(); -IMGUI_IMPL_API void ImGui_ImplOpenGL3_DestroyDeviceObjects(); - + // (Optional) Called by Init/NewFrame/Shutdown + IMGUI_IMPL_API bool ImGui_ImplOpenGL3_CreateFontsTexture(); + IMGUI_IMPL_API void ImGui_ImplOpenGL3_DestroyFontsTexture(); + IMGUI_IMPL_API bool ImGui_ImplOpenGL3_CreateDeviceObjects(); + IMGUI_IMPL_API void ImGui_ImplOpenGL3_DestroyDeviceObjects(); } // Specific OpenGL ES versions @@ -64,4 +66,4 @@ IMGUI_IMPL_API void ImGui_ImplOpenGL3_DestroyDeviceObjects(); #endif -#endif // #ifndef IMGUI_DISABLE \ No newline at end of file +#endif // #ifndef IMGUI_DISABLE diff --git a/libs/imgui/backends/imgui_impl_wgpu.cpp b/libs/imgui/backends/imgui_impl_wgpu.cpp index 6c2b448..e87c5be 100644 --- a/libs/imgui/backends/imgui_impl_wgpu.cpp +++ b/libs/imgui/backends/imgui_impl_wgpu.cpp @@ -1,5 +1,3 @@ -// zig-gamedev changes marked with `FIX(zig-gamedev)` - // dear imgui: Renderer for WebGPU // This needs to be used along with a Platform Binding (e.g. GLFW) // (Please note that WebGPU is currently experimental, will not run on non-beta browsers, and may break.) @@ -7,14 +5,24 @@ // Implemented features: // [X] Renderer: User texture binding. Use 'WGPUTextureView' as ImTextureID. Read the FAQ about ImTextureID! // [X] Renderer: Large meshes support (64k+ vertices) with 16-bit indices. +// Missing features: +// [ ] Renderer: Multi-viewport support (multiple windows). Not meaningful on the web. // You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this. // Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need. -// If you are new to Dear ImGui, read documentation from the docs/ folder + read the top of imgui.cpp. -// Read online: https://github.com/ocornut/imgui/tree/master/docs +// Learn about Dear ImGui: +// - FAQ https://dearimgui.com/faq +// - Getting Started https://dearimgui.com/getting-started +// - Documentation https://dearimgui.com/docs (same as your local docs/ folder). +// - Introduction, links and more at the top of imgui.cpp // CHANGELOG // (minor and older changes stripped away, please see git history for details) +// 2024-01-22: Added configurable PipelineMultisampleState struct. (#7240) +// 2024-01-22: (Breaking) ImGui_ImplWGPU_Init() now takes a ImGui_ImplWGPU_InitInfo structure instead of variety of parameters, allowing for easier further changes. +// 2024-01-22: Fixed pipeline layout leak. (#7245) +// 2024-01-17: Explicitly fill all of WGPUDepthStencilState since standard removed defaults. +// 2023-07-13: Use WGPUShaderModuleWGSLDescriptor's code instead of source. use WGPUMipmapFilterMode_Linear instead of WGPUFilterMode_Linear. (#6602) // 2023-04-11: Align buffer sizes. Use WGSL shaders instead of precompiled SPIR-V. // 2023-04-11: Reorganized backend to pull data from a single structure to facilitate usage with multiple-contexts (all g_XXXX access changed to bd->XXXX). // 2023-01-25: Revert automatic pipeline layout generation (see https://github.com/gpuweb/gpuweb/issues/2470) @@ -30,6 +38,11 @@ // 2021-01-28: Initial version. #include "imgui.h" +#ifndef IMGUI_DISABLE + +// FIX(zig-gamedev): +//#include "imgui_impl_wgpu.h" + #include #include @@ -40,14 +53,31 @@ extern ImGuiID ImHashData(const void* data_p, size_t data_size, ImU32 seed = 0); // FIX(zig-gamedev): We removed header file and declare all our external functions here. extern "C" { -IMGUI_IMPL_API bool ImGui_ImplWGPU_Init(WGPUDevice device, int num_frames_in_flight, WGPUTextureFormat rt_format, WGPUTextureFormat depth_format = WGPUTextureFormat_Undefined); -IMGUI_IMPL_API void ImGui_ImplWGPU_Shutdown(void); -IMGUI_IMPL_API void ImGui_ImplWGPU_NewFrame(void); +// Initialization data, for ImGui_ImplWGPU_Init() +struct ImGui_ImplWGPU_InitInfo +{ + WGPUDevice Device; + int NumFramesInFlight = 3; + WGPUTextureFormat RenderTargetFormat = WGPUTextureFormat_Undefined; + WGPUTextureFormat DepthStencilFormat = WGPUTextureFormat_Undefined; + WGPUMultisampleState PipelineMultisampleState = {}; + + ImGui_ImplWGPU_InitInfo() + { + PipelineMultisampleState.count = 1; + PipelineMultisampleState.mask = -1u; + PipelineMultisampleState.alphaToCoverageEnabled = false; + } +}; + +IMGUI_IMPL_API bool ImGui_ImplWGPU_Init(ImGui_ImplWGPU_InitInfo* init_info); +IMGUI_IMPL_API void ImGui_ImplWGPU_Shutdown(); +IMGUI_IMPL_API void ImGui_ImplWGPU_NewFrame(); IMGUI_IMPL_API void ImGui_ImplWGPU_RenderDrawData(ImDrawData* draw_data, WGPURenderPassEncoder pass_encoder); // Use if you want to reset your rendering device without losing Dear ImGui state. -IMGUI_IMPL_API void ImGui_ImplWGPU_InvalidateDeviceObjects(void); -IMGUI_IMPL_API bool ImGui_ImplWGPU_CreateDeviceObjects(void); +IMGUI_IMPL_API void ImGui_ImplWGPU_InvalidateDeviceObjects(); +IMGUI_IMPL_API bool ImGui_ImplWGPU_CreateDeviceObjects(); } // extern "C" @@ -82,16 +112,17 @@ struct Uniforms struct ImGui_ImplWGPU_Data { - WGPUDevice wgpuDevice = nullptr; - WGPUQueue defaultQueue = nullptr; - WGPUTextureFormat renderTargetFormat = WGPUTextureFormat_Undefined; - WGPUTextureFormat depthStencilFormat = WGPUTextureFormat_Undefined; - WGPURenderPipeline pipelineState = nullptr; + ImGui_ImplWGPU_InitInfo initInfo; + WGPUDevice wgpuDevice = nullptr; + WGPUQueue defaultQueue = nullptr; + WGPUTextureFormat renderTargetFormat = WGPUTextureFormat_Undefined; + WGPUTextureFormat depthStencilFormat = WGPUTextureFormat_Undefined; + WGPURenderPipeline pipelineState = nullptr; - RenderResources renderResources; - FrameResources* pFrameResources = nullptr; - unsigned int numFramesInFlight = 0; - unsigned int frameIndex = UINT_MAX; + RenderResources renderResources; + FrameResources* pFrameResources = nullptr; + unsigned int numFramesInFlight = 0; + unsigned int frameIndex = UINT_MAX; }; // Backend data stored in io.BackendRendererUserData to allow support for multiple Dear ImGui contexts @@ -189,13 +220,12 @@ static void SafeRelease(WGPUBuffer& res) wgpuBufferRelease(res); res = nullptr; } -// FIX(zig-gamedev): https://github.com/ocornut/imgui/commit/9266c0d2d1390e50d2d8070896932c2564594407 static void SafeRelease(WGPUPipelineLayout& res) - { - if (res) - wgpuPipelineLayoutRelease(res); - res = nullptr; - } +{ + if (res) + wgpuPipelineLayoutRelease(res); + res = nullptr; +} static void SafeRelease(WGPURenderPipeline& res) { if (res) @@ -252,7 +282,6 @@ static WGPUProgrammableStageDescriptor ImGui_ImplWGPU_CreateShaderModule(const c WGPUShaderModuleWGSLDescriptor wgsl_desc = {}; wgsl_desc.chain.sType = WGPUSType_ShaderModuleWGSLDescriptor; - // FiX(zig-gamedev): `.source` renamed to `.code` wgsl_desc.code = wgsl_source; WGPUShaderModuleDescriptor desc = {}; @@ -347,7 +376,9 @@ static void ImGui_ImplWGPU_SetupRenderState(ImDrawData* draw_data, WGPURenderPas void ImGui_ImplWGPU_RenderDrawData(ImDrawData* draw_data, WGPURenderPassEncoder pass_encoder) { // Avoid rendering when minimized - if (draw_data->DisplaySize.x <= 0.0f || draw_data->DisplaySize.y <= 0.0f) + 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 || draw_data->CmdListsCount == 0) return; // FIXME: Assuming that this only gets called once per frame! @@ -466,15 +497,15 @@ void ImGui_ImplWGPU_RenderDrawData(ImDrawData* draw_data, WGPURenderPassEncoder // Project scissor/clipping rectangles into framebuffer space ImVec2 clip_min((pcmd->ClipRect.x - clip_off.x) * clip_scale.x, (pcmd->ClipRect.y - clip_off.y) * clip_scale.y); ImVec2 clip_max((pcmd->ClipRect.z - clip_off.x) * clip_scale.x, (pcmd->ClipRect.w - clip_off.y) * clip_scale.y); + + // Clamp to viewport as wgpuRenderPassEncoderSetScissorRect() won't accept values that are off bounds + if (clip_min.x < 0.0f) { clip_min.x = 0.0f; } + if (clip_min.y < 0.0f) { clip_min.y = 0.0f; } + if (clip_max.x > fb_width) { clip_max.x = (float)fb_width; } + if (clip_max.y > fb_height) { clip_max.y = (float)fb_height; } if (clip_max.x <= clip_min.x || clip_max.y <= clip_min.y) continue; - // FIX(zig-gamedev): Fixes 'Popups and Modal windows->Modals->Stacked modals..' from showDemoWindow(). - if (clip_min.x < 0.0f) clip_min.x = 0.0f; - if (clip_min.y < 0.0f) clip_min.y = 0.0f; - if (clip_max.x > draw_data->DisplaySize.x) clip_max.x = draw_data->DisplaySize.x; - if (clip_max.y > draw_data->DisplaySize.y) clip_max.y = draw_data->DisplaySize.y; - // Apply scissor/clipping rectangle, Draw wgpuRenderPassEncoderSetScissorRect(pass_encoder, (uint32_t)clip_min.x, (uint32_t)clip_min.y, (uint32_t)(clip_max.x - clip_min.x), (uint32_t)(clip_max.y - clip_min.y)); wgpuRenderPassEncoderDrawIndexed(pass_encoder, pcmd->ElemCount, 1, pcmd->IdxOffset + global_idx_offset, pcmd->VtxOffset + global_vtx_offset, 0); @@ -540,7 +571,6 @@ static void ImGui_ImplWGPU_CreateFontsTexture() WGPUSamplerDescriptor sampler_desc = {}; sampler_desc.minFilter = WGPUFilterMode_Linear; sampler_desc.magFilter = WGPUFilterMode_Linear; - // FIX(zig-gamedev): WGPUFilterMode_Linear should be WGPUMipmapFilterMode_Linear sampler_desc.mipmapFilter = WGPUMipmapFilterMode_Linear; sampler_desc.addressModeU = WGPUAddressMode_Repeat; sampler_desc.addressModeV = WGPUAddressMode_Repeat; @@ -582,9 +612,7 @@ bool ImGui_ImplWGPU_CreateDeviceObjects() graphics_pipeline_desc.primitive.stripIndexFormat = WGPUIndexFormat_Undefined; graphics_pipeline_desc.primitive.frontFace = WGPUFrontFace_CW; graphics_pipeline_desc.primitive.cullMode = WGPUCullMode_None; - graphics_pipeline_desc.multisample.count = 1; - graphics_pipeline_desc.multisample.mask = UINT_MAX; - graphics_pipeline_desc.multisample.alphaToCoverageEnabled = false; + graphics_pipeline_desc.multisample = bd->initInfo.PipelineMultisampleState; // Bind group layouts WGPUBindGroupLayoutEntry common_bg_layout_entries[2] = {}; @@ -626,9 +654,9 @@ bool ImGui_ImplWGPU_CreateDeviceObjects() // Vertex input configuration WGPUVertexAttribute attribute_desc[] = { - { WGPUVertexFormat_Float32x2, (uint64_t)IM_OFFSETOF(ImDrawVert, pos), 0 }, - { WGPUVertexFormat_Float32x2, (uint64_t)IM_OFFSETOF(ImDrawVert, uv), 1 }, - { WGPUVertexFormat_Unorm8x4, (uint64_t)IM_OFFSETOF(ImDrawVert, col), 2 }, + { WGPUVertexFormat_Float32x2, (uint64_t)offsetof(ImDrawVert, pos), 0 }, + { WGPUVertexFormat_Float32x2, (uint64_t)offsetof(ImDrawVert, uv), 1 }, + { WGPUVertexFormat_Unorm8x4, (uint64_t)offsetof(ImDrawVert, col), 2 }, }; WGPUVertexBufferLayout buffer_layouts[1]; @@ -671,12 +699,10 @@ bool ImGui_ImplWGPU_CreateDeviceObjects() depth_stencil_state.depthWriteEnabled = false; depth_stencil_state.depthCompare = WGPUCompareFunction_Always; depth_stencil_state.stencilFront.compare = WGPUCompareFunction_Always; - // FIX(zig-gamedev): https://github.com/ocornut/imgui/commit/03417cc77d15100b18c486b55db409ee5e9c363e depth_stencil_state.stencilFront.failOp = WGPUStencilOperation_Keep; depth_stencil_state.stencilFront.depthFailOp = WGPUStencilOperation_Keep; depth_stencil_state.stencilFront.passOp = WGPUStencilOperation_Keep; depth_stencil_state.stencilBack.compare = WGPUCompareFunction_Always; - // FIX(zig-gamedev): https://github.com/ocornut/imgui/commit/03417cc77d15100b18c486b55db409ee5e9c363e depth_stencil_state.stencilBack.failOp = WGPUStencilOperation_Keep; depth_stencil_state.stencilBack.depthFailOp = WGPUStencilOperation_Keep; depth_stencil_state.stencilBack.passOp = WGPUStencilOperation_Keep; @@ -709,7 +735,6 @@ bool ImGui_ImplWGPU_CreateDeviceObjects() SafeRelease(vertex_shader_desc.module); SafeRelease(pixel_shader_desc.module); - // FIX(zig-gamedev): https://github.com/ocornut/imgui/commit/9266c0d2d1390e50d2d8070896932c2564594407 SafeRelease(graphics_pipeline_desc.layout); SafeRelease(bg_layouts[0]); @@ -732,7 +757,7 @@ void ImGui_ImplWGPU_InvalidateDeviceObjects() SafeRelease(bd->pFrameResources[i]); } -bool ImGui_ImplWGPU_Init(WGPUDevice device, int num_frames_in_flight, WGPUTextureFormat rt_format, WGPUTextureFormat depth_format) +bool ImGui_ImplWGPU_Init(ImGui_ImplWGPU_InitInfo* init_info) { ImGuiIO& io = ImGui::GetIO(); IM_ASSERT(io.BackendRendererUserData == nullptr && "Already initialized a renderer backend!"); @@ -743,11 +768,12 @@ bool ImGui_ImplWGPU_Init(WGPUDevice device, int num_frames_in_flight, WGPUTextur io.BackendRendererName = "imgui_impl_webgpu"; io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset; // We can honor the ImDrawCmd::VtxOffset field, allowing for large meshes. - bd->wgpuDevice = device; + bd->initInfo = *init_info; + bd->wgpuDevice = init_info->Device; bd->defaultQueue = wgpuDeviceGetQueue(bd->wgpuDevice); - bd->renderTargetFormat = rt_format; - bd->depthStencilFormat = depth_format; - bd->numFramesInFlight = num_frames_in_flight; + bd->renderTargetFormat = init_info->RenderTargetFormat; + bd->depthStencilFormat = init_info->DepthStencilFormat; + bd->numFramesInFlight = init_info->NumFramesInFlight; bd->frameIndex = UINT_MAX; bd->renderResources.FontTexture = nullptr; @@ -760,8 +786,8 @@ bool ImGui_ImplWGPU_Init(WGPUDevice device, int num_frames_in_flight, WGPUTextur bd->renderResources.ImageBindGroupLayout = nullptr; // Create buffers with a default size (they will later be grown as needed) - bd->pFrameResources = new FrameResources[num_frames_in_flight]; - for (int i = 0; i < num_frames_in_flight; i++) + bd->pFrameResources = new FrameResources[bd->numFramesInFlight]; + for (int i = 0; i < bd->numFramesInFlight; i++) { FrameResources* fr = &bd->pFrameResources[i]; fr->IndexBuffer = nullptr; @@ -801,3 +827,7 @@ void ImGui_ImplWGPU_NewFrame() if (!bd->pipelineState) ImGui_ImplWGPU_CreateDeviceObjects(); } + +//----------------------------------------------------------------------------- + +#endif // #ifndef IMGUI_DISABLE diff --git a/libs/imgui/backends/imgui_impl_wgpu.h b/libs/imgui/backends/imgui_impl_wgpu.h new file mode 100644 index 0000000..c0c3b87 --- /dev/null +++ b/libs/imgui/backends/imgui_impl_wgpu.h @@ -0,0 +1,51 @@ +// dear imgui: Renderer for WebGPU +// This needs to be used along with a Platform Binding (e.g. GLFW) +// (Please note that WebGPU is currently experimental, will not run on non-beta browsers, and may break.) + +// Implemented features: +// [X] Renderer: User texture binding. Use 'WGPUTextureView' as ImTextureID. Read the FAQ about ImTextureID! +// [X] Renderer: Large meshes support (64k+ vertices) with 16-bit indices. +// Missing features: +// [ ] Renderer: Multi-viewport support (multiple windows). Not meaningful on the web. + +// You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this. +// Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need. +// Learn about Dear ImGui: +// - FAQ https://dearimgui.com/faq +// - Getting Started https://dearimgui.com/getting-started +// - Documentation https://dearimgui.com/docs (same as your local docs/ folder). +// - Introduction, links and more at the top of imgui.cpp + +#pragma once +#include "imgui.h" // IMGUI_IMPL_API +#ifndef IMGUI_DISABLE + +#include + +// Initialization data, for ImGui_ImplWGPU_Init() +struct ImGui_ImplWGPU_InitInfo +{ + WGPUDevice Device; + int NumFramesInFlight = 3; + WGPUTextureFormat RenderTargetFormat = WGPUTextureFormat_Undefined; + WGPUTextureFormat DepthStencilFormat = WGPUTextureFormat_Undefined; + WGPUMultisampleState PipelineMultisampleState = {}; + + ImGui_ImplWGPU_InitInfo() + { + PipelineMultisampleState.count = 1; + PipelineMultisampleState.mask = -1u; + PipelineMultisampleState.alphaToCoverageEnabled = false; + } +}; + +IMGUI_IMPL_API bool ImGui_ImplWGPU_Init(ImGui_ImplWGPU_InitInfo* init_info); +IMGUI_IMPL_API void ImGui_ImplWGPU_Shutdown(); +IMGUI_IMPL_API void ImGui_ImplWGPU_NewFrame(); +IMGUI_IMPL_API void ImGui_ImplWGPU_RenderDrawData(ImDrawData* draw_data, WGPURenderPassEncoder pass_encoder); + +// Use if you want to reset your rendering device without losing Dear ImGui state. +IMGUI_IMPL_API void ImGui_ImplWGPU_InvalidateDeviceObjects(); +IMGUI_IMPL_API bool ImGui_ImplWGPU_CreateDeviceObjects(); + +#endif // #ifndef IMGUI_DISABLE diff --git a/libs/imgui/backends/imgui_impl_win32.cpp b/libs/imgui/backends/imgui_impl_win32.cpp index b7b941f..3b436c9 100644 --- a/libs/imgui/backends/imgui_impl_win32.cpp +++ b/libs/imgui/backends/imgui_impl_win32.cpp @@ -3,17 +3,24 @@ // Implemented features: // [X] Platform: Clipboard support (for Win32 this is actually part of core dear imgui) +// [X] Platform: Mouse support. Can discriminate Mouse/TouchScreen/Pen. // [X] Platform: Keyboard support. Since 1.87 we are using the io.AddKeyEvent() function. Pass ImGuiKey values to all key functions e.g. ImGui::IsKeyPressed(ImGuiKey_Space). [Legacy VK_* values will also be supported unless IMGUI_DISABLE_OBSOLETE_KEYIO is set] // [X] Platform: Gamepad support. Enabled with 'io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad'. // [X] Platform: Mouse cursor shape and visibility. Disable with 'io.ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange'. +// [X] Platform: Multi-viewport support (multiple windows). Enable with 'io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable'. // You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this. // Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need. -// If you are new to Dear ImGui, read documentation from the docs/ folder + read the top of imgui.cpp. -// Read online: https://github.com/ocornut/imgui/tree/master/docs +// Learn about Dear ImGui: +// - FAQ https://dearimgui.com/faq +// - Getting Started https://dearimgui.com/getting-started +// - Documentation https://dearimgui.com/docs (same as your local docs/ folder). +// - Introduction, links and more at the top of imgui.cpp #include "imgui.h" -#include "imgui_impl_win32.h" +#ifndef IMGUI_DISABLE +// FIX(zig-gamedev): +// #include "imgui_impl_win32.h" #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif @@ -32,8 +39,48 @@ typedef DWORD (WINAPI *PFN_XInputGetCapabilities)(DWORD, DWORD, XINPUT_CAPABILIT typedef DWORD (WINAPI *PFN_XInputGetState)(DWORD, XINPUT_STATE*); #endif +// FIX(zig-gamedev): +extern "C" { + IMGUI_IMPL_API bool ImGui_ImplWin32_Init(void* hwnd); + IMGUI_IMPL_API bool ImGui_ImplWin32_InitForOpenGL(void* hwnd); + IMGUI_IMPL_API void ImGui_ImplWin32_Shutdown(); + IMGUI_IMPL_API void ImGui_ImplWin32_NewFrame(); + + // Win32 message handler your application need to call. + // - Intentionally commented out in a '#if 0' block to avoid dragging dependencies on from this helper. + // - You should COPY the line below into your .cpp code to forward declare the function and then you can call it. + // - Call from your application's message handler. Keep calling your message handler unless this function returns TRUE. + + #if 0 + extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); + #endif + + // DPI-related helpers (optional) + // - Use to enable DPI awareness without having to create an application manifest. + // - Your own app may already do this via a manifest or explicit calls. This is mostly useful for our examples/ apps. + // - In theory we could call simple functions from Windows SDK such as SetProcessDPIAware(), SetProcessDpiAwareness(), etc. + // but most of the functions provided by Microsoft require Windows 8.1/10+ SDK at compile time and Windows 8/10+ at runtime, + // neither we want to require the user to have. So we dynamically select and load those functions to avoid dependencies. + IMGUI_IMPL_API void ImGui_ImplWin32_EnableDpiAwareness(); + IMGUI_IMPL_API float ImGui_ImplWin32_GetDpiScaleForHwnd(void* hwnd); // HWND hwnd + IMGUI_IMPL_API float ImGui_ImplWin32_GetDpiScaleForMonitor(void* monitor); // HMONITOR monitor + + // Transparency related helpers (optional) [experimental] + // - Use to enable alpha compositing transparency with the desktop. + // - Use together with e.g. clearing your framebuffer with zero-alpha. + IMGUI_IMPL_API void ImGui_ImplWin32_EnableAlphaCompositing(void* hwnd); // HWND hwnd +}; // extern "C" + // CHANGELOG // (minor and older changes stripped away, please see git history for details) +// 2024-XX-XX: Platform: Added support for multiple windows via the ImGuiPlatformIO interface. +// 2023-10-05: Inputs: Added support for extra ImGuiKey values: F13 to F24 function keys, app back/forward keys. +// 2023-09-25: Inputs: Synthesize key-down event on key-up for VK_SNAPSHOT / ImGuiKey_PrintScreen as Windows doesn't emit it (same behavior as GLFW/SDL). +// 2023-09-07: Inputs: Added support for keyboard codepage conversion for when application is compiled in MBCS mode and using a non-Unicode window. +// 2023-04-19: Added ImGui_ImplWin32_InitForOpenGL() to facilitate combining raw Win32/Winapi with OpenGL. (#3218) +// 2023-04-04: Inputs: Added support for io.AddMouseSourceEvent() to discriminate ImGuiMouseSource_Mouse/ImGuiMouseSource_TouchScreen/ImGuiMouseSource_Pen. (#2702) +// 2023-02-15: Inputs: Use WM_NCMOUSEMOVE / WM_NCMOUSELEAVE to track mouse position over non-client area (e.g. OS decorations) when app is not focused. (#6045, #6162) +// 2023-02-02: Inputs: Flipping WM_MOUSEHWHEEL (horizontal mouse-wheel) value to match other backends and offer consistent horizontal scrolling direction. (#4019, #6096, #1463) // 2022-10-11: Using 'nullptr' instead of 'NULL' as per our switch to C++11. // 2022-09-28: Inputs: Convert WM_CHAR values with MultiByteToWideChar() when window class was registered as MBCS (not Unicode). // 2022-09-26: Inputs: Renamed ImGuiKey_ModXXX introduced in 1.87 to ImGuiMod_XXX (old names still supported). @@ -79,15 +126,22 @@ typedef DWORD (WINAPI *PFN_XInputGetState)(DWORD, XINPUT_STATE*); // 2017-10-23: Inputs: Using Win32 ::SetCapture/::GetCapture() to retrieve mouse positions outside the client area when dragging. // 2016-11-12: Inputs: Only call Win32 ::SetCursor(nullptr) when io.MouseDrawCursor is set. +// Forward Declarations +static void ImGui_ImplWin32_InitPlatformInterface(bool platformHasOwnDC); +static void ImGui_ImplWin32_ShutdownPlatformInterface(); +static void ImGui_ImplWin32_UpdateMonitors(); + struct ImGui_ImplWin32_Data { HWND hWnd; HWND MouseHwnd; - bool MouseTracked; + int MouseTrackedArea; // 0: not tracked, 1: client are, 2: non-client area int MouseButtonsDown; INT64 Time; INT64 TicksPerSecond; ImGuiMouseCursor LastMouseCursor; + UINT32 KeyboardCodePage; + bool WantUpdateMonitors; #ifndef IMGUI_IMPL_WIN32_DISABLE_GAMEPAD bool HasGamepad; @@ -110,7 +164,17 @@ static ImGui_ImplWin32_Data* ImGui_ImplWin32_GetBackendData() } // Functions -bool ImGui_ImplWin32_Init(void* hwnd) +static void ImGui_ImplWin32_UpdateKeyboardCodePage() +{ + // Retrieve keyboard code page, required for handling of non-Unicode Windows. + ImGui_ImplWin32_Data* bd = ImGui_ImplWin32_GetBackendData(); + HKL keyboard_layout = ::GetKeyboardLayout(0); + LCID keyboard_lcid = MAKELCID(HIWORD(keyboard_layout), SORT_DEFAULT); + if (::GetLocaleInfoA(keyboard_lcid, (LOCALE_RETURN_NUMBER | LOCALE_IDEFAULTANSICODEPAGE), (LPSTR)&bd->KeyboardCodePage, sizeof(bd->KeyboardCodePage)) == 0) + bd->KeyboardCodePage = CP_ACP; // Fallback to default ANSI code page when fails. +} + +static bool ImGui_ImplWin32_InitEx(void* hwnd, bool platform_has_own_dc) { ImGuiIO& io = ImGui::GetIO(); IM_ASSERT(io.BackendPlatformUserData == nullptr && "Already initialized a platform backend!"); @@ -127,14 +191,21 @@ bool ImGui_ImplWin32_Init(void* hwnd) io.BackendPlatformName = "imgui_impl_win32"; io.BackendFlags |= ImGuiBackendFlags_HasMouseCursors; // We can honor GetMouseCursor() values (optional) io.BackendFlags |= ImGuiBackendFlags_HasSetMousePos; // We can honor io.WantSetMousePos requests (optional, rarely used) + io.BackendFlags |= ImGuiBackendFlags_PlatformHasViewports; // We can create multi-viewports on the Platform side (optional) + io.BackendFlags |= ImGuiBackendFlags_HasMouseHoveredViewport; // We can call io.AddMouseViewportEvent() with correct data (optional) bd->hWnd = (HWND)hwnd; + bd->WantUpdateMonitors = true; bd->TicksPerSecond = perf_frequency; bd->Time = perf_counter; bd->LastMouseCursor = ImGuiMouseCursor_COUNT; + ImGui_ImplWin32_UpdateKeyboardCodePage(); - // Set platform dependent data in viewport - ImGui::GetMainViewport()->PlatformHandleRaw = (void*)hwnd; + // Our mouse update function expect PlatformHandle to be filled for the main viewport + ImGuiViewport* main_viewport = ImGui::GetMainViewport(); + main_viewport->PlatformHandle = main_viewport->PlatformHandleRaw = (void*)bd->hWnd; + if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) + ImGui_ImplWin32_InitPlatformInterface(platform_has_own_dc); // Dynamically load XInput library #ifndef IMGUI_IMPL_WIN32_DISABLE_GAMEPAD @@ -160,12 +231,25 @@ bool ImGui_ImplWin32_Init(void* hwnd) return true; } +IMGUI_IMPL_API bool ImGui_ImplWin32_Init(void* hwnd) +{ + return ImGui_ImplWin32_InitEx(hwnd, false); +} + +IMGUI_IMPL_API bool ImGui_ImplWin32_InitForOpenGL(void* hwnd) +{ + // OpenGL needs CS_OWNDC + return ImGui_ImplWin32_InitEx(hwnd, true); +} + void ImGui_ImplWin32_Shutdown() { ImGui_ImplWin32_Data* bd = ImGui_ImplWin32_GetBackendData(); IM_ASSERT(bd != nullptr && "No platform backend to shutdown, or already shutdown?"); ImGuiIO& io = ImGui::GetIO(); + ImGui_ImplWin32_ShutdownPlatformInterface(); + // Unload XInput library #ifndef IMGUI_IMPL_WIN32_DISABLE_GAMEPAD if (bd->XInputDLL) @@ -174,6 +258,7 @@ void ImGui_ImplWin32_Shutdown() io.BackendPlatformName = nullptr; io.BackendPlatformUserData = nullptr; + io.BackendFlags &= ~(ImGuiBackendFlags_HasMouseCursors | ImGuiBackendFlags_HasSetMousePos | ImGuiBackendFlags_HasGamepad | ImGuiBackendFlags_PlatformHasViewports | ImGuiBackendFlags_HasMouseHoveredViewport); IM_DELETE(bd); } @@ -247,31 +332,59 @@ static void ImGui_ImplWin32_UpdateKeyModifiers() io.AddKeyEvent(ImGuiMod_Super, IsVkDown(VK_APPS)); } +// This code supports multi-viewports (multiple OS Windows mapped into different Dear ImGui viewports) +// Because of that, it is a little more complicated than your typical single-viewport binding code! static void ImGui_ImplWin32_UpdateMouseData() { ImGui_ImplWin32_Data* bd = ImGui_ImplWin32_GetBackendData(); ImGuiIO& io = ImGui::GetIO(); IM_ASSERT(bd->hWnd != 0); - const bool is_app_focused = (::GetForegroundWindow() == bd->hWnd); + POINT mouse_screen_pos; + bool has_mouse_screen_pos = ::GetCursorPos(&mouse_screen_pos) != 0; + + HWND focused_window = ::GetForegroundWindow(); + const bool is_app_focused = (focused_window && (focused_window == bd->hWnd || ::IsChild(focused_window, bd->hWnd) || ImGui::FindViewportByPlatformHandle((void*)focused_window))); if (is_app_focused) { // (Optional) Set OS mouse position from Dear ImGui if requested (rarely used, only when ImGuiConfigFlags_NavEnableSetMousePos is enabled by user) + // When multi-viewports are enabled, all Dear ImGui positions are same as OS positions. if (io.WantSetMousePos) { POINT pos = { (int)io.MousePos.x, (int)io.MousePos.y }; - if (::ClientToScreen(bd->hWnd, &pos)) - ::SetCursorPos(pos.x, pos.y); + if ((io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) == 0) + ::ClientToScreen(focused_window, &pos); + ::SetCursorPos(pos.x, pos.y); } // (Optional) Fallback to provide mouse position when focused (WM_MOUSEMOVE already provides this when hovered or captured) - if (!io.WantSetMousePos && !bd->MouseTracked) + // This also fills a short gap when clicking non-client area: WM_NCMOUSELEAVE -> modal OS move -> gap -> WM_NCMOUSEMOVE + if (!io.WantSetMousePos && bd->MouseTrackedArea == 0 && has_mouse_screen_pos) { - POINT pos; - if (::GetCursorPos(&pos) && ::ScreenToClient(bd->hWnd, &pos)) - io.AddMousePosEvent((float)pos.x, (float)pos.y); + // Single viewport mode: mouse position in client window coordinates (io.MousePos is (0,0) when the mouse is on the upper-left corner of the app window) + // (This is the position you can get with ::GetCursorPos() + ::ScreenToClient() or WM_MOUSEMOVE.) + // Multi-viewport mode: mouse position in OS absolute coordinates (io.MousePos is (0,0) when the mouse is on the upper-left of the primary monitor) + // (This is the position you can get with ::GetCursorPos() or WM_MOUSEMOVE + ::ClientToScreen(). In theory adding viewport->Pos to a client position would also be the same.) + POINT mouse_pos = mouse_screen_pos; + if (!(io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)) + ::ScreenToClient(bd->hWnd, &mouse_pos); + io.AddMousePosEvent((float)mouse_pos.x, (float)mouse_pos.y); } } + + // (Optional) When using multiple viewports: call io.AddMouseViewportEvent() with the viewport the OS mouse cursor is hovering. + // If ImGuiBackendFlags_HasMouseHoveredViewport is not set by the backend, Dear imGui will ignore this field and infer the information using its flawed heuristic. + // - [X] Win32 backend correctly ignore viewports with the _NoInputs flag (here using ::WindowFromPoint with WM_NCHITTEST + HTTRANSPARENT in WndProc does that) + // Some backend are not able to handle that correctly. If a backend report an hovered viewport that has the _NoInputs flag (e.g. when dragging a window + // for docking, the viewport has the _NoInputs flag in order to allow us to find the viewport under), then Dear ImGui is forced to ignore the value reported + // by the backend, and use its flawed heuristic to guess the viewport behind. + // - [X] Win32 backend correctly reports this regardless of another viewport behind focused and dragged from (we need this to find a useful drag and drop target). + ImGuiID mouse_viewport_id = 0; + if (has_mouse_screen_pos) + if (HWND hovered_hwnd = ::WindowFromPoint(mouse_screen_pos)) + if (ImGuiViewport* viewport = ImGui::FindViewportByPlatformHandle((void*)hovered_hwnd)) + mouse_viewport_id = viewport->ID; + io.AddMouseViewportEvent(mouse_viewport_id); } // Gamepad navigation mapping @@ -331,6 +444,35 @@ static void ImGui_ImplWin32_UpdateGamepads() #endif // #ifndef IMGUI_IMPL_WIN32_DISABLE_GAMEPAD } +static BOOL CALLBACK ImGui_ImplWin32_UpdateMonitors_EnumFunc(HMONITOR monitor, HDC, LPRECT, LPARAM) +{ + MONITORINFO info = {}; + info.cbSize = sizeof(MONITORINFO); + if (!::GetMonitorInfo(monitor, &info)) + return TRUE; + ImGuiPlatformMonitor imgui_monitor; + imgui_monitor.MainPos = ImVec2((float)info.rcMonitor.left, (float)info.rcMonitor.top); + imgui_monitor.MainSize = ImVec2((float)(info.rcMonitor.right - info.rcMonitor.left), (float)(info.rcMonitor.bottom - info.rcMonitor.top)); + imgui_monitor.WorkPos = ImVec2((float)info.rcWork.left, (float)info.rcWork.top); + imgui_monitor.WorkSize = ImVec2((float)(info.rcWork.right - info.rcWork.left), (float)(info.rcWork.bottom - info.rcWork.top)); + imgui_monitor.DpiScale = ImGui_ImplWin32_GetDpiScaleForMonitor(monitor); + imgui_monitor.PlatformHandle = (void*)monitor; + ImGuiPlatformIO& io = ImGui::GetPlatformIO(); + if (info.dwFlags & MONITORINFOF_PRIMARY) + io.Monitors.push_front(imgui_monitor); + else + io.Monitors.push_back(imgui_monitor); + return TRUE; +} + +static void ImGui_ImplWin32_UpdateMonitors() +{ + ImGui_ImplWin32_Data* bd = ImGui_ImplWin32_GetBackendData(); + ImGui::GetPlatformIO().Monitors.resize(0); + ::EnumDisplayMonitors(nullptr, nullptr, ImGui_ImplWin32_UpdateMonitors_EnumFunc, 0); + bd->WantUpdateMonitors = false; +} + void ImGui_ImplWin32_NewFrame() { ImGuiIO& io = ImGui::GetIO(); @@ -341,6 +483,8 @@ void ImGui_ImplWin32_NewFrame() RECT rect = { 0, 0, 0, 0 }; ::GetClientRect(bd->hWnd, &rect); io.DisplaySize = ImVec2((float)(rect.right - rect.left), (float)(rect.bottom - rect.top)); + if (bd->WantUpdateMonitors) + ImGui_ImplWin32_UpdateMonitors(); // Setup time step INT64 current_time = 0; @@ -478,6 +622,20 @@ static ImGuiKey ImGui_ImplWin32_VirtualKeyToImGuiKey(WPARAM wParam) case VK_F10: return ImGuiKey_F10; case VK_F11: return ImGuiKey_F11; case VK_F12: return ImGuiKey_F12; + case VK_F13: return ImGuiKey_F13; + case VK_F14: return ImGuiKey_F14; + case VK_F15: return ImGuiKey_F15; + case VK_F16: return ImGuiKey_F16; + case VK_F17: return ImGuiKey_F17; + case VK_F18: return ImGuiKey_F18; + case VK_F19: return ImGuiKey_F19; + case VK_F20: return ImGuiKey_F20; + case VK_F21: return ImGuiKey_F21; + case VK_F22: return ImGuiKey_F22; + case VK_F23: return ImGuiKey_F23; + case VK_F24: return ImGuiKey_F24; + case VK_BROWSER_BACK: return ImGuiKey_AppBack; + case VK_BROWSER_FORWARD: return ImGuiKey_AppForward; default: return ImGuiKey_None; } } @@ -502,6 +660,19 @@ static ImGuiKey ImGui_ImplWin32_VirtualKeyToImGuiKey(WPARAM wParam) // Copy this line into your .cpp file to forward declare the function. extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); #endif + +// See https://learn.microsoft.com/en-us/windows/win32/tablet/system-events-and-mouse-messages +// Prefer to call this at the top of the message handler to avoid the possibility of other Win32 calls interfering with this. +static ImGuiMouseSource GetMouseSourceFromMessageExtraInfo() +{ + LPARAM extra_info = ::GetMessageExtraInfo(); + if ((extra_info & 0xFFFFFF80) == 0xFF515700) + return ImGuiMouseSource_Pen; + if ((extra_info & 0xFFFFFF80) == 0xFF515780) + return ImGuiMouseSource_TouchScreen; + return ImGuiMouseSource_Mouse; +} + IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { if (ImGui::GetCurrentContext() == nullptr) @@ -513,27 +684,50 @@ IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hwnd, UINT msg, WPARA switch (msg) { case WM_MOUSEMOVE: + case WM_NCMOUSEMOVE: + { // We need to call TrackMouseEvent in order to receive WM_MOUSELEAVE events + ImGuiMouseSource mouse_source = GetMouseSourceFromMessageExtraInfo(); + const int area = (msg == WM_MOUSEMOVE) ? 1 : 2; bd->MouseHwnd = hwnd; - if (!bd->MouseTracked) + if (bd->MouseTrackedArea != area) { - TRACKMOUSEEVENT tme = { sizeof(tme), TME_LEAVE, hwnd, 0 }; - ::TrackMouseEvent(&tme); - bd->MouseTracked = true; + TRACKMOUSEEVENT tme_cancel = { sizeof(tme_cancel), TME_CANCEL, hwnd, 0 }; + TRACKMOUSEEVENT tme_track = { sizeof(tme_track), (DWORD)((area == 2) ? (TME_LEAVE | TME_NONCLIENT) : TME_LEAVE), hwnd, 0 }; + if (bd->MouseTrackedArea != 0) + ::TrackMouseEvent(&tme_cancel); + ::TrackMouseEvent(&tme_track); + bd->MouseTrackedArea = area; } - io.AddMousePosEvent((float)GET_X_LPARAM(lParam), (float)GET_Y_LPARAM(lParam)); + POINT mouse_pos = { (LONG)GET_X_LPARAM(lParam), (LONG)GET_Y_LPARAM(lParam) }; + bool want_absolute_pos = (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) != 0; + if (msg == WM_MOUSEMOVE && want_absolute_pos) // WM_MOUSEMOVE are client-relative coordinates. + ::ClientToScreen(hwnd, &mouse_pos); + if (msg == WM_NCMOUSEMOVE && !want_absolute_pos) // WM_NCMOUSEMOVE are absolute coordinates. + ::ScreenToClient(hwnd, &mouse_pos); + io.AddMouseSourceEvent(mouse_source); + io.AddMousePosEvent((float)mouse_pos.x, (float)mouse_pos.y); break; + } case WM_MOUSELEAVE: - if (bd->MouseHwnd == hwnd) - bd->MouseHwnd = nullptr; - bd->MouseTracked = false; - io.AddMousePosEvent(-FLT_MAX, -FLT_MAX); + case WM_NCMOUSELEAVE: + { + const int area = (msg == WM_MOUSELEAVE) ? 1 : 2; + if (bd->MouseTrackedArea == area) + { + if (bd->MouseHwnd == hwnd) + bd->MouseHwnd = nullptr; + bd->MouseTrackedArea = 0; + io.AddMousePosEvent(-FLT_MAX, -FLT_MAX); + } break; + } case WM_LBUTTONDOWN: case WM_LBUTTONDBLCLK: case WM_RBUTTONDOWN: case WM_RBUTTONDBLCLK: case WM_MBUTTONDOWN: case WM_MBUTTONDBLCLK: case WM_XBUTTONDOWN: case WM_XBUTTONDBLCLK: { + ImGuiMouseSource mouse_source = GetMouseSourceFromMessageExtraInfo(); int button = 0; if (msg == WM_LBUTTONDOWN || msg == WM_LBUTTONDBLCLK) { button = 0; } if (msg == WM_RBUTTONDOWN || msg == WM_RBUTTONDBLCLK) { button = 1; } @@ -542,6 +736,7 @@ IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hwnd, UINT msg, WPARA if (bd->MouseButtonsDown == 0 && ::GetCapture() == nullptr) ::SetCapture(hwnd); bd->MouseButtonsDown |= 1 << button; + io.AddMouseSourceEvent(mouse_source); io.AddMouseButtonEvent(button, true); return 0; } @@ -550,6 +745,7 @@ IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hwnd, UINT msg, WPARA case WM_MBUTTONUP: case WM_XBUTTONUP: { + ImGuiMouseSource mouse_source = GetMouseSourceFromMessageExtraInfo(); int button = 0; if (msg == WM_LBUTTONUP) { button = 0; } if (msg == WM_RBUTTONUP) { button = 1; } @@ -558,6 +754,7 @@ IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hwnd, UINT msg, WPARA bd->MouseButtonsDown &= ~(1 << button); if (bd->MouseButtonsDown == 0 && ::GetCapture() == hwnd) ::ReleaseCapture(); + io.AddMouseSourceEvent(mouse_source); io.AddMouseButtonEvent(button, false); return 0; } @@ -565,7 +762,7 @@ IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hwnd, UINT msg, WPARA io.AddMouseWheelEvent(0.0f, (float)GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA); return 0; case WM_MOUSEHWHEEL: - io.AddMouseWheelEvent((float)GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA, 0.0f); + io.AddMouseWheelEvent(-(float)GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA, 0.0f); return 0; case WM_KEYDOWN: case WM_KEYUP: @@ -583,10 +780,14 @@ IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hwnd, UINT msg, WPARA int vk = (int)wParam; if ((wParam == VK_RETURN) && (HIWORD(lParam) & KF_EXTENDED)) vk = IM_VK_KEYPAD_ENTER; - - // Submit key event const ImGuiKey key = ImGui_ImplWin32_VirtualKeyToImGuiKey(vk); const int scancode = (int)LOBYTE(HIWORD(lParam)); + + // Special behavior for VK_SNAPSHOT / ImGuiKey_PrintScreen as Windows doesn't emit the key down event. + if (key == ImGuiKey_PrintScreen && !is_key_down) + ImGui_ImplWin32_AddKeyEvent(key, true, vk, scancode); + + // Submit key event if (key != ImGuiKey_None) ImGui_ImplWin32_AddKeyEvent(key, is_key_down, vk, scancode); @@ -614,6 +815,9 @@ IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hwnd, UINT msg, WPARA case WM_KILLFOCUS: io.AddFocusEvent(msg == WM_SETFOCUS); return 0; + case WM_INPUTLANGCHANGE: + ImGui_ImplWin32_UpdateKeyboardCodePage(); + return 0; case WM_CHAR: if (::IsWindowUnicode(hwnd)) { @@ -624,7 +828,7 @@ IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hwnd, UINT msg, WPARA else { wchar_t wch = 0; - ::MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, (char*)&wParam, 1, &wch, 1); + ::MultiByteToWideChar(bd->KeyboardCodePage, MB_PRECOMPOSED, (char*)&wParam, 1, &wch, 1); io.AddInputCharacter(wch); } return 0; @@ -639,6 +843,9 @@ IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hwnd, UINT msg, WPARA bd->WantUpdateHasGamepad = true; #endif return 0; + case WM_DISPLAYCHANGE: + bd->WantUpdateMonitors = true; + return 0; } return 0; } @@ -703,6 +910,10 @@ typedef DPI_AWARENESS_CONTEXT(WINAPI* PFN_SetThreadDpiAwarenessContext)(DPI_AWAR // Helper function to enable DPI awareness without setting up a manifest void ImGui_ImplWin32_EnableDpiAwareness() { + // Make sure monitors will be updated with latest correct scaling + if (ImGui_ImplWin32_Data* bd = ImGui_ImplWin32_GetBackendData()) + bd->WantUpdateMonitors = true; + if (_IsWindows10OrGreater()) { static HINSTANCE user32_dll = ::LoadLibraryA("user32.dll"); // Reference counted per-process @@ -803,3 +1014,347 @@ void ImGui_ImplWin32_EnableAlphaCompositing(void* hwnd) } //--------------------------------------------------------------------------------------------------------- +// MULTI-VIEWPORT / PLATFORM INTERFACE SUPPORT +// This is an _advanced_ and _optional_ feature, allowing the backend to create and handle multiple viewports simultaneously. +// If you are new to dear imgui or creating a new binding for dear imgui, it is recommended that you completely ignore this section first.. +//-------------------------------------------------------------------------------------------------------- + +// Helper structure we store in the void* RendererUserData field of each ImGuiViewport to easily retrieve our backend data. +struct ImGui_ImplWin32_ViewportData +{ + HWND Hwnd; + HWND HwndParent; + bool HwndOwned; + DWORD DwStyle; + DWORD DwExStyle; + + ImGui_ImplWin32_ViewportData() { Hwnd = HwndParent = nullptr; HwndOwned = false; DwStyle = DwExStyle = 0; } + ~ImGui_ImplWin32_ViewportData() { IM_ASSERT(Hwnd == nullptr); } +}; + +static void ImGui_ImplWin32_GetWin32StyleFromViewportFlags(ImGuiViewportFlags flags, DWORD* out_style, DWORD* out_ex_style) +{ + if (flags & ImGuiViewportFlags_NoDecoration) + *out_style = WS_POPUP; + else + *out_style = WS_OVERLAPPEDWINDOW; + + if (flags & ImGuiViewportFlags_NoTaskBarIcon) + *out_ex_style = WS_EX_TOOLWINDOW; + else + *out_ex_style = WS_EX_APPWINDOW; + + if (flags & ImGuiViewportFlags_TopMost) + *out_ex_style |= WS_EX_TOPMOST; +} + +static HWND ImGui_ImplWin32_GetHwndFromViewportID(ImGuiID viewport_id) +{ + if (viewport_id != 0) + if (ImGuiViewport* viewport = ImGui::FindViewportByID(viewport_id)) + return (HWND)viewport->PlatformHandle; + return nullptr; +} + +static void ImGui_ImplWin32_CreateWindow(ImGuiViewport* viewport) +{ + ImGui_ImplWin32_ViewportData* vd = IM_NEW(ImGui_ImplWin32_ViewportData)(); + viewport->PlatformUserData = vd; + + // Select style and parent window + ImGui_ImplWin32_GetWin32StyleFromViewportFlags(viewport->Flags, &vd->DwStyle, &vd->DwExStyle); + vd->HwndParent = ImGui_ImplWin32_GetHwndFromViewportID(viewport->ParentViewportId); + + // Create window + RECT rect = { (LONG)viewport->Pos.x, (LONG)viewport->Pos.y, (LONG)(viewport->Pos.x + viewport->Size.x), (LONG)(viewport->Pos.y + viewport->Size.y) }; + ::AdjustWindowRectEx(&rect, vd->DwStyle, FALSE, vd->DwExStyle); + vd->Hwnd = ::CreateWindowEx( + vd->DwExStyle, _T("ImGui Platform"), _T("Untitled"), vd->DwStyle, // Style, class name, window name + rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, // Window area + vd->HwndParent, nullptr, ::GetModuleHandle(nullptr), nullptr); // Owner window, Menu, Instance, Param + vd->HwndOwned = true; + viewport->PlatformRequestResize = false; + viewport->PlatformHandle = viewport->PlatformHandleRaw = vd->Hwnd; +} + +static void ImGui_ImplWin32_DestroyWindow(ImGuiViewport* viewport) +{ + ImGui_ImplWin32_Data* bd = ImGui_ImplWin32_GetBackendData(); + if (ImGui_ImplWin32_ViewportData* vd = (ImGui_ImplWin32_ViewportData*)viewport->PlatformUserData) + { + if (::GetCapture() == vd->Hwnd) + { + // Transfer capture so if we started dragging from a window that later disappears, we'll still receive the MOUSEUP event. + ::ReleaseCapture(); + ::SetCapture(bd->hWnd); + } + if (vd->Hwnd && vd->HwndOwned) + ::DestroyWindow(vd->Hwnd); + vd->Hwnd = nullptr; + IM_DELETE(vd); + } + viewport->PlatformUserData = viewport->PlatformHandle = nullptr; +} + +static void ImGui_ImplWin32_ShowWindow(ImGuiViewport* viewport) +{ + ImGui_ImplWin32_ViewportData* vd = (ImGui_ImplWin32_ViewportData*)viewport->PlatformUserData; + IM_ASSERT(vd->Hwnd != 0); + if (viewport->Flags & ImGuiViewportFlags_NoFocusOnAppearing) + ::ShowWindow(vd->Hwnd, SW_SHOWNA); + else + ::ShowWindow(vd->Hwnd, SW_SHOW); +} + +static void ImGui_ImplWin32_UpdateWindow(ImGuiViewport* viewport) +{ + ImGui_ImplWin32_ViewportData* vd = (ImGui_ImplWin32_ViewportData*)viewport->PlatformUserData; + IM_ASSERT(vd->Hwnd != 0); + + // Update Win32 parent if it changed _after_ creation + // Unlike style settings derived from configuration flags, this is more likely to change for advanced apps that are manipulating ParentViewportID manually. + HWND new_parent = ImGui_ImplWin32_GetHwndFromViewportID(viewport->ParentViewportId); + if (new_parent != vd->HwndParent) + { + // Win32 windows can either have a "Parent" (for WS_CHILD window) or an "Owner" (which among other thing keeps window above its owner). + // Our Dear Imgui-side concept of parenting only mostly care about what Win32 call "Owner". + // The parent parameter of CreateWindowEx() sets up Parent OR Owner depending on WS_CHILD flag. In our case an Owner as we never use WS_CHILD. + // Calling ::SetParent() here would be incorrect: it will create a full child relation, alter coordinate system and clipping. + // Calling ::SetWindowLongPtr() with GWLP_HWNDPARENT seems correct although poorly documented. + // https://devblogs.microsoft.com/oldnewthing/20100315-00/?p=14613 + vd->HwndParent = new_parent; + ::SetWindowLongPtr(vd->Hwnd, GWLP_HWNDPARENT, (LONG_PTR)vd->HwndParent); + } + + // (Optional) Update Win32 style if it changed _after_ creation. + // Generally they won't change unless configuration flags are changed, but advanced uses (such as manually rewriting viewport flags) make this useful. + DWORD new_style; + DWORD new_ex_style; + ImGui_ImplWin32_GetWin32StyleFromViewportFlags(viewport->Flags, &new_style, &new_ex_style); + + // Only reapply the flags that have been changed from our point of view (as other flags are being modified by Windows) + if (vd->DwStyle != new_style || vd->DwExStyle != new_ex_style) + { + // (Optional) Update TopMost state if it changed _after_ creation + bool top_most_changed = (vd->DwExStyle & WS_EX_TOPMOST) != (new_ex_style & WS_EX_TOPMOST); + HWND insert_after = top_most_changed ? ((viewport->Flags & ImGuiViewportFlags_TopMost) ? HWND_TOPMOST : HWND_NOTOPMOST) : 0; + UINT swp_flag = top_most_changed ? 0 : SWP_NOZORDER; + + // Apply flags and position (since it is affected by flags) + vd->DwStyle = new_style; + vd->DwExStyle = new_ex_style; + ::SetWindowLong(vd->Hwnd, GWL_STYLE, vd->DwStyle); + ::SetWindowLong(vd->Hwnd, GWL_EXSTYLE, vd->DwExStyle); + RECT rect = { (LONG)viewport->Pos.x, (LONG)viewport->Pos.y, (LONG)(viewport->Pos.x + viewport->Size.x), (LONG)(viewport->Pos.y + viewport->Size.y) }; + ::AdjustWindowRectEx(&rect, vd->DwStyle, FALSE, vd->DwExStyle); // Client to Screen + ::SetWindowPos(vd->Hwnd, insert_after, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, swp_flag | SWP_NOACTIVATE | SWP_FRAMECHANGED); + ::ShowWindow(vd->Hwnd, SW_SHOWNA); // This is necessary when we alter the style + viewport->PlatformRequestMove = viewport->PlatformRequestResize = true; + } +} + +static ImVec2 ImGui_ImplWin32_GetWindowPos(ImGuiViewport* viewport) +{ + ImGui_ImplWin32_ViewportData* vd = (ImGui_ImplWin32_ViewportData*)viewport->PlatformUserData; + IM_ASSERT(vd->Hwnd != 0); + POINT pos = { 0, 0 }; + ::ClientToScreen(vd->Hwnd, &pos); + return ImVec2((float)pos.x, (float)pos.y); +} + +static void ImGui_ImplWin32_SetWindowPos(ImGuiViewport* viewport, ImVec2 pos) +{ + ImGui_ImplWin32_ViewportData* vd = (ImGui_ImplWin32_ViewportData*)viewport->PlatformUserData; + IM_ASSERT(vd->Hwnd != 0); + RECT rect = { (LONG)pos.x, (LONG)pos.y, (LONG)pos.x, (LONG)pos.y }; + ::AdjustWindowRectEx(&rect, vd->DwStyle, FALSE, vd->DwExStyle); + ::SetWindowPos(vd->Hwnd, nullptr, rect.left, rect.top, 0, 0, SWP_NOZORDER | SWP_NOSIZE | SWP_NOACTIVATE); +} + +static ImVec2 ImGui_ImplWin32_GetWindowSize(ImGuiViewport* viewport) +{ + ImGui_ImplWin32_ViewportData* vd = (ImGui_ImplWin32_ViewportData*)viewport->PlatformUserData; + IM_ASSERT(vd->Hwnd != 0); + RECT rect; + ::GetClientRect(vd->Hwnd, &rect); + return ImVec2(float(rect.right - rect.left), float(rect.bottom - rect.top)); +} + +static void ImGui_ImplWin32_SetWindowSize(ImGuiViewport* viewport, ImVec2 size) +{ + ImGui_ImplWin32_ViewportData* vd = (ImGui_ImplWin32_ViewportData*)viewport->PlatformUserData; + IM_ASSERT(vd->Hwnd != 0); + RECT rect = { 0, 0, (LONG)size.x, (LONG)size.y }; + ::AdjustWindowRectEx(&rect, vd->DwStyle, FALSE, vd->DwExStyle); // Client to Screen + ::SetWindowPos(vd->Hwnd, nullptr, 0, 0, rect.right - rect.left, rect.bottom - rect.top, SWP_NOZORDER | SWP_NOMOVE | SWP_NOACTIVATE); +} + +static void ImGui_ImplWin32_SetWindowFocus(ImGuiViewport* viewport) +{ + ImGui_ImplWin32_ViewportData* vd = (ImGui_ImplWin32_ViewportData*)viewport->PlatformUserData; + IM_ASSERT(vd->Hwnd != 0); + ::BringWindowToTop(vd->Hwnd); + ::SetForegroundWindow(vd->Hwnd); + ::SetFocus(vd->Hwnd); +} + +static bool ImGui_ImplWin32_GetWindowFocus(ImGuiViewport* viewport) +{ + ImGui_ImplWin32_ViewportData* vd = (ImGui_ImplWin32_ViewportData*)viewport->PlatformUserData; + IM_ASSERT(vd->Hwnd != 0); + return ::GetForegroundWindow() == vd->Hwnd; +} + +static bool ImGui_ImplWin32_GetWindowMinimized(ImGuiViewport* viewport) +{ + ImGui_ImplWin32_ViewportData* vd = (ImGui_ImplWin32_ViewportData*)viewport->PlatformUserData; + IM_ASSERT(vd->Hwnd != 0); + return ::IsIconic(vd->Hwnd) != 0; +} + +static void ImGui_ImplWin32_SetWindowTitle(ImGuiViewport* viewport, const char* title) +{ + // ::SetWindowTextA() doesn't properly handle UTF-8 so we explicitely convert our string. + ImGui_ImplWin32_ViewportData* vd = (ImGui_ImplWin32_ViewportData*)viewport->PlatformUserData; + IM_ASSERT(vd->Hwnd != 0); + int n = ::MultiByteToWideChar(CP_UTF8, 0, title, -1, nullptr, 0); + ImVector title_w; + title_w.resize(n); + ::MultiByteToWideChar(CP_UTF8, 0, title, -1, title_w.Data, n); + ::SetWindowTextW(vd->Hwnd, title_w.Data); +} + +static void ImGui_ImplWin32_SetWindowAlpha(ImGuiViewport* viewport, float alpha) +{ + ImGui_ImplWin32_ViewportData* vd = (ImGui_ImplWin32_ViewportData*)viewport->PlatformUserData; + IM_ASSERT(vd->Hwnd != 0); + IM_ASSERT(alpha >= 0.0f && alpha <= 1.0f); + if (alpha < 1.0f) + { + DWORD style = ::GetWindowLongW(vd->Hwnd, GWL_EXSTYLE) | WS_EX_LAYERED; + ::SetWindowLongW(vd->Hwnd, GWL_EXSTYLE, style); + ::SetLayeredWindowAttributes(vd->Hwnd, 0, (BYTE)(255 * alpha), LWA_ALPHA); + } + else + { + DWORD style = ::GetWindowLongW(vd->Hwnd, GWL_EXSTYLE) & ~WS_EX_LAYERED; + ::SetWindowLongW(vd->Hwnd, GWL_EXSTYLE, style); + } +} + +static float ImGui_ImplWin32_GetWindowDpiScale(ImGuiViewport* viewport) +{ + ImGui_ImplWin32_ViewportData* vd = (ImGui_ImplWin32_ViewportData*)viewport->PlatformUserData; + IM_ASSERT(vd->Hwnd != 0); + return ImGui_ImplWin32_GetDpiScaleForHwnd(vd->Hwnd); +} + +// FIXME-DPI: Testing DPI related ideas +static void ImGui_ImplWin32_OnChangedViewport(ImGuiViewport* viewport) +{ + (void)viewport; +#if 0 + ImGuiStyle default_style; + //default_style.WindowPadding = ImVec2(0, 0); + //default_style.WindowBorderSize = 0.0f; + //default_style.ItemSpacing.y = 3.0f; + //default_style.FramePadding = ImVec2(0, 0); + default_style.ScaleAllSizes(viewport->DpiScale); + ImGuiStyle& style = ImGui::GetStyle(); + style = default_style; +#endif +} + +static LRESULT CALLBACK ImGui_ImplWin32_WndProcHandler_PlatformWindow(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) +{ + if (ImGui_ImplWin32_WndProcHandler(hWnd, msg, wParam, lParam)) + return true; + + if (ImGuiViewport* viewport = ImGui::FindViewportByPlatformHandle((void*)hWnd)) + { + switch (msg) + { + case WM_CLOSE: + viewport->PlatformRequestClose = true; + return 0; + case WM_MOVE: + viewport->PlatformRequestMove = true; + break; + case WM_SIZE: + viewport->PlatformRequestResize = true; + break; + case WM_MOUSEACTIVATE: + if (viewport->Flags & ImGuiViewportFlags_NoFocusOnClick) + return MA_NOACTIVATE; + break; + case WM_NCHITTEST: + // Let mouse pass-through the window. This will allow the backend to call io.AddMouseViewportEvent() correctly. (which is optional). + // The ImGuiViewportFlags_NoInputs flag is set while dragging a viewport, as want to detect the window behind the one we are dragging. + // If you cannot easily access those viewport flags from your windowing/event code: you may manually synchronize its state e.g. in + // your main loop after calling UpdatePlatformWindows(). Iterate all viewports/platform windows and pass the flag to your windowing system. + if (viewport->Flags & ImGuiViewportFlags_NoInputs) + return HTTRANSPARENT; + break; + } + } + + return DefWindowProc(hWnd, msg, wParam, lParam); +} + +static void ImGui_ImplWin32_InitPlatformInterface(bool platform_has_own_dc) +{ + WNDCLASSEX wcex; + wcex.cbSize = sizeof(WNDCLASSEX); + wcex.style = CS_HREDRAW | CS_VREDRAW | (platform_has_own_dc ? CS_OWNDC : 0); + wcex.lpfnWndProc = ImGui_ImplWin32_WndProcHandler_PlatformWindow; + wcex.cbClsExtra = 0; + wcex.cbWndExtra = 0; + wcex.hInstance = ::GetModuleHandle(nullptr); + wcex.hIcon = nullptr; + wcex.hCursor = nullptr; + wcex.hbrBackground = (HBRUSH)(COLOR_BACKGROUND + 1); + wcex.lpszMenuName = nullptr; + wcex.lpszClassName = _T("ImGui Platform"); + wcex.hIconSm = nullptr; + ::RegisterClassEx(&wcex); + + ImGui_ImplWin32_UpdateMonitors(); + + // Register platform interface (will be coupled with a renderer interface) + ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO(); + platform_io.Platform_CreateWindow = ImGui_ImplWin32_CreateWindow; + platform_io.Platform_DestroyWindow = ImGui_ImplWin32_DestroyWindow; + platform_io.Platform_ShowWindow = ImGui_ImplWin32_ShowWindow; + platform_io.Platform_SetWindowPos = ImGui_ImplWin32_SetWindowPos; + platform_io.Platform_GetWindowPos = ImGui_ImplWin32_GetWindowPos; + platform_io.Platform_SetWindowSize = ImGui_ImplWin32_SetWindowSize; + platform_io.Platform_GetWindowSize = ImGui_ImplWin32_GetWindowSize; + platform_io.Platform_SetWindowFocus = ImGui_ImplWin32_SetWindowFocus; + platform_io.Platform_GetWindowFocus = ImGui_ImplWin32_GetWindowFocus; + platform_io.Platform_GetWindowMinimized = ImGui_ImplWin32_GetWindowMinimized; + platform_io.Platform_SetWindowTitle = ImGui_ImplWin32_SetWindowTitle; + platform_io.Platform_SetWindowAlpha = ImGui_ImplWin32_SetWindowAlpha; + platform_io.Platform_UpdateWindow = ImGui_ImplWin32_UpdateWindow; + platform_io.Platform_GetWindowDpiScale = ImGui_ImplWin32_GetWindowDpiScale; // FIXME-DPI + platform_io.Platform_OnChangedViewport = ImGui_ImplWin32_OnChangedViewport; // FIXME-DPI + + // Register main window handle (which is owned by the main application, not by us) + // This is mostly for simplicity and consistency, so that our code (e.g. mouse handling etc.) can use same logic for main and secondary viewports. + ImGuiViewport* main_viewport = ImGui::GetMainViewport(); + ImGui_ImplWin32_Data* bd = ImGui_ImplWin32_GetBackendData(); + ImGui_ImplWin32_ViewportData* vd = IM_NEW(ImGui_ImplWin32_ViewportData)(); + vd->Hwnd = bd->hWnd; + vd->HwndOwned = false; + main_viewport->PlatformUserData = vd; + main_viewport->PlatformHandle = (void*)bd->hWnd; +} + +static void ImGui_ImplWin32_ShutdownPlatformInterface() +{ + ::UnregisterClass(_T("ImGui Platform"), ::GetModuleHandle(nullptr)); + ImGui::DestroyPlatformWindows(); +} + +//--------------------------------------------------------------------------------------------------------- + +#endif // #ifndef IMGUI_DISABLE diff --git a/libs/imgui/backends/imgui_impl_win32.h b/libs/imgui/backends/imgui_impl_win32.h index 3a32540..cebe661 100644 --- a/libs/imgui/backends/imgui_impl_win32.h +++ b/libs/imgui/backends/imgui_impl_win32.h @@ -3,21 +3,26 @@ // Implemented features: // [X] Platform: Clipboard support (for Win32 this is actually part of core dear imgui) +// [X] Platform: Mouse support. Can discriminate Mouse/TouchScreen/Pen. // [X] Platform: Keyboard support. Since 1.87 we are using the io.AddKeyEvent() function. Pass ImGuiKey values to all key functions e.g. ImGui::IsKeyPressed(ImGuiKey_Space). [Legacy VK_* values will also be supported unless IMGUI_DISABLE_OBSOLETE_KEYIO is set] // [X] Platform: Gamepad support. Enabled with 'io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad'. // [X] Platform: Mouse cursor shape and visibility. Disable with 'io.ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange'. +// [X] Platform: Multi-viewport support (multiple windows). Enable with 'io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable'. // You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this. // Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need. -// If you are new to Dear ImGui, read documentation from the docs/ folder + read the top of imgui.cpp. -// Read online: https://github.com/ocornut/imgui/tree/master/docs +// Learn about Dear ImGui: +// - FAQ https://dearimgui.com/faq +// - Getting Started https://dearimgui.com/getting-started +// - Documentation https://dearimgui.com/docs (same as your local docs/ folder). +// - Introduction, links and more at the top of imgui.cpp #pragma once #include "imgui.h" // IMGUI_IMPL_API - -extern "C" { // mziulek +#ifndef IMGUI_DISABLE IMGUI_IMPL_API bool ImGui_ImplWin32_Init(void* hwnd); +IMGUI_IMPL_API bool ImGui_ImplWin32_InitForOpenGL(void* hwnd); IMGUI_IMPL_API void ImGui_ImplWin32_Shutdown(); IMGUI_IMPL_API void ImGui_ImplWin32_NewFrame(); @@ -45,4 +50,4 @@ IMGUI_IMPL_API float ImGui_ImplWin32_GetDpiScaleForMonitor(void* monitor); // // - Use together with e.g. clearing your framebuffer with zero-alpha. IMGUI_IMPL_API void ImGui_ImplWin32_EnableAlphaCompositing(void* hwnd); // HWND hwnd -} +#endif // #ifndef IMGUI_DISABLE diff --git a/libs/imgui/backends/opengl-decls.h b/libs/imgui/backends/opengl-decls.h index 73ff87d..646bf23 100644 --- a/libs/imgui/backends/opengl-decls.h +++ b/libs/imgui/backends/opengl-decls.h @@ -153,6 +153,8 @@ typedef khronos_intptr_t GLintptr; #define GL_ARRAY_BUFFER_BINDING 0x8894 #define GL_ELEMENT_ARRAY_BUFFER_BINDING 0x8895 #define GL_STREAM_DRAW 0x88E0 +#define GL_PIXEL_UNPACK_BUFFER 0x88EC +#define GL_PIXEL_UNPACK_BUFFER_BINDING 0x88EF typedef void (APIENTRYP PFNGLBINDBUFFERPROC) (GLenum target, GLuint buffer); typedef void (APIENTRYP PFNGLDELETEBUFFERSPROC) (GLsizei n, const GLuint *buffers); typedef void (APIENTRYP PFNGLGENBUFFERSPROC) (GLsizei n, GLuint *buffers); diff --git a/libs/imgui/imconfig.h b/libs/imgui/imconfig.h index 0986177..d556cba 100644 --- a/libs/imgui/imconfig.h +++ b/libs/imgui/imconfig.h @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// COMPILE-TIME OPTIONS FOR DEAR IMGUI +// DEAR IMGUI COMPILE-TIME OPTIONS // Runtime options (clipboard callbacks, enabling various features, etc.) can generally be set via the ImGuiIO structure. // You can use ImGui::SetAllocatorFunctions() before calling ImGui::CreateContext() to rewire memory allocation functions. //----------------------------------------------------------------------------- @@ -9,7 +9,7 @@ // You need to make sure that configuration settings are defined consistently _everywhere_ Dear ImGui is used, which include the imgui*.cpp // files but also _any_ of your code that uses Dear ImGui. This is because some compile-time options have an affect on data structures. // Defining those options in imconfig.h will ensure every compilation unit gets to see the same data structure layouts. -// Call IMGUI_CHECKVERSION() from your .cpp files to verify that the data structures your files are using are matching the ones imgui.cpp is using. +// Call IMGUI_CHECKVERSION() from your .cpp file to verify that the data structures your files are using are matching the ones imgui.cpp is using. //----------------------------------------------------------------------------- #pragma once @@ -26,21 +26,21 @@ //#define IMGUI_API __declspec( dllexport ) //#define IMGUI_API __declspec( dllimport ) -//---- Don't define obsolete functions/enums/behaviors. Consider enabling from time to time after updating to avoid using soon-to-be obsolete function/names. -#define IMGUI_DISABLE_OBSOLETE_FUNCTIONS -#define IMGUI_DISABLE_OBSOLETE_KEYIO // 1.87: disable legacy io.KeyMap[]+io.KeysDown[] in favor io.AddKeyEvent(). This will be folded into IMGUI_DISABLE_OBSOLETE_FUNCTIONS in a few versions. +//---- Don't define obsolete functions/enums/behaviors. Consider enabling from time to time after updating to clean your code of obsolete function/names. +//#define IMGUI_DISABLE_OBSOLETE_FUNCTIONS +//#define IMGUI_DISABLE_OBSOLETE_KEYIO // 1.87+ disable legacy io.KeyMap[]+io.KeysDown[] in favor io.AddKeyEvent(). This is automatically done by IMGUI_DISABLE_OBSOLETE_FUNCTIONS. //---- Disable all of Dear ImGui or don't implement standard windows/tools. // It is very strongly recommended to NOT disable the demo windows and debug tool during development. They are extremely useful in day to day work. Please read comments in imgui_demo.cpp. //#define IMGUI_DISABLE // Disable everything: all headers and source files will be empty. //#define IMGUI_DISABLE_DEMO_WINDOWS // Disable demo windows: ShowDemoWindow()/ShowStyleEditor() will be empty. -//#define IMGUI_DISABLE_DEBUG_TOOLS // Disable metrics/debugger and other debug tools: ShowMetricsWindow(), ShowDebugLogWindow() and ShowStackToolWindow() will be empty (this was called IMGUI_DISABLE_METRICS_WINDOW before 1.88). +//#define IMGUI_DISABLE_DEBUG_TOOLS // Disable metrics/debugger and other debug tools: ShowMetricsWindow(), ShowDebugLogWindow() and ShowIDStackToolWindow() will be empty. //---- Don't implement some functions to reduce linkage requirements. //#define IMGUI_DISABLE_WIN32_DEFAULT_CLIPBOARD_FUNCTIONS // [Win32] Don't implement default clipboard handler. Won't use and link with OpenClipboard/GetClipboardData/CloseClipboard etc. (user32.lib/.a, kernel32.lib/.a) //#define IMGUI_ENABLE_WIN32_DEFAULT_IME_FUNCTIONS // [Win32] [Default with Visual Studio] Implement default IME handler (require imm32.lib/.a, auto-link for Visual Studio, -limm32 on command-line for MinGW) //#define IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS // [Win32] [Default with non-Visual Studio compilers] Don't implement default IME handler (won't require imm32.lib/.a) -//#define IMGUI_DISABLE_WIN32_FUNCTIONS // [Win32] Won't use and link with any Win32 function (clipboard, ime). +//#define IMGUI_DISABLE_WIN32_FUNCTIONS // [Win32] Won't use and link with any Win32 function (clipboard, IME). //#define IMGUI_ENABLE_OSX_DEFAULT_CLIPBOARD_FUNCTIONS // [OSX] Implement default OSX clipboard handler (need to link with '-framework ApplicationServices', this is why this is not the default). //#define IMGUI_DISABLE_DEFAULT_FORMAT_FUNCTIONS // Don't implement ImFormatString/ImFormatStringV so you can implement them yourself (e.g. if you don't want to link with vsnprintf) //#define IMGUI_DISABLE_DEFAULT_MATH_FUNCTIONS // Don't implement ImFabs/ImSqrt/ImPow/ImFmod/ImCos/ImSin/ImAcos/ImAtan2 so you can implement them yourself. @@ -50,21 +50,24 @@ //#define IMGUI_DISABLE_SSE // Disable use of SSE intrinsics even if available //---- Include imgui_user.h at the end of imgui.h as a convenience +// May be convenient for some users to only explicitly include vanilla imgui.h and have extra stuff included. //#define IMGUI_INCLUDE_IMGUI_USER_H +//#define IMGUI_USER_H_FILENAME "my_folder/my_imgui_user.h" //---- Pack colors to BGRA8 instead of RGBA8 (to avoid converting from one to another) //#define IMGUI_USE_BGRA_PACKED_COLOR -//---- Use 32-bit for ImWchar (default is 16-bit) to support unicode planes 1-16. (e.g. point beyond 0xFFFF like emoticons, dingbats, symbols, shapes, ancient languages, etc...) +//---- Use 32-bit for ImWchar (default is 16-bit) to support Unicode planes 1-16. (e.g. point beyond 0xFFFF like emoticons, dingbats, symbols, shapes, ancient languages, etc...) //#define IMGUI_USE_WCHAR32 //---- Avoid multiple STB libraries implementations, or redefine path/filenames to prioritize another version // By default the embedded implementations are declared static and not available outside of Dear ImGui sources files. //#define IMGUI_STB_TRUETYPE_FILENAME "my_folder/stb_truetype.h" //#define IMGUI_STB_RECT_PACK_FILENAME "my_folder/stb_rect_pack.h" -//#define IMGUI_STB_SPRINTF_FILENAME "my_folder/stb_sprintf.h" // only used if enabled +//#define IMGUI_STB_SPRINTF_FILENAME "my_folder/stb_sprintf.h" // only used if IMGUI_USE_STB_SPRINTF is defined. //#define IMGUI_DISABLE_STB_TRUETYPE_IMPLEMENTATION //#define IMGUI_DISABLE_STB_RECT_PACK_IMPLEMENTATION +//#define IMGUI_DISABLE_STB_SPRINTF_IMPLEMENTATION // only disabled if IMGUI_USE_STB_SPRINTF is defined. //---- Use stb_sprintf.h for a faster implementation of vsnprintf instead of the one from libc (unless IMGUI_DISABLE_DEFAULT_FORMAT_FUNCTIONS is defined) // Compatibility checks of arguments and formats done by clang and GCC will be disabled in order to support the extra formats provided by stb_sprintf.h. @@ -75,6 +78,12 @@ // On Windows you may use vcpkg with 'vcpkg install freetype --triplet=x64-windows' + 'vcpkg integrate install'. //#define IMGUI_ENABLE_FREETYPE +//---- Use FreeType+lunasvg library to render OpenType SVG fonts (SVGinOT) +// Requires lunasvg headers to be available in the include path + program to be linked with the lunasvg library (not provided). +// Only works in combination with IMGUI_ENABLE_FREETYPE. +// (implementation is based on Freetype's rsvg-port.c which is licensed under CeCILL-C Free Software License Agreement) +//#define IMGUI_ENABLE_FREETYPE_LUNASVG + //---- Use stb_truetype to build and rasterize the font atlas (default) // The only purpose of this define is if you want force compilation of the stb_truetype backend ALONG with the FreeType backend. //#define IMGUI_ENABLE_STB_TRUETYPE @@ -105,7 +114,7 @@ //typedef void (*MyImDrawCallback)(const ImDrawList* draw_list, const ImDrawCmd* cmd, void* my_renderer_user_data); //#define ImDrawCallback MyImDrawCallback -//---- Debug Tools: Macro to break in Debugger +//---- Debug Tools: Macro to break in Debugger (we provide a default implementation of this in the codebase) // (use 'Metrics->Tools->Item Picker' to pick widgets with the mouse and break into them for easy debugging.) //#define IM_DEBUG_BREAK IM_ASSERT(0) //#define IM_DEBUG_BREAK __debugbreak() @@ -113,10 +122,10 @@ //---- Debug Tools: Enable slower asserts //#define IMGUI_DEBUG_PARANOID -//---- Tip: You can add extra functions within the ImGui:: namespace, here or in your own headers files. +//---- Tip: You can add extra functions within the ImGui:: namespace from anywhere (e.g. your own sources/header files) /* namespace ImGui { - void MyFunction(const char* name, const MyMatrix44& v); + void MyFunction(const char* name, MyMatrix44* mtx); } */ diff --git a/libs/imgui/imgui.cpp b/libs/imgui/imgui.cpp index 400dcb2..602fde6 100644 --- a/libs/imgui/imgui.cpp +++ b/libs/imgui/imgui.cpp @@ -1,30 +1,33 @@ -// dear imgui, v1.89.6 +// dear imgui, v1.90.4 // (main code and documentation) // Help: -// - Read FAQ at http://dearimgui.com/faq -// - Newcomers, read 'Programmer guide' below for notes on how to setup Dear ImGui in your codebase. +// - See links below. // - Call and read ImGui::ShowDemoWindow() in imgui_demo.cpp. All applications in examples/ are doing that. -// Read imgui.cpp for details, links and comments. +// - Read top of imgui.cpp for more details, links and comments. // Resources: -// - FAQ http://dearimgui.com/faq -// - Homepage & latest https://github.com/ocornut/imgui +// - FAQ https://dearimgui.com/faq +// - Getting Started https://dearimgui.com/getting-started +// - Homepage https://github.com/ocornut/imgui // - Releases & changelog https://github.com/ocornut/imgui/releases -// - Gallery https://github.com/ocornut/imgui/issues/6478 (please post your screenshots/video there!) +// - Gallery https://github.com/ocornut/imgui/issues/6897 (please post your screenshots/video there!) // - Wiki https://github.com/ocornut/imgui/wiki (lots of good stuff there) // - Glossary https://github.com/ocornut/imgui/wiki/Glossary // - Issues & support https://github.com/ocornut/imgui/issues +// - Tests & Automation https://github.com/ocornut/imgui_test_engine -// Getting Started? -// - For first-time users having issues compiling/linking/running or issues loading fonts: -// please post in https://github.com/ocornut/imgui/discussions if you cannot find a solution in resources above. +// For first-time users having issues compiling/linking/running/loading fonts: +// please post in https://github.com/ocornut/imgui/discussions if you cannot find a solution in resources above. +// Everything else should be asked in 'Issues'! We are building a database of cross-linked knowledge there. +// Copyright (c) 2014-2024 Omar Cornut // Developed by Omar Cornut and every direct or indirect contributors to the GitHub. // See LICENSE.txt for copyright and licensing details (standard MIT License). // This library is free but needs your support to sustain development and maintenance. -// Businesses: you can support continued development via invoiced technical support, maintenance and sponsoring contracts. Please reach out to "contact AT dearimgui.com". -// Individuals: you can support continued development via donations. See docs/README or web page. +// Businesses: you can support continued development via B2B invoiced technical support, maintenance and sponsoring contracts. +// PLEASE reach out at omar AT dearimgui DOT com. See https://github.com/ocornut/imgui/wiki/Sponsors +// Businesses: you can also purchase licenses for the Dear ImGui Automation/Test Engine. // It is recommended that you don't modify imgui.cpp! It will become difficult for you to update the library. // Note that 'ImGui::' being a namespace, you can add functions into the namespace from your own source files, without @@ -72,6 +75,7 @@ CODE // [SECTION] MAIN CODE (most of the code! lots of stuff, needs tidying up!) // [SECTION] INPUTS // [SECTION] ERROR CHECKING +// [SECTION] ITEM SUBMISSION // [SECTION] LAYOUT // [SECTION] SCROLLING // [SECTION] TOOLTIPS @@ -82,10 +86,11 @@ CODE // [SECTION] SETTINGS // [SECTION] LOCALIZATION // [SECTION] VIEWPORTS, PLATFORM WINDOWS +// [SECTION] DOCKING // [SECTION] PLATFORM DEPENDENT HELPERS // [SECTION] METRICS/DEBUGGER WINDOW // [SECTION] DEBUG LOG WINDOW -// [SECTION] OTHER DEBUG TOOLS (ITEM PICKER, STACK TOOL) +// [SECTION] OTHER DEBUG TOOLS (ITEM PICKER, ID STACK TOOL) */ @@ -107,9 +112,10 @@ CODE - Portable, minimize dependencies, run on target (consoles, phones, etc.). - Efficient runtime and memory consumption. - Designed for developers and content-creators, not the typical end-user! Some of the current weaknesses includes: + Designed primarily for developers and content-creators, not the typical end-user! + Some of the current weaknesses (which we aim to address in the future) includes: - - Doesn't look fancy, doesn't animate. + - Doesn't look fancy. - Limited layout features, intricate layouts are typically crafted in code. @@ -188,9 +194,11 @@ CODE READ FIRST ---------- - Remember to check the wonderful Wiki (https://github.com/ocornut/imgui/wiki) - - Your code creates the UI, if your code doesn't run the UI is gone! The UI can be highly dynamic, there are no construction or - destruction steps, less superfluous data retention on your side, less state duplication, less state synchronization, fewer bugs. + - Your code creates the UI every frame of your application loop, if your code doesn't run the UI is gone! + The UI can be highly dynamic, there are no construction or destruction steps, less superfluous + data retention on your side, less state duplication, less state synchronization, fewer bugs. - Call and read ImGui::ShowDemoWindow() for demo code demonstrating most features. + Or browse https://pthom.github.io/imgui_manual_online/manual/imgui_manual.html for interactive web version. - The library is designed to be built from sources. Avoid pre-compiled binaries and packaged versions. See imconfig.h to configure your build. - Dear ImGui is an implementation of the IMGUI paradigm (immediate-mode graphical user interface, a term coined by Casey Muratori). You can learn about IMGUI principles at http://www.johno.se/book/imgui.html, http://mollyrocket.com/861 & more links in Wiki. @@ -198,18 +206,38 @@ CODE For every application frame, your UI code will be called only once. This is in contrast to e.g. Unity's implementation of an IMGUI, where the UI code is called multiple times ("multiple passes") from a single entry point. There are pros and cons to both approaches. - Our origin is on the top-left. In axis aligned bounding boxes, Min = top-left, Max = bottom-right. - - This codebase is also optimized to yield decent performances with typical "Debug" builds settings. - Please make sure you have asserts enabled (IM_ASSERT redirects to assert() by default, but can be redirected). If you get an assert, read the messages and comments around the assert. - - C++: this is a very C-ish codebase: we don't rely on C++11, we don't include any C++ headers, and ImGui:: is a namespace. - - C++: ImVec2/ImVec4 do not expose math operators by default, because it is expected that you use your own math types. - See FAQ "How can I use my own math types instead of ImVec2/ImVec4?" for details about setting up imconfig.h for that. - However, imgui_internal.h can optionally export math operators for ImVec2/ImVec4, which we use in this codebase. - - C++: pay attention that ImVector<> manipulates plain-old-data and does not honor construction/destruction (avoid using it in your code!). + - This codebase aims to be highly optimized: + - A typical idle frame should never call malloc/free. + - We rely on a maximum of constant-time or O(N) algorithms. Limiting searches/scans as much as possible. + - We put particular energy in making sure performances are decent with typical "Debug" build settings as well. + Which mean we tend to avoid over-relying on "zero-cost abstraction" as they aren't zero-cost at all. + - This codebase aims to be both highly opinionated and highly flexible: + - This code works because of the things it choose to solve or not solve. + - C++: this is a pragmatic C-ish codebase: we don't use fancy C++ features, we don't include C++ headers, + and ImGui:: is a namespace. We rarely use member functions (and when we did, I am mostly regretting it now). + This is to increase compatibility, increase maintainability and facilitate use from other languages. + - C++: ImVec2/ImVec4 do not expose math operators by default, because it is expected that you use your own math types. + See FAQ "How can I use my own math types instead of ImVec2/ImVec4?" for details about setting up imconfig.h for that. + We can can optionally export math operators for ImVec2/ImVec4 using IMGUI_DEFINE_MATH_OPERATORS, which we use internally. + - C++: pay attention that ImVector<> manipulates plain-old-data and does not honor construction/destruction + (so don't use ImVector in your code or at our own risk!). + - Building: We don't use nor mandate a build system for the main library. + This is in an effort to ensure that it works in the real world aka with any esoteric build setup. + This is also because providing a build system for the main library would be of little-value. + The build problems are almost never coming from the main library but from specific backends. HOW TO UPDATE TO A NEWER VERSION OF DEAR IMGUI ---------------------------------------------- + - Update submodule or copy/overwrite every file. + - About imconfig.h: + - You may modify your copy of imconfig.h, in this case don't overwrite it. + - or you may locally branch to modify imconfig.h and merge/rebase latest. + - or you may '#define IMGUI_USER_CONFIG "my_config_file.h"' globally from your build system to + specify a custom path for your imconfig.h file and instead not have to modify the default one. + - Overwrite all the sources files except for imconfig.h (if you have modified your copy of imconfig.h) - Or maintain your own branch where you have imconfig.h modified as a top-most commit which you can regularly rebase over "master". - You can also use '#define IMGUI_USER_CONFIG "my_config_file.h" to redirect configuration to your own file. @@ -218,11 +246,12 @@ CODE from the public API. If you have a problem with a missing function/symbols, search for its name in the code, there will likely be a comment about it. Please report any issue to the GitHub page! - To find out usage of old API, you can add '#define IMGUI_DISABLE_OBSOLETE_FUNCTIONS' in your configuration file. - - Try to keep your copy of Dear ImGui reasonably up to date. + - Try to keep your copy of Dear ImGui reasonably up to date! GETTING STARTED WITH INTEGRATING DEAR IMGUI IN YOUR CODE/ENGINE --------------------------------------------------------------- + - See https://github.com/ocornut/imgui/wiki/Getting-Started. - Run and study the examples and demo in imgui_demo.cpp to get acquainted with the library. - In the majority of cases you should be able to use unmodified backends files available in the backends/ folder. - Add the Dear ImGui source files + selected backend source files to your projects or using your preferred build system. @@ -330,7 +359,7 @@ CODE To decide whether to dispatch mouse/keyboard inputs to Dear ImGui to the rest of your application, you should read the 'io.WantCaptureMouse', 'io.WantCaptureKeyboard' and 'io.WantTextInput' flags! - Please read the FAQ and example applications for details about this! + Please read the FAQ entry "How can I tell whether to dispatch mouse/keyboard to Dear ImGui or my application?" about this. HOW A SIMPLE RENDERING FUNCTION MAY LOOK LIKE @@ -397,6 +426,47 @@ CODE When you are not sure about an old symbol or function name, try using the Search/Find function of your IDE to look for comments or references in all imgui files. You can read releases logs https://github.com/ocornut/imgui/releases for more details. +(Docking/Viewport Branch) + - 2024/XX/XX (1.XXXX) - when multi-viewports are enabled, all positions will be in your natural OS coordinates space. It means that: + - reference to hard-coded positions such as in SetNextWindowPos(ImVec2(0,0)) are probably not what you want anymore. + you may use GetMainViewport()->Pos to offset hard-coded positions, e.g. SetNextWindowPos(GetMainViewport()->Pos) + - likewise io.MousePos and GetMousePos() will use OS coordinates. + If you query mouse positions to interact with non-imgui coordinates you will need to offset them, e.g. subtract GetWindowViewport()->Pos. + + - 2024/01/15 (1.90.2) - commented out obsolete ImGuiIO::ImeWindowHandle marked obsolete in 1.87, favor of writing to 'void* ImGuiViewport::PlatformHandleRaw'. + - 2023/12/19 (1.90.1) - commented out obsolete ImGuiKey_KeyPadEnter redirection to ImGuiKey_KeypadEnter. + - 2023/11/06 (1.90.1) - removed CalcListClipping() marked obsolete in 1.86. Prefer using ImGuiListClipper which can return non-contiguous ranges. + - 2023/11/05 (1.90.1) - imgui_freetype: commented out ImGuiFreeType::BuildFontAtlas() obsoleted in 1.81. prefer using #define IMGUI_ENABLE_FREETYPE or see commented code for manual calls. + - 2023/11/05 (1.90.1) - internals,columns: commented out legacy ImGuiColumnsFlags_XXX symbols redirecting to ImGuiOldColumnsFlags_XXX, obsoleted from imgui_internal.h in 1.80. + - 2023/11/09 (1.90.0) - removed IM_OFFSETOF() macro in favor of using offsetof() available in C++11. Kept redirection define (will obsolete). + - 2023/11/07 (1.90.0) - removed BeginChildFrame()/EndChildFrame() in favor of using BeginChild() with the ImGuiChildFlags_FrameStyle flag. kept inline redirection function (will obsolete). + those functions were merely PushStyle/PopStyle helpers, the removal isn't so much motivated by needing to add the feature in BeginChild(), but by the necessity to avoid BeginChildFrame() signature mismatching BeginChild() signature and features. + - 2023/11/02 (1.90.0) - BeginChild: upgraded 'bool border = true' parameter to 'ImGuiChildFlags flags' type, added ImGuiChildFlags_Border equivalent. As with our prior "bool-to-flags" API updates, the ImGuiChildFlags_Border value is guaranteed to be == true forever to ensure a smoother transition, meaning all existing calls will still work. + - old: BeginChild("Name", size, true) + - new: BeginChild("Name", size, ImGuiChildFlags_Border) + - old: BeginChild("Name", size, false) + - new: BeginChild("Name", size) or BeginChild("Name", 0) or BeginChild("Name", size, ImGuiChildFlags_None) + - 2023/11/02 (1.90.0) - BeginChild: added child-flag ImGuiChildFlags_AlwaysUseWindowPadding as a replacement for the window-flag ImGuiWindowFlags_AlwaysUseWindowPadding: the feature only ever made sense for BeginChild() anyhow. + - old: BeginChild("Name", size, 0, ImGuiWindowFlags_AlwaysUseWindowPadding); + - new: BeginChild("Name", size, ImGuiChildFlags_AlwaysUseWindowPadding, 0); + - 2023/09/27 (1.90.0) - io: removed io.MetricsActiveAllocations introduced in 1.63. Same as 'g.DebugMemAllocCount - g.DebugMemFreeCount' (still displayed in Metrics, unlikely to be accessed by end-user). + - 2023/09/26 (1.90.0) - debug tools: Renamed ShowStackToolWindow() ("Stack Tool") to ShowIDStackToolWindow() ("ID Stack Tool"), as earlier name was misleading. Kept inline redirection function. (#4631) + - 2023/09/15 (1.90.0) - ListBox, Combo: changed signature of "name getter" callback in old one-liner ListBox()/Combo() apis. kept inline redirection function (will obsolete). + - old: bool Combo(const char* label, int* current_item, bool (*getter)(void* user_data, int idx, const char** out_text), ...) + - new: bool Combo(const char* label, int* current_item, const char* (*getter)(void* user_data, int idx), ...); + - old: bool ListBox(const char* label, int* current_item, bool (*getting)(void* user_data, int idx, const char** out_text), ...); + - new: bool ListBox(const char* label, int* current_item, const char* (*getter)(void* user_data, int idx), ...); + - 2023/09/08 (1.90.0) - commented out obsolete redirecting functions: + - GetWindowContentRegionWidth() -> use GetWindowContentRegionMax().x - GetWindowContentRegionMin().x. Consider that generally 'GetContentRegionAvail().x' is more useful. + - ImDrawCornerFlags_XXX -> use ImDrawFlags_RoundCornersXXX flags. Read 1.82 Changelog for details + grep commented names in sources. + - commented out runtime support for hardcoded ~0 or 0x01..0x0F rounding flags values for AddRect()/AddRectFilled()/PathRect()/AddImageRounded() -> use ImDrawFlags_RoundCornersXXX flags. Read 1.82 Changelog for details + - 2023/08/25 (1.89.9) - clipper: Renamed IncludeRangeByIndices() (also called ForceDisplayRangeByIndices() before 1.89.6) to IncludeItemsByIndex(). Kept inline redirection function. Sorry! + - 2023/07/12 (1.89.8) - ImDrawData: CmdLists now owned, changed from ImDrawList** to ImVector. Majority of users shouldn't be affected, but you cannot compare to NULL nor reassign manually anymore. Instead use AddDrawList(). (#6406, #4879, #1878) + - 2023/06/28 (1.89.7) - overlapping items: obsoleted 'SetItemAllowOverlap()' (called after item) in favor of calling 'SetNextItemAllowOverlap()' (called before item). 'SetItemAllowOverlap()' didn't and couldn't work reliably since 1.89 (2022-11-15). + - 2023/06/28 (1.89.7) - overlapping items: renamed 'ImGuiTreeNodeFlags_AllowItemOverlap' to 'ImGuiTreeNodeFlags_AllowOverlap', 'ImGuiSelectableFlags_AllowItemOverlap' to 'ImGuiSelectableFlags_AllowOverlap'. Kept redirecting enums (will obsolete). + - 2023/06/28 (1.89.7) - overlapping items: IsItemHovered() now by default return false when querying an item using AllowOverlap mode which is being overlapped. Use ImGuiHoveredFlags_AllowWhenOverlappedByItem to revert to old behavior. + - 2023/06/28 (1.89.7) - overlapping items: Selectable and TreeNode don't allow overlap when active so overlapping widgets won't appear as hovered. While this fixes a common small visual issue, it also means that calling IsItemHovered() after a non-reactive elements - e.g. Text() - overlapping an active one may fail if you don't use IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem). (#6610) + - 2023/06/20 (1.89.7) - moved io.HoverDelayShort/io.HoverDelayNormal to style.HoverDelayShort/style.HoverDelayNormal. As the fields were added in 1.89 and expected to be left unchanged by most users, or only tweaked once during app initialization, we are exceptionally accepting the breakage. - 2023/05/30 (1.89.6) - backends: renamed "imgui_impl_sdlrenderer.cpp" to "imgui_impl_sdlrenderer2.cpp" and "imgui_impl_sdlrenderer.h" to "imgui_impl_sdlrenderer2.h". This is in prevision for the future release of SDL3. - 2023/05/22 (1.89.6) - listbox: commented out obsolete/redirecting functions that were marked obsolete more than two years ago: - ListBoxHeader() -> use BeginListBox() (note how two variants of ListBoxHeader() existed. Check commented versions in imgui.h for reference) @@ -799,11 +869,12 @@ CODE Q: Where is the documentation? A: This library is poorly documented at the moment and expects the user to be acquainted with C/C++. - - Run the examples/ and explore them. + - Run the examples/ applications and explore them. + - Read Getting Started (https://github.com/ocornut/imgui/wiki/Getting-Started) guide. - See demo code in imgui_demo.cpp and particularly the ImGui::ShowDemoWindow() function. - The demo covers most features of Dear ImGui, so you can read the code and see its output. - See documentation and comments at the top of imgui.cpp + effectively imgui.h. - - Dozens of standalone example applications using e.g. OpenGL/DirectX are provided in the + - 20+ standalone example applications using e.g. OpenGL/DirectX are provided in the examples/ folder to explain how to integrate Dear ImGui with your own engine/application. - The Wiki (https://github.com/ocornut/imgui/wiki) has many resources and links. - The Glossary (https://github.com/ocornut/imgui/wiki/Glossary) page also may be useful. @@ -819,14 +890,14 @@ CODE ================ Q: How to get started? - A: Read 'PROGRAMMER GUIDE' above. Read examples/README.txt. + A: Read https://github.com/ocornut/imgui/wiki/Getting-Started. Read 'PROGRAMMER GUIDE' above. Read examples/README.txt. Q: How can I tell whether to dispatch mouse/keyboard to Dear ImGui or my application? A: You should read the 'io.WantCaptureMouse', 'io.WantCaptureKeyboard' and 'io.WantTextInput' flags! >> See https://www.dearimgui.com/faq for a fully detailed answer. You really want to read this. - Q. How can I enable keyboard controls? - Q: How can I use this without a mouse, without a keyboard or without a screen? (gamepad, input share, remote display) + Q. How can I enable keyboard or gamepad controls? + Q: How can I use this on a machine without mouse, keyboard or screen? (input share, remote display) Q: I integrated Dear ImGui in my engine and little squares are showing instead of text... Q: I integrated Dear ImGui in my engine and some elements are clipping or disappearing when I move windows around... Q: I integrated Dear ImGui in my engine and some elements are displaying outside their expected windows boundaries... @@ -841,7 +912,7 @@ CODE - How can I have multiple widgets with the same label? - How can I have multiple windows with the same label? Q: How can I display an image? What is ImTextureID, how does it work? - Q: How can I use my own math types instead of ImVec2/ImVec4? + Q: How can I use my own math types instead of ImVec2? Q: How can I interact with standard C++ types (such as std::string and std::vector)? Q: How can I display custom shapes? (using low-level ImDrawList API) >> See https://www.dearimgui.com/faq @@ -854,7 +925,7 @@ CODE Q: How can I easily use icons in my application? Q: How can I load multiple fonts? Q: How can I display and input non-Latin characters such as Chinese, Japanese, Korean, Cyrillic? - >> See https://www.dearimgui.com/faq and https://github.com/ocornut/imgui/edit/master/docs/FONTS.md + >> See https://www.dearimgui.com/faq and https://github.com/ocornut/imgui/blob/master/docs/FONTS.md Q&A: Concerns ============= @@ -869,12 +940,12 @@ CODE ============== Q: How can I help? - A: - Businesses: please reach out to "contact AT dearimgui.com" if you work in a place using Dear ImGui! + A: - Businesses: please reach out to "omar AT dearimgui DOT com" if you work in a place using Dear ImGui! We can discuss ways for your company to fund development via invoiced technical support, maintenance or sponsoring contacts. - This is among the most useful thing you can do for Dear ImGui. With increased funding, we can hire more people working on this project. - - Individuals: you can support continued development via PayPal donations. See README. - - If you are experienced with Dear ImGui and C++, look at the GitHub issues, look at the Wiki, read docs/TODO.txt - and see how you want to help and can help! + This is among the most useful thing you can do for Dear ImGui. With increased funding, we sustain and grow work on this project. + Also see https://github.com/ocornut/imgui/wiki/Sponsors + - Businesses: you can also purchase licenses for the Dear ImGui Automation/Test Engine. + - If you are experienced with Dear ImGui and C++, look at the GitHub issues, look at the Wiki, and see how you want to help and can help! - Disclose your usage of Dear ImGui via a dev blog post, a tweet, a screenshot, a mention somewhere etc. You may post screenshot or links in the gallery threads. Visuals are ideal as they inspire other programmers. But even without visuals, disclosing your use of dear imgui helps the library grow credibility, and help other teams and programmers with taking decisions. @@ -900,11 +971,7 @@ CODE // System includes #include // vsnprintf, sscanf, printf -#if defined(_MSC_VER) && _MSC_VER <= 1500 // MSVC 2008 or earlier -#include // intptr_t -#else #include // intptr_t -#endif // [Windows] On non-Visual Studio compilers, we default to IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS unless explicitly enabled #if defined(_WIN32) && !defined(_MSC_VER) && !defined(IMGUI_ENABLE_WIN32_DEFAULT_IME_FUNCTIONS) && !defined(IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS) @@ -983,17 +1050,24 @@ CODE // Debug options #define IMGUI_DEBUG_NAV_SCORING 0 // Display navigation scoring preview when hovering items. Display last moving direction matches when holding CTRL #define IMGUI_DEBUG_NAV_RECTS 0 // Display the reference navigation rectangle for each window -#define IMGUI_DEBUG_INI_SETTINGS 0 // Save additional comments in .ini file (particularly helps for Docking, but makes saving slower) // When using CTRL+TAB (or Gamepad Square+L/R) we delay the visual a little in order to reduce visual noise doing a fast switch. static const float NAV_WINDOWING_HIGHLIGHT_DELAY = 0.20f; // Time before the highlight and screen dimming starts fading in static const float NAV_WINDOWING_LIST_APPEAR_DELAY = 0.15f; // Time before the window list starts to appear +static const float NAV_ACTIVATE_HIGHLIGHT_TIMER = 0.10f; // Time to highlight an item activated by a shortcut. + // Window resizing from edges (when io.ConfigWindowsResizeFromEdges = true and ImGuiBackendFlags_HasMouseCursors is set in io.BackendFlags by backend) static const float WINDOWS_HOVER_PADDING = 4.0f; // Extend outside window for hovering/resizing (maxxed with TouchPadding) and inside windows for borders. Affect FindHoveredWindow(). static const float WINDOWS_RESIZE_FROM_EDGES_FEEDBACK_TIMER = 0.04f; // Reduce visual noise by only highlighting the border after a certain time. static const float WINDOWS_MOUSE_WHEEL_SCROLL_LOCK_TIMER = 0.70f; // Lock scrolled window (so it doesn't pick child windows that are scrolling through) for a certain time, unless mouse moved. +// Tooltip offset +static const ImVec2 TOOLTIP_DEFAULT_OFFSET = ImVec2(16, 10); // Multiplied by g.Style.MouseCursorScale + +// Docking +static const float DOCKING_TRANSPARENT_PAYLOAD_ALPHA = 0.50f; // For use with io.ConfigDockingTransparentPayload. Apply to Viewport _or_ WindowBg in host viewport. + //------------------------------------------------------------------------- // [SECTION] FORWARD DECLARATIONS //------------------------------------------------------------------------- @@ -1003,7 +1077,6 @@ static void FindHoveredWindow(); static ImGuiWindow* CreateNewWindow(const char* name, ImGuiWindowFlags flags); static ImVec2 CalcNextScrollFromScrollTargetAndClamp(ImGuiWindow* window); -static void AddDrawListToDrawData(ImVector* out_list, ImDrawList* draw_list); static void AddWindowToSortBuffer(ImVector* out_sorted_windows, ImGuiWindow* window); // Settings @@ -1039,7 +1112,6 @@ static ImVec2 NavCalcPreferredRefPos(); static void NavSaveLastChildNavWindowIntoParent(ImGuiWindow* nav_window); static ImGuiWindow* NavRestoreLastChildNavWindow(ImGuiWindow* window); static void NavRestoreLayer(ImGuiNavLayer layer); -static void NavRestoreHighlightAfterMove(); static int FindWindowFocusIndex(ImGuiWindow* window); // Error Checking and Debug Tools @@ -1047,6 +1119,7 @@ static void ErrorCheckNewFrameSanityChecks(); static void ErrorCheckEndFrameSanityChecks(); static void UpdateDebugToolItemPicker(); static void UpdateDebugToolStackQueries(); +static void UpdateDebugToolFlashStyleColor(); // Inputs static void UpdateKeyboardInputs(); @@ -1056,7 +1129,7 @@ static void UpdateKeyRoutingTable(ImGuiKeyRoutingTable* rt); // Misc static void UpdateSettings(); -static bool UpdateWindowManualResize(ImGuiWindow* window, const ImVec2& size_auto_fit, int* border_held, int resize_grip_count, ImU32 resize_grip_col[4], const ImRect& visibility_rect); +static int UpdateWindowManualResize(ImGuiWindow* window, const ImVec2& size_auto_fit, int* border_hovered, int* border_held, int resize_grip_count, ImU32 resize_grip_col[4], const ImRect& visibility_rect); static void RenderWindowOuterBorders(ImGuiWindow* window); static void RenderWindowDecorations(ImGuiWindow* window, const ImRect& title_bar_rect, bool title_bar_is_highlight, bool handle_borders_and_resize_grips, int resize_grip_count, const ImU32 resize_grip_col[4], float resize_grip_draw_size); static void RenderWindowTitleBarContents(ImGuiWindow* window, const ImRect& title_bar_rect, const char* name, bool* p_open); @@ -1064,7 +1137,19 @@ static void RenderDimmedBackgroundBehindWindow(ImGuiWindow* window, static void RenderDimmedBackgrounds(); // Viewports +const ImGuiID IMGUI_VIEWPORT_DEFAULT_ID = 0x11111111; // Using an arbitrary constant instead of e.g. ImHashStr("ViewportDefault", 0); so it's easier to spot in the debugger. The exact value doesn't matter. +static ImGuiViewportP* AddUpdateViewport(ImGuiWindow* window, ImGuiID id, const ImVec2& platform_pos, const ImVec2& size, ImGuiViewportFlags flags); +static void DestroyViewport(ImGuiViewportP* viewport); static void UpdateViewportsNewFrame(); +static void UpdateViewportsEndFrame(); +static void WindowSelectViewport(ImGuiWindow* window); +static void WindowSyncOwnedViewport(ImGuiWindow* window, ImGuiWindow* parent_window_in_stack); +static bool UpdateTryMergeWindowIntoHostViewport(ImGuiWindow* window, ImGuiViewportP* host_viewport); +static bool UpdateTryMergeWindowIntoHostViewports(ImGuiWindow* window); +static bool GetWindowAlwaysWantOwnViewport(ImGuiWindow* window); +static int FindPlatformMonitorForPos(const ImVec2& pos); +static int FindPlatformMonitorForRect(const ImRect& r); +static void UpdateViewportPlatformMonitor(ImGuiViewportP* viewport); } @@ -1133,7 +1218,7 @@ ImGuiStyle::ImGuiStyle() FrameBorderSize = 0.0f; // Thickness of border around frames. Generally set to 0.0f or 1.0f. Other values not well tested. ItemSpacing = ImVec2(8,4); // Horizontal and vertical spacing between widgets/lines ItemInnerSpacing = ImVec2(4,4); // Horizontal and vertical spacing between within elements of a composed widget (e.g. a slider and its label) - CellPadding = ImVec2(4,2); // Padding within a table cell + CellPadding = ImVec2(4,2); // Padding within a table cell. CellPadding.y may be altered between different rows. TouchExtraPadding = ImVec2(0,0); // Expand reactive bounding box for touch-based system where touch position is not accurate enough. Unfortunately we don't sort widgets so priority on overlap will always be given to the first widget. So don't grow this too much! IndentSpacing = 21.0f; // Horizontal spacing when e.g. entering a tree node. Generally == (FontSize + FramePadding.x*2). ColumnsMinSpacing = 6.0f; // Minimum horizontal spacing between two columns. Preferably > (FramePadding.x + 1). @@ -1145,6 +1230,8 @@ ImGuiStyle::ImGuiStyle() TabRounding = 4.0f; // Radius of upper corners of a tab. Set to 0.0f to have rectangular tabs. TabBorderSize = 0.0f; // Thickness of border around tabs. TabMinWidthForCloseButton = 0.0f; // Minimum width for close button to appear on an unselected tab when hovered. Set to 0.0f to always show when hovering, set to FLT_MAX to never show close button unless selected. + TabBarBorderSize = 1.0f; // Thickness of tab-bar separator, which takes on the tab active color to denote focus. + TableAngledHeadersAngle = 35.0f * (IM_PI / 180.0f); // Angle of angled headers (supported values range from -50 degrees to +50 degrees). ColorButtonPosition = ImGuiDir_Right; // Side of the color button in the ColorEdit4 widget (left/right). Defaults to ImGuiDir_Right. ButtonTextAlign = ImVec2(0.5f,0.5f);// Alignment of button text when button is larger than text. SelectableTextAlign = ImVec2(0.0f,0.0f);// Alignment of selectable text. Defaults to (0.0f, 0.0f) (top-left aligned). It's generally important to keep this left-aligned if you want to lay multiple items on a same line. @@ -1153,6 +1240,7 @@ ImGuiStyle::ImGuiStyle() SeparatorTextPadding = ImVec2(20.0f,3.f);// Horizontal offset of text from each edge of the separator + spacing on other axis. Generally small values. .y is recommended to be == FramePadding.y. DisplayWindowPadding = ImVec2(19,19); // Window position are clamped to be visible within the display area or monitors by at least this amount. Only applies to regular windows. DisplaySafeAreaPadding = ImVec2(3,3); // If you cannot see the edge of your screen (e.g. on a TV) increase the safe area padding. Covers popups/tooltips as well regular windows. + DockingSeparatorSize = 2.0f; // Thickness of resizing border between docked windows MouseCursorScale = 1.0f; // Scale software rendered mouse cursor (when io.MouseDrawCursor is enabled). May be removed later. AntiAliasedLines = true; // Enable anti-aliased lines/borders. Disable if you are really tight on CPU/GPU. AntiAliasedLinesUseTex = true; // Enable anti-aliased lines/borders using textures where possible. Require backend to render with bilinear filtering (NOT point/nearest filtering). @@ -1160,6 +1248,13 @@ ImGuiStyle::ImGuiStyle() CurveTessellationTol = 1.25f; // Tessellation tolerance when using PathBezierCurveTo() without a specific number of segments. Decrease for highly tessellated curves (higher quality, more polygons), increase to reduce quality. CircleTessellationMaxError = 0.30f; // Maximum error (in pixels) allowed when using AddCircle()/AddCircleFilled() or drawing rounded corner rectangles with no explicit segment count specified. Decrease for higher quality but more geometry. + // Behaviors + HoverStationaryDelay = 0.15f; // Delay for IsItemHovered(ImGuiHoveredFlags_Stationary). Time required to consider mouse stationary. + HoverDelayShort = 0.15f; // Delay for IsItemHovered(ImGuiHoveredFlags_DelayShort). Usually used along with HoverStationaryDelay. + HoverDelayNormal = 0.40f; // Delay for IsItemHovered(ImGuiHoveredFlags_DelayNormal). " + HoverFlagsForTooltipMouse = ImGuiHoveredFlags_Stationary | ImGuiHoveredFlags_DelayShort | ImGuiHoveredFlags_AllowWhenDisabled; // Default flags when using IsItemHovered(ImGuiHoveredFlags_ForTooltip) or BeginItemTooltip()/SetItemTooltip() while using mouse. + HoverFlagsForTooltipNav = ImGuiHoveredFlags_NoSharedDelay | ImGuiHoveredFlags_DelayNormal | ImGuiHoveredFlags_AllowWhenDisabled; // Default flags when using IsItemHovered(ImGuiHoveredFlags_ForTooltip) or BeginItemTooltip()/SetItemTooltip() while using keyboard/gamepad. + // Default theme ImGui::StyleColorsDark(this); } @@ -1168,30 +1263,31 @@ ImGuiStyle::ImGuiStyle() // Important: This operation is lossy because we round all sizes to integer. If you need to change your scale multiples, call this over a freshly initialized ImGuiStyle structure rather than scaling multiple times. void ImGuiStyle::ScaleAllSizes(float scale_factor) { - WindowPadding = ImFloor(WindowPadding * scale_factor); - WindowRounding = ImFloor(WindowRounding * scale_factor); - WindowMinSize = ImFloor(WindowMinSize * scale_factor); - ChildRounding = ImFloor(ChildRounding * scale_factor); - PopupRounding = ImFloor(PopupRounding * scale_factor); - FramePadding = ImFloor(FramePadding * scale_factor); - FrameRounding = ImFloor(FrameRounding * scale_factor); - ItemSpacing = ImFloor(ItemSpacing * scale_factor); - ItemInnerSpacing = ImFloor(ItemInnerSpacing * scale_factor); - CellPadding = ImFloor(CellPadding * scale_factor); - TouchExtraPadding = ImFloor(TouchExtraPadding * scale_factor); - IndentSpacing = ImFloor(IndentSpacing * scale_factor); - ColumnsMinSpacing = ImFloor(ColumnsMinSpacing * scale_factor); - ScrollbarSize = ImFloor(ScrollbarSize * scale_factor); - ScrollbarRounding = ImFloor(ScrollbarRounding * scale_factor); - GrabMinSize = ImFloor(GrabMinSize * scale_factor); - GrabRounding = ImFloor(GrabRounding * scale_factor); - LogSliderDeadzone = ImFloor(LogSliderDeadzone * scale_factor); - TabRounding = ImFloor(TabRounding * scale_factor); - TabMinWidthForCloseButton = (TabMinWidthForCloseButton != FLT_MAX) ? ImFloor(TabMinWidthForCloseButton * scale_factor) : FLT_MAX; - SeparatorTextPadding = ImFloor(SeparatorTextPadding * scale_factor); - DisplayWindowPadding = ImFloor(DisplayWindowPadding * scale_factor); - DisplaySafeAreaPadding = ImFloor(DisplaySafeAreaPadding * scale_factor); - MouseCursorScale = ImFloor(MouseCursorScale * scale_factor); + WindowPadding = ImTrunc(WindowPadding * scale_factor); + WindowRounding = ImTrunc(WindowRounding * scale_factor); + WindowMinSize = ImTrunc(WindowMinSize * scale_factor); + ChildRounding = ImTrunc(ChildRounding * scale_factor); + PopupRounding = ImTrunc(PopupRounding * scale_factor); + FramePadding = ImTrunc(FramePadding * scale_factor); + FrameRounding = ImTrunc(FrameRounding * scale_factor); + ItemSpacing = ImTrunc(ItemSpacing * scale_factor); + ItemInnerSpacing = ImTrunc(ItemInnerSpacing * scale_factor); + CellPadding = ImTrunc(CellPadding * scale_factor); + TouchExtraPadding = ImTrunc(TouchExtraPadding * scale_factor); + IndentSpacing = ImTrunc(IndentSpacing * scale_factor); + ColumnsMinSpacing = ImTrunc(ColumnsMinSpacing * scale_factor); + ScrollbarSize = ImTrunc(ScrollbarSize * scale_factor); + ScrollbarRounding = ImTrunc(ScrollbarRounding * scale_factor); + GrabMinSize = ImTrunc(GrabMinSize * scale_factor); + GrabRounding = ImTrunc(GrabRounding * scale_factor); + LogSliderDeadzone = ImTrunc(LogSliderDeadzone * scale_factor); + TabRounding = ImTrunc(TabRounding * scale_factor); + TabMinWidthForCloseButton = (TabMinWidthForCloseButton != FLT_MAX) ? ImTrunc(TabMinWidthForCloseButton * scale_factor) : FLT_MAX; + SeparatorTextPadding = ImTrunc(SeparatorTextPadding * scale_factor); + DockingSeparatorSize = ImTrunc(DockingSeparatorSize * scale_factor); + DisplayWindowPadding = ImTrunc(DisplayWindowPadding * scale_factor); + DisplaySafeAreaPadding = ImTrunc(DisplaySafeAreaPadding * scale_factor); + MouseCursorScale = ImTrunc(MouseCursorScale * scale_factor); } ImGuiIO::ImGuiIO() @@ -1208,16 +1304,10 @@ ImGuiIO::ImGuiIO() IniSavingRate = 5.0f; IniFilename = "imgui.ini"; // Important: "imgui.ini" is relative to current working dir, most apps will want to lock this to an absolute path (e.g. same path as executables). LogFilename = "imgui_log.txt"; - MouseDoubleClickTime = 0.30f; - MouseDoubleClickMaxDist = 6.0f; #ifndef IMGUI_DISABLE_OBSOLETE_KEYIO for (int i = 0; i < ImGuiKey_COUNT; i++) KeyMap[i] = -1; #endif - KeyRepeatDelay = 0.275f; - KeyRepeatRate = 0.050f; - HoverDelayNormal = 0.30f; - HoverDelayShort = 0.10f; UserData = NULL; Fonts = NULL; @@ -1226,6 +1316,18 @@ ImGuiIO::ImGuiIO() FontAllowUserScaling = false; DisplayFramebufferScale = ImVec2(1.0f, 1.0f); + // Docking options (when ImGuiConfigFlags_DockingEnable is set) + ConfigDockingNoSplit = false; + ConfigDockingWithShift = false; + ConfigDockingAlwaysTabBar = false; + ConfigDockingTransparentPayload = false; + + // Viewport options (when ImGuiConfigFlags_ViewportsEnable is set) + ConfigViewportsNoAutoMerge = false; + ConfigViewportsNoTaskBarIcon = false; + ConfigViewportsNoDecoration = true; + ConfigViewportsNoDefaultParent = false; + // Miscellaneous options MouseDrawCursor = false; #ifdef __APPLE__ @@ -1243,16 +1345,23 @@ ImGuiIO::ImGuiIO() ConfigDebugBeginReturnValueOnce = false; ConfigDebugBeginReturnValueLoop = false; + // Inputs Behaviors + MouseDoubleClickTime = 0.30f; + MouseDoubleClickMaxDist = 6.0f; + MouseDragThreshold = 6.0f; + KeyRepeatDelay = 0.275f; + KeyRepeatRate = 0.050f; + // Platform Functions // Note: Initialize() will setup default clipboard/ime handlers. BackendPlatformName = BackendRendererName = NULL; BackendPlatformUserData = BackendRendererUserData = BackendLanguageUserData = NULL; + PlatformLocaleDecimalPoint = '.'; // Input (NB: we already have memset zero the entire structure!) MousePos = ImVec2(-FLT_MAX, -FLT_MAX); MousePosPrev = ImVec2(-FLT_MAX, -FLT_MAX); MouseSource = ImGuiMouseSource_Mouse; - MouseDragThreshold = 6.0f; for (int i = 0; i < IM_ARRAYSIZE(MouseDownDuration); i++) MouseDownDuration[i] = MouseDownDurationPrev[i] = -1.0f; for (int i = 0; i < IM_ARRAYSIZE(KeysData); i++) { KeysData[i].DownDuration = KeysData[i].DownDurationPrev = -1.0f; } AppAcceptingEvents = true; @@ -1327,13 +1436,15 @@ void ImGuiIO::AddInputCharactersUTF8(const char* utf8_chars) } } -// FIXME: Perhaps we could clear queued events as well? -void ImGuiIO::ClearInputCharacters() +// Clear all incoming events. +void ImGuiIO::ClearEventsQueue() { - InputQueueCharacters.resize(0); + IM_ASSERT(Ctx != NULL); + ImGuiContext& g = *Ctx; + g.InputEventsQueue.clear(); } -// FIXME: Perhaps we could clear queued events as well? +// Clear current keyboard/mouse/gamepad state + current frame text input buffer. Equivalent to releasing all keys/buttons. void ImGuiIO::ClearInputKeys() { #ifndef IMGUI_DISABLE_OBSOLETE_KEYIO @@ -1354,8 +1465,18 @@ void ImGuiIO::ClearInputKeys() MouseDownDuration[n] = MouseDownDurationPrev[n] = -1.0f; } MouseWheel = MouseWheelH = 0.0f; + InputQueueCharacters.resize(0); // Behavior of old ClearInputCharacters(). } +// Removed this as it is ambiguous/misleading and generally incorrect to use with the existence of a higher-level input queue. +// Current frame character buffer is now also cleared by ClearInputKeys(). +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS +void ImGuiIO::ClearInputCharacters() +{ + InputQueueCharacters.resize(0); +} +#endif + static ImGuiInputEvent* FindLatestInputEvent(ImGuiContext* ctx, ImGuiInputEventType type, int arg = -1) { ImGuiContext& g = *ctx; @@ -1467,7 +1588,7 @@ void ImGuiIO::AddMousePosEvent(float x, float y) return; // Apply same flooring as UpdateMouseInputs() - ImVec2 pos((x > -FLT_MAX) ? ImFloorSigned(x) : x, (y > -FLT_MAX) ? ImFloorSigned(y) : y); + ImVec2 pos((x > -FLT_MAX) ? ImFloor(x) : x, (y > -FLT_MAX) ? ImFloor(y) : y); // Filter duplicate const ImGuiInputEvent* latest_event = FindLatestInputEvent(&g, ImGuiInputEventType_MousePos); @@ -1481,7 +1602,7 @@ void ImGuiIO::AddMousePosEvent(float x, float y) e.EventId = g.InputEventsNextEventId++; e.MousePos.PosX = pos.x; e.MousePos.PosY = pos.y; - e.MouseWheel.MouseSource = g.InputEventsNextMouseSource; + e.MousePos.MouseSource = g.InputEventsNextMouseSource; g.InputEventsQueue.push_back(e); } @@ -1505,7 +1626,7 @@ void ImGuiIO::AddMouseButtonEvent(int mouse_button, bool down) e.EventId = g.InputEventsNextEventId++; e.MouseButton.Button = mouse_button; e.MouseButton.Down = down; - e.MouseWheel.MouseSource = g.InputEventsNextMouseSource; + e.MouseButton.MouseSource = g.InputEventsNextMouseSource; g.InputEventsQueue.push_back(e); } @@ -1538,6 +1659,27 @@ void ImGuiIO::AddMouseSourceEvent(ImGuiMouseSource source) g.InputEventsNextMouseSource = source; } +void ImGuiIO::AddMouseViewportEvent(ImGuiID viewport_id) +{ + IM_ASSERT(Ctx != NULL); + ImGuiContext& g = *Ctx; + //IM_ASSERT(g.IO.BackendFlags & ImGuiBackendFlags_HasMouseHoveredViewport); + if (!AppAcceptingEvents) + return; + + // Filter duplicate + const ImGuiInputEvent* latest_event = FindLatestInputEvent(&g, ImGuiInputEventType_MouseViewport); + const ImGuiID latest_viewport_id = latest_event ? latest_event->MouseViewport.HoveredViewportID : g.IO.MouseHoveredViewport; + if (latest_viewport_id == viewport_id) + return; + + ImGuiInputEvent e; + e.Type = ImGuiInputEventType_MouseViewport; + e.Source = ImGuiInputSource_Mouse; + e.MouseViewport.HoveredViewportID = viewport_id; + g.InputEventsQueue.push_back(e); +} + void ImGuiIO::AddFocusEvent(bool focused) { IM_ASSERT(Ctx != NULL); @@ -1808,13 +1950,15 @@ const char* ImStrSkipBlank(const char* str) // and setup the wrapper yourself. (FIXME-OPT: Some of our high-level operations such as ImGuiTextBuffer::appendfv() are // designed using two-passes worst case, which probably could be improved using the stbsp_vsprintfcb() function.) #ifdef IMGUI_USE_STB_SPRINTF +#ifndef IMGUI_DISABLE_STB_SPRINTF_IMPLEMENTATION #define STB_SPRINTF_IMPLEMENTATION +#endif #ifdef IMGUI_STB_SPRINTF_FILENAME #include IMGUI_STB_SPRINTF_FILENAME #else #include "stb_sprintf.h" #endif -#endif +#endif // #ifdef IMGUI_USE_STB_SPRINTF #if defined(_MSC_VER) && !defined(vsnprintf) #define vsnprintf _vsnprintf @@ -1856,21 +2000,9 @@ int ImFormatStringV(char* buf, size_t buf_size, const char* fmt, va_list args) void ImFormatStringToTempBuffer(const char** out_buf, const char** out_buf_end, const char* fmt, ...) { - ImGuiContext& g = *GImGui; va_list args; va_start(args, fmt); - if (fmt[0] == '%' && fmt[1] == 's' && fmt[2] == 0) - { - const char* buf = va_arg(args, const char*); // Skip formatting when using "%s" - *out_buf = buf; - if (out_buf_end) { *out_buf_end = buf + strlen(buf); } - } - else - { - int buf_len = ImFormatStringV(g.TempBuffer.Data, g.TempBuffer.Size, fmt, args); - *out_buf = g.TempBuffer.Data; - if (out_buf_end) { *out_buf_end = g.TempBuffer.Data + buf_len; } - } + ImFormatStringToTempBufferV(out_buf, out_buf_end, fmt, args); va_end(args); } @@ -1880,9 +2012,23 @@ void ImFormatStringToTempBufferV(const char** out_buf, const char** out_buf_end, if (fmt[0] == '%' && fmt[1] == 's' && fmt[2] == 0) { const char* buf = va_arg(args, const char*); // Skip formatting when using "%s" + if (buf == NULL) + buf = "(null)"; *out_buf = buf; if (out_buf_end) { *out_buf_end = buf + strlen(buf); } } + else if (fmt[0] == '%' && fmt[1] == '.' && fmt[2] == '*' && fmt[3] == 's' && fmt[4] == 0) + { + int buf_len = va_arg(args, int); // Skip formatting when using "%.*s" + const char* buf = va_arg(args, const char*); + if (buf == NULL) + { + buf = "(null)"; + buf_len = ImMin(buf_len, 6); + } + *out_buf = buf; + *out_buf_end = buf + buf_len; // Disallow not passing 'out_buf_end' here. User is expected to use it. + } else { int buf_len = ImFormatStringV(g.TempBuffer.Data, g.TempBuffer.Size, fmt, args); @@ -1975,11 +2121,18 @@ ImFileHandle ImFileOpen(const char* filename, const char* mode) // Previously we used ImTextCountCharsFromUtf8/ImTextStrFromUtf8 here but we now need to support ImWchar16 and ImWchar32! const int filename_wsize = ::MultiByteToWideChar(CP_UTF8, 0, filename, -1, NULL, 0); const int mode_wsize = ::MultiByteToWideChar(CP_UTF8, 0, mode, -1, NULL, 0); - ImVector buf; - buf.resize(filename_wsize + mode_wsize); - ::MultiByteToWideChar(CP_UTF8, 0, filename, -1, (wchar_t*)&buf[0], filename_wsize); - ::MultiByteToWideChar(CP_UTF8, 0, mode, -1, (wchar_t*)&buf[filename_wsize], mode_wsize); - return ::_wfopen((const wchar_t*)&buf[0], (const wchar_t*)&buf[filename_wsize]); + + // Use stack buffer if possible, otherwise heap buffer. Sizes include zero terminator. + // We don't rely on current ImGuiContext as this is implied to be a helper function which doesn't depend on it (see #7314). + wchar_t local_temp_stack[FILENAME_MAX]; + ImVector local_temp_heap; + if (filename_wsize + mode_wsize > IM_ARRAYSIZE(local_temp_stack)) + local_temp_heap.resize(filename_wsize + mode_wsize); + wchar_t* filename_wbuf = local_temp_heap.Data ? local_temp_heap.Data : local_temp_stack; + wchar_t* mode_wbuf = filename_wbuf + filename_wsize; + ::MultiByteToWideChar(CP_UTF8, 0, filename, -1, filename_wbuf, filename_wsize); + ::MultiByteToWideChar(CP_UTF8, 0, mode, -1, mode_wbuf, mode_wsize); + return ::_wfopen(filename_wbuf, mode_wbuf); #else return fopen(filename, mode); #endif @@ -2211,6 +2364,18 @@ int ImTextCountUtf8BytesFromStr(const ImWchar* in_text, const ImWchar* in_text_e } return bytes_count; } + +const char* ImTextFindPreviousUtf8Codepoint(const char* in_text_start, const char* in_text_curr) +{ + while (in_text_curr > in_text_start) + { + in_text_curr--; + if ((*in_text_curr & 0xC0) != 0x80) + return in_text_curr; + } + return in_text_start; +} + IM_MSVC_RUNTIME_CHECKS_RESTORE //----------------------------------------------------------------------------- @@ -2406,11 +2571,9 @@ void ImGuiStorage::SetInt(ImGuiID key, int val) { ImGuiStoragePair* it = LowerBound(Data, key); if (it == Data.end() || it->key != key) - { Data.insert(it, ImGuiStoragePair(key, val)); - return; - } - it->val_i = val; + else + it->val_i = val; } void ImGuiStorage::SetBool(ImGuiID key, bool val) @@ -2422,22 +2585,18 @@ void ImGuiStorage::SetFloat(ImGuiID key, float val) { ImGuiStoragePair* it = LowerBound(Data, key); if (it == Data.end() || it->key != key) - { Data.insert(it, ImGuiStoragePair(key, val)); - return; - } - it->val_f = val; + else + it->val_f = val; } void ImGuiStorage::SetVoidPtr(ImGuiID key, void* val) { ImGuiStoragePair* it = LowerBound(Data, key); if (it == Data.end() || it->key != key) - { Data.insert(it, ImGuiStoragePair(key, val)); - return; - } - it->val_p = val; + else + it->val_p = val; } void ImGuiStorage::SetAllInt(int v) @@ -2497,16 +2656,15 @@ void ImGuiTextFilter::Build() input_range.split(',', &Filters); CountGrep = 0; - for (int i = 0; i != Filters.Size; i++) + for (ImGuiTextRange& f : Filters) { - ImGuiTextRange& f = Filters[i]; while (f.b < f.e && ImCharIsBlankA(f.b[0])) f.b++; while (f.e > f.b && ImCharIsBlankA(f.e[-1])) f.e--; if (f.empty()) continue; - if (Filters[i].b[0] != '-') + if (f.b[0] != '-') CountGrep += 1; } } @@ -2519,9 +2677,8 @@ bool ImGuiTextFilter::PassFilter(const char* text, const char* text_end) const if (text == NULL) text = ""; - for (int i = 0; i != Filters.Size; i++) + for (const ImGuiTextRange& f : Filters) { - const ImGuiTextRange& f = Filters[i]; if (f.empty()) continue; if (f.b[0] == '-') @@ -2630,8 +2787,6 @@ void ImGuiTextIndex::append(const char* base, int old_size, int new_size) //----------------------------------------------------------------------------- // [SECTION] ImGuiListClipper -// This is currently not as flexible/powerful as it should be and really confusing/spaghetti, mostly because we changed -// the API mid-way through development and support two ways to using the clipper, needs some rework (see TODO) //----------------------------------------------------------------------------- // FIXME-TABLE: This prevents us from using ImGuiListClipper _inside_ a table cell. @@ -2642,54 +2797,6 @@ static bool GetSkipItemForListClipping() return (g.CurrentTable ? g.CurrentTable->HostSkipItems : g.CurrentWindow->SkipItems); } -#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS -// Legacy helper to calculate coarse clipping of large list of evenly sized items. -// This legacy API is not ideal because it assumes we will return a single contiguous rectangle. -// Prefer using ImGuiListClipper which can returns non-contiguous ranges. -void ImGui::CalcListClipping(int items_count, float items_height, int* out_items_display_start, int* out_items_display_end) -{ - ImGuiContext& g = *GImGui; - ImGuiWindow* window = g.CurrentWindow; - if (g.LogEnabled) - { - // If logging is active, do not perform any clipping - *out_items_display_start = 0; - *out_items_display_end = items_count; - return; - } - if (GetSkipItemForListClipping()) - { - *out_items_display_start = *out_items_display_end = 0; - return; - } - - // We create the union of the ClipRect and the scoring rect which at worst should be 1 page away from ClipRect - // We don't include g.NavId's rectangle in there (unless g.NavJustMovedToId is set) because the rectangle enlargement can get costly. - ImRect rect = window->ClipRect; - if (g.NavMoveScoringItems) - rect.Add(g.NavScoringNoClipRect); - if (g.NavJustMovedToId && window->NavLastIds[0] == g.NavJustMovedToId) - rect.Add(WindowRectRelToAbs(window, window->NavRectRel[0])); // Could store and use NavJustMovedToRectRel - - const ImVec2 pos = window->DC.CursorPos; - int start = (int)((rect.Min.y - pos.y) / items_height); - int end = (int)((rect.Max.y - pos.y) / items_height); - - // When performing a navigation request, ensure we have one item extra in the direction we are moving to - // FIXME: Verify this works with tabbing - const bool is_nav_request = (g.NavMoveScoringItems && g.NavWindow && g.NavWindow->RootWindowForNav == window->RootWindowForNav); - if (is_nav_request && g.NavMoveClipDir == ImGuiDir_Up) - start--; - if (is_nav_request && g.NavMoveClipDir == ImGuiDir_Down) - end++; - - start = ImClamp(start, 0, items_count); - end = ImClamp(end + 1, start, items_count); - *out_items_display_start = start; - *out_items_display_end = end; -} -#endif - static void ImGuiListClipper_SortAndFuseRanges(ImVector& ranges, int offset = 0) { if (ranges.Size - offset <= 1) @@ -2751,9 +2858,6 @@ static void ImGuiListClipper_SeekCursorForItem(ImGuiListClipper* clipper, int it ImGuiListClipper::ImGuiListClipper() { memset(this, 0, sizeof(*this)); - Ctx = ImGui::GetCurrentContext(); - IM_ASSERT(Ctx != NULL); - ItemsCount = -1; } ImGuiListClipper::~ImGuiListClipper() @@ -2763,6 +2867,9 @@ ImGuiListClipper::~ImGuiListClipper() void ImGuiListClipper::Begin(int items_count, float items_height) { + if (Ctx == NULL) + Ctx = ImGui::GetCurrentContext(); + ImGuiContext& g = *Ctx; ImGuiWindow* window = g.CurrentWindow; IMGUI_DEBUG_LOG_CLIPPER("Clipper: Begin(%d,%.2f) in '%s'\n", items_count, items_height, window->Name); @@ -2788,10 +2895,10 @@ void ImGuiListClipper::Begin(int items_count, float items_height) void ImGuiListClipper::End() { - ImGuiContext& g = *Ctx; if (ImGuiListClipperData* data = (ImGuiListClipperData*)TempData) { // In theory here we should assert that we are already at the right position, but it seems saner to just seek at the end and not assert/crash the user. + ImGuiContext& g = *Ctx; IMGUI_DEBUG_LOG_CLIPPER("Clipper: End() in '%s'\n", g.CurrentWindow->Name); if (ItemsCount >= 0 && ItemsCount < INT_MAX && DisplayStart >= 0) ImGuiListClipper_SeekCursorForItem(this, ItemsCount); @@ -2809,7 +2916,7 @@ void ImGuiListClipper::End() ItemsCount = -1; } -void ImGuiListClipper::IncludeRangeByIndices(int item_begin, int item_end) +void ImGuiListClipper::IncludeItemsByIndex(int item_begin, int item_end) { ImGuiListClipperData* data = (ImGuiListClipperData*)TempData; IM_ASSERT(DisplayStart < 0); // Only allowed after Begin() and if there has not been a specified range yet. @@ -2892,7 +2999,7 @@ static bool ImGuiListClipper_StepInternal(ImGuiListClipper* clipper) const bool is_nav_request = (g.NavMoveScoringItems && g.NavWindow && g.NavWindow->RootWindowForNav == window->RootWindowForNav); if (is_nav_request) data->Ranges.push_back(ImGuiListClipperRange::FromPositions(g.NavScoringNoClipRect.Min.y, g.NavScoringNoClipRect.Max.y, 0, 0)); - if (is_nav_request && (g.NavMoveFlags & ImGuiNavMoveFlags_Tabbing) && g.NavTabbingDir == -1) + if (is_nav_request && (g.NavMoveFlags & ImGuiNavMoveFlags_IsTabbing) && g.NavTabbingDir == -1) data->Ranges.push_back(ImGuiListClipperRange::FromIndices(clipper->ItemsCount - 1, clipper->ItemsCount)); // Add focused/active item @@ -2910,26 +3017,28 @@ static bool ImGuiListClipper_StepInternal(ImGuiListClipper* clipper) // - Very important: when a starting position is after our maximum item, we set Min to (ItemsCount - 1). This allows us to handle most forms of wrapping. // - Due to how Selectable extra padding they tend to be "unaligned" with exact unit in the item list, // which with the flooring/ceiling tend to lead to 2 items instead of one being submitted. - for (int i = 0; i < data->Ranges.Size; i++) - if (data->Ranges[i].PosToIndexConvert) + for (ImGuiListClipperRange& range : data->Ranges) + if (range.PosToIndexConvert) { - int m1 = (int)(((double)data->Ranges[i].Min - window->DC.CursorPos.y - data->LossynessOffset) / clipper->ItemsHeight); - int m2 = (int)((((double)data->Ranges[i].Max - window->DC.CursorPos.y - data->LossynessOffset) / clipper->ItemsHeight) + 0.999999f); - data->Ranges[i].Min = ImClamp(already_submitted + m1 + data->Ranges[i].PosToIndexOffsetMin, already_submitted, clipper->ItemsCount - 1); - data->Ranges[i].Max = ImClamp(already_submitted + m2 + data->Ranges[i].PosToIndexOffsetMax, data->Ranges[i].Min + 1, clipper->ItemsCount); - data->Ranges[i].PosToIndexConvert = false; + int m1 = (int)(((double)range.Min - window->DC.CursorPos.y - data->LossynessOffset) / clipper->ItemsHeight); + int m2 = (int)((((double)range.Max - window->DC.CursorPos.y - data->LossynessOffset) / clipper->ItemsHeight) + 0.999999f); + range.Min = ImClamp(already_submitted + m1 + range.PosToIndexOffsetMin, already_submitted, clipper->ItemsCount - 1); + range.Max = ImClamp(already_submitted + m2 + range.PosToIndexOffsetMax, range.Min + 1, clipper->ItemsCount); + range.PosToIndexConvert = false; } ImGuiListClipper_SortAndFuseRanges(data->Ranges, data->StepNo); } // Step 0+ (if item height is given in advance) or 1+: Display the next range in line. - if (data->StepNo < data->Ranges.Size) + while (data->StepNo < data->Ranges.Size) { clipper->DisplayStart = ImMax(data->Ranges[data->StepNo].Min, already_submitted); clipper->DisplayEnd = ImMin(data->Ranges[data->StepNo].Max, clipper->ItemsCount); if (clipper->DisplayStart > already_submitted) //-V1051 ImGuiListClipper_SeekCursorForItem(clipper, clipper->DisplayStart); data->StepNo++; + if (clipper->DisplayStart == clipper->DisplayEnd && data->StepNo < data->Ranges.Size) + continue; return true; } @@ -2996,13 +3105,14 @@ const ImVec4& ImGui::GetStyleColorVec4(ImGuiCol idx) return style.Colors[idx]; } -ImU32 ImGui::GetColorU32(ImU32 col) +ImU32 ImGui::GetColorU32(ImU32 col, float alpha_mul) { ImGuiStyle& style = GImGui->Style; - if (style.Alpha >= 1.0f) + alpha_mul *= style.Alpha; + if (alpha_mul >= 1.0f) return col; ImU32 a = (col & IM_COL32_A_MASK) >> IM_COL32_A_SHIFT; - a = (ImU32)(a * style.Alpha); // We don't need to clamp 0..255 because Style.Alpha is in 0..1 range. + a = (ImU32)(a * alpha_mul); // We don't need to clamp 0..255 because alpha is in 0..1 range. return (col & ~IM_COL32_A_MASK) | (a << IM_COL32_A_SHIFT); } @@ -3014,7 +3124,8 @@ void ImGui::PushStyleColor(ImGuiCol idx, ImU32 col) backup.Col = idx; backup.BackupValue = g.Style.Colors[idx]; g.ColorStack.push_back(backup); - g.Style.Colors[idx] = ColorConvertU32ToFloat4(col); + if (g.DebugFlashStyleColorIdx != idx) + g.Style.Colors[idx] = ColorConvertU32ToFloat4(col); } void ImGui::PushStyleColor(ImGuiCol idx, const ImVec4& col) @@ -3024,7 +3135,8 @@ void ImGui::PushStyleColor(ImGuiCol idx, const ImVec4& col) backup.Col = idx; backup.BackupValue = g.Style.Colors[idx]; g.ColorStack.push_back(backup); - g.Style.Colors[idx] = col; + if (g.DebugFlashStyleColorIdx != idx) + g.Style.Colors[idx] = col; } void ImGui::PopStyleColor(int count) @@ -3032,7 +3144,7 @@ void ImGui::PopStyleColor(int count) ImGuiContext& g = *GImGui; if (g.ColorStack.Size < count) { - IM_ASSERT_USER_ERROR(g.ColorStack.Size > count, "Calling PopStyleColor() too many times: stack underflow."); + IM_ASSERT_USER_ERROR(g.ColorStack.Size > count, "Calling PopStyleColor() too many times!"); count = g.ColorStack.Size; } while (count > 0) @@ -3044,36 +3156,43 @@ void ImGui::PopStyleColor(int count) } } +static const ImGuiCol GWindowDockStyleColors[ImGuiWindowDockStyleCol_COUNT] = +{ + ImGuiCol_Text, ImGuiCol_Tab, ImGuiCol_TabHovered, ImGuiCol_TabActive, ImGuiCol_TabUnfocused, ImGuiCol_TabUnfocusedActive +}; + static const ImGuiDataVarInfo GStyleVarInfo[] = { - { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, Alpha) }, // ImGuiStyleVar_Alpha - { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, DisabledAlpha) }, // ImGuiStyleVar_DisabledAlpha - { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImGuiStyle, WindowPadding) }, // ImGuiStyleVar_WindowPadding - { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, WindowRounding) }, // ImGuiStyleVar_WindowRounding - { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, WindowBorderSize) }, // ImGuiStyleVar_WindowBorderSize - { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImGuiStyle, WindowMinSize) }, // ImGuiStyleVar_WindowMinSize - { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImGuiStyle, WindowTitleAlign) }, // ImGuiStyleVar_WindowTitleAlign - { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, ChildRounding) }, // ImGuiStyleVar_ChildRounding - { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, ChildBorderSize) }, // ImGuiStyleVar_ChildBorderSize - { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, PopupRounding) }, // ImGuiStyleVar_PopupRounding - { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, PopupBorderSize) }, // ImGuiStyleVar_PopupBorderSize - { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImGuiStyle, FramePadding) }, // ImGuiStyleVar_FramePadding - { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, FrameRounding) }, // ImGuiStyleVar_FrameRounding - { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, FrameBorderSize) }, // ImGuiStyleVar_FrameBorderSize - { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImGuiStyle, ItemSpacing) }, // ImGuiStyleVar_ItemSpacing - { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImGuiStyle, ItemInnerSpacing) }, // ImGuiStyleVar_ItemInnerSpacing - { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, IndentSpacing) }, // ImGuiStyleVar_IndentSpacing - { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImGuiStyle, CellPadding) }, // ImGuiStyleVar_CellPadding - { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, ScrollbarSize) }, // ImGuiStyleVar_ScrollbarSize - { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, ScrollbarRounding) }, // ImGuiStyleVar_ScrollbarRounding - { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, GrabMinSize) }, // ImGuiStyleVar_GrabMinSize - { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, GrabRounding) }, // ImGuiStyleVar_GrabRounding - { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, TabRounding) }, // ImGuiStyleVar_TabRounding - { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImGuiStyle, ButtonTextAlign) }, // ImGuiStyleVar_ButtonTextAlign - { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImGuiStyle, SelectableTextAlign) }, // ImGuiStyleVar_SelectableTextAlign - { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, SeparatorTextBorderSize) },// ImGuiStyleVar_SeparatorTextBorderSize - { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImGuiStyle, SeparatorTextAlign) }, // ImGuiStyleVar_SeparatorTextAlign - { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImGuiStyle, SeparatorTextPadding) }, // ImGuiStyleVar_SeparatorTextPadding + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, Alpha) }, // ImGuiStyleVar_Alpha + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, DisabledAlpha) }, // ImGuiStyleVar_DisabledAlpha + { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, WindowPadding) }, // ImGuiStyleVar_WindowPadding + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, WindowRounding) }, // ImGuiStyleVar_WindowRounding + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, WindowBorderSize) }, // ImGuiStyleVar_WindowBorderSize + { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, WindowMinSize) }, // ImGuiStyleVar_WindowMinSize + { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, WindowTitleAlign) }, // ImGuiStyleVar_WindowTitleAlign + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, ChildRounding) }, // ImGuiStyleVar_ChildRounding + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, ChildBorderSize) }, // ImGuiStyleVar_ChildBorderSize + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, PopupRounding) }, // ImGuiStyleVar_PopupRounding + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, PopupBorderSize) }, // ImGuiStyleVar_PopupBorderSize + { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, FramePadding) }, // ImGuiStyleVar_FramePadding + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, FrameRounding) }, // ImGuiStyleVar_FrameRounding + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, FrameBorderSize) }, // ImGuiStyleVar_FrameBorderSize + { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, ItemSpacing) }, // ImGuiStyleVar_ItemSpacing + { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, ItemInnerSpacing) }, // ImGuiStyleVar_ItemInnerSpacing + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, IndentSpacing) }, // ImGuiStyleVar_IndentSpacing + { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, CellPadding) }, // ImGuiStyleVar_CellPadding + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, ScrollbarSize) }, // ImGuiStyleVar_ScrollbarSize + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, ScrollbarRounding) }, // ImGuiStyleVar_ScrollbarRounding + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, GrabMinSize) }, // ImGuiStyleVar_GrabMinSize + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, GrabRounding) }, // ImGuiStyleVar_GrabRounding + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, TabRounding) }, // ImGuiStyleVar_TabRounding + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, TabBarBorderSize) }, // ImGuiStyleVar_TabBarBorderSize + { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, ButtonTextAlign) }, // ImGuiStyleVar_ButtonTextAlign + { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, SelectableTextAlign) }, // ImGuiStyleVar_SelectableTextAlign + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, SeparatorTextBorderSize) },// ImGuiStyleVar_SeparatorTextBorderSize + { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, SeparatorTextAlign) }, // ImGuiStyleVar_SeparatorTextAlign + { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, SeparatorTextPadding) }, // ImGuiStyleVar_SeparatorTextPadding + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, DockingSeparatorSize) }, // ImGuiStyleVar_DockingSeparatorSize }; const ImGuiDataVarInfo* ImGui::GetStyleVarInfo(ImGuiStyleVar idx) @@ -3094,7 +3213,7 @@ void ImGui::PushStyleVar(ImGuiStyleVar idx, float val) *pvar = val; return; } - IM_ASSERT_USER_ERROR(0, "Called PushStyleVar() variant with wrong type!"); + IM_ASSERT_USER_ERROR(0, "Calling PushStyleVar() variant with wrong type!"); } void ImGui::PushStyleVar(ImGuiStyleVar idx, const ImVec2& val) @@ -3108,7 +3227,7 @@ void ImGui::PushStyleVar(ImGuiStyleVar idx, const ImVec2& val) *pvar = val; return; } - IM_ASSERT_USER_ERROR(0, "Called PushStyleVar() variant with wrong type!"); + IM_ASSERT_USER_ERROR(0, "Calling PushStyleVar() variant with wrong type!"); } void ImGui::PopStyleVar(int count) @@ -3116,7 +3235,7 @@ void ImGui::PopStyleVar(int count) ImGuiContext& g = *GImGui; if (g.StyleVarStack.Size < count) { - IM_ASSERT_USER_ERROR(g.StyleVarStack.Size > count, "Calling PopStyleVar() too many times: stack underflow."); + IM_ASSERT_USER_ERROR(g.StyleVarStack.Size > count, "Calling PopStyleVar() too many times!"); count = g.StyleVarStack.Size; } while (count > 0) @@ -3175,6 +3294,8 @@ const char* ImGui::GetStyleColorName(ImGuiCol idx) case ImGuiCol_TabActive: return "TabActive"; case ImGuiCol_TabUnfocused: return "TabUnfocused"; case ImGuiCol_TabUnfocusedActive: return "TabUnfocusedActive"; + case ImGuiCol_DockingPreview: return "DockingPreview"; + case ImGuiCol_DockingEmptyBg: return "DockingEmptyBg"; case ImGuiCol_PlotLines: return "PlotLines"; case ImGuiCol_PlotLinesHovered: return "PlotLinesHovered"; case ImGuiCol_PlotHistogram: return "PlotHistogram"; @@ -3351,7 +3472,7 @@ void ImGui::RenderTextEllipsis(ImDrawList* draw_list, const ImVec2& pos_min, con // Render text, render ellipsis RenderTextClippedEx(draw_list, pos_min, ImVec2(clip_max_x, pos_max.y), text, text_end_ellipsis, &text_size, ImVec2(0.0f, 0.0f)); - ImVec2 ellipsis_pos = ImFloor(ImVec2(pos_min.x + text_size_clipped_x, pos_min.y)); + ImVec2 ellipsis_pos = ImTrunc(ImVec2(pos_min.x + text_size_clipped_x, pos_min.y)); if (ellipsis_pos.x + ellipsis_width <= ellipsis_max_x) for (int i = 0; i < font->EllipsisCharCount; i++, ellipsis_pos.x += font->EllipsisCharStep * font_scale) font->RenderChar(draw_list, font_size, ellipsis_pos, GetColorU32(ImGuiCol_Text), font->EllipsisChar); @@ -3405,22 +3526,22 @@ void ImGui::RenderNavHighlight(const ImRect& bb, ImGuiID id, ImGuiNavHighlightFl float rounding = (flags & ImGuiNavHighlightFlags_NoRounding) ? 0.0f : g.Style.FrameRounding; ImRect display_rect = bb; display_rect.ClipWith(window->ClipRect); - if (flags & ImGuiNavHighlightFlags_TypeDefault) + const float thickness = 2.0f; + if (flags & ImGuiNavHighlightFlags_Compact) { - const float THICKNESS = 2.0f; - const float DISTANCE = 3.0f + THICKNESS * 0.5f; - display_rect.Expand(ImVec2(DISTANCE, DISTANCE)); + window->DrawList->AddRect(display_rect.Min, display_rect.Max, GetColorU32(ImGuiCol_NavHighlight), rounding, 0, thickness); + } + else + { + const float distance = 3.0f + thickness * 0.5f; + display_rect.Expand(ImVec2(distance, distance)); bool fully_visible = window->ClipRect.Contains(display_rect); if (!fully_visible) window->DrawList->PushClipRect(display_rect.Min, display_rect.Max); - window->DrawList->AddRect(display_rect.Min + ImVec2(THICKNESS * 0.5f, THICKNESS * 0.5f), display_rect.Max - ImVec2(THICKNESS * 0.5f, THICKNESS * 0.5f), GetColorU32(ImGuiCol_NavHighlight), rounding, 0, THICKNESS); + window->DrawList->AddRect(display_rect.Min, display_rect.Max, GetColorU32(ImGuiCol_NavHighlight), rounding, 0, thickness); if (!fully_visible) window->DrawList->PopClipRect(); } - if (flags & ImGuiNavHighlightFlags_TypeThin) - { - window->DrawList->AddRect(display_rect.Min, display_rect.Max, GetColorU32(ImGuiCol_NavHighlight), rounding, 0, 1.0f); - } } void ImGui::RenderMouseCursor(ImVec2 base_pos, float base_scale, ImGuiMouseCursor mouse_cursor, ImU32 col_fill, ImU32 col_border, ImU32 col_shadow) @@ -3428,15 +3549,14 @@ void ImGui::RenderMouseCursor(ImVec2 base_pos, float base_scale, ImGuiMouseCurso ImGuiContext& g = *GImGui; IM_ASSERT(mouse_cursor > ImGuiMouseCursor_None && mouse_cursor < ImGuiMouseCursor_COUNT); ImFontAtlas* font_atlas = g.DrawListSharedData.Font->ContainerAtlas; - for (int n = 0; n < g.Viewports.Size; n++) + for (ImGuiViewportP* viewport : g.Viewports) { // We scale cursor with current viewport/monitor, however Windows 10 for its own hardware cursor seems to be using a different scale factor. ImVec2 offset, size, uv[4]; if (!font_atlas->GetMouseCursorTexData(mouse_cursor, &offset, &size, &uv[0], &uv[2])) continue; - ImGuiViewportP* viewport = g.Viewports[n]; const ImVec2 pos = base_pos - offset; - const float scale = base_scale; + const float scale = base_scale * viewport->DpiScale; if (!viewport->GetMainRect().Overlaps(ImRect(pos, pos + ImVec2(size.x + 2, size.y + 2) * scale))) continue; ImDrawList* draw_list = GetForegroundDrawList(viewport); @@ -3510,6 +3630,7 @@ void ImGui::DestroyContext(ImGuiContext* ctx) // IMPORTANT: ###xxx suffixes must be same in ALL languages static const ImGuiLocEntry GLocalizationEntriesEnUS[] = { + { ImGuiLocKey_VersionStr, "Dear ImGui " IMGUI_VERSION " (" IM_STRINGIFY(IMGUI_VERSION_NUM) ")" }, { ImGuiLocKey_TableSizeOne, "Size column to fit###SizeOne" }, { ImGuiLocKey_TableSizeAllFit, "Size all columns to fit###SizeAll" }, { ImGuiLocKey_TableSizeAllDefault, "Size all columns to default###SizeAll" }, @@ -3517,6 +3638,9 @@ static const ImGuiLocEntry GLocalizationEntriesEnUS[] = { ImGuiLocKey_WindowingMainMenuBar, "(Main menu bar)" }, { ImGuiLocKey_WindowingPopup, "(Popup)" }, { ImGuiLocKey_WindowingUntitled, "(Untitled)" }, + { ImGuiLocKey_DockingHideTabBar, "Hide tab bar###HideTabBar" }, + { ImGuiLocKey_DockingHoldShiftToDock, "Hold SHIFT to enable Docking window." }, + { ImGuiLocKey_DockingDragToUndockOrMoveNode,"Click and drag to move or undock whole node." }, }; void ImGui::Initialize() @@ -3549,10 +3673,26 @@ void ImGui::Initialize() // Create default viewport ImGuiViewportP* viewport = IM_NEW(ImGuiViewportP)(); + viewport->ID = IMGUI_VIEWPORT_DEFAULT_ID; + viewport->Idx = 0; + viewport->PlatformWindowCreated = true; + viewport->Flags = ImGuiViewportFlags_OwnedByApp; g.Viewports.push_back(viewport); g.TempBuffer.resize(1024 * 3 + 1, 0); + g.ViewportCreatedCount++; + g.PlatformIO.Viewports.push_back(g.Viewports[0]); + + // Build KeysMayBeCharInput[] lookup table (1 bool per named key) + for (ImGuiKey key = ImGuiKey_NamedKey_BEGIN; key < ImGuiKey_NamedKey_END; key = (ImGuiKey)(key + 1)) + if ((key >= ImGuiKey_0 && key <= ImGuiKey_9) || (key >= ImGuiKey_A && key <= ImGuiKey_Z) || (key >= ImGuiKey_Keypad0 && key <= ImGuiKey_Keypad9) + || key == ImGuiKey_Tab || key == ImGuiKey_Space || key == ImGuiKey_Apostrophe || key == ImGuiKey_Comma || key == ImGuiKey_Minus || key == ImGuiKey_Period + || key == ImGuiKey_Slash || key == ImGuiKey_Semicolon || key == ImGuiKey_Equal || key == ImGuiKey_LeftBracket || key == ImGuiKey_RightBracket || key == ImGuiKey_GraveAccent + || key == ImGuiKey_KeypadDecimal || key == ImGuiKey_KeypadDivide || key == ImGuiKey_KeypadMultiply || key == ImGuiKey_KeypadSubtract || key == ImGuiKey_KeypadAdd || key == ImGuiKey_KeypadEqual) + g.KeysMayBeCharInput.SetBit(key); #ifdef IMGUI_HAS_DOCK + // Initialize Docking + DockContextInitialize(&g); #endif g.Initialized = true; @@ -3561,8 +3701,11 @@ void ImGui::Initialize() // This function is merely here to free heap allocations. void ImGui::Shutdown() { - // The fonts atlas can be used prior to calling NewFrame(), so we clear it even if g.Initialized is FALSE (which would happen if we never called NewFrame) ImGuiContext& g = *GImGui; + IM_ASSERT_USER_ERROR(g.IO.BackendPlatformUserData == NULL, "Forgot to shutdown Platform backend?"); + IM_ASSERT_USER_ERROR(g.IO.BackendRendererUserData == NULL, "Forgot to shutdown Renderer backend?"); + + // The fonts atlas can be used prior to calling NewFrame(), so we clear it even if g.Initialized is FALSE (which would happen if we never called NewFrame) if (g.IO.Fonts && g.FontAtlasOwnedByContext) { g.IO.Fonts->Locked = false; @@ -3579,6 +3722,12 @@ void ImGui::Shutdown() if (g.SettingsLoaded && g.IO.IniFilename != NULL) SaveIniSettingsToDisk(g.IO.IniFilename); + // Destroy platform windows + DestroyPlatformWindows(); + + // Shutdown extensions + DockContextShutdown(&g); + CallContextHooks(&g, ImGuiContextHookType_Shutdown); // Clear everything else @@ -3600,7 +3749,9 @@ void ImGui::Shutdown() g.FontStack.clear(); g.OpenPopupStack.clear(); g.BeginPopupStack.clear(); + g.NavTreeNodeStack.clear(); + g.CurrentViewport = g.MouseViewport = g.MouseLastHoveredViewport = NULL; g.Viewports.clear_delete(); g.TabBars.Clear(); @@ -3651,9 +3802,9 @@ void ImGui::RemoveContextHook(ImGuiContext* ctx, ImGuiID hook_id) { ImGuiContext& g = *ctx; IM_ASSERT(hook_id != 0); - for (int n = 0; n < g.Hooks.Size; n++) - if (g.Hooks[n].HookId == hook_id) - g.Hooks[n].Type = ImGuiContextHookType_PendingRemoval_; + for (ImGuiContextHook& hook : g.Hooks) + if (hook.HookId == hook_id) + hook.Type = ImGuiContextHookType_PendingRemoval_; } // Call context hooks (used by e.g. test engine) @@ -3661,9 +3812,9 @@ void ImGui::RemoveContextHook(ImGuiContext* ctx, ImGuiID hook_id) void ImGui::CallContextHooks(ImGuiContext* ctx, ImGuiContextHookType hook_type) { ImGuiContext& g = *ctx; - for (int n = 0; n < g.Hooks.Size; n++) - if (g.Hooks[n].Type == hook_type) - g.Hooks[n].Callback(&g, &g.Hooks[n]); + for (ImGuiContextHook& hook : g.Hooks) + if (hook.Type == hook_type) + hook.Callback(&g, &hook); } @@ -3680,21 +3831,27 @@ ImGuiWindow::ImGuiWindow(ImGuiContext* ctx, const char* name) : DrawListInst(NUL NameBufLen = (int)strlen(name) + 1; ID = ImHashStr(name); IDStack.push_back(ID); + ViewportAllowPlatformMonitorExtend = -1; + ViewportPos = ImVec2(FLT_MAX, FLT_MAX); MoveId = GetID("#MOVE"); + TabId = GetID("#TAB"); ScrollTarget = ImVec2(FLT_MAX, FLT_MAX); ScrollTargetCenterRatio = ImVec2(0.5f, 0.5f); AutoFitFramesX = AutoFitFramesY = -1; AutoPosLastDirection = ImGuiDir_None; - SetWindowPosAllowFlags = SetWindowSizeAllowFlags = SetWindowCollapsedAllowFlags = 0; + SetWindowPosAllowFlags = SetWindowSizeAllowFlags = SetWindowCollapsedAllowFlags = SetWindowDockAllowFlags = 0; SetWindowPosVal = SetWindowPosPivot = ImVec2(FLT_MAX, FLT_MAX); LastFrameActive = -1; + LastFrameJustFocused = -1; LastTimeActive = -1.0f; - FontWindowScale = 1.0f; + FontWindowScale = FontDpiScale = 1.0f; SettingsOffset = -1; + DockOrder = -1; DrawList = &DrawListInst; DrawList->_Data = &Ctx->DrawListSharedData; DrawList->_OwnerName = Name; NavPreferredScoringPosRel[0] = NavPreferredScoringPosRel[1] = ImVec2(FLT_MAX, FLT_MAX); + IM_PLACEMENT_NEW(&WindowClass) ImGuiWindowClass(); } ImGuiWindow::~ImGuiWindow() @@ -3832,6 +3989,7 @@ void ImGui::SetActiveID(ImGuiID id, ImGuiWindow* window) g.ActiveIdNoClearOnFocusLoss = false; g.ActiveIdWindow = window; g.ActiveIdHasBeenEditedThisFrame = false; + g.ActiveIdFromShortcut = false; if (id) { g.ActiveIdIsAlive = id; @@ -3868,22 +4026,13 @@ ImGuiID ImGui::GetHoveredID() return g.HoveredId ? g.HoveredId : g.HoveredIdPreviousFrame; } -// This is called by ItemAdd(). -// Code not using ItemAdd() may need to call this manually otherwise ActiveId will be cleared. In IMGUI_VERSION_NUM < 18717 this was called by GetID(). -void ImGui::KeepAliveID(ImGuiID id) -{ - ImGuiContext& g = *GImGui; - if (g.ActiveId == id) - g.ActiveIdIsAlive = id; - if (g.ActiveIdPreviousFrame == id) - g.ActiveIdPreviousFrameIsAlive = true; -} - void ImGui::MarkItemEdited(ImGuiID id) { // This marking is solely to be able to provide info for IsItemDeactivatedAfterEdit(). // ActiveId might have been released by the time we call this (as in the typical press/release button behavior) but still need to fill the data. ImGuiContext& g = *GImGui; + if (g.LockMarkEdited > 0) + return; if (g.ActiveId == id || g.ActiveId == 0) { g.ActiveIdHasBeenEditedThisFrame = true; @@ -3904,8 +4053,8 @@ bool ImGui::IsWindowContentHoverable(ImGuiWindow* window, ImGuiHoveredFlags flag // FIXME-OPT: This could be cached/stored within the window. ImGuiContext& g = *GImGui; if (g.NavWindow) - if (ImGuiWindow* focused_root_window = g.NavWindow->RootWindow) - if (focused_root_window->WasActive && focused_root_window != window->RootWindow) + if (ImGuiWindow* focused_root_window = g.NavWindow->RootWindowDockTree) + if (focused_root_window->WasActive && focused_root_window != window->RootWindowDockTree) { // For the purpose of those flags we differentiate "standard popup" from "modal popup" // NB: The 'else' is important because Modal windows are also Popups. @@ -3920,9 +4069,33 @@ bool ImGui::IsWindowContentHoverable(ImGuiWindow* window, ImGuiHoveredFlags flag if (!IsWindowWithinBeginStackOf(window->RootWindow, focused_root_window)) return false; } + + // Filter by viewport + if (window->Viewport != g.MouseViewport) + if (g.MovingWindow == NULL || window->RootWindowDockTree != g.MovingWindow->RootWindowDockTree) + return false; + return true; } +static inline float CalcDelayFromHoveredFlags(ImGuiHoveredFlags flags) +{ + ImGuiContext& g = *GImGui; + if (flags & ImGuiHoveredFlags_DelayNormal) + return g.Style.HoverDelayNormal; + if (flags & ImGuiHoveredFlags_DelayShort) + return g.Style.HoverDelayShort; + return 0.0f; +} + +static ImGuiHoveredFlags ApplyHoverFlagsForTooltip(ImGuiHoveredFlags user_flags, ImGuiHoveredFlags shared_flags) +{ + // Allow instance flags to override shared flags + if (user_flags & (ImGuiHoveredFlags_DelayNone | ImGuiHoveredFlags_DelayShort | ImGuiHoveredFlags_DelayNormal)) + shared_flags &= ~(ImGuiHoveredFlags_DelayNone | ImGuiHoveredFlags_DelayShort | ImGuiHoveredFlags_DelayNormal); + return user_flags | shared_flags; +} + // This is roughly matching the behavior of internal-facing ItemHoverable() // - we allow hovering to be true when ActiveId==window->MoveID, so that clicking on non-interactive items such as a Text() item still returns true with IsItemHovered() // - this should work even for non-interactive items that have no ID, so we cannot use LastItemId @@ -3930,12 +4103,17 @@ bool ImGui::IsItemHovered(ImGuiHoveredFlags flags) { ImGuiContext& g = *GImGui; ImGuiWindow* window = g.CurrentWindow; + IM_ASSERT((flags & ~ImGuiHoveredFlags_AllowedMaskForIsItemHovered) == 0 && "Invalid flags for IsItemHovered()!"); + if (g.NavDisableMouseHover && !g.NavDisableHighlight && !(flags & ImGuiHoveredFlags_NoNavOverride)) { if ((g.LastItemData.InFlags & ImGuiItemFlags_Disabled) && !(flags & ImGuiHoveredFlags_AllowWhenDisabled)) return false; if (!IsItemFocused()) return false; + + if (flags & ImGuiHoveredFlags_ForTooltip) + flags = ApplyHoverFlagsForTooltip(flags, g.Style.HoverFlagsForTooltipNav); } else { @@ -3943,7 +4121,11 @@ bool ImGui::IsItemHovered(ImGuiHoveredFlags flags) ImGuiItemStatusFlags status_flags = g.LastItemData.StatusFlags; if (!(status_flags & ImGuiItemStatusFlags_HoveredRect)) return false; - IM_ASSERT((flags & (ImGuiHoveredFlags_AnyWindow | ImGuiHoveredFlags_RootWindow | ImGuiHoveredFlags_ChildWindows | ImGuiHoveredFlags_NoPopupHierarchy)) == 0); // Flags not supported by this function + + if (flags & ImGuiHoveredFlags_ForTooltip) + flags = ApplyHoverFlagsForTooltip(flags, g.Style.HoverFlagsForTooltipMouse); + + IM_ASSERT((flags & (ImGuiHoveredFlags_AnyWindow | ImGuiHoveredFlags_RootWindow | ImGuiHoveredFlags_ChildWindows | ImGuiHoveredFlags_NoPopupHierarchy | ImGuiHoveredFlags_DockHierarchy)) == 0); // Flags not supported by this function // Done with rectangle culling so we can perform heavier checks now // Test if we are hovering the right window (our window could be behind another window) @@ -3952,13 +4134,15 @@ bool ImGui::IsItemHovered(ImGuiHoveredFlags flags) // to use IsItemHovered() after EndChild() itself. Until a solution is found I believe reverting to the test from 2017/09/27 is safe since this was // the test that has been running for a long while. if (g.HoveredWindow != window && (status_flags & ImGuiItemStatusFlags_HoveredWindow) == 0) - if ((flags & ImGuiHoveredFlags_AllowWhenOverlapped) == 0) + if ((flags & ImGuiHoveredFlags_AllowWhenOverlappedByWindow) == 0) return false; // Test if another item is active (e.g. being dragged) + const ImGuiID id = g.LastItemData.ID; if ((flags & ImGuiHoveredFlags_AllowWhenBlockedByActiveItem) == 0) - if (g.ActiveId != 0 && g.ActiveId != g.LastItemData.ID && !g.ActiveIdAllowOverlap && g.ActiveId != window->MoveId) - return false; + if (g.ActiveId != 0 && g.ActiveId != id && !g.ActiveIdAllowOverlap) + if (g.ActiveId != window->MoveId && g.ActiveId != window->TabId) + return false; // Test if interactions on this window are blocked by an active popup or modal. // The ImGuiHoveredFlags_AllowWhenBlockedByPopup flag will be tested here. @@ -3970,49 +4154,63 @@ bool ImGui::IsItemHovered(ImGuiHoveredFlags flags) return false; // Special handling for calling after Begin() which represent the title bar or tab. - // When the window is skipped/collapsed (SkipItems==true) that last item will never be overwritten so we need to detect the case. - if (g.LastItemData.ID == window->MoveId && window->WriteAccessed) + // When the window is skipped/collapsed (SkipItems==true) that last item (always ->MoveId submitted by Begin) + // will never be overwritten so we need to detect the case. + if (id == window->MoveId && window->WriteAccessed) return false; + + // Test if using AllowOverlap and overlapped + if ((g.LastItemData.InFlags & ImGuiItemFlags_AllowOverlap) && id != 0) + if ((flags & ImGuiHoveredFlags_AllowWhenOverlappedByItem) == 0) + if (g.HoveredIdPreviousFrame != g.LastItemData.ID) + return false; } // Handle hover delay // (some ideas: https://www.nngroup.com/articles/timing-exposing-content) - float delay; - if (flags & ImGuiHoveredFlags_DelayNormal) - delay = g.IO.HoverDelayNormal; - else if (flags & ImGuiHoveredFlags_DelayShort) - delay = g.IO.HoverDelayShort; - else - delay = 0.0f; - if (delay > 0.0f) + const float delay = CalcDelayFromHoveredFlags(flags); + if (delay > 0.0f || (flags & ImGuiHoveredFlags_Stationary)) { ImGuiID hover_delay_id = (g.LastItemData.ID != 0) ? g.LastItemData.ID : window->GetIDFromRectangle(g.LastItemData.Rect); - if ((flags & ImGuiHoveredFlags_NoSharedDelay) && (g.HoverDelayIdPreviousFrame != hover_delay_id)) - g.HoverDelayTimer = 0.0f; - g.HoverDelayId = hover_delay_id; - return g.HoverDelayTimer >= delay; + if ((flags & ImGuiHoveredFlags_NoSharedDelay) && (g.HoverItemDelayIdPreviousFrame != hover_delay_id)) + g.HoverItemDelayTimer = 0.0f; + g.HoverItemDelayId = hover_delay_id; + + // When changing hovered item we requires a bit of stationary delay before activating hover timer, + // but once unlocked on a given item we also moving. + //if (g.HoverDelayTimer >= delay && (g.HoverDelayTimer - g.IO.DeltaTime < delay || g.MouseStationaryTimer - g.IO.DeltaTime < g.Style.HoverStationaryDelay)) { IMGUI_DEBUG_LOG("HoverDelayTimer = %f/%f, MouseStationaryTimer = %f\n", g.HoverDelayTimer, delay, g.MouseStationaryTimer); } + if ((flags & ImGuiHoveredFlags_Stationary) != 0 && g.HoverItemUnlockedStationaryId != hover_delay_id) + return false; + + if (g.HoverItemDelayTimer < delay) + return false; } return true; } // Internal facing ItemHoverable() used when submitting widgets. Differs slightly from IsItemHovered(). -bool ImGui::ItemHoverable(const ImRect& bb, ImGuiID id) +// (this does not rely on LastItemData it can be called from a ButtonBehavior() call not following an ItemAdd() call) +// FIXME-LEGACY: the 'ImGuiItemFlags item_flags' parameter was added on 2023-06-28. +// If you used this in your legacy/custom widgets code: +// - Commonly: if your ItemHoverable() call comes after an ItemAdd() call: pass 'item_flags = g.LastItemData.InFlags'. +// - Rare: otherwise you may pass 'item_flags = 0' (ImGuiItemFlags_None) unless you want to benefit from special behavior handled by ItemHoverable. +bool ImGui::ItemHoverable(const ImRect& bb, ImGuiID id, ImGuiItemFlags item_flags) { ImGuiContext& g = *GImGui; - if (g.HoveredId != 0 && g.HoveredId != id && !g.HoveredIdAllowOverlap) - return false; - ImGuiWindow* window = g.CurrentWindow; if (g.HoveredWindow != window) return false; - if (g.ActiveId != 0 && g.ActiveId != id && !g.ActiveIdAllowOverlap) - return false; if (!IsMouseHoveringRect(bb.Min, bb.Max)) return false; + if (g.HoveredId != 0 && g.HoveredId != id && !g.HoveredIdAllowOverlap) + return false; + if (g.ActiveId != 0 && g.ActiveId != id && !g.ActiveIdAllowOverlap) + if (!g.ActiveIdFromShortcut) + return false; + // Done with rectangle culling so we can perform heavier checks now. - ImGuiItemFlags item_flags = (g.LastItemData.ID == id ? g.LastItemData.InFlags : g.CurrentItemFlags); if (!(item_flags & ImGuiItemFlags_NoWindowHoverableCheck) && !IsWindowContentHoverable(window, ImGuiHoveredFlags_None)) { g.HoveredIdDisabled = true; @@ -4022,29 +4220,46 @@ bool ImGui::ItemHoverable(const ImRect& bb, ImGuiID id) // We exceptionally allow this function to be called with id==0 to allow using it for easy high-level // hover test in widgets code. We could also decide to split this function is two. if (id != 0) + { + // Drag source doesn't report as hovered + if (g.DragDropActive && g.DragDropPayload.SourceId == id && !(g.DragDropSourceFlags & ImGuiDragDropFlags_SourceNoDisableHover)) + return false; + SetHoveredID(id); + // AllowOverlap mode (rarely used) requires previous frame HoveredId to be null or to match. + // This allows using patterns where a later submitted widget overlaps a previous one. Generally perceived as a front-to-back hit-test. + if (item_flags & ImGuiItemFlags_AllowOverlap) + { + g.HoveredIdAllowOverlap = true; + if (g.HoveredIdPreviousFrame != id) + return false; + } + } + // When disabled we'll return false but still set HoveredId if (item_flags & ImGuiItemFlags_Disabled) { // Release active id if turning disabled - if (g.ActiveId == id) + if (g.ActiveId == id && id != 0) ClearActiveID(); g.HoveredIdDisabled = true; return false; } +#ifndef IMGUI_DISABLE_DEBUG_TOOLS if (id != 0) { // [DEBUG] Item Picker tool! - // We perform the check here because SetHoveredID() is not frequently called (1~ time a frame), making - // the cost of this tool near-zero. We can get slightly better call-stack and support picking non-hovered - // items if we performed the test in ItemAdd(), but that would incur a small runtime cost. + // We perform the check here because reaching is path is rare (1~ time a frame), + // making the cost of this tool near-zero! We could get better call-stack and support picking non-hovered + // items if we performed the test in ItemAdd(), but that would incur a bigger runtime cost. if (g.DebugItemPickerActive && g.HoveredIdPreviousFrame == id) GetForegroundDrawList()->AddRect(bb.Min, bb.Max, IM_COL32(255, 255, 0, 255)); if (g.DebugItemPickerBreakId == id) IM_DEBUG_BREAK(); } +#endif if (g.NavDisableMouseHover) return false; @@ -4053,12 +4268,13 @@ bool ImGui::ItemHoverable(const ImRect& bb, ImGuiID id) } // FIXME: This is inlined/duplicated in ItemAdd() +// FIXME: The id != 0 path is not used by our codebase, may get rid of it? bool ImGui::IsClippedEx(const ImRect& bb, ImGuiID id) { ImGuiContext& g = *GImGui; ImGuiWindow* window = g.CurrentWindow; if (!bb.Overlaps(window->ClipRect)) - if (id == 0 || (id != g.ActiveId && id != g.NavId)) + if (id == 0 || (id != g.ActiveId && id != g.ActiveIdPreviousFrame && id != g.NavId && id != g.NavActivateId)) if (!g.LogEnabled) return true; return false; @@ -4102,20 +4318,51 @@ float ImGui::CalcWrapWidthForPos(const ImVec2& pos, float wrap_pos_x) // IM_ALLOC() == ImGui::MemAlloc() void* ImGui::MemAlloc(size_t size) { + void* ptr = (*GImAllocatorAllocFunc)(size, GImAllocatorUserData); +#ifndef IMGUI_DISABLE_DEBUG_TOOLS if (ImGuiContext* ctx = GImGui) - ctx->IO.MetricsActiveAllocations++; - return (*GImAllocatorAllocFunc)(size, GImAllocatorUserData); + DebugAllocHook(&ctx->DebugAllocInfo, ctx->FrameCount, ptr, size); +#endif + return ptr; } // IM_FREE() == ImGui::MemFree() void ImGui::MemFree(void* ptr) { - if (ptr) +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + if (ptr != NULL) if (ImGuiContext* ctx = GImGui) - ctx->IO.MetricsActiveAllocations--; + DebugAllocHook(&ctx->DebugAllocInfo, ctx->FrameCount, ptr, (size_t)-1); +#endif return (*GImAllocatorFreeFunc)(ptr, GImAllocatorUserData); } +// We record the number of allocation in recent frames, as a way to audit/sanitize our guiding principles of "no allocations on idle/repeating frames" +void ImGui::DebugAllocHook(ImGuiDebugAllocInfo* info, int frame_count, void* ptr, size_t size) +{ + ImGuiDebugAllocEntry* entry = &info->LastEntriesBuf[info->LastEntriesIdx]; + IM_UNUSED(ptr); + if (entry->FrameCount != frame_count) + { + info->LastEntriesIdx = (info->LastEntriesIdx + 1) % IM_ARRAYSIZE(info->LastEntriesBuf); + entry = &info->LastEntriesBuf[info->LastEntriesIdx]; + entry->FrameCount = frame_count; + entry->AllocCount = entry->FreeCount = 0; + } + if (size != (size_t)-1) + { + entry->AllocCount++; + info->TotalAllocCount++; + //printf("[%05d] MemAlloc(%d) -> 0x%p\n", frame_count, size, ptr); + } + else + { + entry->FreeCount++; + info->TotalFreeCount++; + //printf("[%05d] MemFree(0x%p)\n", frame_count, ptr); + } +} + const char* ImGui::GetClipboardText() { ImGuiContext& g = *GImGui; @@ -4140,6 +4387,12 @@ ImGuiIO& ImGui::GetIO() return GImGui->IO; } +ImGuiPlatformIO& ImGui::GetPlatformIO() +{ + IM_ASSERT(GImGui != NULL && "No current context. Did you call ImGui::CreateContext() or ImGui::SetCurrentContext()?"); + return GImGui->PlatformIO; +} + // Pass this to your backend rendering function! Valid after Render() and until the next call to NewFrame() ImDrawData* ImGui::GetDrawData() { @@ -4158,50 +4411,50 @@ int ImGui::GetFrameCount() return GImGui->FrameCount; } -static ImDrawList* GetViewportDrawList(ImGuiViewportP* viewport, size_t drawlist_no, const char* drawlist_name) +static ImDrawList* GetViewportBgFgDrawList(ImGuiViewportP* viewport, size_t drawlist_no, const char* drawlist_name) { // Create the draw list on demand, because they are not frequently used for all viewports ImGuiContext& g = *GImGui; - IM_ASSERT(drawlist_no < IM_ARRAYSIZE(viewport->DrawLists)); - ImDrawList* draw_list = viewport->DrawLists[drawlist_no]; + IM_ASSERT(drawlist_no < IM_ARRAYSIZE(viewport->BgFgDrawLists)); + ImDrawList* draw_list = viewport->BgFgDrawLists[drawlist_no]; if (draw_list == NULL) { draw_list = IM_NEW(ImDrawList)(&g.DrawListSharedData); draw_list->_OwnerName = drawlist_name; - viewport->DrawLists[drawlist_no] = draw_list; + viewport->BgFgDrawLists[drawlist_no] = draw_list; } // Our ImDrawList system requires that there is always a command - if (viewport->DrawListsLastFrame[drawlist_no] != g.FrameCount) + if (viewport->BgFgDrawListsLastFrame[drawlist_no] != g.FrameCount) { draw_list->_ResetForNewFrame(); draw_list->PushTextureID(g.IO.Fonts->TexID); draw_list->PushClipRect(viewport->Pos, viewport->Pos + viewport->Size, false); - viewport->DrawListsLastFrame[drawlist_no] = g.FrameCount; + viewport->BgFgDrawListsLastFrame[drawlist_no] = g.FrameCount; } return draw_list; } ImDrawList* ImGui::GetBackgroundDrawList(ImGuiViewport* viewport) { - return GetViewportDrawList((ImGuiViewportP*)viewport, 0, "##Background"); + return GetViewportBgFgDrawList((ImGuiViewportP*)viewport, 0, "##Background"); } ImDrawList* ImGui::GetBackgroundDrawList() { ImGuiContext& g = *GImGui; - return GetBackgroundDrawList(g.Viewports[0]); + return GetBackgroundDrawList(g.CurrentWindow->Viewport); } ImDrawList* ImGui::GetForegroundDrawList(ImGuiViewport* viewport) { - return GetViewportDrawList((ImGuiViewportP*)viewport, 1, "##Foreground"); + return GetViewportBgFgDrawList((ImGuiViewportP*)viewport, 1, "##Foreground"); } ImDrawList* ImGui::GetForegroundDrawList() { ImGuiContext& g = *GImGui; - return GetForegroundDrawList(g.Viewports[0]); + return GetForegroundDrawList(g.CurrentWindow->Viewport); } ImDrawListSharedData* ImGui::GetDrawListSharedData() @@ -4218,17 +4471,43 @@ void ImGui::StartMouseMovingWindow(ImGuiWindow* window) FocusWindow(window); SetActiveID(window->MoveId, window); g.NavDisableHighlight = true; - g.ActiveIdClickOffset = g.IO.MouseClickedPos[0] - window->RootWindow->Pos; + g.ActiveIdClickOffset = g.IO.MouseClickedPos[0] - window->RootWindowDockTree->Pos; g.ActiveIdNoClearOnFocusLoss = true; SetActiveIdUsingAllKeyboardKeys(); bool can_move_window = true; - if ((window->Flags & ImGuiWindowFlags_NoMove) || (window->RootWindow->Flags & ImGuiWindowFlags_NoMove)) + if ((window->Flags & ImGuiWindowFlags_NoMove) || (window->RootWindowDockTree->Flags & ImGuiWindowFlags_NoMove)) can_move_window = false; + if (ImGuiDockNode* node = window->DockNodeAsHost) + if (node->VisibleWindow && (node->VisibleWindow->Flags & ImGuiWindowFlags_NoMove)) + can_move_window = false; if (can_move_window) g.MovingWindow = window; } +// We use 'undock == false' when dragging from title bar to allow moving groups of floating nodes without undocking them. +void ImGui::StartMouseMovingWindowOrNode(ImGuiWindow* window, ImGuiDockNode* node, bool undock) +{ + ImGuiContext& g = *GImGui; + bool can_undock_node = false; + if (undock && node != NULL && node->VisibleWindow && (node->VisibleWindow->Flags & ImGuiWindowFlags_NoMove) == 0 && (node->MergedFlags & ImGuiDockNodeFlags_NoUndocking) == 0) + { + // Can undock if: + // - part of a hierarchy with more than one visible node (if only one is visible, we'll just move the root window) + // - part of a dockspace node hierarchy: so we can undock the last single visible node too (trivia: undocking from a fixed/central node will create a new node and copy windows) + ImGuiDockNode* root_node = DockNodeGetRootNode(node); + if (root_node->OnlyNodeWithWindows != node || root_node->CentralNode != NULL) // -V1051 PVS-Studio thinks node should be root_node and is wrong about that. + can_undock_node = true; + } + + const bool clicked = IsMouseClicked(0); + const bool dragging = IsMouseDragging(0); + if (can_undock_node && dragging) + DockContextQueueUndockNode(&g, node); // Will lead to DockNodeStartMouseMovingWindow() -> StartMouseMovingWindow() being called next frame + else if (!can_undock_node && (clicked || dragging) && g.MovingWindow != window) + StartMouseMovingWindow(window); +} + // Handle mouse moving window // Note: moving window with the navigation keys (Square + d-pad / CTRL+TAB + Arrows) are processed in NavUpdateWindowing() // FIXME: We don't have strong guarantee that g.MovingWindow stay synched with g.ActiveId == g.MovingWindow->MoveId. @@ -4242,16 +4521,43 @@ void ImGui::UpdateMouseMovingWindowNewFrame() // We actually want to move the root window. g.MovingWindow == window we clicked on (could be a child window). // We track it to preserve Focus and so that generally ActiveIdWindow == MovingWindow and ActiveId == MovingWindow->MoveId for consistency. KeepAliveID(g.ActiveId); - IM_ASSERT(g.MovingWindow && g.MovingWindow->RootWindow); - ImGuiWindow* moving_window = g.MovingWindow->RootWindow; - if (g.IO.MouseDown[0] && IsMousePosValid(&g.IO.MousePos)) + IM_ASSERT(g.MovingWindow && g.MovingWindow->RootWindowDockTree); + ImGuiWindow* moving_window = g.MovingWindow->RootWindowDockTree; + + // When a window stop being submitted while being dragged, it may will its viewport until next Begin() + const bool window_disappared = (!moving_window->WasActive && !moving_window->Active); + if (g.IO.MouseDown[0] && IsMousePosValid(&g.IO.MousePos) && !window_disappared) { ImVec2 pos = g.IO.MousePos - g.ActiveIdClickOffset; - SetWindowPos(moving_window, pos, ImGuiCond_Always); + if (moving_window->Pos.x != pos.x || moving_window->Pos.y != pos.y) + { + SetWindowPos(moving_window, pos, ImGuiCond_Always); + if (moving_window->Viewport && moving_window->ViewportOwned) // Synchronize viewport immediately because some overlays may relies on clipping rectangle before we Begin() into the window. + { + moving_window->Viewport->Pos = pos; + moving_window->Viewport->UpdateWorkRect(); + } + } FocusWindow(g.MovingWindow); } else { + if (!window_disappared) + { + // Try to merge the window back into the main viewport. + // This works because MouseViewport should be != MovingWindow->Viewport on release (as per code in UpdateViewports) + if (g.ConfigFlagsCurrFrame & ImGuiConfigFlags_ViewportsEnable) + UpdateTryMergeWindowIntoHostViewport(moving_window, g.MouseViewport); + + // Restore the mouse viewport so that we don't hover the viewport _under_ the moved window during the frame we released the mouse button. + if (moving_window->Viewport && !IsDragDropPayloadBeingAccepted()) + g.MouseViewport = moving_window->Viewport; + + // Clear the NoInput window flag set by the Viewport system + if (moving_window->Viewport) + moving_window->Viewport->Flags &= ~ImGuiViewportFlags_NoInputs; + } + g.MovingWindow = NULL; ClearActiveID(); } @@ -4281,7 +4587,7 @@ void ImGui::UpdateMouseMovingWindowEndFrame() return; // Click on empty space to focus window and start moving - // (after we're done with all our widgets) + // (after we're done with all our widgets, so e.g. clicking on docking tab-bar which have set HoveredId already and not get us here!) if (g.IO.MouseClicked[0]) { // Handle the edge case of a popup being closed while clicking in its empty space. @@ -4294,9 +4600,10 @@ void ImGui::UpdateMouseMovingWindowEndFrame() StartMouseMovingWindow(g.HoveredWindow); //-V595 // Cancel moving if clicked outside of title bar - if (g.IO.ConfigWindowsMoveFromTitleBarOnly && !(root_window->Flags & ImGuiWindowFlags_NoTitleBar)) - if (!root_window->TitleBarRect().Contains(g.IO.MouseClickedPos[0])) - g.MovingWindow = NULL; + if (g.IO.ConfigWindowsMoveFromTitleBarOnly) + if (!(root_window->Flags & ImGuiWindowFlags_NoTitleBar) || root_window->DockIsActive) + if (!root_window->TitleBarRect().Contains(g.IO.MouseClickedPos[0])) + g.MovingWindow = NULL; // Cancel moving if clicked over an item which was disabled or inhibited by popups (note that we know HoveredId == 0 already) if (g.HoveredIdDisabled) @@ -4322,6 +4629,29 @@ void ImGui::UpdateMouseMovingWindowEndFrame() } } +// This is called during NewFrame()->UpdateViewportsNewFrame() only. +// Need to keep in sync with SetWindowPos() +static void TranslateWindow(ImGuiWindow* window, const ImVec2& delta) +{ + window->Pos += delta; + window->ClipRect.Translate(delta); + window->OuterRectClipped.Translate(delta); + window->InnerRect.Translate(delta); + window->DC.CursorPos += delta; + window->DC.CursorStartPos += delta; + window->DC.CursorMaxPos += delta; + window->DC.IdealMaxPos += delta; +} + +static void ScaleWindow(ImGuiWindow* window, float scale) +{ + ImVec2 origin = window->Viewport->Pos; + window->Pos = ImFloor((window->Pos - origin) * scale + origin); + window->Size = ImTrunc(window->Size * scale); + window->SizeFull = ImTrunc(window->SizeFull * scale); + window->ContentSize = ImTrunc(window->ContentSize * scale); +} + static bool IsWindowActiveAndVisible(ImGuiWindow* window) { return (window->Active) && (!window->Hidden); @@ -4340,10 +4670,11 @@ void ImGui::UpdateHoveredWindowAndCaptureFlags() // - We also support the moved window toggling the NoInputs flag after moving has started in order to be able to detect windows below it, which is useful for e.g. docking mechanisms. bool clear_hovered_windows = false; FindHoveredWindow(); + IM_ASSERT(g.HoveredWindow == NULL || g.HoveredWindow == g.MovingWindow || g.HoveredWindow->Viewport == g.MouseViewport); // Modal windows prevents mouse from hovering behind them. ImGuiWindow* modal_window = GetTopMostPopupModal(); - if (modal_window && g.HoveredWindow && !IsWindowWithinBeginStackOf(g.HoveredWindow->RootWindow, modal_window)) + if (modal_window && g.HoveredWindow && !IsWindowWithinBeginStackOf(g.HoveredWindow->RootWindow, modal_window)) // FIXME-MERGE: RootWindowDockTree ? clear_hovered_windows = true; // Disabled mouse? @@ -4393,12 +4724,11 @@ void ImGui::UpdateHoveredWindowAndCaptureFlags() } // Update io.WantCaptureKeyboard for the user application (true = dispatch keyboard info to Dear ImGui only, false = dispatch keyboard info to Dear ImGui + underlying app) - if (g.WantCaptureKeyboardNextFrame != -1) - io.WantCaptureKeyboard = (g.WantCaptureKeyboardNextFrame != 0); - else - io.WantCaptureKeyboard = (g.ActiveId != 0) || (modal_window != NULL); + io.WantCaptureKeyboard = (g.ActiveId != 0) || (modal_window != NULL); if (io.NavActive && (io.ConfigFlags & ImGuiConfigFlags_NavEnableKeyboard) && !(io.ConfigFlags & ImGuiConfigFlags_NavNoCaptureKeyboard)) io.WantCaptureKeyboard = true; + if (g.WantCaptureKeyboardNextFrame != -1) // Manual override + io.WantCaptureKeyboard = (g.WantCaptureKeyboardNextFrame != 0); // Update io.WantTextInput flag, this is to allow systems without a keyboard (e.g. mobile, hand-held) to show a software keyboard if possible io.WantTextInput = (g.WantTextInputNextFrame != -1) ? (g.WantTextInputNextFrame != 0) : false; @@ -4418,7 +4748,9 @@ void ImGui::NewFrame() CallContextHooks(&g, ImGuiContextHookType_NewFramePre); // Check and assert for various common IO and Configuration mistakes + g.ConfigFlagsLastFrame = g.ConfigFlagsCurrFrame; ErrorCheckNewFrameSanityChecks(); + g.ConfigFlagsCurrFrame = g.IO.ConfigFlags; // Load settings on first frame, save settings when modified (after a delay) UpdateSettings(); @@ -4445,12 +4777,13 @@ void ImGui::NewFrame() UpdateViewportsNewFrame(); // Setup current font and draw list shared data + // FIXME-VIEWPORT: the concept of a single ClipRectFullscreen is not ideal! g.IO.Fonts->Locked = true; SetCurrentFont(GetDefaultFont()); IM_ASSERT(g.Font->IsLoaded()); ImRect virtual_space(FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX); - for (int n = 0; n < g.Viewports.Size; n++) - virtual_space.Add(g.Viewports[n]->GetMainRect()); + for (ImGuiViewportP* viewport : g.Viewports) + virtual_space.Add(viewport->GetMainRect()); g.DrawListSharedData.ClipRectFullscreen = virtual_space.ToVec4(); g.DrawListSharedData.CurveTessellationTol = g.Style.CurveTessellationTol; g.DrawListSharedData.SetCircleTessellationMaxError(g.Style.CircleTessellationMaxError); @@ -4465,10 +4798,10 @@ void ImGui::NewFrame() g.DrawListSharedData.InitialFlags |= ImDrawListFlags_AllowVtxOffset; // Mark rendering data as invalid to prevent user who may have a handle on it to use it. - for (int n = 0; n < g.Viewports.Size; n++) + for (ImGuiViewportP* viewport : g.Viewports) { - ImGuiViewportP* viewport = g.Viewports[n]; - viewport->DrawDataP.Clear(); + viewport->DrawData = NULL; + viewport->DrawDataP.Valid = false; } // Drag and drop keep the source ID alive so even if the source disappear our state is consistent @@ -4534,21 +4867,33 @@ void ImGui::NewFrame() } #endif + // Record when we have been stationary as this state is preserved while over same item. + // FIXME: The way this is expressed means user cannot alter HoverStationaryDelay during the frame to use varying values. + // To allow this we should store HoverItemMaxStationaryTime+ID and perform the >= check in IsItemHovered() function. + if (g.HoverItemDelayId != 0 && g.MouseStationaryTimer >= g.Style.HoverStationaryDelay) + g.HoverItemUnlockedStationaryId = g.HoverItemDelayId; + else if (g.HoverItemDelayId == 0) + g.HoverItemUnlockedStationaryId = 0; + if (g.HoveredWindow != NULL && g.MouseStationaryTimer >= g.Style.HoverStationaryDelay) + g.HoverWindowUnlockedStationaryId = g.HoveredWindow->ID; + else if (g.HoveredWindow == NULL) + g.HoverWindowUnlockedStationaryId = 0; + // Update hover delay for IsItemHovered() with delays and tooltips - g.HoverDelayIdPreviousFrame = g.HoverDelayId; - if (g.HoverDelayId != 0) + g.HoverItemDelayIdPreviousFrame = g.HoverItemDelayId; + if (g.HoverItemDelayId != 0) { - //if (g.IO.MouseDelta.x == 0.0f && g.IO.MouseDelta.y == 0.0f) // Need design/flags - g.HoverDelayTimer += g.IO.DeltaTime; - g.HoverDelayClearTimer = 0.0f; - g.HoverDelayId = 0; + g.HoverItemDelayTimer += g.IO.DeltaTime; + g.HoverItemDelayClearTimer = 0.0f; + g.HoverItemDelayId = 0; } - else if (g.HoverDelayTimer > 0.0f) + else if (g.HoverItemDelayTimer > 0.0f) { // This gives a little bit of leeway before clearing the hover timer, allowing mouse to cross gaps - g.HoverDelayClearTimer += g.IO.DeltaTime; - if (g.HoverDelayClearTimer >= ImMax(0.20f, g.IO.DeltaTime * 2.0f)) // ~6 frames at 30 Hz + allow for low framerate - g.HoverDelayTimer = g.HoverDelayClearTimer = 0.0f; // May want a decaying timer, in which case need to clamp at max first, based on max of caller last requested timer. + // We could expose 0.25f as style.HoverClearDelay but I am not sure of the logic yet, this is particularly subtle. + g.HoverItemDelayClearTimer += g.IO.DeltaTime; + if (g.HoverItemDelayClearTimer >= ImMax(0.25f, g.IO.DeltaTime * 2.0f)) // ~7 frames at 30 Hz + allow for low framerate + g.HoverItemDelayTimer = g.HoverItemDelayClearTimer = 0.0f; // May want a decaying timer, in which case need to clamp at max first, based on max of caller last requested timer. } // Drag and drop @@ -4577,6 +4922,10 @@ void ImGui::NewFrame() // Update mouse input state UpdateMouseInputs(); + // Undocking + // (needs to be before UpdateMouseMovingWindowNewFrame so the window is already offset and following the mouse on the detaching frame) + DockContextNewFrameUpdateUndocking(&g); + // Find hovered window // (needs to be before UpdateMouseMovingWindowNewFrame so we fill g.HoveredWindowUnderMovingWindow on the mouse release frame) UpdateHoveredWindowAndCaptureFlags(); @@ -4603,9 +4952,8 @@ void ImGui::NewFrame() // Mark all windows as not visible and compact unused memory. IM_ASSERT(g.WindowsFocusOrder.Size <= g.Windows.Size); const float memory_compact_start_time = (g.GcCompactAll || g.IO.ConfigMemoryCompactTimer < 0.0f) ? FLT_MAX : (float)g.Time - g.IO.ConfigMemoryCompactTimer; - for (int i = 0; i != g.Windows.Size; i++) + for (ImGuiWindow* window : g.Windows) { - ImGuiWindow* window = g.Windows[i]; window->WasActive = window->Active; window->Active = false; window->WriteAccessed = false; @@ -4621,9 +4969,9 @@ void ImGui::NewFrame() for (int i = 0; i < g.TablesLastTimeActive.Size; i++) if (g.TablesLastTimeActive[i] >= 0.0f && g.TablesLastTimeActive[i] < memory_compact_start_time) TableGcCompactTransientBuffers(g.Tables.GetByIndex(i)); - for (int i = 0; i < g.TablesTempData.Size; i++) - if (g.TablesTempData[i].LastTimeActive >= 0.0f && g.TablesTempData[i].LastTimeActive < memory_compact_start_time) - TableGcCompactTransientBuffers(&g.TablesTempData[i]); + for (ImGuiTableTempData& table_temp_data : g.TablesTempData) + if (table_temp_data.LastTimeActive >= 0.0f && table_temp_data.LastTimeActive < memory_compact_start_time) + TableGcCompactTransientBuffers(&table_temp_data); if (g.GcCompactAll) GcCompactTransientMiscBuffers(); g.GcCompactAll = false; @@ -4640,16 +4988,26 @@ void ImGui::NewFrame() g.ItemFlagsStack.push_back(ImGuiItemFlags_None); g.GroupStack.resize(0); + // Docking + DockContextNewFrameUpdateDocking(&g); + // [DEBUG] Update debug features +#ifndef IMGUI_DISABLE_DEBUG_TOOLS UpdateDebugToolItemPicker(); UpdateDebugToolStackQueries(); + UpdateDebugToolFlashStyleColor(); if (g.DebugLocateFrames > 0 && --g.DebugLocateFrames == 0) - g.DebugLocateId = 0; - if (g.DebugLogClipperAutoDisableFrames > 0 && --g.DebugLogClipperAutoDisableFrames == 0) { - DebugLog("(Auto-disabled ImGuiDebugLogFlags_EventClipper to avoid spamming)\n"); - g.DebugLogFlags &= ~ImGuiDebugLogFlags_EventClipper; + g.DebugLocateId = 0; + g.DebugBreakInLocateId = false; } + if (g.DebugLogAutoDisableFrames > 0 && --g.DebugLogAutoDisableFrames == 0) + { + DebugLog("(Debug Log: Auto-disabled some ImGuiDebugLogFlags after 2 frames)\n"); + g.DebugLogFlags &= ~g.DebugLogAutoDisableFlags; + g.DebugLogAutoDisableFlags = ImGuiDebugLogFlags_None; + } +#endif // Create implicit/fallback window - which we will only render it if the user has added something to it. // We don't use "Debug" to avoid colliding with user trying to create a "Debug" window with custom flags. @@ -4661,10 +5019,12 @@ void ImGui::NewFrame() // [DEBUG] When io.ConfigDebugBeginReturnValue is set, we make Begin()/BeginChild() return false at different level of the window-stack, // allowing to validate correct Begin/End behavior in user code. +#ifndef IMGUI_DISABLE_DEBUG_TOOLS if (g.IO.ConfigDebugBeginReturnValueLoop) g.DebugBeginReturnValueCullDepth = (g.DebugBeginReturnValueCullDepth == -1) ? 0 : ((g.DebugBeginReturnValueCullDepth + ((g.FrameCount % 4) == 0 ? 1 : 0)) % 10); else g.DebugBeginReturnValueCullDepth = -1; +#endif CallContextHooks(&g, ImGuiContextHookType_NewFramePost); } @@ -4697,53 +5057,18 @@ static void AddWindowToSortBuffer(ImVector* out_sorted_windows, Im } } -static void AddDrawListToDrawData(ImVector* out_list, ImDrawList* draw_list) -{ - if (draw_list->CmdBuffer.Size == 0) - return; - if (draw_list->CmdBuffer.Size == 1 && draw_list->CmdBuffer[0].ElemCount == 0 && draw_list->CmdBuffer[0].UserCallback == NULL) - return; - - // Draw list sanity check. Detect mismatch between PrimReserve() calls and incrementing _VtxCurrentIdx, _VtxWritePtr etc. - // May trigger for you if you are using PrimXXX functions incorrectly. - IM_ASSERT(draw_list->VtxBuffer.Size == 0 || draw_list->_VtxWritePtr == draw_list->VtxBuffer.Data + draw_list->VtxBuffer.Size); - IM_ASSERT(draw_list->IdxBuffer.Size == 0 || draw_list->_IdxWritePtr == draw_list->IdxBuffer.Data + draw_list->IdxBuffer.Size); - if (!(draw_list->Flags & ImDrawListFlags_AllowVtxOffset)) - IM_ASSERT((int)draw_list->_VtxCurrentIdx == draw_list->VtxBuffer.Size); - - // Check that draw_list doesn't use more vertices than indexable (default ImDrawIdx = unsigned short = 2 bytes = 64K vertices per ImDrawList = per window) - // If this assert triggers because you are drawing lots of stuff manually: - // - First, make sure you are coarse clipping yourself and not trying to draw many things outside visible bounds. - // Be mindful that the ImDrawList API doesn't filter vertices. Use the Metrics/Debugger window to inspect draw list contents. - // - If you want large meshes with more than 64K vertices, you can either: - // (A) Handle the ImDrawCmd::VtxOffset value in your renderer backend, and set 'io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset'. - // Most example backends already support this from 1.71. Pre-1.71 backends won't. - // Some graphics API such as GL ES 1/2 don't have a way to offset the starting vertex so it is not supported for them. - // (B) Or handle 32-bit indices in your renderer backend, and uncomment '#define ImDrawIdx unsigned int' line in imconfig.h. - // Most example backends already support this. For example, the OpenGL example code detect index size at compile-time: - // glDrawElements(GL_TRIANGLES, (GLsizei)pcmd->ElemCount, sizeof(ImDrawIdx) == 2 ? GL_UNSIGNED_SHORT : GL_UNSIGNED_INT, idx_buffer_offset); - // Your own engine or render API may use different parameters or function calls to specify index sizes. - // 2 and 4 bytes indices are generally supported by most graphics API. - // - If for some reason neither of those solutions works for you, a workaround is to call BeginChild()/EndChild() before reaching - // the 64K limit to split your draw commands in multiple draw lists. - if (sizeof(ImDrawIdx) == 2) - IM_ASSERT(draw_list->_VtxCurrentIdx < (1 << 16) && "Too many vertices in ImDrawList using 16-bit indices. Read comment above"); - - out_list->push_back(draw_list); -} - static void AddWindowToDrawData(ImGuiWindow* window, int layer) { ImGuiContext& g = *GImGui; - ImGuiViewportP* viewport = g.Viewports[0]; + ImGuiViewportP* viewport = window->Viewport; + IM_ASSERT(viewport != NULL); g.IO.MetricsRenderWindows++; - AddDrawListToDrawData(&viewport->DrawDataBuilder.Layers[layer], window->DrawList); - for (int i = 0; i < window->DC.ChildWindows.Size; i++) - { - ImGuiWindow* child = window->DC.ChildWindows[i]; + if (window->DrawList->_Splitter._Count > 1) + window->DrawList->ChannelsMerge(); // Merge if user forgot to merge back. Also required in Docking branch for ImGuiWindowFlags_DockNodeHost windows. + ImGui::AddDrawListToDrawDataEx(&viewport->DrawDataP, viewport->DrawDataBuilder.Layers[layer], window->DrawList); + for (ImGuiWindow* child : window->DC.ChildWindows) if (IsWindowActiveAndVisible(child)) // Clipped children may have been marked not active AddWindowToDrawData(child, layer); - } } static inline int GetWindowDisplayLayer(ImGuiWindow* window) @@ -4757,42 +5082,49 @@ static inline void AddRootWindowToDrawData(ImGuiWindow* window) AddWindowToDrawData(window, GetWindowDisplayLayer(window)); } -void ImDrawDataBuilder::FlattenIntoSingleLayer() +static void FlattenDrawDataIntoSingleLayer(ImDrawDataBuilder* builder) { - int n = Layers[0].Size; - int size = n; - for (int i = 1; i < IM_ARRAYSIZE(Layers); i++) - size += Layers[i].Size; - Layers[0].resize(size); - for (int layer_n = 1; layer_n < IM_ARRAYSIZE(Layers); layer_n++) + int n = builder->Layers[0]->Size; + int full_size = n; + for (int i = 1; i < IM_ARRAYSIZE(builder->Layers); i++) + full_size += builder->Layers[i]->Size; + builder->Layers[0]->resize(full_size); + for (int layer_n = 1; layer_n < IM_ARRAYSIZE(builder->Layers); layer_n++) { - ImVector& layer = Layers[layer_n]; - if (layer.empty()) + ImVector* layer = builder->Layers[layer_n]; + if (layer->empty()) continue; - memcpy(&Layers[0][n], &layer[0], layer.Size * sizeof(ImDrawList*)); - n += layer.Size; - layer.resize(0); + memcpy(builder->Layers[0]->Data + n, layer->Data, layer->Size * sizeof(ImDrawList*)); + n += layer->Size; + layer->resize(0); } } -static void SetupViewportDrawData(ImGuiViewportP* viewport, ImVector* draw_lists) +static void InitViewportDrawData(ImGuiViewportP* viewport) { ImGuiIO& io = ImGui::GetIO(); ImDrawData* draw_data = &viewport->DrawDataP; + + viewport->DrawData = draw_data; // Make publicly accessible + viewport->DrawDataBuilder.Layers[0] = &draw_data->CmdLists; + viewport->DrawDataBuilder.Layers[1] = &viewport->DrawDataBuilder.LayerData1; + viewport->DrawDataBuilder.Layers[0]->resize(0); + viewport->DrawDataBuilder.Layers[1]->resize(0); + + // When minimized, we report draw_data->DisplaySize as zero to be consistent with non-viewport mode, + // and to allow applications/backends to easily skip rendering. + // FIXME: Note that we however do NOT attempt to report "zero drawlist / vertices" into the ImDrawData structure. + // This is because the work has been done already, and its wasted! We should fix that and add optimizations for + // it earlier in the pipeline, rather than pretend to hide the data at the end of the pipeline. + const bool is_minimized = (viewport->Flags & ImGuiViewportFlags_IsMinimized) != 0; + draw_data->Valid = true; - draw_data->CmdLists = (draw_lists->Size > 0) ? draw_lists->Data : NULL; - draw_data->CmdListsCount = draw_lists->Size; + draw_data->CmdListsCount = 0; draw_data->TotalVtxCount = draw_data->TotalIdxCount = 0; draw_data->DisplayPos = viewport->Pos; - draw_data->DisplaySize = viewport->Size; - draw_data->FramebufferScale = io.DisplayFramebufferScale; - for (int n = 0; n < draw_lists->Size; n++) - { - ImDrawList* draw_list = draw_lists->Data[n]; - draw_list->_PopUnusedDrawCmd(); - draw_data->TotalVtxCount += draw_list->VtxBuffer.Size; - draw_data->TotalIdxCount += draw_list->IdxBuffer.Size; - } + draw_data->DisplaySize = is_minimized ? ImVec2(0.0f, 0.0f) : viewport->Size; + draw_data->FramebufferScale = io.DisplayFramebufferScale; // FIXME-VIEWPORT: This may vary on a per-monitor/viewport basis? + draw_data->OwnerViewport = viewport; } // Push a clipping rectangle for both ImGui logic (hit-testing etc.) and low-level ImDrawList rendering. @@ -4815,30 +5147,50 @@ void ImGui::PopClipRect() window->ClipRect = window->DrawList->_ClipRectStack.back(); } +static ImGuiWindow* FindFrontMostVisibleChildWindow(ImGuiWindow* window) +{ + for (int n = window->DC.ChildWindows.Size - 1; n >= 0; n--) + if (IsWindowActiveAndVisible(window->DC.ChildWindows[n])) + return FindFrontMostVisibleChildWindow(window->DC.ChildWindows[n]); + return window; +} + static void ImGui::RenderDimmedBackgroundBehindWindow(ImGuiWindow* window, ImU32 col) { if ((col & IM_COL32_A_MASK) == 0) return; - ImGuiViewportP* viewport = (ImGuiViewportP*)GetMainViewport(); + ImGuiViewportP* viewport = window->Viewport; ImRect viewport_rect = viewport->GetMainRect(); // Draw behind window by moving the draw command at the FRONT of the draw list { - // We've already called AddWindowToDrawData() which called DrawList->ChannelsMerge() on DockNodeHost windows, - // and draw list have been trimmed already, hence the explicit recreation of a draw command if missing. + // Draw list have been trimmed already, hence the explicit recreation of a draw command if missing. // FIXME: This is creating complication, might be simpler if we could inject a drawlist in drawdata at a given position and not attempt to manipulate ImDrawCmd order. - ImDrawList* draw_list = window->RootWindow->DrawList; + ImDrawList* draw_list = window->RootWindowDockTree->DrawList; + draw_list->ChannelsMerge(); if (draw_list->CmdBuffer.Size == 0) draw_list->AddDrawCmd(); - draw_list->PushClipRect(viewport_rect.Min - ImVec2(1, 1), viewport_rect.Max + ImVec2(1, 1), false); // Ensure ImDrawCmd are not merged + draw_list->PushClipRect(viewport_rect.Min - ImVec2(1, 1), viewport_rect.Max + ImVec2(1, 1), false); // FIXME: Need to stricty ensure ImDrawCmd are not merged (ElemCount==6 checks below will verify that) draw_list->AddRectFilled(viewport_rect.Min, viewport_rect.Max, col); ImDrawCmd cmd = draw_list->CmdBuffer.back(); IM_ASSERT(cmd.ElemCount == 6); draw_list->CmdBuffer.pop_back(); draw_list->CmdBuffer.push_front(cmd); - draw_list->PopClipRect(); draw_list->AddDrawCmd(); // We need to create a command as CmdBuffer.back().IdxOffset won't be correct if we append to same command. + draw_list->PopClipRect(); + } + + // Draw over sibling docking nodes in a same docking tree + if (window->RootWindow->DockIsActive) + { + ImDrawList* draw_list = FindFrontMostVisibleChildWindow(window->RootWindowDockTree)->DrawList; + draw_list->ChannelsMerge(); + if (draw_list->CmdBuffer.Size == 0) + draw_list->AddDrawCmd(); + draw_list->PushClipRect(viewport_rect.Min, viewport_rect.Max, false); + RenderRectFilledWithHole(draw_list, window->RootWindowDockTree->Rect(), window->RootWindow->Rect(), col, 0.0f);// window->RootWindowDockTree->WindowRounding); + draw_list->PopClipRect(); } } @@ -4859,6 +5211,8 @@ ImGuiWindow* ImGui::FindBottomMostVisibleWindowWithinBeginStack(ImGuiWindow* par return bottom_most_visible_window; } +// Important: AddWindowToDrawData() has not been called yet, meaning DockNodeHost windows needs a DrawList->ChannelsMerge() before usage. +// We call ChannelsMerge() lazily here at it is faster that doing a full iteration of g.Windows[] prior to calling RenderDimmedBackgrounds(). static void ImGui::RenderDimmedBackgrounds() { ImGuiContext& g = *GImGui; @@ -4870,31 +5224,50 @@ static void ImGui::RenderDimmedBackgrounds() if (!dim_bg_for_modal && !dim_bg_for_window_list) return; + ImGuiViewport* viewports_already_dimmed[2] = { NULL, NULL }; if (dim_bg_for_modal) { // Draw dimming behind modal or a begin stack child, whichever comes first in draw order. ImGuiWindow* dim_behind_window = FindBottomMostVisibleWindowWithinBeginStack(modal_window); - RenderDimmedBackgroundBehindWindow(dim_behind_window, GetColorU32(ImGuiCol_ModalWindowDimBg, g.DimBgRatio)); + RenderDimmedBackgroundBehindWindow(dim_behind_window, GetColorU32(modal_window->DC.ModalDimBgColor, g.DimBgRatio)); + viewports_already_dimmed[0] = modal_window->Viewport; } else if (dim_bg_for_window_list) { - // Draw dimming behind CTRL+Tab target window + // Draw dimming behind CTRL+Tab target window and behind CTRL+Tab UI window RenderDimmedBackgroundBehindWindow(g.NavWindowingTargetAnim, GetColorU32(ImGuiCol_NavWindowingDimBg, g.DimBgRatio)); + if (g.NavWindowingListWindow != NULL && g.NavWindowingListWindow->Viewport && g.NavWindowingListWindow->Viewport != g.NavWindowingTargetAnim->Viewport) + RenderDimmedBackgroundBehindWindow(g.NavWindowingListWindow, GetColorU32(ImGuiCol_NavWindowingDimBg, g.DimBgRatio)); + viewports_already_dimmed[0] = g.NavWindowingTargetAnim->Viewport; + viewports_already_dimmed[1] = g.NavWindowingListWindow ? g.NavWindowingListWindow->Viewport : NULL; // Draw border around CTRL+Tab target window ImGuiWindow* window = g.NavWindowingTargetAnim; - ImGuiViewport* viewport = GetMainViewport(); + ImGuiViewport* viewport = window->Viewport; float distance = g.FontSize; ImRect bb = window->Rect(); bb.Expand(distance); if (bb.GetWidth() >= viewport->Size.x && bb.GetHeight() >= viewport->Size.y) bb.Expand(-distance - 1.0f); // If a window fits the entire viewport, adjust its highlight inward + window->DrawList->ChannelsMerge(); if (window->DrawList->CmdBuffer.Size == 0) window->DrawList->AddDrawCmd(); window->DrawList->PushClipRect(viewport->Pos, viewport->Pos + viewport->Size); window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(ImGuiCol_NavWindowingHighlight, g.NavWindowingHighlightAlpha), window->WindowRounding, 0, 3.0f); window->DrawList->PopClipRect(); } + + // Draw dimming background on _other_ viewports than the ones our windows are in + for (ImGuiViewportP* viewport : g.Viewports) + { + if (viewport == viewports_already_dimmed[0] || viewport == viewports_already_dimmed[1]) + continue; + if (modal_window && viewport->Window && IsWindowAbove(viewport->Window, modal_window)) + continue; + ImDrawList* draw_list = GetForegroundDrawList(viewport); + const ImU32 dim_bg_col = GetColorU32(dim_bg_for_modal ? ImGuiCol_ModalWindowDimBg : ImGuiCol_NavWindowingDimBg, g.DimBgRatio); + draw_list->AddRectFilled(viewport->Pos, viewport->Pos + viewport->Size, dim_bg_col); + } } // This is normally called by Render(). You may want to call it directly if you want to avoid calling Render() but the gain will be very minimal. @@ -4916,20 +5289,11 @@ void ImGui::EndFrame() ImGuiPlatformImeData* ime_data = &g.PlatformImeData; if (g.IO.SetPlatformImeDataFn && memcmp(ime_data, &g.PlatformImeDataPrev, sizeof(ImGuiPlatformImeData)) != 0) { + ImGuiViewport* viewport = FindViewportByID(g.PlatformImeViewport); IMGUI_DEBUG_LOG_IO("[io] Calling io.SetPlatformImeDataFn(): WantVisible: %d, InputPos (%.2f,%.2f)\n", ime_data->WantVisible, ime_data->InputPos.x, ime_data->InputPos.y); - ImGuiViewport* viewport = GetMainViewport(); -#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS - if (viewport->PlatformHandleRaw == NULL && g.IO.ImeWindowHandle != NULL) - { - viewport->PlatformHandleRaw = g.IO.ImeWindowHandle; - g.IO.SetPlatformImeDataFn(viewport, ime_data); - viewport->PlatformHandleRaw = NULL; - } - else -#endif - { - g.IO.SetPlatformImeDataFn(viewport, ime_data); - } + if (viewport == NULL) + viewport = GetMainViewport(); + g.IO.SetPlatformImeDataFn(viewport, ime_data); } // Hide implicit/fallback "Debug" window if it hasn't been used @@ -4941,6 +5305,11 @@ void ImGui::EndFrame() // Update navigation: CTRL+Tab, wrap-around requests NavEndFrame(); + // Update docking + DockContextEndFrame(&g); + + SetCurrentViewport(NULL, NULL); + // Drag and Drop: Elapse payload (if delivered, or if source stops being submitted) if (g.DragDropActive) { @@ -4965,13 +5334,15 @@ void ImGui::EndFrame() // Initiate moving window + handle left-click and right-click focus UpdateMouseMovingWindowEndFrame(); + // Update user-facing viewport list (g.Viewports -> g.PlatformIO.Viewports after filtering out some) + UpdateViewportsEndFrame(); + // Sort the window list so that all child windows are after their parent // We cannot do that on FocusWindow() because children may not exist yet g.WindowsTempSortBuffer.resize(0); g.WindowsTempSortBuffer.reserve(g.Windows.Size); - for (int i = 0; i != g.Windows.Size; i++) + for (ImGuiWindow* window : g.Windows) { - ImGuiWindow* window = g.Windows[i]; if (window->Active && (window->Flags & ImGuiWindowFlags_ChildWindow)) // if a child is active its parent will add it continue; AddWindowToSortBuffer(&g.WindowsTempSortBuffer, window); @@ -4986,6 +5357,7 @@ void ImGui::EndFrame() g.IO.Fonts->Locked = false; // Clear Input data for next frame + g.IO.MousePosPrev = g.IO.MousePos; g.IO.AppFocusLost = false; g.IO.MouseWheel = g.IO.MouseWheelH = 0.0f; g.IO.InputQueueCharacters.resize(0); @@ -5003,32 +5375,30 @@ void ImGui::Render() if (g.FrameCountEnded != g.FrameCount) EndFrame(); - const bool first_render_of_frame = (g.FrameCountRendered != g.FrameCount); + if (g.FrameCountRendered == g.FrameCount) + return; g.FrameCountRendered = g.FrameCount; - g.IO.MetricsRenderWindows = 0; + g.IO.MetricsRenderWindows = 0; CallContextHooks(&g, ImGuiContextHookType_RenderPre); // Add background ImDrawList (for each active viewport) - for (int n = 0; n != g.Viewports.Size; n++) + for (ImGuiViewportP* viewport : g.Viewports) { - ImGuiViewportP* viewport = g.Viewports[n]; - viewport->DrawDataBuilder.Clear(); - if (viewport->DrawLists[0] != NULL) - AddDrawListToDrawData(&viewport->DrawDataBuilder.Layers[0], GetBackgroundDrawList(viewport)); + InitViewportDrawData(viewport); + if (viewport->BgFgDrawLists[0] != NULL) + AddDrawListToDrawDataEx(&viewport->DrawDataP, viewport->DrawDataBuilder.Layers[0], GetBackgroundDrawList(viewport)); } // Draw modal/window whitening backgrounds - if (first_render_of_frame) - RenderDimmedBackgrounds(); + RenderDimmedBackgrounds(); // Add ImDrawList to render ImGuiWindow* windows_to_render_top_most[2]; - windows_to_render_top_most[0] = (g.NavWindowingTarget && !(g.NavWindowingTarget->Flags & ImGuiWindowFlags_NoBringToFrontOnFocus)) ? g.NavWindowingTarget->RootWindow : NULL; + windows_to_render_top_most[0] = (g.NavWindowingTarget && !(g.NavWindowingTarget->Flags & ImGuiWindowFlags_NoBringToFrontOnFocus)) ? g.NavWindowingTarget->RootWindowDockTree : NULL; windows_to_render_top_most[1] = (g.NavWindowingTarget ? g.NavWindowingListWindow : NULL); - for (int n = 0; n != g.Windows.Size; n++) + for (ImGuiWindow* window : g.Windows) { - ImGuiWindow* window = g.Windows[n]; IM_MSVC_WARNING_SUPPRESS(6011); // Static Analysis false positive "warning C6011: Dereferencing NULL pointer 'window'" if (IsWindowActiveAndVisible(window) && (window->Flags & ImGuiWindowFlags_ChildWindow) == 0 && window != windows_to_render_top_most[0] && window != windows_to_render_top_most[1]) AddRootWindowToDrawData(window); @@ -5038,22 +5408,25 @@ void ImGui::Render() AddRootWindowToDrawData(windows_to_render_top_most[n]); // Draw software mouse cursor if requested by io.MouseDrawCursor flag - if (g.IO.MouseDrawCursor && first_render_of_frame && g.MouseCursor != ImGuiMouseCursor_None) + if (g.IO.MouseDrawCursor && g.MouseCursor != ImGuiMouseCursor_None) RenderMouseCursor(g.IO.MousePos, g.Style.MouseCursorScale, g.MouseCursor, IM_COL32_WHITE, IM_COL32_BLACK, IM_COL32(0, 0, 0, 48)); // Setup ImDrawData structures for end-user g.IO.MetricsRenderVertices = g.IO.MetricsRenderIndices = 0; - for (int n = 0; n < g.Viewports.Size; n++) + for (ImGuiViewportP* viewport : g.Viewports) { - ImGuiViewportP* viewport = g.Viewports[n]; - viewport->DrawDataBuilder.FlattenIntoSingleLayer(); + FlattenDrawDataIntoSingleLayer(&viewport->DrawDataBuilder); // Add foreground ImDrawList (for each active viewport) - if (viewport->DrawLists[1] != NULL) - AddDrawListToDrawData(&viewport->DrawDataBuilder.Layers[0], GetForegroundDrawList(viewport)); + if (viewport->BgFgDrawLists[1] != NULL) + AddDrawListToDrawDataEx(&viewport->DrawDataP, viewport->DrawDataBuilder.Layers[0], GetForegroundDrawList(viewport)); - SetupViewportDrawData(viewport, &viewport->DrawDataBuilder.Layers[0]); + // We call _PopUnusedDrawCmd() last thing, as RenderDimmedBackgrounds() rely on a valid command being there (especially in docking branch). ImDrawData* draw_data = &viewport->DrawDataP; + IM_ASSERT(draw_data->CmdLists.Size == draw_data->CmdListsCount); + for (ImDrawList* draw_list : draw_data->CmdLists) + draw_list->_PopUnusedDrawCmd(); + g.IO.MetricsRenderVertices += draw_data->TotalVtxCount; g.IO.MetricsRenderIndices += draw_data->TotalIdxCount; } @@ -5084,7 +5457,7 @@ ImVec2 ImGui::CalcTextSize(const char* text, const char* text_end, bool hide_tex // FIXME: Investigate using ceilf or e.g. // - https://git.musl-libc.org/cgit/musl/tree/src/math/ceilf.c // - https://embarkstudios.github.io/rust-gpu/api/src/libm/math/ceilf.rs.html - text_size.x = IM_FLOOR(text_size.x + 0.99999f); + text_size.x = IM_TRUNC(text_size.x + 0.99999f); return text_size; } @@ -5097,6 +5470,11 @@ static void FindHoveredWindow() { ImGuiContext& g = *GImGui; + // Special handling for the window being moved: Ignore the mouse viewport check (because it may reset/lose its viewport during the undocking frame) + ImGuiViewportP* moving_window_viewport = g.MovingWindow ? g.MovingWindow->Viewport : NULL; + if (g.MovingWindow) + g.MovingWindow->Viewport = g.MouseViewport; + ImGuiWindow* hovered_window = NULL; ImGuiWindow* hovered_window_ignoring_moving_window = NULL; if (g.MovingWindow && !(g.MovingWindow->Flags & ImGuiWindowFlags_NoMouseInputs)) @@ -5112,14 +5490,13 @@ static void FindHoveredWindow() continue; if (window->Flags & ImGuiWindowFlags_NoMouseInputs) continue; + IM_ASSERT(window->Viewport); + if (window->Viewport != g.MouseViewport) + continue; // Using the clipped AABB, a child window will typically be clipped by its parent (not always) - ImRect bb(window->OuterRectClipped); - if (window->Flags & (ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_AlwaysAutoResize)) - bb.Expand(padding_regular); - else - bb.Expand(padding_for_resize); - if (!bb.Contains(g.IO.MousePos)) + ImVec2 hit_padding = (window->Flags & (ImGuiWindowFlags_NoResize | ImGuiWindowFlags_AlwaysAutoResize)) ? padding_regular : padding_for_resize; + if (!window->OuterRectClipped.ContainsWithPad(g.IO.MousePos, hit_padding)) continue; // Support for one rectangular hole in any given window @@ -5135,7 +5512,7 @@ static void FindHoveredWindow() if (hovered_window == NULL) hovered_window = window; IM_MSVC_WARNING_SUPPRESS(28182); // [Static Analyzer] Dereferencing NULL pointer. - if (hovered_window_ignoring_moving_window == NULL && (!g.MovingWindow || window->RootWindow != g.MovingWindow->RootWindow)) + if (hovered_window_ignoring_moving_window == NULL && (!g.MovingWindow || window->RootWindowDockTree != g.MovingWindow->RootWindowDockTree)) hovered_window_ignoring_moving_window = window; if (hovered_window && hovered_window_ignoring_moving_window) break; @@ -5143,6 +5520,9 @@ static void FindHoveredWindow() g.HoveredWindow = hovered_window; g.HoveredWindowUnderMovingWindow = hovered_window_ignoring_moving_window; + + if (g.MovingWindow) + g.MovingWindow->Viewport = moving_window_viewport; } bool ImGui::IsItemActive() @@ -5182,6 +5562,13 @@ bool ImGui::IsItemFocused() ImGuiContext& g = *GImGui; if (g.NavId != g.LastItemData.ID || g.NavId == 0) return false; + + // Special handling for the dummy item after Begin() which represent the title bar or tab. + // When the window is collapsed (SkipItems==true) that last item will never be overwritten so we need to detect the case. + ImGuiWindow* window = g.CurrentWindow; + if (g.LastItemData.ID == window->ID && window->WriteAccessed) + return false; + return true; } @@ -5204,6 +5591,9 @@ bool ImGui::IsItemToggledSelection() return (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_ToggledSelection) ? true : false; } +// IMPORTANT: If you are trying to check whether your mouse should be dispatched to Dear ImGui or to your underlying app, +// you should not use this function! Use the 'io.WantCaptureMouse' boolean for that! +// Refer to FAQ entry "How can I tell whether to dispatch mouse/keyboard to Dear ImGui or my application?" for details. bool ImGui::IsAnyItemHovered() { ImGuiContext& g = *GImGui; @@ -5234,17 +5624,28 @@ bool ImGui::IsItemEdited() return (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Edited) != 0; } +// Allow next item to be overlapped by subsequent items. +// This works by requiring HoveredId to match for two subsequent frames, +// so if a following items overwrite it our interactions will naturally be disabled. +void ImGui::SetNextItemAllowOverlap() +{ + ImGuiContext& g = *GImGui; + g.NextItemData.ItemFlags |= ImGuiItemFlags_AllowOverlap; +} + +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS // Allow last item to be overlapped by a subsequent item. Both may be activated during the same frame before the later one takes priority. -// FIXME: Although this is exposed, its interaction and ideal idiom with using ImGuiButtonFlags_AllowItemOverlap flag are extremely confusing, need rework. +// FIXME-LEGACY: Use SetNextItemAllowOverlap() *before* your item instead. void ImGui::SetItemAllowOverlap() { ImGuiContext& g = *GImGui; ImGuiID id = g.LastItemData.ID; if (g.HoveredId == id) g.HoveredIdAllowOverlap = true; - if (g.ActiveId == id) + if (g.ActiveId == id) // Before we made this obsolete, most calls to SetItemAllowOverlap() used to avoid this path by testing g.ActiveId != id. g.ActiveIdAllowOverlap = true; } +#endif // FIXME: It might be undesirable that this will likely disable KeyOwner-aware shortcuts systems. Consider a more fine-tuned version for the two users of this function. void ImGui::SetActiveIdUsingAllKeyboardKeys() @@ -5280,40 +5681,105 @@ ImVec2 ImGui::GetItemRectSize() return g.LastItemData.Rect.GetSize(); } -bool ImGui::BeginChildEx(const char* name, ImGuiID id, const ImVec2& size_arg, bool border, ImGuiWindowFlags flags) +// Prior to v1.90 2023/10/16, the BeginChild() function took a 'bool border = false' parameter instead of 'ImGuiChildFlags child_flags = 0'. +// ImGuiChildFlags_Border is defined as always == 1 in order to allow old code passing 'true'. Read comments in imgui.h for details! +bool ImGui::BeginChild(const char* str_id, const ImVec2& size_arg, ImGuiChildFlags child_flags, ImGuiWindowFlags window_flags) +{ + ImGuiID id = GetCurrentWindow()->GetID(str_id); + return BeginChildEx(str_id, id, size_arg, child_flags, window_flags); +} + +bool ImGui::BeginChild(ImGuiID id, const ImVec2& size_arg, ImGuiChildFlags child_flags, ImGuiWindowFlags window_flags) +{ + return BeginChildEx(NULL, id, size_arg, child_flags, window_flags); +} + +bool ImGui::BeginChildEx(const char* name, ImGuiID id, const ImVec2& size_arg, ImGuiChildFlags child_flags, ImGuiWindowFlags window_flags) { ImGuiContext& g = *GImGui; ImGuiWindow* parent_window = g.CurrentWindow; + IM_ASSERT(id != 0); - flags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_ChildWindow; - flags |= (parent_window->Flags & ImGuiWindowFlags_NoMove); // Inherit the NoMove flag + // Sanity check as it is likely that some user will accidentally pass ImGuiWindowFlags into the ImGuiChildFlags argument. + const ImGuiChildFlags ImGuiChildFlags_SupportedMask_ = ImGuiChildFlags_Border | ImGuiChildFlags_AlwaysUseWindowPadding | ImGuiChildFlags_ResizeX | ImGuiChildFlags_ResizeY | ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysAutoResize | ImGuiChildFlags_FrameStyle; + IM_UNUSED(ImGuiChildFlags_SupportedMask_); + IM_ASSERT((child_flags & ~ImGuiChildFlags_SupportedMask_) == 0 && "Illegal ImGuiChildFlags value. Did you pass ImGuiWindowFlags values instead of ImGuiChildFlags?"); + IM_ASSERT((window_flags & ImGuiWindowFlags_AlwaysAutoResize) == 0 && "Cannot specify ImGuiWindowFlags_AlwaysAutoResize for BeginChild(). Use ImGuiChildFlags_AlwaysAutoResize!"); + if (child_flags & ImGuiChildFlags_AlwaysAutoResize) + { + IM_ASSERT((child_flags & (ImGuiChildFlags_ResizeX | ImGuiChildFlags_ResizeY)) == 0 && "Cannot use ImGuiChildFlags_ResizeX or ImGuiChildFlags_ResizeY with ImGuiChildFlags_AlwaysAutoResize!"); + IM_ASSERT((child_flags & (ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY)) != 0 && "Must use ImGuiChildFlags_AutoResizeX or ImGuiChildFlags_AutoResizeY with ImGuiChildFlags_AlwaysAutoResize!"); + } +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + if (window_flags & ImGuiWindowFlags_AlwaysUseWindowPadding) + child_flags |= ImGuiChildFlags_AlwaysUseWindowPadding; +#endif + if (child_flags & ImGuiChildFlags_AutoResizeX) + child_flags &= ~ImGuiChildFlags_ResizeX; + if (child_flags & ImGuiChildFlags_AutoResizeY) + child_flags &= ~ImGuiChildFlags_ResizeY; - // Size - const ImVec2 content_avail = GetContentRegionAvail(); - ImVec2 size = ImFloor(size_arg); - const int auto_fit_axises = ((size.x == 0.0f) ? (1 << ImGuiAxis_X) : 0x00) | ((size.y == 0.0f) ? (1 << ImGuiAxis_Y) : 0x00); - if (size.x <= 0.0f) - size.x = ImMax(content_avail.x + size.x, 4.0f); // Arbitrary minimum child size (0.0f causing too many issues) - if (size.y <= 0.0f) - size.y = ImMax(content_avail.y + size.y, 4.0f); + // Set window flags + window_flags |= ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoDocking; + window_flags |= (parent_window->Flags & ImGuiWindowFlags_NoMove); // Inherit the NoMove flag + if (child_flags & (ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysAutoResize)) + window_flags |= ImGuiWindowFlags_AlwaysAutoResize; + if ((child_flags & (ImGuiChildFlags_ResizeX | ImGuiChildFlags_ResizeY)) == 0) + window_flags |= ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings; + + // Special framed style + if (child_flags & ImGuiChildFlags_FrameStyle) + { + PushStyleColor(ImGuiCol_ChildBg, g.Style.Colors[ImGuiCol_FrameBg]); + PushStyleVar(ImGuiStyleVar_ChildRounding, g.Style.FrameRounding); + PushStyleVar(ImGuiStyleVar_ChildBorderSize, g.Style.FrameBorderSize); + PushStyleVar(ImGuiStyleVar_WindowPadding, g.Style.FramePadding); + child_flags |= ImGuiChildFlags_Border | ImGuiChildFlags_AlwaysUseWindowPadding; + window_flags |= ImGuiWindowFlags_NoMove; + } + + // Forward child flags + g.NextWindowData.Flags |= ImGuiNextWindowDataFlags_HasChildFlags; + g.NextWindowData.ChildFlags = child_flags; + + // Forward size + // Important: Begin() has special processing to switch condition to ImGuiCond_FirstUseEver for a given axis when ImGuiChildFlags_ResizeXXX is set. + // (the alternative would to store conditional flags per axis, which is possible but more code) + const ImVec2 size_avail = GetContentRegionAvail(); + const ImVec2 size_default((child_flags & ImGuiChildFlags_AutoResizeX) ? 0.0f : size_avail.x, (child_flags & ImGuiChildFlags_AutoResizeY) ? 0.0f : size_avail.y); + const ImVec2 size = CalcItemSize(size_arg, size_default.x, size_default.y); SetNextWindowSize(size); // Build up name. If you need to append to a same child from multiple location in the ID stack, use BeginChild(ImGuiID id) with a stable value. + // FIXME: 2023/11/14: commented out shorted version. We had an issue with multiple ### in child window path names, which the trailing hash helped workaround. + // e.g. "ParentName###ParentIdentifier/ChildName###ChildIdentifier" would get hashed incorrectly by ImHashStr(), trailing _%08X somehow fixes it. const char* temp_window_name; + /*if (name && parent_window->IDStack.back() == parent_window->ID) + ImFormatStringToTempBuffer(&temp_window_name, NULL, "%s/%s", parent_window->Name, name); // May omit ID if in root of ID stack + else*/ if (name) ImFormatStringToTempBuffer(&temp_window_name, NULL, "%s/%s_%08X", parent_window->Name, name, id); else ImFormatStringToTempBuffer(&temp_window_name, NULL, "%s/%08X", parent_window->Name, id); + // Set style const float backup_border_size = g.Style.ChildBorderSize; - if (!border) + if ((child_flags & ImGuiChildFlags_Border) == 0) g.Style.ChildBorderSize = 0.0f; - bool ret = Begin(temp_window_name, NULL, flags); + + // Begin into window + const bool ret = Begin(temp_window_name, NULL, window_flags); + + // Restore style g.Style.ChildBorderSize = backup_border_size; + if (child_flags & ImGuiChildFlags_FrameStyle) + { + PopStyleVar(3); + PopStyleColor(); + } ImGuiWindow* child_window = g.CurrentWindow; child_window->ChildId = id; - child_window->AutoFitChildAxises = (ImS8)auto_fit_axises; // Set the cursor to handle case where the user called SetNextWindowPos()+BeginChild() manually. // While this is not really documented/defined, it seems that the expected thing to do. @@ -5321,11 +5787,11 @@ bool ImGui::BeginChildEx(const char* name, ImGuiID id, const ImVec2& size_arg, b parent_window->DC.CursorPos = child_window->Pos; // Process navigation-in immediately so NavInit can run on first frame - // Can enter a child if (A) it has navigatable items or (B) it can be scrolled. - const ImGuiID temp_id_for_activation = (id + 1); + // Can enter a child if (A) it has navigable items or (B) it can be scrolled. + const ImGuiID temp_id_for_activation = ImHashStr("##Child", 0, id); if (g.ActiveId == temp_id_for_activation) ClearActiveID(); - if (g.NavActivateId == id && !(flags & ImGuiWindowFlags_NavFlattened) && (child_window->DC.NavLayersActiveMask != 0 || child_window->DC.NavWindowHasScrollY)) + if (g.NavActivateId == id && !(window_flags & ImGuiWindowFlags_NavFlattened) && (child_window->DC.NavLayersActiveMask != 0 || child_window->DC.NavWindowHasScrollY)) { FocusWindow(child_window); NavInitWindow(child_window, false); @@ -5335,51 +5801,30 @@ bool ImGui::BeginChildEx(const char* name, ImGuiID id, const ImVec2& size_arg, b return ret; } -bool ImGui::BeginChild(const char* str_id, const ImVec2& size_arg, bool border, ImGuiWindowFlags extra_flags) -{ - ImGuiWindow* window = GetCurrentWindow(); - return BeginChildEx(str_id, window->GetID(str_id), size_arg, border, extra_flags); -} - -bool ImGui::BeginChild(ImGuiID id, const ImVec2& size_arg, bool border, ImGuiWindowFlags extra_flags) -{ - IM_ASSERT(id != 0); - return BeginChildEx(NULL, id, size_arg, border, extra_flags); -} - void ImGui::EndChild() { ImGuiContext& g = *GImGui; - ImGuiWindow* window = g.CurrentWindow; + ImGuiWindow* child_window = g.CurrentWindow; IM_ASSERT(g.WithinEndChild == false); - IM_ASSERT(window->Flags & ImGuiWindowFlags_ChildWindow); // Mismatched BeginChild()/EndChild() calls + IM_ASSERT(child_window->Flags & ImGuiWindowFlags_ChildWindow); // Mismatched BeginChild()/EndChild() calls g.WithinEndChild = true; - if (window->BeginCount > 1) + ImVec2 child_size = child_window->Size; + End(); + if (child_window->BeginCount == 1) { - End(); - } - else - { - ImVec2 sz = window->Size; - if (window->AutoFitChildAxises & (1 << ImGuiAxis_X)) // Arbitrary minimum zero-ish child size of 4.0f causes less trouble than a 0.0f - sz.x = ImMax(4.0f, sz.x); - if (window->AutoFitChildAxises & (1 << ImGuiAxis_Y)) - sz.y = ImMax(4.0f, sz.y); - End(); - ImGuiWindow* parent_window = g.CurrentWindow; - ImRect bb(parent_window->DC.CursorPos, parent_window->DC.CursorPos + sz); - ItemSize(sz); - if ((window->DC.NavLayersActiveMask != 0 || window->DC.NavWindowHasScrollY) && !(window->Flags & ImGuiWindowFlags_NavFlattened)) + ImRect bb(parent_window->DC.CursorPos, parent_window->DC.CursorPos + child_size); + ItemSize(child_size); + if ((child_window->DC.NavLayersActiveMask != 0 || child_window->DC.NavWindowHasScrollY) && !(child_window->Flags & ImGuiWindowFlags_NavFlattened)) { - ItemAdd(bb, window->ChildId); - RenderNavHighlight(bb, window->ChildId); + ItemAdd(bb, child_window->ChildId); + RenderNavHighlight(bb, child_window->ChildId); // When browsing a window that has no activable items (scroll only) we keep a highlight on the child (pass g.NavId to trick into always displaying) - if (window->DC.NavLayersActiveMask == 0 && window == g.NavWindow) - RenderNavHighlight(ImRect(bb.Min - ImVec2(2, 2), bb.Max + ImVec2(2, 2)), g.NavId, ImGuiNavHighlightFlags_TypeThin); + if (child_window->DC.NavLayersActiveMask == 0 && child_window == g.NavWindow) + RenderNavHighlight(ImRect(bb.Min - ImVec2(2, 2), bb.Max + ImVec2(2, 2)), g.NavId, ImGuiNavHighlightFlags_Compact); } else { @@ -5387,41 +5832,22 @@ void ImGui::EndChild() ItemAdd(bb, 0); // But when flattened we directly reach items, adjust active layer mask accordingly - if (window->Flags & ImGuiWindowFlags_NavFlattened) - parent_window->DC.NavLayersActiveMaskNext |= window->DC.NavLayersActiveMaskNext; + if (child_window->Flags & ImGuiWindowFlags_NavFlattened) + parent_window->DC.NavLayersActiveMaskNext |= child_window->DC.NavLayersActiveMaskNext; } - if (g.HoveredWindow == window) + if (g.HoveredWindow == child_window) g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredWindow; } g.WithinEndChild = false; g.LogLinePosY = -FLT_MAX; // To enforce a carriage return } -// Helper to create a child window / scrolling region that looks like a normal widget frame. -bool ImGui::BeginChildFrame(ImGuiID id, const ImVec2& size, ImGuiWindowFlags extra_flags) -{ - ImGuiContext& g = *GImGui; - const ImGuiStyle& style = g.Style; - PushStyleColor(ImGuiCol_ChildBg, style.Colors[ImGuiCol_FrameBg]); - PushStyleVar(ImGuiStyleVar_ChildRounding, style.FrameRounding); - PushStyleVar(ImGuiStyleVar_ChildBorderSize, style.FrameBorderSize); - PushStyleVar(ImGuiStyleVar_WindowPadding, style.FramePadding); - bool ret = BeginChild(id, size, true, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysUseWindowPadding | extra_flags); - PopStyleVar(3); - PopStyleColor(); - return ret; -} - -void ImGui::EndChildFrame() -{ - EndChild(); -} - static void SetWindowConditionAllowFlags(ImGuiWindow* window, ImGuiCond flags, bool enabled) { window->SetWindowPosAllowFlags = enabled ? (window->SetWindowPosAllowFlags | flags) : (window->SetWindowPosAllowFlags & ~flags); window->SetWindowSizeAllowFlags = enabled ? (window->SetWindowSizeAllowFlags | flags) : (window->SetWindowSizeAllowFlags & ~flags); window->SetWindowCollapsedAllowFlags = enabled ? (window->SetWindowCollapsedAllowFlags | flags) : (window->SetWindowCollapsedAllowFlags & ~flags); + window->SetWindowDockAllowFlags = enabled ? (window->SetWindowDockAllowFlags | flags) : (window->SetWindowDockAllowFlags & ~flags); } ImGuiWindow* ImGui::FindWindowByID(ImGuiID id) @@ -5438,10 +5864,19 @@ ImGuiWindow* ImGui::FindWindowByName(const char* name) static void ApplyWindowSettings(ImGuiWindow* window, ImGuiWindowSettings* settings) { - window->Pos = ImFloor(ImVec2(settings->Pos.x, settings->Pos.y)); + const ImGuiViewport* main_viewport = ImGui::GetMainViewport(); + window->ViewportPos = main_viewport->Pos; + if (settings->ViewportId) + { + window->ViewportId = settings->ViewportId; + window->ViewportPos = ImVec2(settings->ViewportPos.x, settings->ViewportPos.y); + } + window->Pos = ImTrunc(ImVec2(settings->Pos.x + window->ViewportPos.x, settings->Pos.y + window->ViewportPos.y)); if (settings->Size.x > 0 && settings->Size.y > 0) - window->Size = window->SizeFull = ImFloor(ImVec2(settings->Size.x, settings->Size.y)); + window->Size = window->SizeFull = ImTrunc(ImVec2(settings->Size.x, settings->Size.y)); window->Collapsed = settings->Collapsed; + window->DockId = settings->DockId; + window->DockOrder = settings->DockOrder; } static void UpdateWindowInFocusOrderList(ImGuiWindow* window, bool just_created, ImGuiWindowFlags new_flags) @@ -5473,7 +5908,9 @@ static void InitOrLoadWindowSettings(ImGuiWindow* window, ImGuiWindowSettings* s // Use SetNextWindowPos() with the appropriate condition flag to change the initial position of a window. const ImGuiViewport* main_viewport = ImGui::GetMainViewport(); window->Pos = main_viewport->Pos + ImVec2(60, 60); - window->SetWindowPosAllowFlags = window->SetWindowSizeAllowFlags = window->SetWindowCollapsedAllowFlags = ImGuiCond_Always | ImGuiCond_Once | ImGuiCond_FirstUseEver | ImGuiCond_Appearing; + window->Size = window->SizeFull = ImVec2(0, 0); + window->ViewportPos = main_viewport->Pos; + window->SetWindowPosAllowFlags = window->SetWindowSizeAllowFlags = window->SetWindowCollapsedAllowFlags = window->SetWindowDockAllowFlags = ImGuiCond_Always | ImGuiCond_Once | ImGuiCond_FirstUseEver | ImGuiCond_Appearing; if (settings != NULL) { @@ -5521,13 +5958,47 @@ static ImGuiWindow* CreateNewWindow(const char* name, ImGuiWindowFlags flags) return window; } +static ImGuiWindow* GetWindowForTitleDisplay(ImGuiWindow* window) +{ + return window->DockNodeAsHost ? window->DockNodeAsHost->VisibleWindow : window; +} + +static ImGuiWindow* GetWindowForTitleAndMenuHeight(ImGuiWindow* window) +{ + return (window->DockNodeAsHost && window->DockNodeAsHost->VisibleWindow) ? window->DockNodeAsHost->VisibleWindow : window; +} + +static inline ImVec2 CalcWindowMinSize(ImGuiWindow* window) +{ + // We give windows non-zero minimum size to facilitate understanding problematic cases (e.g. empty popups) + // FIXME: Essentially we want to restrict manual resizing to WindowMinSize+Decoration, and allow api resizing to be smaller. + // Perhaps should tend further a neater test for this. + ImGuiContext& g = *GImGui; + ImVec2 size_min; + if ((window->Flags & ImGuiWindowFlags_ChildWindow) && !(window->Flags & ImGuiWindowFlags_Popup)) + { + size_min.x = (window->ChildFlags & ImGuiChildFlags_ResizeX) ? g.Style.WindowMinSize.x : 4.0f; + size_min.y = (window->ChildFlags & ImGuiChildFlags_ResizeY) ? g.Style.WindowMinSize.y : 4.0f; + } + else + { + size_min.x = ((window->Flags & ImGuiWindowFlags_AlwaysAutoResize) == 0) ? g.Style.WindowMinSize.x : 4.0f; + size_min.y = ((window->Flags & ImGuiWindowFlags_AlwaysAutoResize) == 0) ? g.Style.WindowMinSize.y : 4.0f; + } + + // Reduce artifacts with very small windows + ImGuiWindow* window_for_height = GetWindowForTitleAndMenuHeight(window); + size_min.y = ImMax(size_min.y, window_for_height->TitleBarHeight() + window_for_height->MenuBarHeight() + ImMax(0.0f, g.Style.WindowRounding - 1.0f)); + return size_min; +} + static ImVec2 CalcWindowSizeAfterConstraint(ImGuiWindow* window, const ImVec2& size_desired) { ImGuiContext& g = *GImGui; ImVec2 new_size = size_desired; if (g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasSizeConstraint) { - // Using -1,-1 on either X/Y axis to preserve the current size. + // See comments in SetNextWindowSizeConstraints() for details about setting size_min an size_max. ImRect cr = g.NextWindowData.SizeConstraintRect; new_size.x = (cr.Min.x >= 0 && cr.Max.x >= 0) ? ImClamp(new_size.x, cr.Min.x, cr.Max.x) : window->SizeFull.x; new_size.y = (cr.Min.y >= 0 && cr.Max.y >= 0) ? ImClamp(new_size.y, cr.Min.y, cr.Max.y) : window->SizeFull.y; @@ -5541,19 +6012,13 @@ static ImVec2 CalcWindowSizeAfterConstraint(ImGuiWindow* window, const ImVec2& s g.NextWindowData.SizeCallback(&data); new_size = data.DesiredSize; } - new_size.x = IM_FLOOR(new_size.x); - new_size.y = IM_FLOOR(new_size.y); + new_size.x = IM_TRUNC(new_size.x); + new_size.y = IM_TRUNC(new_size.y); } // Minimum size - if (!(window->Flags & (ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_AlwaysAutoResize))) - { - ImGuiWindow* window_for_height = window; - new_size = ImMax(new_size, g.Style.WindowMinSize); - const float minimum_height = window_for_height->TitleBarHeight() + window_for_height->MenuBarHeight() + ImMax(0.0f, g.Style.WindowRounding - 1.0f); - new_size.y = ImMax(new_size.y, minimum_height); // Reduce artifacts with very small windows - } - return new_size; + ImVec2 size_min = CalcWindowMinSize(window); + return ImMax(new_size, size_min); } static void CalcWindowContentSizes(ImGuiWindow* window, ImVec2* content_size_current, ImVec2* content_size_ideal) @@ -5570,10 +6035,10 @@ static void CalcWindowContentSizes(ImGuiWindow* window, ImVec2* content_size_cur return; } - content_size_current->x = (window->ContentSizeExplicit.x != 0.0f) ? window->ContentSizeExplicit.x : IM_FLOOR(window->DC.CursorMaxPos.x - window->DC.CursorStartPos.x); - content_size_current->y = (window->ContentSizeExplicit.y != 0.0f) ? window->ContentSizeExplicit.y : IM_FLOOR(window->DC.CursorMaxPos.y - window->DC.CursorStartPos.y); - content_size_ideal->x = (window->ContentSizeExplicit.x != 0.0f) ? window->ContentSizeExplicit.x : IM_FLOOR(ImMax(window->DC.CursorMaxPos.x, window->DC.IdealMaxPos.x) - window->DC.CursorStartPos.x); - content_size_ideal->y = (window->ContentSizeExplicit.y != 0.0f) ? window->ContentSizeExplicit.y : IM_FLOOR(ImMax(window->DC.CursorMaxPos.y, window->DC.IdealMaxPos.y) - window->DC.CursorStartPos.y); + content_size_current->x = (window->ContentSizeExplicit.x != 0.0f) ? window->ContentSizeExplicit.x : IM_TRUNC(window->DC.CursorMaxPos.x - window->DC.CursorStartPos.x); + content_size_current->y = (window->ContentSizeExplicit.y != 0.0f) ? window->ContentSizeExplicit.y : IM_TRUNC(window->DC.CursorMaxPos.y - window->DC.CursorStartPos.y); + content_size_ideal->x = (window->ContentSizeExplicit.x != 0.0f) ? window->ContentSizeExplicit.x : IM_TRUNC(ImMax(window->DC.CursorMaxPos.x, window->DC.IdealMaxPos.x) - window->DC.CursorStartPos.x); + content_size_ideal->y = (window->ContentSizeExplicit.y != 0.0f) ? window->ContentSizeExplicit.y : IM_TRUNC(ImMax(window->DC.CursorMaxPos.y, window->DC.IdealMaxPos.y) - window->DC.CursorStartPos.y); } static ImVec2 CalcWindowAutoFitSize(ImGuiWindow* window, const ImVec2& size_contents) @@ -5592,14 +6057,12 @@ static ImVec2 CalcWindowAutoFitSize(ImGuiWindow* window, const ImVec2& size_cont else { // Maximum window size is determined by the viewport size or monitor size - const bool is_popup = (window->Flags & ImGuiWindowFlags_Popup) != 0; - const bool is_menu = (window->Flags & ImGuiWindowFlags_ChildMenu) != 0; - ImVec2 size_min = style.WindowMinSize; - if (is_popup || is_menu) // Popups and menus bypass style.WindowMinSize by default, but we give then a non-zero minimum size to facilitate understanding problematic cases (e.g. empty popups) - size_min = ImMin(size_min, ImVec2(4.0f, 4.0f)); - - ImVec2 avail_size = ImGui::GetMainViewport()->WorkSize; - ImVec2 size_auto_fit = ImClamp(size_desired, size_min, ImMax(size_min, avail_size - style.DisplaySafeAreaPadding * 2.0f)); + ImVec2 size_min = CalcWindowMinSize(window); + ImVec2 size_max = (window->ViewportOwned || ((window->Flags & ImGuiWindowFlags_ChildWindow) && !(window->Flags & ImGuiWindowFlags_Popup))) ? ImVec2(FLT_MAX, FLT_MAX) : ImGui::GetMainViewport()->WorkSize - style.DisplaySafeAreaPadding * 2.0f; + const int monitor_idx = window->ViewportAllowPlatformMonitorExtend; + if (monitor_idx >= 0 && monitor_idx < g.PlatformIO.Monitors.Size && (window->Flags & ImGuiWindowFlags_ChildWindow) == 0) + size_max = g.PlatformIO.Monitors[monitor_idx].WorkSize - style.DisplaySafeAreaPadding * 2.0f; + ImVec2 size_auto_fit = ImClamp(size_desired, size_min, ImMax(size_min, size_max)); // When the window cannot fit all contents (either because of constraints, either because screen is too small), // we are growing the size on the other axis to compensate for expected scrollbar. FIXME: Might turn bigger than ViewportSize-WindowPadding. @@ -5628,7 +6091,7 @@ static ImGuiCol GetWindowBgColorIdx(ImGuiWindow* window) { if (window->Flags & (ImGuiWindowFlags_Tooltip | ImGuiWindowFlags_Popup)) return ImGuiCol_PopupBg; - if (window->Flags & ImGuiWindowFlags_ChildWindow) + if ((window->Flags & ImGuiWindowFlags_ChildWindow) && !window->DockIsActive) return ImGuiCol_ChildBg; return ImGuiCol_WindowBg; } @@ -5647,7 +6110,7 @@ static void CalcResizePosSizeFromAnyCorner(ImGuiWindow* window, const ImVec2& co *out_size = size_constrained; } -// Data for resizing from corner +// Data for resizing from resize grip / corner struct ImGuiResizeGripDef { ImVec2 CornerPosN; @@ -5665,9 +6128,9 @@ static const ImGuiResizeGripDef resize_grip_def[4] = // Data for resizing from borders struct ImGuiResizeBorderDef { - ImVec2 InnerDir; - ImVec2 SegmentN1, SegmentN2; - float OuterAngle; + ImVec2 InnerDir; // Normal toward inside + ImVec2 SegmentN1, SegmentN2; // End positions, normalized (0,0: upper left) + float OuterAngle; // Angle toward outside }; static const ImGuiResizeBorderDef resize_border_def[4] = { @@ -5694,7 +6157,7 @@ static ImRect GetResizeBorderRect(ImGuiWindow* window, int border_n, float perp_ ImGuiID ImGui::GetWindowResizeCornerID(ImGuiWindow* window, int n) { IM_ASSERT(n >= 0 && n < 4); - ImGuiID id = window->ID; + ImGuiID id = window->DockIsActive ? window->DockNode->HostWindow->ID : window->ID; id = ImHashStr("#RESIZE", 0, id); id = ImHashData(&n, sizeof(int), id); return id; @@ -5705,7 +6168,7 @@ ImGuiID ImGui::GetWindowResizeBorderID(ImGuiWindow* window, ImGuiDir dir) { IM_ASSERT(dir >= 0 && dir < 4); int n = (int)dir + 4; - ImGuiID id = window->ID; + ImGuiID id = window->DockIsActive ? window->DockNode->HostWindow->ID : window->ID; id = ImHashStr("#RESIZE", 0, id); id = ImHashData(&n, sizeof(int), id); return id; @@ -5713,7 +6176,7 @@ ImGuiID ImGui::GetWindowResizeBorderID(ImGuiWindow* window, ImGuiDir dir) // Handle resize for: Resize Grips, Borders, Gamepad // Return true when using auto-fit (double-click on resize grip) -static bool ImGui::UpdateWindowManualResize(ImGuiWindow* window, const ImVec2& size_auto_fit, int* border_held, int resize_grip_count, ImU32 resize_grip_col[4], const ImRect& visibility_rect) +static int ImGui::UpdateWindowManualResize(ImGuiWindow* window, const ImVec2& size_auto_fit, int* border_hovered, int* border_held, int resize_grip_count, ImU32 resize_grip_col[4], const ImRect& visibility_rect) { ImGuiContext& g = *GImGui; ImGuiWindowFlags flags = window->Flags; @@ -5723,10 +6186,9 @@ static bool ImGui::UpdateWindowManualResize(ImGuiWindow* window, const ImVec2& s if (window->WasActive == false) // Early out to avoid running this code for e.g. a hidden implicit/fallback Debug window. return false; - bool ret_auto_fit = false; - const int resize_border_count = g.IO.ConfigWindowsResizeFromEdges ? 4 : 0; - const float grip_draw_size = IM_FLOOR(ImMax(g.FontSize * 1.35f, window->WindowRounding + 1.0f + g.FontSize * 0.2f)); - const float grip_hover_inner_size = IM_FLOOR(grip_draw_size * 0.75f); + int ret_auto_fit_mask = 0x00; + const float grip_draw_size = IM_TRUNC(ImMax(g.FontSize * 1.35f, window->WindowRounding + 1.0f + g.FontSize * 0.2f)); + const float grip_hover_inner_size = IM_TRUNC(grip_draw_size * 0.75f); const float grip_hover_outer_size = g.IO.ConfigWindowsResizeFromEdges ? WINDOWS_HOVER_PADDING : 0.0f; ImRect clamp_rect = visibility_rect; @@ -5737,6 +6199,16 @@ static bool ImGui::UpdateWindowManualResize(ImGuiWindow* window, const ImVec2& s ImVec2 pos_target(FLT_MAX, FLT_MAX); ImVec2 size_target(FLT_MAX, FLT_MAX); + // Clip mouse interaction rectangles within the viewport rectangle (in practice the narrowing is going to happen most of the time). + // - Not narrowing would mostly benefit the situation where OS windows _without_ decoration have a threshold for hovering when outside their limits. + // This is however not the case with current backends under Win32, but a custom borderless window implementation would benefit from it. + // - When decoration are enabled we typically benefit from that distance, but then our resize elements would be conflicting with OS resize elements, so we also narrow. + // - Note that we are unable to tell if the platform setup allows hovering with a distance threshold (on Win32, decorated window have such threshold). + // We only clip interaction so we overwrite window->ClipRect, cannot call PushClipRect() yet as DrawList is not yet setup. + const bool clip_with_viewport_rect = !(g.IO.BackendFlags & ImGuiBackendFlags_HasMouseHoveredViewport) || (g.IO.MouseHoveredViewport != window->ViewportId) || !(window->Viewport->Flags & ImGuiViewportFlags_NoDecoration); + if (clip_with_viewport_rect) + window->ClipRect = window->Viewport->GetMainRect(); + // Resize grips and borders are on layer 1 window->DC.NavLayerCurrent = ImGuiNavLayer_Menu; @@ -5759,11 +6231,11 @@ static bool ImGui::UpdateWindowManualResize(ImGuiWindow* window, const ImVec2& s if (hovered || held) g.MouseCursor = (resize_grip_n & 1) ? ImGuiMouseCursor_ResizeNESW : ImGuiMouseCursor_ResizeNWSE; - if (held && g.IO.MouseClickedCount[0] == 2 && resize_grip_n == 0) + if (held && g.IO.MouseDoubleClicked[0]) { - // Manual auto-fit when double-clicking + // Auto-fit when double-clicking size_target = CalcWindowSizeAfterConstraint(window, size_auto_fit); - ret_auto_fit = true; + ret_auto_fit_mask = 0x03; // Both axises ClearActiveID(); } else if (held) @@ -5781,8 +6253,16 @@ static bool ImGui::UpdateWindowManualResize(ImGuiWindow* window, const ImVec2& s if (resize_grip_n == 0 || held || hovered) resize_grip_col[resize_grip_n] = GetColorU32(held ? ImGuiCol_ResizeGripActive : hovered ? ImGuiCol_ResizeGripHovered : ImGuiCol_ResizeGrip); } - for (int border_n = 0; border_n < resize_border_count; border_n++) + + int resize_border_mask = 0x00; + if (window->Flags & ImGuiWindowFlags_ChildWindow) + resize_border_mask |= ((window->ChildFlags & ImGuiChildFlags_ResizeX) ? 0x02 : 0) | ((window->ChildFlags & ImGuiChildFlags_ResizeY) ? 0x08 : 0); + else + resize_border_mask = g.IO.ConfigWindowsResizeFromEdges ? 0x0F : 0x00; + for (int border_n = 0; border_n < 4; border_n++) { + if ((resize_border_mask & (1 << border_n)) == 0) + continue; const ImGuiResizeBorderDef& def = resize_border_def[border_n]; const ImGuiAxis axis = (border_n == ImGuiDir_Left || border_n == ImGuiDir_Right) ? ImGuiAxis_X : ImGuiAxis_Y; @@ -5791,22 +6271,73 @@ static bool ImGui::UpdateWindowManualResize(ImGuiWindow* window, const ImVec2& s ImGuiID border_id = window->GetID(border_n + 4); // == GetWindowResizeBorderID() ItemAdd(border_rect, border_id, NULL, ImGuiItemFlags_NoNav); ButtonBehavior(border_rect, border_id, &hovered, &held, ImGuiButtonFlags_FlattenChildren | ImGuiButtonFlags_NoNavFocus); - //GetForegroundDrawLists(window)->AddRect(border_rect.Min, border_rect.Max, IM_COL32(255, 255, 0, 255)); - if ((hovered && g.HoveredIdTimer > WINDOWS_RESIZE_FROM_EDGES_FEEDBACK_TIMER) || held) - { + //GetForegroundDrawList(window)->AddRect(border_rect.Min, border_rect.Max, IM_COL32(255, 255, 0, 255)); + if (hovered && g.HoveredIdTimer <= WINDOWS_RESIZE_FROM_EDGES_FEEDBACK_TIMER) + hovered = false; + if (hovered || held) g.MouseCursor = (axis == ImGuiAxis_X) ? ImGuiMouseCursor_ResizeEW : ImGuiMouseCursor_ResizeNS; - if (held) - *border_held = border_n; - } - if (held) + if (held && g.IO.MouseDoubleClicked[0]) { + // Double-clicking bottom or right border auto-fit on this axis + // FIXME: Support top and right borders: rework CalcResizePosSizeFromAnyCorner() to be reusable in both cases. + if (border_n == 1 || border_n == 3) // Right and bottom border + { + size_target[axis] = CalcWindowSizeAfterConstraint(window, size_auto_fit)[axis]; + ret_auto_fit_mask |= (1 << axis); + hovered = held = false; // So border doesn't show highlighted at new position + } + ClearActiveID(); + } + else if (held) + { + // Switch to relative resizing mode when border geometry moved (e.g. resizing a child altering parent scroll), in order to avoid resizing feedback loop. + // Currently only using relative mode on resizable child windows, as the problem to solve is more likely noticeable for them, but could apply for all windows eventually. + // FIXME: May want to generalize this idiom at lower-level, so more widgets can use it! + const bool just_scrolled_manually_while_resizing = (g.WheelingWindow != NULL && g.WheelingWindowScrolledFrame == g.FrameCount && IsWindowChildOf(window, g.WheelingWindow, false, true)); + if (g.ActiveIdIsJustActivated || just_scrolled_manually_while_resizing) + { + g.WindowResizeBorderExpectedRect = border_rect; + g.WindowResizeRelativeMode = false; + } + if ((window->Flags & ImGuiWindowFlags_ChildWindow) && memcmp(&g.WindowResizeBorderExpectedRect, &border_rect, sizeof(ImRect)) != 0) + g.WindowResizeRelativeMode = true; + + const ImVec2 border_curr = (window->Pos + ImMin(def.SegmentN1, def.SegmentN2) * window->Size); + const float border_target_rel_mode_for_axis = border_curr[axis] + g.IO.MouseDelta[axis]; + const float border_target_abs_mode_for_axis = g.IO.MousePos[axis] - g.ActiveIdClickOffset[axis] + WINDOWS_HOVER_PADDING; // Match ButtonBehavior() padding above. + + // Use absolute mode position + ImVec2 border_target = window->Pos; + border_target[axis] = border_target_abs_mode_for_axis; + + // Use relative mode target for child window, ignore resize when moving back toward the ideal absolute position. + bool ignore_resize = false; + if (g.WindowResizeRelativeMode) + { + //GetForegroundDrawList()->AddText(GetMainViewport()->WorkPos, IM_COL32_WHITE, "Relative Mode"); + border_target[axis] = border_target_rel_mode_for_axis; + if (g.IO.MouseDelta[axis] == 0.0f || (g.IO.MouseDelta[axis] > 0.0f) == (border_target_rel_mode_for_axis > border_target_abs_mode_for_axis)) + ignore_resize = true; + } + + // Clamp, apply ImVec2 clamp_min(border_n == ImGuiDir_Right ? clamp_rect.Min.x : -FLT_MAX, border_n == ImGuiDir_Down || (border_n == ImGuiDir_Up && window_move_from_title_bar) ? clamp_rect.Min.y : -FLT_MAX); ImVec2 clamp_max(border_n == ImGuiDir_Left ? clamp_rect.Max.x : +FLT_MAX, border_n == ImGuiDir_Up ? clamp_rect.Max.y : +FLT_MAX); - ImVec2 border_target = window->Pos; - border_target[axis] = g.IO.MousePos[axis] - g.ActiveIdClickOffset[axis] + WINDOWS_HOVER_PADDING; border_target = ImClamp(border_target, clamp_min, clamp_max); - CalcResizePosSizeFromAnyCorner(window, border_target, ImMin(def.SegmentN1, def.SegmentN2), &pos_target, &size_target); + if (flags & ImGuiWindowFlags_ChildWindow) // Clamp resizing of childs within parent + { + if ((flags & (ImGuiWindowFlags_HorizontalScrollbar | ImGuiWindowFlags_AlwaysHorizontalScrollbar)) == 0 || (flags & ImGuiWindowFlags_NoScrollbar)) + border_target.x = ImClamp(border_target.x, window->ParentWindow->InnerClipRect.Min.x, window->ParentWindow->InnerClipRect.Max.x); + if (flags & ImGuiWindowFlags_NoScrollbar) + border_target.y = ImClamp(border_target.y, window->ParentWindow->InnerClipRect.Min.y, window->ParentWindow->InnerClipRect.Max.y); + } + if (!ignore_resize) + CalcResizePosSizeFromAnyCorner(window, border_target, ImMin(def.SegmentN1, def.SegmentN2), &pos_target, &size_target); } + if (hovered) + *border_hovered = border_n; + if (held) + *border_held = border_n; } PopID(); @@ -5816,7 +6347,7 @@ static bool ImGui::UpdateWindowManualResize(ImGuiWindow* window, const ImVec2& s // Navigation resize (keyboard/gamepad) // FIXME: This cannot be moved to NavUpdateWindowing() because CalcWindowSizeAfterConstraint() need to callback into user. // Not even sure the callback works here. - if (g.NavWindowingTarget && g.NavWindowingTarget->RootWindow == window) + if (g.NavWindowingTarget && g.NavWindowingTarget->RootWindowDockTree == window) { ImVec2 nav_resize_dir; if (g.NavInputSource == ImGuiInputSource_Keyboard && g.IO.KeyShift) @@ -5832,7 +6363,7 @@ static bool ImGui::UpdateWindowManualResize(ImGuiWindow* window, const ImVec2& s g.NavWindowingToggleLayer = false; g.NavDisableMouseHover = true; resize_grip_col[0] = GetColorU32(ImGuiCol_ResizeGripActive); - ImVec2 accum_floored = ImFloor(g.NavWindowingAccumDeltaSize); + ImVec2 accum_floored = ImTrunc(g.NavWindowingAccumDeltaSize); if (accum_floored.x != 0.0f || accum_floored.y != 0.0f) { // FIXME-NAV: Should store and accumulate into a separate size buffer to handle sizing constraints properly, right now a constraint will make us stuck. @@ -5843,51 +6374,69 @@ static bool ImGui::UpdateWindowManualResize(ImGuiWindow* window, const ImVec2& s } // Apply back modified position/size to window - if (size_target.x != FLT_MAX) - { - window->SizeFull = size_target; + const ImVec2 curr_pos = window->Pos; + const ImVec2 curr_size = window->SizeFull; + if (size_target.x != FLT_MAX && (window->Size.x != size_target.x || window->SizeFull.x != size_target.x)) + window->Size.x = window->SizeFull.x = size_target.x; + if (size_target.y != FLT_MAX && (window->Size.y != size_target.y || window->SizeFull.y != size_target.y)) + window->Size.y = window->SizeFull.y = size_target.y; + if (pos_target.x != FLT_MAX && window->Pos.x != ImTrunc(pos_target.x)) + window->Pos.x = ImTrunc(pos_target.x); + if (pos_target.y != FLT_MAX && window->Pos.y != ImTrunc(pos_target.y)) + window->Pos.y = ImTrunc(pos_target.y); + if (curr_pos.x != window->Pos.x || curr_pos.y != window->Pos.y || curr_size.x != window->SizeFull.x || curr_size.y != window->SizeFull.y) MarkIniSettingsDirty(window); - } - if (pos_target.x != FLT_MAX) - { - window->Pos = ImFloor(pos_target); - MarkIniSettingsDirty(window); - } - window->Size = window->SizeFull; - return ret_auto_fit; + // Recalculate next expected border expected coordinates + if (*border_held != -1) + g.WindowResizeBorderExpectedRect = GetResizeBorderRect(window, *border_held, grip_hover_inner_size, WINDOWS_HOVER_PADDING); + + return ret_auto_fit_mask; } static inline void ClampWindowPos(ImGuiWindow* window, const ImRect& visibility_rect) { ImGuiContext& g = *GImGui; ImVec2 size_for_clamping = window->Size; - if (g.IO.ConfigWindowsMoveFromTitleBarOnly && !(window->Flags & ImGuiWindowFlags_NoTitleBar)) - size_for_clamping.y = window->TitleBarHeight(); + if (g.IO.ConfigWindowsMoveFromTitleBarOnly && (!(window->Flags & ImGuiWindowFlags_NoTitleBar) || window->DockNodeAsHost)) + size_for_clamping.y = ImGui::GetFrameHeight(); // Not using window->TitleBarHeight() as DockNodeAsHost will report 0.0f here. window->Pos = ImClamp(window->Pos, visibility_rect.Min - size_for_clamping, visibility_rect.Max); } +static void RenderWindowOuterSingleBorder(ImGuiWindow* window, int border_n, ImU32 border_col, float border_size) +{ + const ImGuiResizeBorderDef& def = resize_border_def[border_n]; + const float rounding = window->WindowRounding; + const ImRect border_r = GetResizeBorderRect(window, border_n, rounding, 0.0f); + window->DrawList->PathArcTo(ImLerp(border_r.Min, border_r.Max, def.SegmentN1) + ImVec2(0.5f, 0.5f) + def.InnerDir * rounding, rounding, def.OuterAngle - IM_PI * 0.25f, def.OuterAngle); + window->DrawList->PathArcTo(ImLerp(border_r.Min, border_r.Max, def.SegmentN2) + ImVec2(0.5f, 0.5f) + def.InnerDir * rounding, rounding, def.OuterAngle, def.OuterAngle + IM_PI * 0.25f); + window->DrawList->PathStroke(border_col, ImDrawFlags_None, border_size); +} + static void ImGui::RenderWindowOuterBorders(ImGuiWindow* window) { ImGuiContext& g = *GImGui; - float rounding = window->WindowRounding; - float border_size = window->WindowBorderSize; - if (border_size > 0.0f && !(window->Flags & ImGuiWindowFlags_NoBackground)) - window->DrawList->AddRect(window->Pos, window->Pos + window->Size, GetColorU32(ImGuiCol_Border), rounding, 0, border_size); - - int border_held = window->ResizeBorderHeld; - if (border_held != -1) + const float border_size = window->WindowBorderSize; + const ImU32 border_col = GetColorU32(ImGuiCol_Border); + if (border_size > 0.0f && (window->Flags & ImGuiWindowFlags_NoBackground) == 0) + window->DrawList->AddRect(window->Pos, window->Pos + window->Size, border_col, window->WindowRounding, 0, window->WindowBorderSize); + else if (border_size > 0.0f) { - const ImGuiResizeBorderDef& def = resize_border_def[border_held]; - ImRect border_r = GetResizeBorderRect(window, border_held, rounding, 0.0f); - window->DrawList->PathArcTo(ImLerp(border_r.Min, border_r.Max, def.SegmentN1) + ImVec2(0.5f, 0.5f) + def.InnerDir * rounding, rounding, def.OuterAngle - IM_PI * 0.25f, def.OuterAngle); - window->DrawList->PathArcTo(ImLerp(border_r.Min, border_r.Max, def.SegmentN2) + ImVec2(0.5f, 0.5f) + def.InnerDir * rounding, rounding, def.OuterAngle, def.OuterAngle + IM_PI * 0.25f); - window->DrawList->PathStroke(GetColorU32(ImGuiCol_SeparatorActive), 0, ImMax(2.0f, border_size)); // Thicker than usual + if (window->ChildFlags & ImGuiChildFlags_ResizeX) // Similar code as 'resize_border_mask' computation in UpdateWindowManualResize() but we specifically only always draw explicit child resize border. + RenderWindowOuterSingleBorder(window, 1, border_col, border_size); + if (window->ChildFlags & ImGuiChildFlags_ResizeY) + RenderWindowOuterSingleBorder(window, 3, border_col, border_size); } - if (g.Style.FrameBorderSize > 0 && !(window->Flags & ImGuiWindowFlags_NoTitleBar)) + if (window->ResizeBorderHovered != -1 || window->ResizeBorderHeld != -1) + { + const int border_n = (window->ResizeBorderHeld != -1) ? window->ResizeBorderHeld : window->ResizeBorderHovered; + const ImU32 border_col_resizing = GetColorU32((window->ResizeBorderHeld != -1) ? ImGuiCol_SeparatorActive : ImGuiCol_SeparatorHovered); + RenderWindowOuterSingleBorder(window, border_n, border_col_resizing, ImMax(2.0f, window->WindowBorderSize)); // Thicker than usual + } + if (g.Style.FrameBorderSize > 0 && !(window->Flags & ImGuiWindowFlags_NoTitleBar) && !window->DockIsActive) { float y = window->Pos.y + window->TitleBarHeight() - 1; - window->DrawList->AddLine(ImVec2(window->Pos.x + border_size, y), ImVec2(window->Pos.x + window->Size.x - border_size, y), GetColorU32(ImGuiCol_Border), g.Style.FrameBorderSize); + window->DrawList->AddLine(ImVec2(window->Pos.x + border_size, y), ImVec2(window->Pos.x + window->Size.x - border_size, y), border_col, g.Style.FrameBorderSize); } } @@ -5913,6 +6462,8 @@ void ImGui::RenderWindowDecorations(ImGuiWindow* window, const ImRect& title_bar const float backup_border_size = style.FrameBorderSize; g.Style.FrameBorderSize = window->WindowBorderSize; ImU32 title_bar_col = GetColorU32((title_bar_is_highlight && !g.NavDisableHighlight) ? ImGuiCol_TitleBgActive : ImGuiCol_TitleBgCollapsed); + if (window->ViewportOwned) + title_bar_col |= IM_COL32_A_MASK; // No alpha (we don't support is_docking_transparent_payload here because simpler and less meaningful, but could with a bit of code shuffle/reuse) RenderFrame(title_bar_rect.Min, title_bar_rect.Max, title_bar_col, true, window_rounding); g.Style.FrameBorderSize = backup_border_size; } @@ -5921,23 +6472,58 @@ void ImGui::RenderWindowDecorations(ImGuiWindow* window, const ImRect& title_bar // Window background if (!(flags & ImGuiWindowFlags_NoBackground)) { + bool is_docking_transparent_payload = false; + if (g.DragDropActive && (g.FrameCount - g.DragDropAcceptFrameCount) <= 1 && g.IO.ConfigDockingTransparentPayload) + if (g.DragDropPayload.IsDataType(IMGUI_PAYLOAD_TYPE_WINDOW) && *(ImGuiWindow**)g.DragDropPayload.Data == window) + is_docking_transparent_payload = true; + ImU32 bg_col = GetColorU32(GetWindowBgColorIdx(window)); - bool override_alpha = false; - float alpha = 1.0f; - if (g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasBgAlpha) + if (window->ViewportOwned) { - alpha = g.NextWindowData.BgAlphaVal; - override_alpha = true; + bg_col |= IM_COL32_A_MASK; // No alpha + if (is_docking_transparent_payload) + window->Viewport->Alpha *= DOCKING_TRANSPARENT_PAYLOAD_ALPHA; } - if (override_alpha) - bg_col = (bg_col & ~IM_COL32_A_MASK) | (IM_F32_TO_INT8_SAT(alpha) << IM_COL32_A_SHIFT); - window->DrawList->AddRectFilled(window->Pos + ImVec2(0, window->TitleBarHeight()), window->Pos + window->Size, bg_col, window_rounding, (flags & ImGuiWindowFlags_NoTitleBar) ? 0 : ImDrawFlags_RoundCornersBottom); + else + { + // Adjust alpha. For docking + bool override_alpha = false; + float alpha = 1.0f; + if (g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasBgAlpha) + { + alpha = g.NextWindowData.BgAlphaVal; + override_alpha = true; + } + if (is_docking_transparent_payload) + { + alpha *= DOCKING_TRANSPARENT_PAYLOAD_ALPHA; // FIXME-DOCK: Should that be an override? + override_alpha = true; + } + if (override_alpha) + bg_col = (bg_col & ~IM_COL32_A_MASK) | (IM_F32_TO_INT8_SAT(alpha) << IM_COL32_A_SHIFT); + } + + // Render, for docked windows and host windows we ensure bg goes before decorations + if (window->DockIsActive) + window->DockNode->LastBgColor = bg_col; + ImDrawList* bg_draw_list = window->DockIsActive ? window->DockNode->HostWindow->DrawList : window->DrawList; + if (window->DockIsActive || (flags & ImGuiWindowFlags_DockNodeHost)) + bg_draw_list->ChannelsSetCurrent(DOCKING_HOST_DRAW_CHANNEL_BG); + bg_draw_list->AddRectFilled(window->Pos + ImVec2(0, window->TitleBarHeight()), window->Pos + window->Size, bg_col, window_rounding, (flags & ImGuiWindowFlags_NoTitleBar) ? 0 : ImDrawFlags_RoundCornersBottom); + if (window->DockIsActive || (flags & ImGuiWindowFlags_DockNodeHost)) + bg_draw_list->ChannelsSetCurrent(DOCKING_HOST_DRAW_CHANNEL_FG); } + if (window->DockIsActive) + window->DockNode->IsBgDrawnThisFrame = true; // Title bar - if (!(flags & ImGuiWindowFlags_NoTitleBar)) + // (when docked, DockNode are drawing their own title bar. Individual windows however do NOT set the _NoTitleBar flag, + // in order for their pos/size to be matching their undocking state.) + if (!(flags & ImGuiWindowFlags_NoTitleBar) && !window->DockIsActive) { ImU32 title_bar_col = GetColorU32(title_bar_is_highlight ? ImGuiCol_TitleBgActive : ImGuiCol_TitleBg); + if (window->ViewportOwned) + title_bar_col |= IM_COL32_A_MASK; // No alpha window->DrawList->AddRectFilled(title_bar_rect.Min, title_bar_rect.Max, title_bar_col, window_rounding, ImDrawFlags_RoundCornersTop); } @@ -5951,6 +6537,27 @@ void ImGui::RenderWindowDecorations(ImGuiWindow* window, const ImRect& title_bar window->DrawList->AddLine(menu_bar_rect.GetBL(), menu_bar_rect.GetBR(), GetColorU32(ImGuiCol_Border), style.FrameBorderSize); } + // Docking: Unhide tab bar (small triangle in the corner), drag from small triangle to quickly undock + ImGuiDockNode* node = window->DockNode; + if (window->DockIsActive && node->IsHiddenTabBar() && !node->IsNoTabBar()) + { + float unhide_sz_draw = ImTrunc(g.FontSize * 0.70f); + float unhide_sz_hit = ImTrunc(g.FontSize * 0.55f); + ImVec2 p = node->Pos; + ImRect r(p, p + ImVec2(unhide_sz_hit, unhide_sz_hit)); + ImGuiID unhide_id = window->GetID("#UNHIDE"); + KeepAliveID(unhide_id); + bool hovered, held; + if (ButtonBehavior(r, unhide_id, &hovered, &held, ImGuiButtonFlags_FlattenChildren)) + node->WantHiddenTabBarToggle = true; + else if (held && IsMouseDragging(0)) + StartMouseMovingWindowOrNode(window, node, true); // Undock from tab-bar triangle = same as window/collapse menu button + + // FIXME-DOCK: Ideally we'd use ImGuiCol_TitleBgActive/ImGuiCol_TitleBg here, but neither is guaranteed to be visible enough at this sort of size.. + ImU32 col = GetColorU32(((held && hovered) || (node->IsFocused && !hovered)) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button); + window->DrawList->AddTriangleFilled(p, p + ImVec2(unhide_sz_draw, 0.0f), p + ImVec2(0.0f, unhide_sz_draw), col); + } + // Scrollbars if (window->ScrollbarX) Scrollbar(ImGuiAxis_X); @@ -5974,12 +6581,13 @@ void ImGui::RenderWindowDecorations(ImGuiWindow* window, const ImRect& title_bar } } - // Borders - if (handle_borders_and_resize_grips) + // Borders (for dock node host they will be rendered over after the tab bar) + if (handle_borders_and_resize_grips && !window->DockNodeAsHost) RenderWindowOuterBorders(window); } } +// When inside a dock node, this is handled in DockNodeCalcTabBarLayout() instead. // Render title text, collapse button, close button void ImGui::RenderWindowTitleBarContents(ImGuiWindow* window, const ImRect& title_bar_rect, const char* name, bool* p_open) { @@ -6005,23 +6613,23 @@ void ImGui::RenderWindowTitleBarContents(ImGuiWindow* window, const ImRect& titl ImVec2 collapse_button_pos; if (has_close_button) { - pad_r += button_sz; - close_button_pos = ImVec2(title_bar_rect.Max.x - pad_r - style.FramePadding.x, title_bar_rect.Min.y); + close_button_pos = ImVec2(title_bar_rect.Max.x - pad_r - button_sz, title_bar_rect.Min.y + style.FramePadding.y); + pad_r += button_sz + style.ItemInnerSpacing.x; } if (has_collapse_button && style.WindowMenuButtonPosition == ImGuiDir_Right) { - pad_r += button_sz; - collapse_button_pos = ImVec2(title_bar_rect.Max.x - pad_r - style.FramePadding.x, title_bar_rect.Min.y); + collapse_button_pos = ImVec2(title_bar_rect.Max.x - pad_r - button_sz, title_bar_rect.Min.y + style.FramePadding.y); + pad_r += button_sz + style.ItemInnerSpacing.x; } if (has_collapse_button && style.WindowMenuButtonPosition == ImGuiDir_Left) { - collapse_button_pos = ImVec2(title_bar_rect.Min.x + pad_l - style.FramePadding.x, title_bar_rect.Min.y); - pad_l += button_sz; + collapse_button_pos = ImVec2(title_bar_rect.Min.x + pad_l, title_bar_rect.Min.y + style.FramePadding.y); + pad_l += button_sz + style.ItemInnerSpacing.x; } // Collapse button (submitting first so it gets priority when choosing a navigation init fallback) if (has_collapse_button) - if (CollapseButton(window->GetID("#COLLAPSE"), collapse_button_pos)) + if (CollapseButton(window->GetID("#COLLAPSE"), collapse_button_pos, NULL)) window->WantCollapseToggle = true; // Defer actual collapsing to next frame as we are too far in the Begin() function // Close button @@ -6072,12 +6680,16 @@ void ImGui::RenderWindowTitleBarContents(ImGuiWindow* window, const ImRect& titl void ImGui::UpdateWindowParentAndRootLinks(ImGuiWindow* window, ImGuiWindowFlags flags, ImGuiWindow* parent_window) { window->ParentWindow = parent_window; - window->RootWindow = window->RootWindowPopupTree = window->RootWindowForTitleBarHighlight = window->RootWindowForNav = window; + window->RootWindow = window->RootWindowPopupTree = window->RootWindowDockTree = window->RootWindowForTitleBarHighlight = window->RootWindowForNav = window; if (parent_window && (flags & ImGuiWindowFlags_ChildWindow) && !(flags & ImGuiWindowFlags_Tooltip)) - window->RootWindow = parent_window->RootWindow; + { + window->RootWindowDockTree = parent_window->RootWindowDockTree; + if (!window->DockIsActive && !(parent_window->Flags & ImGuiWindowFlags_DockNodeHost)) + window->RootWindow = parent_window->RootWindow; + } if (parent_window && (flags & ImGuiWindowFlags_Popup)) window->RootWindowPopupTree = parent_window->RootWindowPopupTree; - if (parent_window && !(flags & ImGuiWindowFlags_Modal) && (flags & (ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_Popup))) + if (parent_window && !(flags & ImGuiWindowFlags_Modal) && (flags & (ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_Popup))) // FIXME: simply use _NoTitleBar ? window->RootWindowForTitleBarHighlight = parent_window->RootWindowForTitleBarHighlight; while (window->RootWindowForNav->Flags & ImGuiWindowFlags_NavFlattened) { @@ -6089,12 +6701,13 @@ void ImGui::UpdateWindowParentAndRootLinks(ImGuiWindow* window, ImGuiWindowFlags // When a modal popup is open, newly created windows that want focus (i.e. are not popups and do not specify ImGuiWindowFlags_NoFocusOnAppearing) // should be positioned behind that modal window, unless the window was created inside the modal begin-stack. // In case of multiple stacked modals newly created window honors begin stack order and does not go below its own modal parent. -// - Window // FindBlockingModal() returns Modal1 -// - Window // .. returns Modal1 +// - WindowA // FindBlockingModal() returns Modal1 +// - WindowB // .. returns Modal1 // - Modal1 // .. returns Modal2 -// - Window // .. returns Modal2 -// - Window // .. returns Modal2 +// - WindowC // .. returns Modal2 +// - WindowD // .. returns Modal2 // - Modal2 // .. returns Modal2 +// - WindowE // .. returns NULL // Notes: // - FindBlockingModal(NULL) == NULL is generally equivalent to GetTopMostPopupModal() == NULL. // Only difference is here we check for ->Active/WasActive but it may be unecessary. @@ -6105,20 +6718,18 @@ ImGuiWindow* ImGui::FindBlockingModal(ImGuiWindow* window) return NULL; // Find a modal that has common parent with specified window. Specified window should be positioned behind that modal. - for (int i = g.OpenPopupStack.Size - 1; i >= 0; i--) + for (ImGuiPopupData& popup_data : g.OpenPopupStack) { - ImGuiWindow* popup_window = g.OpenPopupStack.Data[i].Window; + ImGuiWindow* popup_window = popup_data.Window; if (popup_window == NULL || !(popup_window->Flags & ImGuiWindowFlags_Modal)) continue; if (!popup_window->Active && !popup_window->WasActive) // Check WasActive, because this code may run before popup renders on current frame, also check Active to handle newly created windows. continue; if (window == NULL) // FindBlockingModal(NULL) test for if FocusWindow(NULL) is naturally possible via a mouse click. return popup_window; - if (IsWindowWithinBeginStackOf(window, popup_window)) // Window is rendered over last modal, no render order change needed. - break; - for (ImGuiWindow* parent = popup_window->ParentWindowInBeginStack->RootWindow; parent != NULL; parent = parent->ParentWindowInBeginStack->RootWindow) - if (IsWindowWithinBeginStackOf(window, parent)) - return popup_window; // Place window above its begin stack parent. + if (IsWindowWithinBeginStackOf(window, popup_window)) // Window may be over modal + continue; + return popup_window; // Place window right below first block modal } return NULL; } @@ -6144,6 +6755,10 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) if (window_just_created) window = CreateNewWindow(name, flags); + // [DEBUG] Debug break requested by user + if (g.DebugBreakInWindow == window->ID) + IM_DEBUG_BREAK(); + // Automatically disable manual moving/resizing when NoInputs is set if ((flags & ImGuiWindowFlags_NoInputs) == ImGuiWindowFlags_NoInputs) flags |= ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize; @@ -6155,23 +6770,26 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) const bool first_begin_of_the_frame = (window->LastFrameActive != current_frame); window->IsFallbackWindow = (g.CurrentWindowStack.Size == 0 && g.WithinFrameScopeWithImplicitWindow); - // Update the Appearing flag - bool window_just_activated_by_user = (window->LastFrameActive < current_frame - 1); // Not using !WasActive because the implicit "Debug" window would always toggle off->on + // Update the Appearing flag (note: the BeginDocked() path may also set this to true later) + bool window_just_activated_by_user = (window->LastFrameActive < current_frame - 1); // Not using !WasActive because the implicit "Debug" window would always toggle off->on if (flags & ImGuiWindowFlags_Popup) { ImGuiPopupData& popup_ref = g.OpenPopupStack[g.BeginPopupStack.Size]; window_just_activated_by_user |= (window->PopupId != popup_ref.PopupId); // We recycle popups so treat window as activated if popup id changed window_just_activated_by_user |= (window != popup_ref.Window); } - window->Appearing = window_just_activated_by_user; - if (window->Appearing) - SetWindowConditionAllowFlags(window, ImGuiCond_Appearing, true); // Update Flags, LastFrameActive, BeginOrderXXX fields + const bool window_was_appearing = window->Appearing; if (first_begin_of_the_frame) { UpdateWindowInFocusOrderList(window, window_just_created, flags); + window->Appearing = window_just_activated_by_user; + if (window->Appearing) + SetWindowConditionAllowFlags(window, ImGuiCond_Appearing, true); + window->FlagsPreviousFrame = window->Flags; window->Flags = (ImGuiWindowFlags)flags; + window->ChildFlags = (g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasChildFlags) ? g.NextWindowData.ChildFlags : 0; window->LastFrameActive = current_frame; window->LastTimeActive = (float)g.Time; window->BeginOrderWithinParent = 0; @@ -6182,8 +6800,42 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) flags = window->Flags; } + // Docking + // (NB: during the frame dock nodes are created, it is possible that (window->DockIsActive == false) even though (window->DockNode->Windows.Size > 1) + IM_ASSERT(window->DockNode == NULL || window->DockNodeAsHost == NULL); // Cannot be both + if (g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasDock) + SetWindowDock(window, g.NextWindowData.DockId, g.NextWindowData.DockCond); + if (first_begin_of_the_frame) + { + bool has_dock_node = (window->DockId != 0 || window->DockNode != NULL); + bool new_auto_dock_node = !has_dock_node && GetWindowAlwaysWantOwnTabBar(window); + bool dock_node_was_visible = window->DockNodeIsVisible; + bool dock_tab_was_visible = window->DockTabIsVisible; + if (has_dock_node || new_auto_dock_node) + { + BeginDocked(window, p_open); + flags = window->Flags; + if (window->DockIsActive) + { + IM_ASSERT(window->DockNode != NULL); + g.NextWindowData.Flags &= ~ImGuiNextWindowDataFlags_HasSizeConstraint; // Docking currently override constraints + } + + // Amend the Appearing flag + if (window->DockTabIsVisible && !dock_tab_was_visible && dock_node_was_visible && !window->Appearing && !window_was_appearing) + { + window->Appearing = true; + SetWindowConditionAllowFlags(window, ImGuiCond_Appearing, true); + } + } + else + { + window->DockIsActive = window->DockNodeIsVisible = window->DockTabIsVisible = false; + } + } + // Parent window is latched only on the first call to Begin() of the frame, so further append-calls can be done from a different window stack - ImGuiWindow* parent_window_in_stack = g.CurrentWindowStack.empty() ? NULL : g.CurrentWindowStack.back().Window; + ImGuiWindow* parent_window_in_stack = (window->DockIsActive && window->DockNode->HostWindow) ? window->DockNode->HostWindow : g.CurrentWindowStack.empty() ? NULL : g.CurrentWindowStack.back().Window; ImGuiWindow* parent_window = first_begin_of_the_frame ? ((flags & (ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_Popup)) ? parent_window_in_stack : NULL) : window->ParentWindow; IM_ASSERT(parent_window != NULL || !(flags & ImGuiWindowFlags_ChildWindow)); @@ -6192,7 +6844,6 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) window->IDStack.push_back(window->ID); // Add to stack - // We intentionally set g.CurrentWindow to NULL to prevent usage until when the viewport is set, then will call SetCurrentWindow() g.CurrentWindow = window; ImGuiWindowStackData window_stack_data; window_stack_data.Window = window; @@ -6200,19 +6851,33 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) window_stack_data.StackSizesOnBegin.SetToContextState(&g); g.CurrentWindowStack.push_back(window_stack_data); if (flags & ImGuiWindowFlags_ChildMenu) - g.BeginMenuCount++; + g.BeginMenuDepth++; // Update ->RootWindow and others pointers (before any possible call to FocusWindow) if (first_begin_of_the_frame) { UpdateWindowParentAndRootLinks(window, flags, parent_window); window->ParentWindowInBeginStack = parent_window_in_stack; + + // Focus route + // There's little point to expose a flag to set this: because the interesting cases won't be using parent_window_in_stack, + // Use for e.g. linking a tool window in a standalone viewport to a document window, regardless of their Begin() stack parenting. (#6798) + window->ParentWindowForFocusRoute = (window->RootWindow != window) ? parent_window_in_stack : NULL; + if (window->ParentWindowForFocusRoute == NULL && window->DockNode != NULL) + if (window->DockNode->MergedFlags & ImGuiDockNodeFlags_DockedWindowsInFocusRoute) + window->ParentWindowForFocusRoute = window->DockNode->HostWindow; + + // Override with SetNextWindowClass() field or direct call to SetWindowParentWindowForFocusRoute() + if (window->WindowClass.FocusRouteParentWindowId != 0) + { + window->ParentWindowForFocusRoute = FindWindowByID(window->WindowClass.FocusRouteParentWindowId); + IM_ASSERT(window->ParentWindowForFocusRoute != 0); // Invalid value for FocusRouteParentWindowId. + } } // Add to focus scope stack - PushFocusScope(window->ID); + PushFocusScope((flags & ImGuiWindowFlags_NavFlattened) ? g.CurrentFocusScopeId : window->ID); window->NavRootFocusScopeId = g.CurrentFocusScopeId; - g.CurrentWindow = NULL; // Add to popup stack if (flags & ImGuiWindowFlags_Popup) @@ -6248,6 +6913,10 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) { window_size_x_set_by_api = (window->SetWindowSizeAllowFlags & g.NextWindowData.SizeCond) != 0 && (g.NextWindowData.SizeVal.x > 0.0f); window_size_y_set_by_api = (window->SetWindowSizeAllowFlags & g.NextWindowData.SizeCond) != 0 && (g.NextWindowData.SizeVal.y > 0.0f); + if ((window->ChildFlags & ImGuiChildFlags_ResizeX) && (window->SetWindowSizeAllowFlags & ImGuiCond_FirstUseEver) == 0) // Axis-specific conditions for BeginChild() + g.NextWindowData.SizeVal.x = window->SizeFull.x; + if ((window->ChildFlags & ImGuiChildFlags_ResizeY) && (window->SetWindowSizeAllowFlags & ImGuiCond_FirstUseEver) == 0) + g.NextWindowData.SizeVal.y = window->SizeFull.y; SetWindowSize(window, g.NextWindowData.SizeVal, g.NextWindowData.SizeCond); } if (g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasScroll) @@ -6267,6 +6936,8 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) window->ContentSizeExplicit = g.NextWindowData.ContentSizeVal; else if (first_begin_of_the_frame) window->ContentSizeExplicit = ImVec2(0.0f, 0.0f); + if (g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasWindowClass) + window->WindowClass = g.NextWindowData.WindowClass; if (g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasCollapsed) SetWindowCollapsed(window, g.NextWindowData.CollapsedVal, g.NextWindowData.CollapsedCond); if (g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasFocus) @@ -6274,6 +6945,9 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) if (window->Appearing) SetWindowConditionAllowFlags(window, ImGuiCond_Appearing, false); + // We intentionally set g.CurrentWindow to NULL to prevent usage until when the viewport is set, then will call SetCurrentWindow() + g.CurrentWindow = NULL; + // When reusing window again multiple times a frame, just append content (don't need to setup again) if (first_begin_of_the_frame) { @@ -6286,6 +6960,11 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) window->IDStack.resize(1); window->DrawList->_ResetForNewFrame(); window->DC.CurrentTableIdx = -1; + if (flags & ImGuiWindowFlags_DockNodeHost) + { + window->DrawList->ChannelsSplit(2); + window->DrawList->ChannelsSetCurrent(DOCKING_HOST_DRAW_CHANNEL_FG); // Render decorations on channel 1 as we will render the backgrounds manually later + } // Restore buffer capacity when woken from a compacted state, to avoid if (window->MemoryCompacted) @@ -6294,7 +6973,9 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) // Update stored window name when it changes (which can _only_ happen with the "###" operator, so the ID would stay unchanged). // The title bar always display the 'name' parameter, so we only update the string storage if it needs to be visible to the end-user elsewhere. bool window_title_visible_elsewhere = false; - if (g.NavWindowingListWindow != NULL && (window->Flags & ImGuiWindowFlags_NoNavFocus) == 0) // Window titles visible when using CTRL+TAB + if ((window->Viewport && window->Viewport->Window == window) || (window->DockIsActive)) + window_title_visible_elsewhere = true; + else if (g.NavWindowingListWindow != NULL && (window->Flags & ImGuiWindowFlags_NoNavFocus) == 0) // Window titles visible when using CTRL+TAB window_title_visible_elsewhere = true; if (window_title_visible_elsewhere && !window_just_created && strcmp(name, window->Name) != 0) { @@ -6307,6 +6988,10 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) // Update contents size from last frame for auto-fitting (or use explicit size) CalcWindowContentSizes(window, &window->ContentSize, &window->ContentSizeIdeal); + + // FIXME: These flags are decremented before they are used. This means that in order to have these fields produce their intended behaviors + // for one frame we must set them to at least 2, which is counter-intuitive. HiddenFramesCannotSkipItems is a more complicated case because + // it has a single usage before this code block and may be set below before it is finally checked. if (window->HiddenFramesCanSkipItems > 0) window->HiddenFramesCanSkipItems--; if (window->HiddenFramesCannotSkipItems > 0) @@ -6334,20 +7019,23 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) } // SELECT VIEWPORT - // FIXME-VIEWPORT: In the docking/viewport branch, this is the point where we select the current viewport (which may affect the style) + // We need to do this before using any style/font sizes, as viewport with a different DPI may affect font sizes. - ImGuiViewportP* viewport = (ImGuiViewportP*)(void*)GetMainViewport(); - SetWindowViewport(window, viewport); + WindowSelectViewport(window); + SetCurrentViewport(window, window->Viewport); + window->FontDpiScale = (g.IO.ConfigFlags & ImGuiConfigFlags_DpiEnableScaleFonts) ? window->Viewport->DpiScale : 1.0f; SetCurrentWindow(window); + flags = window->Flags; // LOCK BORDER SIZE AND PADDING FOR THE FRAME (so that altering them doesn't cause inconsistencies) + // We read Style data after the call to UpdateSelectWindowViewport() which might be swapping the style. - if (flags & ImGuiWindowFlags_ChildWindow) + if (!window->DockIsActive && (flags & ImGuiWindowFlags_ChildWindow)) window->WindowBorderSize = style.ChildBorderSize; else window->WindowBorderSize = ((flags & (ImGuiWindowFlags_Popup | ImGuiWindowFlags_Tooltip)) && !(flags & ImGuiWindowFlags_Modal)) ? style.PopupBorderSize : style.WindowBorderSize; window->WindowPadding = style.WindowPadding; - if ((flags & ImGuiWindowFlags_ChildWindow) && !(flags & (ImGuiWindowFlags_AlwaysUseWindowPadding | ImGuiWindowFlags_Popup)) && window->WindowBorderSize == 0.0f) + if (!window->DockIsActive && (flags & ImGuiWindowFlags_ChildWindow) && !(flags & ImGuiWindowFlags_Popup) && !(window->ChildFlags & ImGuiChildFlags_AlwaysUseWindowPadding) && window->WindowBorderSize == 0.0f) window->WindowPadding = ImVec2(0.0f, (flags & ImGuiWindowFlags_MenuBar) ? style.WindowPadding.y : 0.0f); // Lock menu offset so size calculation can use it as menu-bar windows need a minimum size. @@ -6359,7 +7047,7 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) // Collapse window by double-clicking on title bar // At this point we don't have a clipping rectangle setup yet, so we can use the title bar area for hit detection and drawing - if (!(flags & ImGuiWindowFlags_NoTitleBar) && !(flags & ImGuiWindowFlags_NoCollapse)) + if (!(flags & ImGuiWindowFlags_NoTitleBar) && !(flags & ImGuiWindowFlags_NoCollapse) && !window->DockIsActive) { // We don't use a regular button+id to test for double-click on title bar (mostly due to legacy reason, could be fixed), so verify that we don't have items over the title bar. ImRect title_bar_rect = window->TitleBarRect(); @@ -6458,23 +7146,66 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) else if ((flags & ImGuiWindowFlags_Tooltip) != 0 && !window_pos_set_by_api && !window_is_child_tooltip) window->Pos = FindBestWindowPosForPopup(window); + // Late create viewport if we don't fit within our current host viewport. + if (window->ViewportAllowPlatformMonitorExtend >= 0 && !window->ViewportOwned && !(window->Viewport->Flags & ImGuiViewportFlags_IsMinimized)) + if (!window->Viewport->GetMainRect().Contains(window->Rect())) + { + // This is based on the assumption that the DPI will be known ahead (same as the DPI of the selection done in UpdateSelectWindowViewport) + //ImGuiViewport* old_viewport = window->Viewport; + window->Viewport = AddUpdateViewport(window, window->ID, window->Pos, window->Size, ImGuiViewportFlags_NoFocusOnAppearing); + + // FIXME-DPI + //IM_ASSERT(old_viewport->DpiScale == window->Viewport->DpiScale); // FIXME-DPI: Something went wrong + SetCurrentViewport(window, window->Viewport); + window->FontDpiScale = (g.IO.ConfigFlags & ImGuiConfigFlags_DpiEnableScaleFonts) ? window->Viewport->DpiScale : 1.0f; + SetCurrentWindow(window); + } + + if (window->ViewportOwned) + WindowSyncOwnedViewport(window, parent_window_in_stack); + // Calculate the range of allowed position for that window (to be movable and visible past safe area padding) // When clamping to stay visible, we will enforce that window->Pos stays inside of visibility_rect. - ImRect viewport_rect(viewport->GetMainRect()); - ImRect viewport_work_rect(viewport->GetWorkRect()); + ImRect viewport_rect(window->Viewport->GetMainRect()); + ImRect viewport_work_rect(window->Viewport->GetWorkRect()); ImVec2 visibility_padding = ImMax(style.DisplayWindowPadding, style.DisplaySafeAreaPadding); ImRect visibility_rect(viewport_work_rect.Min + visibility_padding, viewport_work_rect.Max - visibility_padding); // Clamp position/size so window stays visible within its viewport or monitor // Ignore zero-sized display explicitly to avoid losing positions if a window manager reports zero-sized window when initializing or minimizing. + // FIXME: Similar to code in GetWindowAllowedExtentRect() if (!window_pos_set_by_api && !(flags & ImGuiWindowFlags_ChildWindow)) - if (viewport_rect.GetWidth() > 0.0f && viewport_rect.GetHeight() > 0.0f) + { + if (!window->ViewportOwned && viewport_rect.GetWidth() > 0 && viewport_rect.GetHeight() > 0.0f) + { ClampWindowPos(window, visibility_rect); - window->Pos = ImFloor(window->Pos); + } + else if (window->ViewportOwned && g.PlatformIO.Monitors.Size > 0) + { + if (g.MovingWindow != NULL && window->RootWindowDockTree == g.MovingWindow->RootWindowDockTree) + { + // While moving windows we allow them to straddle monitors (#7299, #3071) + visibility_rect = g.PlatformMonitorsFullWorkRect; + } + else + { + // When not moving ensure visible in its monitor + // Lost windows (e.g. a monitor disconnected) will naturally moved to the fallback/dummy monitor aka the main viewport. + const ImGuiPlatformMonitor* monitor = GetViewportPlatformMonitor(window->Viewport); + visibility_rect = ImRect(monitor->WorkPos, monitor->WorkPos + monitor->WorkSize); + } + visibility_rect.Expand(-visibility_padding); + ClampWindowPos(window, visibility_rect); + } + } + window->Pos = ImTrunc(window->Pos); // Lock window rounding for the frame (so that altering them doesn't cause inconsistencies) // Large values tend to lead to variety of artifacts and are not recommended. - window->WindowRounding = (flags & ImGuiWindowFlags_ChildWindow) ? style.ChildRounding : ((flags & ImGuiWindowFlags_Popup) && !(flags & ImGuiWindowFlags_Modal)) ? style.PopupRounding : style.WindowRounding; + if (window->ViewportOwned || window->DockIsActive) + window->WindowRounding = 0.0f; + else + window->WindowRounding = (flags & ImGuiWindowFlags_ChildWindow) ? style.ChildRounding : ((flags & ImGuiWindowFlags_Popup) && !(flags & ImGuiWindowFlags_Modal)) ? style.PopupRounding : style.WindowRounding; // For windows with title bar or menu bar, we clamp to FrameHeight(FontSize + FramePadding.y * 2.0f) to completely hide artifacts. //if ((window->Flags & ImGuiWindowFlags_MenuBar) || !(window->Flags & ImGuiWindowFlags_NoTitleBar)) @@ -6486,7 +7217,7 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) { if (flags & ImGuiWindowFlags_Popup) want_focus = true; - else if ((flags & (ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_Tooltip)) == 0) + else if ((window->DockIsActive || (flags & ImGuiWindowFlags_ChildWindow) == 0) && !(flags & ImGuiWindowFlags_Tooltip)) want_focus = true; } @@ -6502,16 +7233,39 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) } #endif + // Decide if we are going to handle borders and resize grips + const bool handle_borders_and_resize_grips = (window->DockNodeAsHost || !window->DockIsActive); + // Handle manual resize: Resize Grips, Borders, Gamepad - int border_held = -1; + int border_hovered = -1, border_held = -1; ImU32 resize_grip_col[4] = {}; - const int resize_grip_count = g.IO.ConfigWindowsResizeFromEdges ? 2 : 1; // Allow resize from lower-left if we have the mouse cursor feedback for it. - const float resize_grip_draw_size = IM_FLOOR(ImMax(g.FontSize * 1.10f, window->WindowRounding + 1.0f + g.FontSize * 0.2f)); - if (!window->Collapsed) - if (UpdateWindowManualResize(window, size_auto_fit, &border_held, resize_grip_count, &resize_grip_col[0], visibility_rect)) - use_current_size_for_scrollbar_x = use_current_size_for_scrollbar_y = true; + const int resize_grip_count = ((flags & ImGuiWindowFlags_ChildWindow) && !(flags & ImGuiWindowFlags_Popup)) ? 0 : g.IO.ConfigWindowsResizeFromEdges ? 2 : 1; // Allow resize from lower-left if we have the mouse cursor feedback for it. + const float resize_grip_draw_size = IM_TRUNC(ImMax(g.FontSize * 1.10f, window->WindowRounding + 1.0f + g.FontSize * 0.2f)); + if (handle_borders_and_resize_grips && !window->Collapsed) + if (int auto_fit_mask = UpdateWindowManualResize(window, size_auto_fit, &border_hovered, &border_held, resize_grip_count, &resize_grip_col[0], visibility_rect)) + { + if (auto_fit_mask & (1 << ImGuiAxis_X)) + use_current_size_for_scrollbar_x = true; + if (auto_fit_mask & (1 << ImGuiAxis_Y)) + use_current_size_for_scrollbar_y = true; + } + window->ResizeBorderHovered = (signed char)border_hovered; window->ResizeBorderHeld = (signed char)border_held; + // Synchronize window --> viewport again and one last time (clamping and manual resize may have affected either) + if (window->ViewportOwned) + { + if (!window->Viewport->PlatformRequestMove) + window->Viewport->Pos = window->Pos; + if (!window->Viewport->PlatformRequestResize) + window->Viewport->Size = window->Size; + window->Viewport->UpdateWorkRect(); + viewport_rect = window->Viewport->GetMainRect(); + } + + // Save last known viewport position within the window itself (so it can be saved in .ini file and restored) + window->ViewportPos = window->Viewport->Pos; + // SCROLLBAR VISIBILITY // Update scrollbar visibility (based on the Size that was effective during last frame or the auto-resized Size). @@ -6550,6 +7304,8 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) const ImRect outer_rect = window->Rect(); const ImRect title_bar_rect = window->TitleBarRect(); window->OuterRectClipped = outer_rect; + if (window->DockIsActive) + window->OuterRectClipped.Min.y += window->TitleBarHeight(); window->OuterRectClipped.ClipWith(host_rect); // Inner rectangle @@ -6571,17 +7327,17 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) // Affected by window/frame border size. Used by: // - Begin() initial clip rect float top_border_size = (((flags & ImGuiWindowFlags_MenuBar) || !(flags & ImGuiWindowFlags_NoTitleBar)) ? style.FrameBorderSize : window->WindowBorderSize); - window->InnerClipRect.Min.x = ImFloor(0.5f + window->InnerRect.Min.x + ImMax(ImFloor(window->WindowPadding.x * 0.5f), window->WindowBorderSize)); + window->InnerClipRect.Min.x = ImFloor(0.5f + window->InnerRect.Min.x + ImMax(ImTrunc(window->WindowPadding.x * 0.5f), window->WindowBorderSize)); window->InnerClipRect.Min.y = ImFloor(0.5f + window->InnerRect.Min.y + top_border_size); - window->InnerClipRect.Max.x = ImFloor(0.5f + window->InnerRect.Max.x - ImMax(ImFloor(window->WindowPadding.x * 0.5f), window->WindowBorderSize)); + window->InnerClipRect.Max.x = ImFloor(0.5f + window->InnerRect.Max.x - ImMax(ImTrunc(window->WindowPadding.x * 0.5f), window->WindowBorderSize)); window->InnerClipRect.Max.y = ImFloor(0.5f + window->InnerRect.Max.y - window->WindowBorderSize); window->InnerClipRect.ClipWithFull(host_rect); // Default item width. Make it proportional to window size if window manually resizes if (window->Size.x > 0.0f && !(flags & ImGuiWindowFlags_Tooltip) && !(flags & ImGuiWindowFlags_AlwaysAutoResize)) - window->ItemWidthDefault = ImFloor(window->Size.x * 0.65f); + window->ItemWidthDefault = ImTrunc(window->Size.x * 0.65f); else - window->ItemWidthDefault = ImFloor(g.FontSize * 16.0f); + window->ItemWidthDefault = ImTrunc(g.FontSize * 16.0f); // SCROLLING @@ -6606,6 +7362,8 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) // Child windows can render their decoration (bg color, border, scrollbars, etc.) within their parent to save a draw call (since 1.71) // When using overlapping child windows, this will break the assumption that child z-order is mapped to submission order. // FIXME: User code may rely on explicit sorting of overlapping child window and would need to disable this somehow. Please get in contact if you are affected (github #4493) + const bool is_undocked_or_docked_visible = !window->DockIsActive || window->DockTabIsVisible; + if (is_undocked_or_docked_visible) { bool render_decorations_in_parent = false; if ((flags & ImGuiWindowFlags_ChildWindow) && !(flags & ImGuiWindowFlags_Popup) && !window_is_child_tooltip) @@ -6623,8 +7381,7 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) // Handle title bar, scrollbar, resize grips and resize borders const ImGuiWindow* window_to_highlight = g.NavWindowingTarget ? g.NavWindowingTarget : g.NavWindow; - const bool title_bar_is_highlight = want_focus || (window_to_highlight && window->RootWindowForTitleBarHighlight == window_to_highlight->RootWindowForTitleBarHighlight); - const bool handle_borders_and_resize_grips = true; // This exists to facilitate merge with 'docking' branch. + const bool title_bar_is_highlight = want_focus || (window_to_highlight && (window->RootWindowForTitleBarHighlight == window_to_highlight->RootWindowForTitleBarHighlight || (window->DockNode && window->DockNode == window_to_highlight->DockNode))); RenderWindowDecorations(window, title_bar_rect, title_bar_is_highlight, handle_borders_and_resize_grips, resize_grip_count, resize_grip_col, resize_grip_draw_size); if (render_decorations_in_parent) @@ -6642,14 +7399,15 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) const bool allow_scrollbar_y = !(flags & ImGuiWindowFlags_NoScrollbar); const float work_rect_size_x = (window->ContentSizeExplicit.x != 0.0f ? window->ContentSizeExplicit.x : ImMax(allow_scrollbar_x ? window->ContentSize.x : 0.0f, window->Size.x - window->WindowPadding.x * 2.0f - (window->DecoOuterSizeX1 + window->DecoOuterSizeX2))); const float work_rect_size_y = (window->ContentSizeExplicit.y != 0.0f ? window->ContentSizeExplicit.y : ImMax(allow_scrollbar_y ? window->ContentSize.y : 0.0f, window->Size.y - window->WindowPadding.y * 2.0f - (window->DecoOuterSizeY1 + window->DecoOuterSizeY2))); - window->WorkRect.Min.x = ImFloor(window->InnerRect.Min.x - window->Scroll.x + ImMax(window->WindowPadding.x, window->WindowBorderSize)); - window->WorkRect.Min.y = ImFloor(window->InnerRect.Min.y - window->Scroll.y + ImMax(window->WindowPadding.y, window->WindowBorderSize)); + window->WorkRect.Min.x = ImTrunc(window->InnerRect.Min.x - window->Scroll.x + ImMax(window->WindowPadding.x, window->WindowBorderSize)); + window->WorkRect.Min.y = ImTrunc(window->InnerRect.Min.y - window->Scroll.y + ImMax(window->WindowPadding.y, window->WindowBorderSize)); window->WorkRect.Max.x = window->WorkRect.Min.x + work_rect_size_x; window->WorkRect.Max.y = window->WorkRect.Min.y + work_rect_size_y; window->ParentWorkRect = window->WorkRect; // [LEGACY] Content Region // FIXME-OBSOLETE: window->ContentRegionRect.Max is currently very misleading / partly faulty, but some BeginChild() patterns relies on it. + // Unless explicit content size is specified by user, this currently represent the region leading to no scrolling. // Used by: // - Mouse wheel scrolling + many other things window->ContentRegionRect.Min.x = window->Pos.x - window->Scroll.x + window->WindowPadding.x + window->DecoOuterSizeX1; @@ -6698,6 +7456,8 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) window->DC.TextWrapPos = -1.0f; // disabled window->DC.ItemWidthStack.resize(0); window->DC.TextWrapPosStack.resize(0); + if (flags & ImGuiWindowFlags_Modal) + window->DC.ModalDimBgColor = ColorConvertFloat4ToU32(GetStyleColorVec4(ImGuiCol_ModalWindowDimBg)); if (window->AutoFitFramesX > 0) window->AutoFitFramesX--; @@ -6713,8 +7473,16 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) if (want_focus && window == g.NavWindow) NavInitWindow(window, false); // <-- this is in the way for us to be able to defer and sort reappearing FocusWindow() calls + // Close requested by platform window (apply to all windows in this viewport) + if (p_open != NULL && window->Viewport->PlatformRequestClose && window->Viewport != GetMainViewport()) + { + IMGUI_DEBUG_LOG_VIEWPORT("[viewport] Window '%s' closed by PlatformRequestClose\n", window->Name); + *p_open = false; + g.NavWindowingToggleLayer = false; // Assume user mapped PlatformRequestClose on ALT-F4 so we disable ALT for menu toggle. False positive not an issue. // FIXME-NAV: Try removing. + } + // Title bar - if (!(flags & ImGuiWindowFlags_NoTitleBar)) + if (!(flags & ImGuiWindowFlags_NoTitleBar) && !window->DockIsActive) RenderWindowTitleBarContents(window, ImRect(title_bar_rect.Min.x + window->WindowBorderSize, title_bar_rect.Min.y, title_bar_rect.Max.x - window->WindowBorderSize, title_bar_rect.Max.y), name, p_open); // Clear hit test shape every frame @@ -6730,9 +7498,26 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) LogToClipboard(); */ + if (g.IO.ConfigFlags & ImGuiConfigFlags_DockingEnable) + { + // Docking: Dragging a dockable window (or any of its child) turns it into a drag and drop source. + // We need to do this _before_ we overwrite window->DC.LastItemId below because BeginDockableDragDropSource() also overwrites it. + if (g.MovingWindow == window && (window->RootWindowDockTree->Flags & ImGuiWindowFlags_NoDocking) == 0) + BeginDockableDragDropSource(window); + + // Docking: Any dockable window can act as a target. For dock node hosts we call BeginDockableDragDropTarget() in DockNodeUpdate() instead. + if (g.DragDropActive && !(flags & ImGuiWindowFlags_NoDocking)) + if (g.MovingWindow == NULL || g.MovingWindow->RootWindowDockTree != window) + if ((window == window->RootWindowDockTree) && !(window->Flags & ImGuiWindowFlags_DockNodeHost)) + BeginDockableDragDropTarget(window); + } + // We fill last item data based on Title Bar/Tab, in order for IsItemHovered() and IsItemActive() to be usable after Begin(). // This is useful to allow creating context menus on title bar only, etc. - SetLastItemData(window->MoveId, g.CurrentItemFlags, IsMouseHoveringRect(title_bar_rect.Min, title_bar_rect.Max, false) ? ImGuiItemStatusFlags_HoveredRect : 0, title_bar_rect); + if (window->DockIsActive) + SetLastItemData(window->MoveId, g.CurrentItemFlags, window->DockTabItemStatusFlags, window->DockTabItemRect); + else + SetLastItemData(window->MoveId, g.CurrentItemFlags, IsMouseHoveringRect(title_bar_rect.Min, title_bar_rect.Max, false) ? ImGuiItemStatusFlags_HoveredRect : 0, title_bar_rect); // [DEBUG] #ifndef IMGUI_DISABLE_DEBUG_TOOLS @@ -6749,10 +7534,12 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) else { // Append + SetCurrentViewport(window, window->Viewport); SetCurrentWindow(window); } - PushClipRect(window->InnerClipRect.Min, window->InnerClipRect.Max, true); + if (!(flags & ImGuiWindowFlags_DockNodeHost)) + PushClipRect(window->InnerClipRect.Min, window->InnerClipRect.Max, true); // Clear 'accessed' flag last thing (After PushClipRect which will set the flag. We want the flag to stay false when the default "Debug" window is unused) window->WriteAccessed = false; @@ -6762,18 +7549,32 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) // Update visibility if (first_begin_of_the_frame) { - if (flags & ImGuiWindowFlags_ChildWindow) + // When we are about to select this tab (which will only be visible on the _next frame_), flag it with a non-zero HiddenFramesCannotSkipItems. + // This will have the important effect of actually returning true in Begin() and not setting SkipItems, allowing an earlier submission of the window contents. + // This is analogous to regular windows being hidden from one frame. + // It is especially important as e.g. nested TabBars would otherwise generate flicker in the form of one empty frame, or focus requests won't be processed. + if (window->DockIsActive && !window->DockTabIsVisible) + { + if (window->LastFrameJustFocused == g.FrameCount) + window->HiddenFramesCannotSkipItems = 1; + else + window->HiddenFramesCanSkipItems = 1; + } + + if ((flags & ImGuiWindowFlags_ChildWindow) && !(flags & ImGuiWindowFlags_ChildMenu)) { // Child window can be out of sight and have "negative" clip windows. // Mark them as collapsed so commands are skipped earlier (we can't manually collapse them because they have no title bar). - IM_ASSERT((flags & ImGuiWindowFlags_NoTitleBar) != 0); - if (!(flags & ImGuiWindowFlags_AlwaysAutoResize) && window->AutoFitFramesX <= 0 && window->AutoFitFramesY <= 0) // FIXME: Doesn't make sense for ChildWindow?? - { - const bool nav_request = (flags & ImGuiWindowFlags_NavFlattened) && (g.NavAnyRequest && g.NavWindow && g.NavWindow->RootWindowForNav == window->RootWindowForNav); - if (!g.LogEnabled && !nav_request) - if (window->OuterRectClipped.Min.x >= window->OuterRectClipped.Max.x || window->OuterRectClipped.Min.y >= window->OuterRectClipped.Max.y) + IM_ASSERT((flags& ImGuiWindowFlags_NoTitleBar) != 0 || window->DockIsActive); + const bool nav_request = (flags & ImGuiWindowFlags_NavFlattened) && (g.NavAnyRequest && g.NavWindow && g.NavWindow->RootWindowForNav == window->RootWindowForNav); + if (!g.LogEnabled && !nav_request) + if (window->OuterRectClipped.Min.x >= window->OuterRectClipped.Max.x || window->OuterRectClipped.Min.y >= window->OuterRectClipped.Max.y) + { + if (window->AutoFitFramesX > 0 || window->AutoFitFramesY > 0) + window->HiddenFramesCannotSkipItems = 1; + else window->HiddenFramesCanSkipItems = 1; - } + } // Hide along with parent or if parent is collapsed if (parent_window && (parent_window->Collapsed || parent_window->HiddenFramesCanSkipItems > 0)) @@ -6803,16 +7604,29 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) if (window->AutoFitFramesX <= 0 && window->AutoFitFramesY <= 0 && window->HiddenFramesCannotSkipItems <= 0) skip_items = true; window->SkipItems = skip_items; + + // Restore NavLayersActiveMaskNext to previous value when not visible, so a CTRL+Tab back can use a safe value. + if (window->SkipItems) + window->DC.NavLayersActiveMaskNext = window->DC.NavLayersActiveMask; + + // Sanity check: there are two spots which can set Appearing = true + // - when 'window_just_activated_by_user' is set -> HiddenFramesCannotSkipItems is set -> SkipItems always false + // - in BeginDocked() path when DockNodeIsVisible == DockTabIsVisible == true -> hidden _should_ be all zero // FIXME: Not formally proven, hence the assert. + if (window->SkipItems && !window->Appearing) + IM_ASSERT(window->Appearing == false); // Please report on GitHub if this triggers: https://github.com/ocornut/imgui/issues/4177 } // [DEBUG] io.ConfigDebugBeginReturnValue override return value to test Begin/End and BeginChild/EndChild behaviors. // (The implicit fallback window is NOT automatically ended allowing it to always be able to receive commands without crashing) - if (!window->IsFallbackWindow && ((g.IO.ConfigDebugBeginReturnValueOnce && window_just_created) || (g.IO.ConfigDebugBeginReturnValueLoop && g.DebugBeginReturnValueCullDepth == g.CurrentWindowStack.Size))) - { - if (window->AutoFitFramesX > 0) { window->AutoFitFramesX++; } - if (window->AutoFitFramesY > 0) { window->AutoFitFramesY++; } - return false; - } +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + if (!window->IsFallbackWindow) + if ((g.IO.ConfigDebugBeginReturnValueOnce && window_just_created) || (g.IO.ConfigDebugBeginReturnValueLoop && g.DebugBeginReturnValueCullDepth == g.CurrentWindowStack.Size)) + { + if (window->AutoFitFramesX > 0) { window->AutoFitFramesX++; } + if (window->AutoFitFramesY > 0) { window->AutoFitFramesY++; } + return false; + } +#endif return !window->SkipItems; } @@ -6831,13 +7645,14 @@ void ImGui::End() IM_ASSERT(g.CurrentWindowStack.Size > 0); // Error checking: verify that user doesn't directly call End() on a child window. - if (window->Flags & ImGuiWindowFlags_ChildWindow) + if ((window->Flags & ImGuiWindowFlags_ChildWindow) && !(window->Flags & ImGuiWindowFlags_DockNodeHost) && !window->DockIsActive) IM_ASSERT_USER_ERROR(g.WithinEndChild, "Must call EndChild() and not End()!"); // Close anything that is open if (window->DC.CurrentColumns) EndColumns(); - PopClipRect(); // Inner window clip rectangle + if (!(window->Flags & ImGuiWindowFlags_DockNodeHost)) // Pop inner window clip rectangle + PopClipRect(); PopFocusScope(); // Stop logging @@ -6847,15 +7662,22 @@ void ImGui::End() if (window->DC.IsSetPos) ErrorCheckUsingSetCursorPosToExtendParentBoundaries(); + // Docking: report contents sizes to parent to allow for auto-resize + if (window->DockNode && window->DockTabIsVisible) + if (ImGuiWindow* host_window = window->DockNode->HostWindow) // FIXME-DOCK + host_window->DC.CursorMaxPos = window->DC.CursorMaxPos + window->WindowPadding - host_window->WindowPadding; + // Pop from window stack g.LastItemData = g.CurrentWindowStack.back().ParentLastItemDataBackup; if (window->Flags & ImGuiWindowFlags_ChildMenu) - g.BeginMenuCount--; + g.BeginMenuDepth--; if (window->Flags & ImGuiWindowFlags_Popup) g.BeginPopupStack.pop_back(); g.CurrentWindowStack.back().StackSizesOnBegin.CompareWithContextState(&g); g.CurrentWindowStack.pop_back(); SetCurrentWindow(g.CurrentWindowStack.Size == 0 ? NULL : g.CurrentWindowStack.back().Window); + if (g.CurrentWindow) + SetCurrentViewport(g.CurrentWindow, g.CurrentWindow->Viewport); } void ImGui::BringWindowToFocusFront(ImGuiWindow* window) @@ -6883,7 +7705,7 @@ void ImGui::BringWindowToDisplayFront(ImGuiWindow* window) { ImGuiContext& g = *GImGui; ImGuiWindow* current_front_window = g.Windows.back(); - if (current_front_window == window || current_front_window->RootWindow == window) // Cheap early out (could be better) + if (current_front_window == window || current_front_window->RootWindowDockTree == window) // Cheap early out (could be better) return; for (int i = g.Windows.Size - 2; i >= 0; i--) // We can ignore the top-most window if (g.Windows[i] == window) @@ -6963,39 +7785,48 @@ void ImGui::FocusWindow(ImGuiWindow* window, ImGuiFocusRequestFlags flags) g.NavMousePosDirty = true; g.NavId = window ? window->NavLastIds[0] : 0; // Restore NavId g.NavLayer = ImGuiNavLayer_Main; - g.NavFocusScopeId = window ? window->NavRootFocusScopeId : 0; + SetNavFocusScope(window ? window->NavRootFocusScopeId : 0); g.NavIdIsAlive = false; + g.NavLastValidSelectionUserData = ImGuiSelectionUserData_Invalid; // Close popups if any ClosePopupsOverWindow(window, false); } // Move the root window to the top of the pile - IM_ASSERT(window == NULL || window->RootWindow != NULL); - ImGuiWindow* focus_front_window = window ? window->RootWindow : NULL; // NB: In docking branch this is window->RootWindowDockStop - ImGuiWindow* display_front_window = window ? window->RootWindow : NULL; + IM_ASSERT(window == NULL || window->RootWindowDockTree != NULL); + ImGuiWindow* focus_front_window = window ? window->RootWindow : NULL; + ImGuiWindow* display_front_window = window ? window->RootWindowDockTree : NULL; + ImGuiDockNode* dock_node = window ? window->DockNode : NULL; + bool active_id_window_is_dock_node_host = (g.ActiveIdWindow && dock_node && dock_node->HostWindow == g.ActiveIdWindow); // Steal active widgets. Some of the cases it triggers includes: // - Focus a window while an InputText in another window is active, if focus happens before the old InputText can run. // - When using Nav to activate menu items (due to timing of activating on press->new window appears->losing ActiveId) + // - Using dock host items (tab, collapse button) can trigger this before we redirect the ActiveIdWindow toward the child window. if (g.ActiveId != 0 && g.ActiveIdWindow && g.ActiveIdWindow->RootWindow != focus_front_window) - if (!g.ActiveIdNoClearOnFocusLoss) + if (!g.ActiveIdNoClearOnFocusLoss && !active_id_window_is_dock_node_host) ClearActiveID(); // Passing NULL allow to disable keyboard focus if (!window) return; + window->LastFrameJustFocused = g.FrameCount; + + // Select in dock node + // For #2304 we avoid applying focus immediately before the tabbar is visible. + //if (dock_node && dock_node->TabBar) + // dock_node->TabBar->SelectedTabId = dock_node->TabBar->NextSelectedTabId = window->TabId; // Bring to front BringWindowToFocusFront(focus_front_window); - if (((window->Flags | display_front_window->Flags) & ImGuiWindowFlags_NoBringToFrontOnFocus) == 0) + if (((window->Flags | focus_front_window->Flags | display_front_window->Flags) & ImGuiWindowFlags_NoBringToFrontOnFocus) == 0) BringWindowToDisplayFront(display_front_window); } void ImGui::FocusTopMostWindowUnderOne(ImGuiWindow* under_this_window, ImGuiWindow* ignore_window, ImGuiViewport* filter_viewport, ImGuiFocusRequestFlags flags) { ImGuiContext& g = *GImGui; - IM_UNUSED(filter_viewport); // Unused in master branch. int start_idx = g.WindowsFocusOrder.Size - 1; if (under_this_window != NULL) { @@ -7012,11 +7843,17 @@ void ImGui::FocusTopMostWindowUnderOne(ImGuiWindow* under_this_window, ImGuiWind { // We may later decide to test for different NoXXXInputs based on the active navigation input (mouse vs nav) but that may feel more confusing to the user. ImGuiWindow* window = g.WindowsFocusOrder[i]; - IM_ASSERT(window == window->RootWindow); if (window == ignore_window || !window->WasActive) continue; + if (filter_viewport != NULL && window->Viewport != filter_viewport) + continue; if ((window->Flags & (ImGuiWindowFlags_NoMouseInputs | ImGuiWindowFlags_NoNavInputs)) != (ImGuiWindowFlags_NoMouseInputs | ImGuiWindowFlags_NoNavInputs)) { + // FIXME-DOCK: When ImGuiFocusRequestFlags_RestoreFocusedChild is set... + // This is failing (lagging by one frame) for docked windows. + // If A and B are docked into window and B disappear, at the NewFrame() call site window->NavLastChildNavWindow will still point to B. + // We might leverage the tab order implicitly stored in window->DockNodeAsHost->TabBar (essentially the 'most_recently_selected_tab' code in tab bar will do that but on next update) + // to tell which is the "previous" window. Or we may leverage 'LastFrameFocused/LastFrameJustFocused' and have this function handle child window itself? FocusWindow(window, flags); return; } @@ -7148,7 +7985,7 @@ void ImGui::PopTextWrapPos() window->DC.TextWrapPosStack.pop_back(); } -static ImGuiWindow* GetCombinedRootWindow(ImGuiWindow* window, bool popup_hierarchy) +static ImGuiWindow* GetCombinedRootWindow(ImGuiWindow* window, bool popup_hierarchy, bool dock_hierarchy) { ImGuiWindow* last_window = NULL; while (last_window != window) @@ -7157,13 +7994,15 @@ static ImGuiWindow* GetCombinedRootWindow(ImGuiWindow* window, bool popup_hierar window = window->RootWindow; if (popup_hierarchy) window = window->RootWindowPopupTree; - } + if (dock_hierarchy) + window = window->RootWindowDockTree; + } return window; } -bool ImGui::IsWindowChildOf(ImGuiWindow* window, ImGuiWindow* potential_parent, bool popup_hierarchy) +bool ImGui::IsWindowChildOf(ImGuiWindow* window, ImGuiWindow* potential_parent, bool popup_hierarchy, bool dock_hierarchy) { - ImGuiWindow* window_root = GetCombinedRootWindow(window, popup_hierarchy); + ImGuiWindow* window_root = GetCombinedRootWindow(window, popup_hierarchy, dock_hierarchy); if (window_root == potential_parent) return true; while (window != NULL) @@ -7210,9 +8049,14 @@ bool ImGui::IsWindowAbove(ImGuiWindow* potential_above, ImGuiWindow* potential_b return false; } +// Is current window hovered and hoverable (e.g. not blocked by a popup/modal)? See ImGuiHoveredFlags_ for options. +// IMPORTANT: If you are trying to check whether your mouse should be dispatched to Dear ImGui or to your underlying app, +// you should not use this function! Use the 'io.WantCaptureMouse' boolean for that! +// Refer to FAQ entry "How can I tell whether to dispatch mouse/keyboard to Dear ImGui or my application?" for details. bool ImGui::IsWindowHovered(ImGuiHoveredFlags flags) { - IM_ASSERT((flags & (ImGuiHoveredFlags_AllowWhenOverlapped | ImGuiHoveredFlags_AllowWhenDisabled)) == 0); // Flags not supported by this function + IM_ASSERT((flags & ~ImGuiHoveredFlags_AllowedMaskForIsWindowHovered) == 0 && "Invalid flags for IsWindowHovered()!"); + ImGuiContext& g = *GImGui; ImGuiWindow* ref_window = g.HoveredWindow; ImGuiWindow* cur_window = g.CurrentWindow; @@ -7223,12 +8067,13 @@ bool ImGui::IsWindowHovered(ImGuiHoveredFlags flags) { IM_ASSERT(cur_window); // Not inside a Begin()/End() const bool popup_hierarchy = (flags & ImGuiHoveredFlags_NoPopupHierarchy) == 0; + const bool dock_hierarchy = (flags & ImGuiHoveredFlags_DockHierarchy) != 0; if (flags & ImGuiHoveredFlags_RootWindow) - cur_window = GetCombinedRootWindow(cur_window, popup_hierarchy); + cur_window = GetCombinedRootWindow(cur_window, popup_hierarchy, dock_hierarchy); bool result; if (flags & ImGuiHoveredFlags_ChildWindows) - result = IsWindowChildOf(ref_window, cur_window, popup_hierarchy); + result = IsWindowChildOf(ref_window, cur_window, popup_hierarchy, dock_hierarchy); else result = (ref_window == cur_window); if (!result) @@ -7240,6 +8085,17 @@ bool ImGui::IsWindowHovered(ImGuiHoveredFlags flags) if (!(flags & ImGuiHoveredFlags_AllowWhenBlockedByActiveItem)) if (g.ActiveId != 0 && !g.ActiveIdAllowOverlap && g.ActiveId != ref_window->MoveId) return false; + + // When changing hovered window we requires a bit of stationary delay before activating hover timer. + // FIXME: We don't support delay other than stationary one for now, other delay would need a way + // to fullfill the possibility that multiple IsWindowHovered() with varying flag could return true + // for different windows of the hierarchy. Possibly need a Hash(Current+Flags) ==> (Timer) cache. + // We can implement this for _Stationary because the data is linked to HoveredWindow rather than CurrentWindow. + if (flags & ImGuiHoveredFlags_ForTooltip) + flags = ApplyHoverFlagsForTooltip(flags, g.Style.HoverFlagsForTooltipMouse); + if ((flags & ImGuiHoveredFlags_Stationary) != 0 && g.HoverWindowUnlockedStationaryId != ref_window->ID) + return false; + return true; } @@ -7256,15 +8112,28 @@ bool ImGui::IsWindowFocused(ImGuiFocusedFlags flags) IM_ASSERT(cur_window); // Not inside a Begin()/End() const bool popup_hierarchy = (flags & ImGuiFocusedFlags_NoPopupHierarchy) == 0; + const bool dock_hierarchy = (flags & ImGuiFocusedFlags_DockHierarchy) != 0; if (flags & ImGuiHoveredFlags_RootWindow) - cur_window = GetCombinedRootWindow(cur_window, popup_hierarchy); + cur_window = GetCombinedRootWindow(cur_window, popup_hierarchy, dock_hierarchy); if (flags & ImGuiHoveredFlags_ChildWindows) - return IsWindowChildOf(ref_window, cur_window, popup_hierarchy); + return IsWindowChildOf(ref_window, cur_window, popup_hierarchy, dock_hierarchy); else return (ref_window == cur_window); } +ImGuiID ImGui::GetWindowDockID() +{ + ImGuiContext& g = *GImGui; + return g.CurrentWindow->DockId; +} + +bool ImGui::IsWindowDocked() +{ + ImGuiContext& g = *GImGui; + return g.CurrentWindow->DockIsActive; +} + // Can we focus this window with CTRL+TAB (or PadMenu + PadFocusPrev/PadFocusNext) // Note that NoNavFocus makes the window not reachable with CTRL+TAB but it can still be focused with mouse or programmatically. // If you want a window to never be focused, you may use the e.g. NoInputs flag. @@ -7304,11 +8173,12 @@ void ImGui::SetWindowPos(ImGuiWindow* window, const ImVec2& pos, ImGuiCond cond) // Set const ImVec2 old_pos = window->Pos; - window->Pos = ImFloor(pos); + window->Pos = ImTrunc(pos); ImVec2 offset = window->Pos - old_pos; if (offset.x == 0.0f && offset.y == 0.0f) return; MarkIniSettingsDirty(window); + // FIXME: share code with TranslateWindow(), need to confirm whether the 3 rect modified by TranslateWindow() are desirable here. window->DC.CursorPos += offset; // As we happen to move the window while it is being appended to (which is a bad idea - will smear) let's at least offset the cursor window->DC.CursorMaxPos += offset; // And more importantly we need to offset CursorMaxPos/CursorStartPos this so ContentSize calculation doesn't get affected. window->DC.IdealMaxPos += offset; @@ -7342,18 +8212,22 @@ void ImGui::SetWindowSize(ImGuiWindow* window, const ImVec2& size, ImGuiCond con IM_ASSERT(cond == 0 || ImIsPowerOfTwo(cond)); // Make sure the user doesn't attempt to combine multiple condition flags. window->SetWindowSizeAllowFlags &= ~(ImGuiCond_Once | ImGuiCond_FirstUseEver | ImGuiCond_Appearing); + // Enable auto-fit (not done in BeginChild() path unless appearing or combined with ImGuiChildFlags_AlwaysAutoResize) + if ((window->Flags & ImGuiWindowFlags_ChildWindow) == 0 || window->Appearing || (window->ChildFlags & ImGuiChildFlags_AlwaysAutoResize) != 0) + window->AutoFitFramesX = (size.x <= 0.0f) ? 2 : 0; + if ((window->Flags & ImGuiWindowFlags_ChildWindow) == 0 || window->Appearing || (window->ChildFlags & ImGuiChildFlags_AlwaysAutoResize) != 0) + window->AutoFitFramesY = (size.y <= 0.0f) ? 2 : 0; + // Set ImVec2 old_size = window->SizeFull; - window->AutoFitFramesX = (size.x <= 0.0f) ? 2 : 0; - window->AutoFitFramesY = (size.y <= 0.0f) ? 2 : 0; if (size.x <= 0.0f) window->AutoFitOnlyGrows = false; else - window->SizeFull.x = IM_FLOOR(size.x); + window->SizeFull.x = IM_TRUNC(size.x); if (size.y <= 0.0f) window->AutoFitOnlyGrows = false; else - window->SizeFull.y = IM_FLOOR(size.y); + window->SizeFull.y = IM_TRUNC(size.y); if (old_size.x != window->SizeFull.x || old_size.y != window->SizeFull.y) MarkIniSettingsDirty(window); } @@ -7387,7 +8261,7 @@ void ImGui::SetWindowHitTestHole(ImGuiWindow* window, const ImVec2& pos, const I window->HitTestHoleOffset = ImVec2ih(pos - window->Pos); } -void ImGui::SetWindowHiddendAndSkipItemsForCurrentFrame(ImGuiWindow* window) +void ImGui::SetWindowHiddenAndSkipItemsForCurrentFrame(ImGuiWindow* window) { window->Hidden = window->SkipItems = true; window->HiddenFramesCanSkipItems = 1; @@ -7442,6 +8316,7 @@ void ImGui::SetNextWindowPos(const ImVec2& pos, ImGuiCond cond, const ImVec2& pi g.NextWindowData.PosVal = pos; g.NextWindowData.PosPivotVal = pivot; g.NextWindowData.PosCond = cond ? cond : ImGuiCond_Always; + g.NextWindowData.PosUndock = true; } void ImGui::SetNextWindowSize(const ImVec2& size, ImGuiCond cond) @@ -7453,6 +8328,10 @@ void ImGui::SetNextWindowSize(const ImVec2& size, ImGuiCond cond) g.NextWindowData.SizeCond = cond ? cond : ImGuiCond_Always; } +// For each axis: +// - Use 0.0f as min or FLT_MAX as max if you don't want limits, e.g. size_min = (500.0f, 0.0f), size_max = (FLT_MAX, FLT_MAX) sets a minimum width. +// - Use -1 for both min and max of same axis to preserve current size which itself is a constraint. +// - See "Demo->Examples->Constrained-resizing window" for examples. void ImGui::SetNextWindowSizeConstraints(const ImVec2& size_min, const ImVec2& size_max, ImGuiSizeCallback custom_callback, void* custom_callback_user_data) { ImGuiContext& g = *GImGui; @@ -7468,7 +8347,7 @@ void ImGui::SetNextWindowContentSize(const ImVec2& size) { ImGuiContext& g = *GImGui; g.NextWindowData.Flags |= ImGuiNextWindowDataFlags_HasContentSize; - g.NextWindowData.ContentSizeVal = ImFloor(size); + g.NextWindowData.ContentSizeVal = ImTrunc(size); } void ImGui::SetNextWindowScroll(const ImVec2& scroll) @@ -7500,12 +8379,48 @@ void ImGui::SetNextWindowBgAlpha(float alpha) g.NextWindowData.BgAlphaVal = alpha; } +void ImGui::SetNextWindowViewport(ImGuiID id) +{ + ImGuiContext& g = *GImGui; + g.NextWindowData.Flags |= ImGuiNextWindowDataFlags_HasViewport; + g.NextWindowData.ViewportId = id; +} + +void ImGui::SetNextWindowDockID(ImGuiID id, ImGuiCond cond) +{ + ImGuiContext& g = *GImGui; + g.NextWindowData.Flags |= ImGuiNextWindowDataFlags_HasDock; + g.NextWindowData.DockCond = cond ? cond : ImGuiCond_Always; + g.NextWindowData.DockId = id; +} + +void ImGui::SetNextWindowClass(const ImGuiWindowClass* window_class) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT((window_class->ViewportFlagsOverrideSet & window_class->ViewportFlagsOverrideClear) == 0); // Cannot set both set and clear for the same bit + g.NextWindowData.Flags |= ImGuiNextWindowDataFlags_HasWindowClass; + g.NextWindowData.WindowClass = *window_class; +} + ImDrawList* ImGui::GetWindowDrawList() { ImGuiWindow* window = GetCurrentWindow(); return window->DrawList; } +float ImGui::GetWindowDpiScale() +{ + ImGuiContext& g = *GImGui; + return g.CurrentDpiScale; +} + +ImGuiViewport* ImGui::GetWindowViewport() +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.CurrentViewport != NULL && g.CurrentViewport == g.CurrentWindow->Viewport); + return g.CurrentViewport; +} + ImFont* ImGui::GetFont() { return GImGui->Font; @@ -7530,35 +8445,89 @@ void ImGui::SetWindowFontScale(float scale) g.FontSize = g.DrawListSharedData.FontSize = window->CalcFontSize(); } -void ImGui::ActivateItem(ImGuiID id) -{ - ImGuiContext& g = *GImGui; - g.NavNextActivateId = id; - g.NavNextActivateFlags = ImGuiActivateFlags_None; -} - void ImGui::PushFocusScope(ImGuiID id) { ImGuiContext& g = *GImGui; - g.FocusScopeStack.push_back(id); + ImGuiFocusScopeData data; + data.ID = id; + data.WindowID = g.CurrentWindow->ID; + g.FocusScopeStack.push_back(data); g.CurrentFocusScopeId = id; } void ImGui::PopFocusScope() { ImGuiContext& g = *GImGui; - IM_ASSERT(g.FocusScopeStack.Size > 0); // Too many PopFocusScope() ? + if (g.FocusScopeStack.Size == 0) + { + IM_ASSERT_USER_ERROR(g.FocusScopeStack.Size > 0, "Calling PopFocusScope() too many times!"); + return; + } g.FocusScopeStack.pop_back(); - g.CurrentFocusScopeId = g.FocusScopeStack.Size ? g.FocusScopeStack.back() : 0; + g.CurrentFocusScopeId = g.FocusScopeStack.Size ? g.FocusScopeStack.back().ID : 0; +} + +void ImGui::SetNavFocusScope(ImGuiID focus_scope_id) +{ + ImGuiContext& g = *GImGui; + g.NavFocusScopeId = focus_scope_id; + g.NavFocusRoute.resize(0); // Invalidate + if (focus_scope_id == 0) + return; + IM_ASSERT(g.NavWindow != NULL); + + // Store current path (in reverse order) + if (focus_scope_id == g.CurrentFocusScopeId) + { + // Top of focus stack contains local focus scopes inside current window + for (int n = g.FocusScopeStack.Size - 1; n >= 0 && g.FocusScopeStack.Data[n].WindowID == g.CurrentWindow->ID; n--) + g.NavFocusRoute.push_back(g.FocusScopeStack.Data[n]); + } + else if (focus_scope_id == g.NavWindow->NavRootFocusScopeId) + g.NavFocusRoute.push_back({ focus_scope_id, g.NavWindow->ID }); + else + return; + + // Then follow on manually set ParentWindowForFocusRoute field (#6798) + for (ImGuiWindow* window = g.NavWindow->ParentWindowForFocusRoute; window != NULL; window = window->ParentWindowForFocusRoute) + g.NavFocusRoute.push_back({ window->NavRootFocusScopeId, window->ID }); + IM_ASSERT(g.NavFocusRoute.Size < 100); // Maximum depth is technically 251 as per CalcRoutingScore(): 254 - 3 +} + +// Focus = move navigation cursor, set scrolling, set focus window. +void ImGui::FocusItem() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + IMGUI_DEBUG_LOG_FOCUS("FocusItem(0x%08x) in window \"%s\"\n", g.LastItemData.ID, window->Name); + if (g.DragDropActive || g.MovingWindow != NULL) // FIXME: Opt-in flags for this? + { + IMGUI_DEBUG_LOG_FOCUS("FocusItem() ignored while DragDropActive!\n"); + return; + } + + ImGuiNavMoveFlags move_flags = ImGuiNavMoveFlags_IsTabbing | ImGuiNavMoveFlags_FocusApi | ImGuiNavMoveFlags_NoSetNavHighlight | ImGuiNavMoveFlags_NoSelect; + ImGuiScrollFlags scroll_flags = window->Appearing ? ImGuiScrollFlags_KeepVisibleEdgeX | ImGuiScrollFlags_AlwaysCenterY : ImGuiScrollFlags_KeepVisibleEdgeX | ImGuiScrollFlags_KeepVisibleEdgeY; + SetNavWindow(window); + NavMoveRequestSubmit(ImGuiDir_None, ImGuiDir_Up, move_flags, scroll_flags); + NavMoveRequestResolveWithLastItem(&g.NavMoveResultLocal); +} + +void ImGui::ActivateItemByID(ImGuiID id) +{ + ImGuiContext& g = *GImGui; + g.NavNextActivateId = id; + g.NavNextActivateFlags = ImGuiActivateFlags_None; } // Note: this will likely be called ActivateItem() once we rework our Focus/Activation system! +// But ActivateItem() should function without altering scroll/focus? void ImGui::SetKeyboardFocusHere(int offset) { ImGuiContext& g = *GImGui; ImGuiWindow* window = g.CurrentWindow; IM_ASSERT(offset >= -1); // -1 is allowed but not below - IMGUI_DEBUG_LOG_ACTIVEID("SetKeyboardFocusHere(%d) in window \"%s\"\n", offset, window->Name); + IMGUI_DEBUG_LOG_FOCUS("SetKeyboardFocusHere(%d) in window \"%s\"\n", offset, window->Name); // It makes sense in the vast majority of cases to never interrupt a drag and drop. // When we refactor this function into ActivateItem() we may want to make this an option. @@ -7566,14 +8535,15 @@ void ImGui::SetKeyboardFocusHere(int offset) // is also automatically dropped in the event g.ActiveId is stolen. if (g.DragDropActive || g.MovingWindow != NULL) { - IMGUI_DEBUG_LOG_ACTIVEID("SetKeyboardFocusHere() ignored while DragDropActive!\n"); + IMGUI_DEBUG_LOG_FOCUS("SetKeyboardFocusHere() ignored while DragDropActive!\n"); return; } SetNavWindow(window); + ImGuiNavMoveFlags move_flags = ImGuiNavMoveFlags_IsTabbing | ImGuiNavMoveFlags_Activate | ImGuiNavMoveFlags_FocusApi | ImGuiNavMoveFlags_NoSetNavHighlight; ImGuiScrollFlags scroll_flags = window->Appearing ? ImGuiScrollFlags_KeepVisibleEdgeX | ImGuiScrollFlags_AlwaysCenterY : ImGuiScrollFlags_KeepVisibleEdgeX | ImGuiScrollFlags_KeepVisibleEdgeY; - NavMoveRequestSubmit(ImGuiDir_None, offset < 0 ? ImGuiDir_Up : ImGuiDir_Down, ImGuiNavMoveFlags_Tabbing | ImGuiNavMoveFlags_FocusApi, scroll_flags); // FIXME-NAV: Once we refactor tabbing, add LegacyApi flag to not activate non-inputable. + NavMoveRequestSubmit(ImGuiDir_None, offset < 0 ? ImGuiDir_Up : ImGuiDir_Down, move_flags, scroll_flags); // FIXME-NAV: Once we refactor tabbing, add LegacyApi flag to not activate non-inputable. if (offset == -1) { NavMoveRequestResolveWithLastItem(&g.NavMoveResultLocal); @@ -7658,7 +8628,7 @@ void ImGui::PushOverrideID(ImGuiID id) } // Helper to avoid a common series of PushOverrideID -> GetID() -> PopID() call -// (note that when using this pattern, TestEngine's "Stack Tool" will tend to not display the intermediate stack level. +// (note that when using this pattern, ID Stack Tool will tend to not display the intermediate stack level. // for that to work we would need to do PushOverrideID() -> ItemAdd() -> PopID() which would alter widget code a little more) ImGuiID ImGui::GetIDWithSeed(const char* str, const char* str_end, ImGuiID seed) { @@ -7748,6 +8718,7 @@ bool ImGui::IsRectVisible(const ImVec2& rect_min, const ImVec2& rect_max) // - IsMouseDragPastThreshold() [Internal] // - IsMouseDragging() // - GetMousePos() +// - SetMousePos() [Internal] // - GetMousePosOnOpeningCurrentPopup() // - IsMousePosValid() // - IsAnyMouseDown() @@ -7779,6 +8750,26 @@ bool ImGui::IsRectVisible(const ImVec2& rect_min, const ImVec2& rect_max) // - Shortcut() [Internal] //----------------------------------------------------------------------------- +ImGuiKeyChord ImGui::FixupKeyChord(ImGuiContext* ctx, ImGuiKeyChord key_chord) +{ + // Convert ImGuiMod_Shortcut and add ImGuiMod_XXXX when a corresponding ImGuiKey_LeftXXX/ImGuiKey_RightXXX is specified. + ImGuiKey key = (ImGuiKey)(key_chord & ~ImGuiMod_Mask_); + if (IsModKey(key)) + { + if (key == ImGuiKey_LeftCtrl || key == ImGuiKey_RightCtrl) + key_chord |= ImGuiMod_Ctrl; + if (key == ImGuiKey_LeftShift || key == ImGuiKey_RightShift) + key_chord |= ImGuiMod_Shift; + if (key == ImGuiKey_LeftAlt || key == ImGuiKey_RightAlt) + key_chord |= ImGuiMod_Alt; + if (key == ImGuiKey_LeftSuper || key == ImGuiKey_RightSuper) + key_chord |= ImGuiMod_Super; + } + if (key_chord & ImGuiMod_Shortcut) + return (key_chord & ~ImGuiMod_Shortcut) | (ctx->IO.ConfigMacOSXBehaviors ? ImGuiMod_Super : ImGuiMod_Ctrl); + return key_chord; +} + ImGuiKeyData* ImGui::GetKeyData(ImGuiContext* ctx, ImGuiKey key) { ImGuiContext& g = *ctx; @@ -7816,11 +8807,13 @@ static const char* const GKeyNames[] = "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "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", "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12", + "F13", "F14", "F15", "F16", "F17", "F18", "F19", "F20", "F21", "F22", "F23", "F24", "Apostrophe", "Comma", "Minus", "Period", "Slash", "Semicolon", "Equal", "LeftBracket", "Backslash", "RightBracket", "GraveAccent", "CapsLock", "ScrollLock", "NumLock", "PrintScreen", "Pause", "Keypad0", "Keypad1", "Keypad2", "Keypad3", "Keypad4", "Keypad5", "Keypad6", "Keypad7", "Keypad8", "Keypad9", "KeypadDecimal", "KeypadDivide", "KeypadMultiply", "KeypadSubtract", "KeypadAdd", "KeypadEnter", "KeypadEqual", + "AppBack", "AppForward", "GamepadStart", "GamepadBack", "GamepadFaceLeft", "GamepadFaceRight", "GamepadFaceUp", "GamepadFaceDown", "GamepadDpadLeft", "GamepadDpadRight", "GamepadDpadUp", "GamepadDpadDown", @@ -7836,7 +8829,7 @@ const char* ImGui::GetKeyName(ImGuiKey key) { ImGuiContext& g = *GImGui; #ifdef IMGUI_DISABLE_OBSOLETE_KEYIO - IM_ASSERT((IsNamedKey(key) || key == ImGuiKey_None) && "Support for user key indices was dropped in favor of ImGuiKey. Please update backend and user code."); + IM_ASSERT((IsNamedKeyOrModKey(key) || key == ImGuiKey_None) && "Support for user key indices was dropped in favor of ImGuiKey. Please update backend and user code."); #else if (IsLegacyKey(key)) { @@ -7857,17 +8850,17 @@ const char* ImGui::GetKeyName(ImGuiKey key) } // ImGuiMod_Shortcut is translated to either Ctrl or Super. -void ImGui::GetKeyChordName(ImGuiKeyChord key_chord, char* out_buf, int out_buf_size) +const char* ImGui::GetKeyChordName(ImGuiKeyChord key_chord) { ImGuiContext& g = *GImGui; - if (key_chord & ImGuiMod_Shortcut) - key_chord = ConvertShortcutMod(key_chord); - ImFormatString(out_buf, (size_t)out_buf_size, "%s%s%s%s%s", + key_chord = FixupKeyChord(&g, key_chord); + ImFormatString(g.TempKeychordName, IM_ARRAYSIZE(g.TempKeychordName), "%s%s%s%s%s", (key_chord & ImGuiMod_Ctrl) ? "Ctrl+" : "", (key_chord & ImGuiMod_Shift) ? "Shift+" : "", (key_chord & ImGuiMod_Alt) ? "Alt+" : "", (key_chord & ImGuiMod_Super) ? (g.IO.ConfigMacOSXBehaviors ? "Cmd+" : "Super+") : "", GetKeyName((ImGuiKey)(key_chord & ~ImGuiMod_Mask_))); + return g.TempKeychordName; } // t0 = previous time (e.g.: g.Time - g.IO.DeltaTime) @@ -7934,6 +8927,7 @@ static void ImGui::UpdateKeyRoutingTable(ImGuiKeyRoutingTable* rt) for (int old_routing_idx = rt->Index[key - ImGuiKey_NamedKey_BEGIN]; old_routing_idx != -1; old_routing_idx = routing_entry->NextEntryIndex) { routing_entry = &rt->Entries[old_routing_idx]; + routing_entry->RoutingCurrScore = routing_entry->RoutingNextScore; routing_entry->RoutingCurr = routing_entry->RoutingNext; // Update entry routing_entry->RoutingNext = ImGuiKeyOwner_None; routing_entry->RoutingNextScore = 255; @@ -7942,11 +8936,15 @@ static void ImGui::UpdateKeyRoutingTable(ImGuiKeyRoutingTable* rt) rt->EntriesNext.push_back(*routing_entry); // Write alive ones into new buffer // Apply routing to owner if there's no owner already (RoutingCurr == None at this point) + // This is the result of previous frame's SetShortcutRouting() call. if (routing_entry->Mods == g.IO.KeyMods) { ImGuiKeyOwnerData* owner_data = GetKeyOwnerData(&g, key); if (owner_data->OwnerCurr == ImGuiKeyOwner_None) + { owner_data->OwnerCurr = routing_entry->RoutingCurr; + //IMGUI_DEBUG_LOG("SetKeyOwner(%s, owner_id=0x%08X) via Routing\n", GetKeyName(key), routing_entry->RoutingCurr); + } } } @@ -7976,13 +8974,11 @@ ImGuiKeyRoutingData* ImGui::GetShortcutRoutingData(ImGuiKeyChord key_chord) ImGuiContext& g = *GImGui; ImGuiKeyRoutingTable* rt = &g.KeysRoutingTable; ImGuiKeyRoutingData* routing_data; - if (key_chord & ImGuiMod_Shortcut) - key_chord = ConvertShortcutMod(key_chord); ImGuiKey key = (ImGuiKey)(key_chord & ~ImGuiMod_Mask_); ImGuiKey mods = (ImGuiKey)(key_chord & ImGuiMod_Mask_); if (key == ImGuiKey_None) key = ConvertSingleModFlagToKey(&g, mods); - IM_ASSERT(IsNamedKey(key)); + IM_ASSERT(IsNamedKey(key) && (key_chord & ImGuiMod_Shortcut) == 0); // Please call ConvertShortcutMod() in calling function. // Get (in the majority of case, the linked list will have one element so this should be 2 reads. // Subsequent elements will be contiguous in memory as list is sorted/rebuilt in NewFrame). @@ -8011,12 +9007,11 @@ ImGuiKeyRoutingData* ImGui::GetShortcutRoutingData(ImGuiKeyChord key_chord) // - 254: ImGuiInputFlags_RouteGlobalLow // - 255: never route // 'flags' should include an explicit routing policy -static int CalcRoutingScore(ImGuiWindow* location, ImGuiID owner_id, ImGuiInputFlags flags) +static int CalcRoutingScore(ImGuiID focus_scope_id, ImGuiID owner_id, ImGuiInputFlags flags) { if (flags & ImGuiInputFlags_RouteFocused) { ImGuiContext& g = *GImGui; - ImGuiWindow* focused = g.NavWindow; // ActiveID gets top priority // (we don't check g.ActiveIdUsingAllKeys here. Routing is applied but if input ownership is tested later it may discard it) @@ -8029,16 +9024,13 @@ static int CalcRoutingScore(ImGuiWindow* location, ImGuiID owner_id, ImGuiInputF // - When Window/ChildB is focused -> Window scores 4, Window/ChildB scores 3 (best) // Assuming only WindowA is submitting a routing request, // - When Window/ChildB is focused -> Window scores 4 (best), Window/ChildB doesn't have a score. - if (focused != NULL && focused->RootWindow == location->RootWindow) - for (int next_score = 3; focused != NULL; next_score++) - { - if (focused == location) - { - IM_ASSERT(next_score < 255); - return next_score; - } - focused = (focused->RootWindow != focused) ? focused->ParentWindow : NULL; // FIXME: This could be later abstracted as a focus path - } + // This essentially follow the window->ParentWindowForFocusRoute chain. + if (focus_scope_id == 0) + return 255; + for (int index_in_focus_path = 0; index_in_focus_path < g.NavFocusRoute.Size; index_in_focus_path++) + if (g.NavFocusRoute.Data[index_in_focus_path].ID == focus_scope_id) + return 3 + index_in_focus_path; + return 255; } @@ -8050,13 +9042,29 @@ static int CalcRoutingScore(ImGuiWindow* location, ImGuiID owner_id, ImGuiInputF return 0; } +// We need this to filter some Shortcut() routes when an item e.g. an InputText() is active +// e.g. ImGuiKey_G won't be considered a shortcut when item is active, but ImGuiMod|ImGuiKey_G can be. +static bool IsKeyChordPotentiallyCharInput(ImGuiKeyChord key_chord) +{ + // Mimic 'ignore_char_inputs' logic in InputText() + ImGuiContext& g = *GImGui; + + // When the right mods are pressed it cannot be a char input so we won't filter the shortcut out. + ImGuiKey mods = (ImGuiKey)(key_chord & ImGuiMod_Mask_); + const bool ignore_char_inputs = ((mods & ImGuiMod_Ctrl) && !(mods & ImGuiMod_Alt)) || (g.IO.ConfigMacOSXBehaviors && (mods & ImGuiMod_Super)); + if (ignore_char_inputs) + return false; + + // Return true for A-Z, 0-9 and other keys associated to char inputs. Other keys such as F1-F12 won't be filtered. + ImGuiKey key = (ImGuiKey)(key_chord & ~ImGuiMod_Mask_); + return g.KeysMayBeCharInput.TestBit(key); +} + // Request a desired route for an input chord (key + mods). // Return true if the route is available this frame. // - Routes and key ownership are attributed at the beginning of next frame based on best score and mod state. // (Conceptually this does a "Submit for next frame" + "Test for current frame". // As such, it could be called TrySetXXX or SubmitXXX, or the Submit and Test operations should be separate.) -// - Using 'owner_id == ImGuiKeyOwner_Any/0': auto-assign an owner based on current focus scope (each window has its focus scope by default) -// - Using 'owner_id == ImGuiKeyOwner_None': allows disabling/locking a shortcut. bool ImGui::SetShortcutRouting(ImGuiKeyChord key_chord, ImGuiID owner_id, ImGuiInputFlags flags) { ImGuiContext& g = *GImGui; @@ -8064,37 +9072,80 @@ bool ImGui::SetShortcutRouting(ImGuiKeyChord key_chord, ImGuiID owner_id, ImGuiI flags |= ImGuiInputFlags_RouteGlobalHigh; // IMPORTANT: This is the default for SetShortcutRouting() but NOT Shortcut() else IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiInputFlags_RouteMask_)); // Check that only 1 routing flag is used + IM_ASSERT(owner_id != ImGuiKeyOwner_Any && owner_id != ImGuiKeyOwner_None); + + // Convert ImGuiMod_Shortcut and add ImGuiMod_XXXX when a corresponding ImGuiKey_LeftXXX/ImGuiKey_RightXXX is specified. + key_chord = FixupKeyChord(&g, key_chord); + + // [DEBUG] Debug break requested by user + if (g.DebugBreakInShortcutRouting == key_chord) + IM_DEBUG_BREAK(); if (flags & ImGuiInputFlags_RouteUnlessBgFocused) if (g.NavWindow == NULL) return false; - if (flags & ImGuiInputFlags_RouteAlways) - return true; - const int score = CalcRoutingScore(g.CurrentWindow, owner_id, flags); + // Note how ImGuiInputFlags_RouteAlways won't set routing and thus won't set owner. May want to rework this? + if (flags & ImGuiInputFlags_RouteAlways) + { + IMGUI_DEBUG_LOG_INPUTROUTING("SetShortcutRouting(%s, owner_id=0x%08X, flags=%04X) -> always\n", GetKeyChordName(key_chord), owner_id, flags); + return true; + } + + // Specific culling when there's an active. + if (g.ActiveId != 0 && g.ActiveId != owner_id) + { + // Cull shortcuts with no modifiers when it could generate a character. + // e.g. Shortcut(ImGuiKey_G) also generates 'g' character, should not trigger when InputText() is active. + // but Shortcut(Ctrl+G) should generally trigger when InputText() is active. + // TL;DR: lettered shortcut with no mods or with only Alt mod will not trigger while an item reading text input is active. + // (We cannot filter based on io.InputQueueCharacters[] contents because of trickling and key<>chars submission order are undefined) + if ((flags & ImGuiInputFlags_RouteFocused) && g.IO.WantTextInput && IsKeyChordPotentiallyCharInput(key_chord)) + { + IMGUI_DEBUG_LOG_INPUTROUTING("SetShortcutRouting(%s, owner_id=0x%08X, flags=%04X) -> filtered as potential char input\n", GetKeyChordName(key_chord), owner_id, flags); + return false; + } + + // ActiveIdUsingAllKeyboardKeys trumps all for ActiveId + if ((flags & ImGuiInputFlags_RouteGlobalHigh) == 0 && g.ActiveIdUsingAllKeyboardKeys) + { + ImGuiKey key = (ImGuiKey)(key_chord & ~ImGuiMod_Mask_); + if (key == ImGuiKey_None) + key = ConvertSingleModFlagToKey(&g, (ImGuiKey)(key_chord & ImGuiMod_Mask_)); + if (key >= ImGuiKey_Keyboard_BEGIN && key < ImGuiKey_Keyboard_END) + return false; + } + } + + // FIXME-SHORTCUT: A way to configure the location/focus-scope to test would render this more flexible. + const int score = CalcRoutingScore(g.CurrentFocusScopeId, owner_id, flags); + IMGUI_DEBUG_LOG_INPUTROUTING("SetShortcutRouting(%s, owner_id=0x%08X, flags=%04X) -> score %d\n", GetKeyChordName(key_chord), owner_id, flags, score); if (score == 255) return false; // Submit routing for NEXT frame (assuming score is sufficient) // FIXME: Could expose a way to use a "serve last" policy for same score resolution (using <= instead of <). ImGuiKeyRoutingData* routing_data = GetShortcutRoutingData(key_chord); - const ImGuiID routing_id = GetRoutingIdFromOwnerId(owner_id); //const bool set_route = (flags & ImGuiInputFlags_ServeLast) ? (score <= routing_data->RoutingNextScore) : (score < routing_data->RoutingNextScore); if (score < routing_data->RoutingNextScore) { - routing_data->RoutingNext = routing_id; + routing_data->RoutingNext = owner_id; routing_data->RoutingNextScore = (ImU8)score; } // Return routing state for CURRENT frame - return routing_data->RoutingCurr == routing_id; + if (routing_data->RoutingCurr == owner_id) + IMGUI_DEBUG_LOG_INPUTROUTING("--> granting current route\n"); + return routing_data->RoutingCurr == owner_id; } // Currently unused by core (but used by tests) // Note: this cannot be turned into GetShortcutRouting() because we do the owner_id->routing_id translation, name would be more misleading. bool ImGui::TestShortcutRouting(ImGuiKeyChord key_chord, ImGuiID owner_id) { + ImGuiContext& g = *GImGui; const ImGuiID routing_id = GetRoutingIdFromOwnerId(owner_id); + key_chord = FixupKeyChord(&g, key_chord); ImGuiKeyRoutingData* routing_data = GetShortcutRoutingData(key_chord); // FIXME: Could avoid creating entry. return routing_data->RoutingCurr == routing_id; } @@ -8131,13 +9182,28 @@ bool ImGui::IsKeyPressed(ImGuiKey key, ImGuiID owner_id, ImGuiInputFlags flags) if (t < 0.0f) return false; IM_ASSERT((flags & ~ImGuiInputFlags_SupportedByIsKeyPressed) == 0); // Passing flags not supported by this function! + if (flags & (ImGuiInputFlags_RepeatRateMask_ | ImGuiInputFlags_RepeatUntilMask_)) // Setting any _RepeatXXX option enables _Repeat + flags |= ImGuiInputFlags_Repeat; bool pressed = (t == 0.0f); - if (!pressed && ((flags & ImGuiInputFlags_Repeat) != 0)) + if (!pressed && (flags & ImGuiInputFlags_Repeat) != 0) { float repeat_delay, repeat_rate; GetTypematicRepeatRate(flags, &repeat_delay, &repeat_rate); pressed = (t > repeat_delay) && GetKeyPressedAmount(key, repeat_delay, repeat_rate) > 0; + if (pressed && (flags & ImGuiInputFlags_RepeatUntilMask_)) + { + // Slightly bias 'key_pressed_time' as DownDuration is an accumulation of DeltaTime which we compare to an absolute time value. + // Ideally we'd replace DownDuration with KeyPressedTime but it would break user's code. + ImGuiContext& g = *GImGui; + double key_pressed_time = g.Time - t + 0.00001f; + if ((flags & ImGuiInputFlags_RepeatUntilKeyModsChange) && (g.LastKeyModsChangeTime > key_pressed_time)) + pressed = false; + if ((flags & ImGuiInputFlags_RepeatUntilKeyModsChangeFromNone) && (g.LastKeyModsChangeFromNoneTime > key_pressed_time)) + pressed = false; + if ((flags & ImGuiInputFlags_RepeatUntilOtherKeyPress) && (g.LastKeyboardKeyPressTime > key_pressed_time)) + pressed = false; + } } if (!pressed) return false; @@ -8189,7 +9255,7 @@ bool ImGui::IsMouseClicked(ImGuiMouseButton button, ImGuiID owner_id, ImGuiInput const float t = g.IO.MouseDownDuration[button]; if (t < 0.0f) return false; - IM_ASSERT((flags & ~ImGuiInputFlags_SupportedByIsKeyPressed) == 0); // Passing flags not supported by this function! + IM_ASSERT((flags & ~ImGuiInputFlags_SupportedByIsMouseClicked) == 0); // Passing flags not supported by this function! // FIXME: Could support RepeatRate and RepeatUntil flags here. const bool repeat = (flags & ImGuiInputFlags_Repeat) != 0; const bool pressed = (t == 0.0f) || (repeat && t > g.IO.KeyRepeatDelay && CalcTypematicRepeatAmount(t - g.IO.DeltaTime, t, g.IO.KeyRepeatDelay, g.IO.KeyRepeatRate) > 0); @@ -8223,6 +9289,13 @@ bool ImGui::IsMouseDoubleClicked(ImGuiMouseButton button) return g.IO.MouseClickedCount[button] == 2 && TestKeyOwner(MouseButtonToKey(button), ImGuiKeyOwner_Any); } +bool ImGui::IsMouseDoubleClicked(ImGuiMouseButton button, ImGuiID owner_id) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + return g.IO.MouseClickedCount[button] == 2 && TestKeyOwner(MouseButtonToKey(button), owner_id); +} + int ImGui::GetMouseClickedCount(ImGuiMouseButton button) { ImGuiContext& g = *GImGui; @@ -8242,9 +9315,10 @@ bool ImGui::IsMouseHoveringRect(const ImVec2& r_min, const ImVec2& r_max, bool c if (clip) rect_clipped.ClipWith(g.CurrentWindow->ClipRect); - // Expand for touch input - const ImRect rect_for_touch(rect_clipped.Min - g.Style.TouchExtraPadding, rect_clipped.Max + g.Style.TouchExtraPadding); - if (!rect_for_touch.Contains(g.IO.MousePos)) + // Hit testing, expanded for touch input + if (!rect_clipped.ContainsWithPad(g.IO.MousePos, g.Style.TouchExtraPadding)) + return false; + if (!g.MouseViewport->GetMainRect().Overlaps(rect_clipped)) return false; return true; } @@ -8275,6 +9349,17 @@ ImVec2 ImGui::GetMousePos() return g.IO.MousePos; } +// This is called TeleportMousePos() and not SetMousePos() to emphasis that setting MousePosPrev will effectively clear mouse delta as well. +// It is expected you only call this if (io.BackendFlags & ImGuiBackendFlags_HasSetMousePos) is set and supported by backend. +void ImGui::TeleportMousePos(const ImVec2& pos) +{ + ImGuiContext& g = *GImGui; + g.IO.MousePos = g.IO.MousePosPrev = pos; + g.IO.MouseDelta = ImVec2(0.0f, 0.0f); + g.IO.WantSetMousePos = true; + //IMGUI_DEBUG_LOG_IO("TeleportMousePos: (%.1f,%.1f)\n", io.MousePos.x, io.MousePos.y); +} + // NB: prefer to call right after BeginPopup(). At the time Selectable/MenuItem is activated, the popup is already closed! ImVec2 ImGui::GetMousePosOnOpeningCurrentPopup() { @@ -8410,7 +9495,9 @@ static void ImGui::UpdateKeyboardInputs() GetKeyData(ImGuiMod_Super)->Down = io.KeySuper; } } +#endif + // Import legacy ImGuiNavInput_ io inputs and convert to gamepad keys #ifndef IMGUI_DISABLE_OBSOLETE_KEYIO const bool nav_gamepad_active = (io.ConfigFlags & ImGuiConfigFlags_NavEnableGamepad) != 0 && (io.BackendFlags & ImGuiBackendFlags_HasGamepad) != 0; if (io.BackendUsingLegacyNavInputArray && nav_gamepad_active) @@ -8433,7 +9520,6 @@ static void ImGui::UpdateKeyboardInputs() MAP_LEGACY_NAV_INPUT_TO_KEY1(ImGuiKey_GamepadLStickDown, ImGuiNavInput_LStickDown); #undef NAV_MAP_KEY } -#endif #endif // Update aliases @@ -8442,15 +9528,20 @@ static void ImGui::UpdateKeyboardInputs() UpdateAliasKey(ImGuiKey_MouseWheelX, io.MouseWheelH != 0.0f, io.MouseWheelH); UpdateAliasKey(ImGuiKey_MouseWheelY, io.MouseWheel != 0.0f, io.MouseWheel); - // Synchronize io.KeyMods and io.KeyXXX values. + // Synchronize io.KeyMods and io.KeyCtrl/io.KeyShift/etc. values. // - New backends (1.87+): send io.AddKeyEvent(ImGuiMod_XXX) -> -> (here) deriving io.KeyMods + io.KeyXXX from key array. // - Legacy backends: set io.KeyXXX bools -> (above) set key array from io.KeyXXX -> (here) deriving io.KeyMods + io.KeyXXX from key array. // So with legacy backends the 4 values will do a unnecessary back-and-forth but it makes the code simpler and future facing. + const ImGuiKeyChord prev_key_mods = io.KeyMods; io.KeyMods = GetMergedModsFromKeys(); io.KeyCtrl = (io.KeyMods & ImGuiMod_Ctrl) != 0; io.KeyShift = (io.KeyMods & ImGuiMod_Shift) != 0; io.KeyAlt = (io.KeyMods & ImGuiMod_Alt) != 0; io.KeySuper = (io.KeyMods & ImGuiMod_Super) != 0; + if (prev_key_mods != io.KeyMods) + g.LastKeyModsChangeTime = g.Time; + if (prev_key_mods != io.KeyMods && prev_key_mods == 0) + g.LastKeyModsChangeFromNoneTime = g.Time; // Clear gamepad data if disabled if ((io.BackendFlags & ImGuiBackendFlags_HasGamepad) == 0) @@ -8466,6 +9557,14 @@ static void ImGui::UpdateKeyboardInputs() ImGuiKeyData* key_data = &io.KeysData[i]; key_data->DownDurationPrev = key_data->DownDuration; key_data->DownDuration = key_data->Down ? (key_data->DownDuration < 0.0f ? 0.0f : key_data->DownDuration + io.DeltaTime) : -1.0f; + if (key_data->DownDuration == 0.0f) + { + ImGuiKey key = (ImGuiKey)(ImGuiKey_KeysData_OFFSET + i); + if (IsKeyboardKey(key)) + g.LastKeyboardKeyPressTime = g.Time; + else if (key == ImGuiKey_ReservedForModCtrl || key == ImGuiKey_ReservedForModShift || key == ImGuiKey_ReservedForModAlt || key == ImGuiKey_ReservedForModSuper) + g.LastKeyboardKeyPressTime = g.Time; + } } // Update keys/input owner (named keys only): one entry per key @@ -8479,6 +9578,7 @@ static void ImGui::UpdateKeyboardInputs() owner_data->LockThisFrame = owner_data->LockUntilRelease = owner_data->LockUntilRelease && key_data->Down; // Clear LockUntilRelease when key is not Down anymore } + // Update key routing (for e.g. shortcuts) UpdateKeyRoutingTable(&g.KeysRoutingTable); } @@ -8496,7 +9596,7 @@ static void ImGui::UpdateMouseInputs() // Round mouse position to avoid spreading non-rounded position (e.g. UpdateManualResize doesn't support them well) if (IsMousePosValid(&io.MousePos)) - io.MousePos = g.MouseLastValidPos = ImFloorSigned(io.MousePos); + io.MousePos = g.MouseLastValidPos = ImFloor(io.MousePos); // If mouse just appeared or disappeared (usually denoted by -FLT_MAX components) we cancel out movement in MouseDelta if (IsMousePosValid(&io.MousePos) && IsMousePosValid(&io.MousePosPrev)) @@ -8504,11 +9604,17 @@ static void ImGui::UpdateMouseInputs() else io.MouseDelta = ImVec2(0.0f, 0.0f); + // Update stationary timer. + // FIXME: May need to rework again to have some tolerance for occasional small movement, while being functional on high-framerates. + const float mouse_stationary_threshold = (io.MouseSource == ImGuiMouseSource_Mouse) ? 2.0f : 3.0f; // Slightly higher threshold for ImGuiMouseSource_TouchScreen/ImGuiMouseSource_Pen, may need rework. + const bool mouse_stationary = (ImLengthSqr(io.MouseDelta) <= mouse_stationary_threshold * mouse_stationary_threshold); + g.MouseStationaryTimer = mouse_stationary ? (g.MouseStationaryTimer + io.DeltaTime) : 0.0f; + //IMGUI_DEBUG_LOG("%.4f\n", g.MouseStationaryTimer); + // If mouse moved we re-enable mouse hovering in case it was disabled by gamepad/keyboard. In theory should use a >0.0f threshold but would need to reset in everywhere we set this to true. if (io.MouseDelta.x != 0.0f || io.MouseDelta.y != 0.0f) g.NavDisableMouseHover = false; - io.MousePosPrev = io.MousePos; for (int i = 0; i < IM_ARRAYSIZE(io.MouseDown); i++) { io.MouseClicked[i] = io.MouseDown[i] && io.MouseDownDuration[i] < 0.0f; @@ -8532,13 +9638,16 @@ static void ImGui::UpdateMouseInputs() io.MouseClickedTime[i] = g.Time; io.MouseClickedPos[i] = io.MousePos; io.MouseClickedCount[i] = io.MouseClickedLastCount[i]; + io.MouseDragMaxDistanceAbs[i] = ImVec2(0.0f, 0.0f); io.MouseDragMaxDistanceSqr[i] = 0.0f; } else if (io.MouseDown[i]) { // Maintain the maximum distance we reaching from the initial click position, which is used with dragging threshold - float delta_sqr_click_pos = IsMousePosValid(&io.MousePos) ? ImLengthSqr(io.MousePos - io.MouseClickedPos[i]) : 0.0f; - io.MouseDragMaxDistanceSqr[i] = ImMax(io.MouseDragMaxDistanceSqr[i], delta_sqr_click_pos); + ImVec2 delta_from_click_pos = IsMousePosValid(&io.MousePos) ? (io.MousePos - io.MouseClickedPos[i]) : ImVec2(0.0f, 0.0f); + io.MouseDragMaxDistanceSqr[i] = ImMax(io.MouseDragMaxDistanceSqr[i], ImLengthSqr(delta_from_click_pos)); + io.MouseDragMaxDistanceAbs[i].x = ImMax(io.MouseDragMaxDistanceAbs[i].x, delta_from_click_pos.x < 0.0f ? -delta_from_click_pos.x : delta_from_click_pos.x); + io.MouseDragMaxDistanceAbs[i].y = ImMax(io.MouseDragMaxDistanceAbs[i].y, delta_from_click_pos.y < 0.0f ? -delta_from_click_pos.y : delta_from_click_pos.y); } // We provide io.MouseDoubleClicked[] as a legacy service @@ -8645,8 +9754,8 @@ void ImGui::UpdateMouseWheel() { const ImVec2 offset = window->Size * (1.0f - scale) * (g.IO.MousePos - window->Pos) / window->Size; SetWindowPos(window, window->Pos + offset, 0); - window->Size = ImFloor(window->Size * scale); - window->SizeFull = ImFloor(window->SizeFull * scale); + window->Size = ImTrunc(window->Size * scale); + window->SizeFull = ImTrunc(window->SizeFull * scale); } return; } @@ -8682,15 +9791,17 @@ void ImGui::UpdateMouseWheel() { LockWheelingWindow(window, wheel.x); float max_step = window->InnerRect.GetWidth() * 0.67f; - float scroll_step = ImFloor(ImMin(2 * window->CalcFontSize(), max_step)); + float scroll_step = ImTrunc(ImMin(2 * window->CalcFontSize(), max_step)); SetScrollX(window, window->Scroll.x - wheel.x * scroll_step); + g.WheelingWindowScrolledFrame = g.FrameCount; } if (do_scroll[ImGuiAxis_Y]) { LockWheelingWindow(window, wheel.y); float max_step = window->InnerRect.GetHeight() * 0.67f; - float scroll_step = ImFloor(ImMin(5 * window->CalcFontSize(), max_step)); + float scroll_step = ImTrunc(ImMin(5 * window->CalcFontSize(), max_step)); SetScrollY(window, window->Scroll.y - wheel.y * scroll_step); + g.WheelingWindowScrolledFrame = g.FrameCount; } } } @@ -8723,9 +9834,10 @@ static const char* GetMouseSourceName(ImGuiMouseSource source) static void DebugPrintInputEvent(const char* prefix, const ImGuiInputEvent* e) { ImGuiContext& g = *GImGui; - if (e->Type == ImGuiInputEventType_MousePos) { if (e->MousePos.PosX == -FLT_MAX && e->MousePos.PosY == -FLT_MAX) IMGUI_DEBUG_LOG_IO("[io] %s: MousePos (-FLT_MAX, -FLT_MAX)\n", prefix); else IMGUI_DEBUG_LOG_IO("[io] %s: MousePos (%.1f, %.1f) (%s)\n", prefix, e->MousePos.PosX, e->MousePos.PosY, GetMouseSourceName(e->MouseWheel.MouseSource)); return; } - if (e->Type == ImGuiInputEventType_MouseButton) { IMGUI_DEBUG_LOG_IO("[io] %s: MouseButton %d %s (%s)\n", prefix, e->MouseButton.Button, e->MouseButton.Down ? "Down" : "Up", GetMouseSourceName(e->MouseWheel.MouseSource)); return; } + if (e->Type == ImGuiInputEventType_MousePos) { if (e->MousePos.PosX == -FLT_MAX && e->MousePos.PosY == -FLT_MAX) IMGUI_DEBUG_LOG_IO("[io] %s: MousePos (-FLT_MAX, -FLT_MAX)\n", prefix); else IMGUI_DEBUG_LOG_IO("[io] %s: MousePos (%.1f, %.1f) (%s)\n", prefix, e->MousePos.PosX, e->MousePos.PosY, GetMouseSourceName(e->MousePos.MouseSource)); return; } + if (e->Type == ImGuiInputEventType_MouseButton) { IMGUI_DEBUG_LOG_IO("[io] %s: MouseButton %d %s (%s)\n", prefix, e->MouseButton.Button, e->MouseButton.Down ? "Down" : "Up", GetMouseSourceName(e->MouseButton.MouseSource)); return; } if (e->Type == ImGuiInputEventType_MouseWheel) { IMGUI_DEBUG_LOG_IO("[io] %s: MouseWheel (%.3f, %.3f) (%s)\n", prefix, e->MouseWheel.WheelX, e->MouseWheel.WheelY, GetMouseSourceName(e->MouseWheel.MouseSource)); return; } + if (e->Type == ImGuiInputEventType_MouseViewport){IMGUI_DEBUG_LOG_IO("[io] %s: MouseViewport (0x%08X)\n", prefix, e->MouseViewport.HoveredViewportID); return; } if (e->Type == ImGuiInputEventType_Key) { IMGUI_DEBUG_LOG_IO("[io] %s: Key \"%s\" %s\n", prefix, ImGui::GetKeyName(e->Key.Key), e->Key.Down ? "Down" : "Up"); return; } if (e->Type == ImGuiInputEventType_Text) { IMGUI_DEBUG_LOG_IO("[io] %s: Text: %c (U+%08X)\n", prefix, e->Text.Char, e->Text.Char); return; } if (e->Type == ImGuiInputEventType_Focus) { IMGUI_DEBUG_LOG_IO("[io] %s: AppFocused %d\n", prefix, e->AppFocused.Focused); return; } @@ -8756,6 +9868,8 @@ void ImGui::UpdateInputEvents(bool trickle_fast_inputs) ImGuiInputEvent* e = &g.InputEventsQueue[event_n]; if (e->Type == ImGuiInputEventType_MousePos) { + if (g.IO.WantSetMousePos) + continue; // Trickling Rule: Stop processing queued events if we already handled a mouse button change ImVec2 event_pos(e->MousePos.PosX, e->MousePos.PosY); if (trickle_fast_inputs && (mouse_button_changed != 0 || mouse_wheeled || key_changed || text_inputted)) @@ -8787,6 +9901,10 @@ void ImGui::UpdateInputEvents(bool trickle_fast_inputs) io.MouseSource = e->MouseWheel.MouseSource; mouse_wheeled = true; } + else if (e->Type == ImGuiInputEventType_MouseViewport) + { + io.MouseHoveredViewport = e->MouseViewport.HoveredViewportID; + } else if (e->Type == ImGuiInputEventType_Key) { // Trickling Rule: Stop processing queued events if we got multiple action on the same button @@ -8912,10 +10030,11 @@ bool ImGui::TestKeyOwner(ImGuiKey key, ImGuiID owner_id) // - SetKeyOwner(..., Any or None, Lock) : set lock void ImGui::SetKeyOwner(ImGuiKey key, ImGuiID owner_id, ImGuiInputFlags flags) { + ImGuiContext& g = *GImGui; IM_ASSERT(IsNamedKeyOrModKey(key) && (owner_id != ImGuiKeyOwner_Any || (flags & (ImGuiInputFlags_LockThisFrame | ImGuiInputFlags_LockUntilRelease)))); // Can only use _Any with _LockXXX flags (to eat a key away without an ID to retrieve it) IM_ASSERT((flags & ~ImGuiInputFlags_SupportedBySetKeyOwner) == 0); // Passing flags not supported by this function! + //IMGUI_DEBUG_LOG("SetKeyOwner(%s, owner_id=0x%08X, flags=%08X)\n", GetKeyName(key), owner_id, flags); - ImGuiContext& g = *GImGui; ImGuiKeyOwnerData* owner_data = GetKeyOwnerData(&g, key); owner_data->OwnerCurr = owner_data->OwnerNext = owner_id; @@ -8957,18 +10076,17 @@ void ImGui::SetItemKeyOwner(ImGuiKey key, ImGuiInputFlags flags) } } -bool ImGui::Shortcut(ImGuiKeyChord key_chord, ImGuiID owner_id, ImGuiInputFlags flags) +// This is the only public API until we expose owner_id versions of the API as replacements. +bool ImGui::IsKeyChordPressed(ImGuiKeyChord key_chord) +{ + return IsKeyChordPressed(key_chord, 0, ImGuiInputFlags_None); +} + +// This is equivalent to comparing KeyMods + doing a IsKeyPressed() +bool ImGui::IsKeyChordPressed(ImGuiKeyChord key_chord, ImGuiID owner_id, ImGuiInputFlags flags) { ImGuiContext& g = *GImGui; - - // When using (owner_id == 0/Any): SetShortcutRouting() will use CurrentFocusScopeId and filter with this, so IsKeyPressed() is fine with he 0/Any. - if ((flags & ImGuiInputFlags_RouteMask_) == 0) - flags |= ImGuiInputFlags_RouteFocused; - if (!SetShortcutRouting(key_chord, owner_id, flags)) - return false; - - if (key_chord & ImGuiMod_Shortcut) - key_chord = ConvertShortcutMod(key_chord); + key_chord = FixupKeyChord(&g, key_chord); ImGuiKey mods = (ImGuiKey)(key_chord & ImGuiMod_Mask_); if (g.IO.KeyMods != mods) return false; @@ -8977,11 +10095,44 @@ bool ImGui::Shortcut(ImGuiKeyChord key_chord, ImGuiID owner_id, ImGuiInputFlags ImGuiKey key = (ImGuiKey)(key_chord & ~ImGuiMod_Mask_); if (key == ImGuiKey_None) key = ConvertSingleModFlagToKey(&g, mods); + if (!IsKeyPressed(key, owner_id, (flags & ImGuiInputFlags_RepeatMask_))) + return false; + return true; +} - if (!IsKeyPressed(key, owner_id, (flags & (ImGuiInputFlags_Repeat | (ImGuiInputFlags)ImGuiInputFlags_RepeatRateMask_)))) +void ImGui::SetNextItemShortcut(ImGuiKeyChord key_chord) +{ + ImGuiContext& g = *GImGui; + g.NextItemData.Flags |= ImGuiNextItemDataFlags_HasShortcut; + g.NextItemData.Shortcut = key_chord; +} + +bool ImGui::Shortcut(ImGuiKeyChord key_chord, ImGuiID owner_id, ImGuiInputFlags flags) +{ + //ImGuiContext& g = *GImGui; + //IMGUI_DEBUG_LOG("Shortcut(%s, owner_id=0x%08X, flags=%X)\n", GetKeyChordName(key_chord, g.TempBuffer.Data, g.TempBuffer.Size), owner_id, flags); + + // When using (owner_id == 0/Any): SetShortcutRouting() will use CurrentFocusScopeId and filter with this, so IsKeyPressed() is fine with he 0/Any. + if ((flags & ImGuiInputFlags_RouteMask_) == 0) + flags |= ImGuiInputFlags_RouteFocused; + + // Using 'owner_id == ImGuiKeyOwner_Any/0': auto-assign an owner based on current focus scope (each window has its focus scope by default) + // Effectively makes Shortcut() always input-owner aware. + if (owner_id == ImGuiKeyOwner_Any || owner_id == ImGuiKeyOwner_None) + owner_id = GetRoutingIdFromOwnerId(owner_id); + + // Submit route + if (!SetShortcutRouting(key_chord, owner_id, flags)) + return false; + + // Default repeat behavior for Shortcut() + // So e.g. pressing Ctrl+W and releasing Ctrl while holding W will not trigger the W shortcut. + if ((flags & ImGuiInputFlags_Repeat) != 0 && (flags & ImGuiInputFlags_RepeatUntilMask_) == 0) + flags |= ImGuiInputFlags_RepeatUntilKeyModsChange; + + if (!IsKeyChordPressed(key_chord, owner_id, flags)) return false; IM_ASSERT((flags & ~ImGuiInputFlags_SupportedByShortcut) == 0); // Passing flags not supported by this function! - return true; } @@ -9087,6 +10238,45 @@ static void ImGui::ErrorCheckNewFrameSanityChecks() // Check: the io.ConfigWindowsResizeFromEdges option requires backend to honor mouse cursor changes and set the ImGuiBackendFlags_HasMouseCursors flag accordingly. if (g.IO.ConfigWindowsResizeFromEdges && !(g.IO.BackendFlags & ImGuiBackendFlags_HasMouseCursors)) g.IO.ConfigWindowsResizeFromEdges = false; + + // Perform simple check: error if Docking or Viewport are enabled _exactly_ on frame 1 (instead of frame 0 or later), which is a common error leading to loss of .ini data. + if (g.FrameCount == 1 && (g.IO.ConfigFlags & ImGuiConfigFlags_DockingEnable) && (g.ConfigFlagsLastFrame & ImGuiConfigFlags_DockingEnable) == 0) + IM_ASSERT(0 && "Please set DockingEnable before the first call to NewFrame()! Otherwise you will lose your .ini settings!"); + if (g.FrameCount == 1 && (g.IO.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) && (g.ConfigFlagsLastFrame & ImGuiConfigFlags_ViewportsEnable) == 0) + IM_ASSERT(0 && "Please set ViewportsEnable before the first call to NewFrame()! Otherwise you will lose your .ini settings!"); + + // Perform simple checks: multi-viewport and platform windows support + if (g.IO.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) + { + if ((g.IO.BackendFlags & ImGuiBackendFlags_PlatformHasViewports) && (g.IO.BackendFlags & ImGuiBackendFlags_RendererHasViewports)) + { + IM_ASSERT((g.FrameCount == 0 || g.FrameCount == g.FrameCountPlatformEnded) && "Forgot to call UpdatePlatformWindows() in main loop after EndFrame()? Check examples/ applications for reference."); + IM_ASSERT(g.PlatformIO.Platform_CreateWindow != NULL && "Platform init didn't install handlers?"); + IM_ASSERT(g.PlatformIO.Platform_DestroyWindow != NULL && "Platform init didn't install handlers?"); + IM_ASSERT(g.PlatformIO.Platform_GetWindowPos != NULL && "Platform init didn't install handlers?"); + IM_ASSERT(g.PlatformIO.Platform_SetWindowPos != NULL && "Platform init didn't install handlers?"); + IM_ASSERT(g.PlatformIO.Platform_GetWindowSize != NULL && "Platform init didn't install handlers?"); + IM_ASSERT(g.PlatformIO.Platform_SetWindowSize != NULL && "Platform init didn't install handlers?"); + IM_ASSERT(g.PlatformIO.Monitors.Size > 0 && "Platform init didn't setup Monitors list?"); + IM_ASSERT((g.Viewports[0]->PlatformUserData != NULL || g.Viewports[0]->PlatformHandle != NULL) && "Platform init didn't setup main viewport."); + if (g.IO.ConfigDockingTransparentPayload && (g.IO.ConfigFlags & ImGuiConfigFlags_DockingEnable)) + IM_ASSERT(g.PlatformIO.Platform_SetWindowAlpha != NULL && "Platform_SetWindowAlpha handler is required to use io.ConfigDockingTransparent!"); + } + else + { + // Disable feature, our backends do not support it + g.IO.ConfigFlags &= ~ImGuiConfigFlags_ViewportsEnable; + } + + // Perform simple checks on platform monitor data + compute a total bounding box for quick early outs + for (ImGuiPlatformMonitor& mon : g.PlatformIO.Monitors) + { + IM_UNUSED(mon); + IM_ASSERT(mon.MainSize.x > 0.0f && mon.MainSize.y > 0.0f && "Monitor main bounds not setup properly."); + IM_ASSERT(ImRect(mon.MainPos, mon.MainPos + mon.MainSize).Contains(ImRect(mon.WorkPos, mon.WorkPos + mon.WorkSize)) && "Monitor work bounds not setup properly. If you don't have work area information, just copy MainPos/MainSize into them."); + IM_ASSERT(mon.DpiScale != 0.0f); + } + } } static void ImGui::ErrorCheckEndFrameSanityChecks() @@ -9262,12 +10452,135 @@ void ImGuiStackSizes::CompareWithContextState(ImGuiContext* ctx) IM_ASSERT(SizeOfFocusScopeStack == g.FocusScopeStack.Size && "PushFocusScope/PopFocusScope Mismatch!"); } +//----------------------------------------------------------------------------- +// [SECTION] ITEM SUBMISSION +//----------------------------------------------------------------------------- +// - KeepAliveID() +// - ItemHandleShortcut() [Internal] +// - ItemAdd() +//----------------------------------------------------------------------------- + +// Code not using ItemAdd() may need to call this manually otherwise ActiveId will be cleared. In IMGUI_VERSION_NUM < 18717 this was called by GetID(). +void ImGui::KeepAliveID(ImGuiID id) +{ + ImGuiContext& g = *GImGui; + if (g.ActiveId == id) + g.ActiveIdIsAlive = id; + if (g.ActiveIdPreviousFrame == id) + g.ActiveIdPreviousFrameIsAlive = true; +} + +static void ItemHandleShortcut(ImGuiID id) +{ + // FIXME: Generalize Activation queue? + ImGuiContext& g = *GImGui; + if (ImGui::Shortcut(g.NextItemData.Shortcut, id, ImGuiInputFlags_None) && g.NavActivateId == 0) + { + g.NavActivateId = id; // Will effectively disable clipping. + g.NavActivateFlags = ImGuiActivateFlags_PreferInput | ImGuiActivateFlags_FromShortcut; + if (g.ActiveId == 0 || g.ActiveId == id) + g.NavActivateDownId = g.NavActivatePressedId = id; + ImGui::NavHighlightActivated(id); + } +} + +// Declare item bounding box for clipping and interaction. +// Note that the size can be different than the one provided to ItemSize(). Typically, widgets that spread over available surface +// declare their minimum size requirement to ItemSize() and provide a larger region to ItemAdd() which is used drawing/interaction. +// THIS IS IN THE PERFORMANCE CRITICAL PATH (UNTIL THE CLIPPING TEST AND EARLY-RETURN) +bool ImGui::ItemAdd(const ImRect& bb, ImGuiID id, const ImRect* nav_bb_arg, ImGuiItemFlags extra_flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + // Set item data + // (DisplayRect is left untouched, made valid when ImGuiItemStatusFlags_HasDisplayRect is set) + g.LastItemData.ID = id; + g.LastItemData.Rect = bb; + g.LastItemData.NavRect = nav_bb_arg ? *nav_bb_arg : bb; + g.LastItemData.InFlags = g.CurrentItemFlags | g.NextItemData.ItemFlags | extra_flags; + g.LastItemData.StatusFlags = ImGuiItemStatusFlags_None; + // Note: we don't copy 'g.NextItemData.SelectionUserData' to an hypothetical g.LastItemData.SelectionUserData: since the former is not cleared. + + if (id != 0) + { + KeepAliveID(id); + + // Directional navigation processing + // Runs prior to clipping early-out + // (a) So that NavInitRequest can be honored, for newly opened windows to select a default widget + // (b) So that we can scroll up/down past clipped items. This adds a small O(N) cost to regular navigation requests + // unfortunately, but it is still limited to one window. It may not scale very well for windows with ten of + // thousands of item, but at least NavMoveRequest is only set on user interaction, aka maximum once a frame. + // We could early out with "if (is_clipped && !g.NavInitRequest) return false;" but when we wouldn't be able + // to reach unclipped widgets. This would work if user had explicit scrolling control (e.g. mapped on a stick). + // We intentionally don't check if g.NavWindow != NULL because g.NavAnyRequest should only be set when it is non null. + // If we crash on a NULL g.NavWindow we need to fix the bug elsewhere. + if (!(g.LastItemData.InFlags & ImGuiItemFlags_NoNav)) + { + // FIMXE-NAV: investigate changing the window tests into a simple 'if (g.NavFocusScopeId == g.CurrentFocusScopeId)' test. + window->DC.NavLayersActiveMaskNext |= (1 << window->DC.NavLayerCurrent); + if (g.NavId == id || g.NavAnyRequest) + if (g.NavWindow->RootWindowForNav == window->RootWindowForNav) + if (window == g.NavWindow || ((window->Flags | g.NavWindow->Flags) & ImGuiWindowFlags_NavFlattened)) + NavProcessItem(); + } + + if (g.NextItemData.Flags & ImGuiNextItemDataFlags_HasShortcut) + ItemHandleShortcut(id); + } + + // Lightweight clear of SetNextItemXXX data. + g.NextItemData.Flags = ImGuiNextItemDataFlags_None; + g.NextItemData.ItemFlags = ImGuiItemFlags_None; + +#ifdef IMGUI_ENABLE_TEST_ENGINE + if (id != 0) + IMGUI_TEST_ENGINE_ITEM_ADD(id, g.LastItemData.NavRect, &g.LastItemData); +#endif + + // Clipping test + // (this is a modified copy of IsClippedEx() so we can reuse the is_rect_visible value) + //const bool is_clipped = IsClippedEx(bb, id); + //if (is_clipped) + // return false; + // g.NavActivateId is not necessarily == g.NavId, in the case of remote activation (e.g. shortcuts) + const bool is_rect_visible = bb.Overlaps(window->ClipRect); + if (!is_rect_visible) + if (id == 0 || (id != g.ActiveId && id != g.ActiveIdPreviousFrame && id != g.NavId && id != g.NavActivateId)) + if (!g.LogEnabled) + return false; + + // [DEBUG] +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + if (id != 0) + { + if (id == g.DebugLocateId) + DebugLocateItemResolveWithLastItem(); + + // [DEBUG] People keep stumbling on this problem and using "" as identifier in the root of a window instead of "##something". + // Empty identifier are valid and useful in a small amount of cases, but 99.9% of the time you want to use "##something". + // READ THE FAQ: https://dearimgui.com/faq + IM_ASSERT(id != window->ID && "Cannot have an empty ID at the root of a window. If you need an empty label, use ## and read the FAQ about how the ID Stack works!"); + } + //if (g.IO.KeyAlt) window->DrawList->AddRect(bb.Min, bb.Max, IM_COL32(255,255,0,120)); // [DEBUG] + //if ((g.LastItemData.InFlags & ImGuiItemFlags_NoNav) == 0) + // window->DrawList->AddRect(g.LastItemData.NavRect.Min, g.LastItemData.NavRect.Max, IM_COL32(255,255,0,255)); // [DEBUG] +#endif + + // We need to calculate this now to take account of the current clipping rectangle (as items like Selectable may change them) + if (is_rect_visible) + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_Visible; + if (IsMouseHoveringRect(bb.Min, bb.Max)) + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredRect; + return true; +} + //----------------------------------------------------------------------------- // [SECTION] LAYOUT //----------------------------------------------------------------------------- // - ItemSize() -// - ItemAdd() // - SameLine() // - GetCursorScreenPos() // - SetCursorScreenPos() @@ -9298,6 +10611,7 @@ void ImGuiStackSizes::CompareWithContextState(ImGuiContext* ctx) // Advance cursor given item size for layout. // Register minimum needed size so it can extend the bounding box used for auto-fit calculation. // See comments in ItemAdd() about how/why the size provided to ItemSize() vs ItemAdd() may often different. +// THIS IS IN THE PERFORMANCE CRITICAL PATH. void ImGui::ItemSize(const ImVec2& size, float text_baseline_y) { ImGuiContext& g = *GImGui; @@ -9317,8 +10631,8 @@ void ImGui::ItemSize(const ImVec2& size, float text_baseline_y) //if (g.IO.KeyAlt) window->DrawList->AddRect(window->DC.CursorPos, window->DC.CursorPos + ImVec2(size.x, line_height), IM_COL32(255,0,0,200)); // [DEBUG] window->DC.CursorPosPrevLine.x = window->DC.CursorPos.x + size.x; window->DC.CursorPosPrevLine.y = line_y1; - window->DC.CursorPos.x = IM_FLOOR(window->Pos.x + window->DC.Indent.x + window->DC.ColumnsOffset.x); // Next line - window->DC.CursorPos.y = IM_FLOOR(line_y1 + line_height + g.Style.ItemSpacing.y); // Next line + window->DC.CursorPos.x = IM_TRUNC(window->Pos.x + window->DC.Indent.x + window->DC.ColumnsOffset.x); // Next line + window->DC.CursorPos.y = IM_TRUNC(line_y1 + line_height + g.Style.ItemSpacing.y); // Next line window->DC.CursorMaxPos.x = ImMax(window->DC.CursorMaxPos.x, window->DC.CursorPosPrevLine.x); window->DC.CursorMaxPos.y = ImMax(window->DC.CursorMaxPos.y, window->DC.CursorPos.y - g.Style.ItemSpacing.y); //if (g.IO.KeyAlt) window->DrawList->AddCircle(window->DC.CursorMaxPos, 3.0f, IM_COL32(255,0,0,255), 4); // [DEBUG] @@ -9334,89 +10648,10 @@ void ImGui::ItemSize(const ImVec2& size, float text_baseline_y) SameLine(); } -// Declare item bounding box for clipping and interaction. -// Note that the size can be different than the one provided to ItemSize(). Typically, widgets that spread over available surface -// declare their minimum size requirement to ItemSize() and provide a larger region to ItemAdd() which is used drawing/interaction. -bool ImGui::ItemAdd(const ImRect& bb, ImGuiID id, const ImRect* nav_bb_arg, ImGuiItemFlags extra_flags) -{ - ImGuiContext& g = *GImGui; - ImGuiWindow* window = g.CurrentWindow; - - // Set item data - // (DisplayRect is left untouched, made valid when ImGuiItemStatusFlags_HasDisplayRect is set) - g.LastItemData.ID = id; - g.LastItemData.Rect = bb; - g.LastItemData.NavRect = nav_bb_arg ? *nav_bb_arg : bb; - g.LastItemData.InFlags = g.CurrentItemFlags | extra_flags; - g.LastItemData.StatusFlags = ImGuiItemStatusFlags_None; - - // Directional navigation processing - if (id != 0) - { - KeepAliveID(id); - - // Runs prior to clipping early-out - // (a) So that NavInitRequest can be honored, for newly opened windows to select a default widget - // (b) So that we can scroll up/down past clipped items. This adds a small O(N) cost to regular navigation requests - // unfortunately, but it is still limited to one window. It may not scale very well for windows with ten of - // thousands of item, but at least NavMoveRequest is only set on user interaction, aka maximum once a frame. - // We could early out with "if (is_clipped && !g.NavInitRequest) return false;" but when we wouldn't be able - // to reach unclipped widgets. This would work if user had explicit scrolling control (e.g. mapped on a stick). - // We intentionally don't check if g.NavWindow != NULL because g.NavAnyRequest should only be set when it is non null. - // If we crash on a NULL g.NavWindow we need to fix the bug elsewhere. - if (!(g.LastItemData.InFlags & ImGuiItemFlags_NoNav)) - { - window->DC.NavLayersActiveMaskNext |= (1 << window->DC.NavLayerCurrent); - if (g.NavId == id || g.NavAnyRequest) - if (g.NavWindow->RootWindowForNav == window->RootWindowForNav) - if (window == g.NavWindow || ((window->Flags | g.NavWindow->Flags) & ImGuiWindowFlags_NavFlattened)) - NavProcessItem(); - } - - // [DEBUG] People keep stumbling on this problem and using "" as identifier in the root of a window instead of "##something". - // Empty identifier are valid and useful in a small amount of cases, but 99.9% of the time you want to use "##something". - // READ THE FAQ: https://dearimgui.com/faq - IM_ASSERT(id != window->ID && "Cannot have an empty ID at the root of a window. If you need an empty label, use ## and read the FAQ about how the ID Stack works!"); - } - g.NextItemData.Flags = ImGuiNextItemDataFlags_None; - -#ifdef IMGUI_ENABLE_TEST_ENGINE - if (id != 0) - IMGUI_TEST_ENGINE_ITEM_ADD(id, g.LastItemData.NavRect, &g.LastItemData); -#endif - - // Clipping test - // (FIXME: This is a modified copy of IsClippedEx() so we can reuse the is_rect_visible value) - //const bool is_clipped = IsClippedEx(bb, id); - //if (is_clipped) - // return false; - const bool is_rect_visible = bb.Overlaps(window->ClipRect); - if (!is_rect_visible) - if (id == 0 || (id != g.ActiveId && id != g.ActiveIdPreviousFrame && id != g.NavId)) - if (!g.LogEnabled) - return false; - - // [DEBUG] -#ifndef IMGUI_DISABLE_DEBUG_TOOLS - if (id != 0 && id == g.DebugLocateId) - DebugLocateItemResolveWithLastItem(); -#endif - //if (g.IO.KeyAlt) window->DrawList->AddRect(bb.Min, bb.Max, IM_COL32(255,255,0,120)); // [DEBUG] - //if ((g.LastItemData.InFlags & ImGuiItemFlags_NoNav) == 0) - // window->DrawList->AddRect(g.LastItemData.NavRect.Min, g.LastItemData.NavRect.Max, IM_COL32(255,255,0,255)); // [DEBUG] - - // We need to calculate this now to take account of the current clipping rectangle (as items like Selectable may change them) - if (is_rect_visible) - g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_Visible; - if (IsMouseHoveringRect(bb.Min, bb.Max)) - g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredRect; - return true; -} - // Gets back to previous line and continue with horizontal layout // offset_from_start_x == 0 : follow right after previous item // offset_from_start_x != 0 : align to specified x position (relative to window/group left) -// spacing_w < 0 : use default spacing if pos_x == 0, no spacing if pos_x != 0 +// spacing_w < 0 : use default spacing if offset_from_start_x == 0, no spacing if offset_from_start_x != 0 // spacing_w >= 0 : enforce spacing amount void ImGui::SameLine(float offset_from_start_x, float spacing_w) { @@ -9450,9 +10685,6 @@ ImVec2 ImGui::GetCursorScreenPos() return window->DC.CursorPos; } -// 2022/08/05: Setting cursor position also extend boundaries (via modifying CursorMaxPos) used to compute window size, group size etc. -// I believe this was is a judicious choice but it's probably being relied upon (it has been the case since 1.31 and 1.50) -// It would be sane if we requested user to use SetCursorPos() + Dummy(ImVec2(0,0)) to extend CursorMaxPos... void ImGui::SetCursorScreenPos(const ImVec2& pos) { ImGuiWindow* window = GetCurrentWindow(); @@ -9549,14 +10781,18 @@ void ImGui::PushMultiItemsWidths(int components, float w_full) { ImGuiContext& g = *GImGui; ImGuiWindow* window = g.CurrentWindow; + IM_ASSERT(components > 0); const ImGuiStyle& style = g.Style; - const float w_item_one = ImMax(1.0f, IM_FLOOR((w_full - (style.ItemInnerSpacing.x) * (components - 1)) / (float)components)); - const float w_item_last = ImMax(1.0f, IM_FLOOR(w_full - (w_item_one + style.ItemInnerSpacing.x) * (components - 1))); window->DC.ItemWidthStack.push_back(window->DC.ItemWidth); // Backup current width - window->DC.ItemWidthStack.push_back(w_item_last); - for (int i = 0; i < components - 2; i++) - window->DC.ItemWidthStack.push_back(w_item_one); - window->DC.ItemWidth = (components == 1) ? w_item_last : w_item_one; + float w_items = w_full - style.ItemInnerSpacing.x * (components - 1); + float prev_split = w_items; + for (int i = components - 1; i > 0; i--) + { + float next_split = IM_TRUNC(w_items * i / components); + window->DC.ItemWidthStack.push_back(ImMax(prev_split - next_split, 1.0f)); + prev_split = next_split; + } + window->DC.ItemWidth = ImMax(prev_split, 1.0f); g.NextItemData.Flags &= ~ImGuiNextItemDataFlags_HasWidth; } @@ -9583,7 +10819,7 @@ float ImGui::CalcItemWidth() float region_max_x = GetContentRegionMaxAbs().x; w = ImMax(1.0f, region_max_x - window->DC.CursorPos.x + w); } - w = IM_FLOOR(w); + w = IM_TRUNC(w); return w; } @@ -9644,10 +10880,8 @@ ImVec2 ImGui::GetContentRegionMax() { ImGuiContext& g = *GImGui; ImGuiWindow* window = g.CurrentWindow; - ImVec2 mx = window->ContentRegionRect.Max - window->Pos; - if (window->DC.CurrentColumns || g.CurrentTable) - mx.x = window->WorkRect.Max.x - window->Pos.x; - return mx; + ImVec2 mx = (window->DC.CurrentColumns || g.CurrentTable) ? window->WorkRect.Max : window->ContentRegionRect.Max; + return mx - window->Pos; } // [Internal] Absolute coordinate. Saner. This is not exposed until we finishing refactoring work rect features. @@ -9655,9 +10889,7 @@ ImVec2 ImGui::GetContentRegionMaxAbs() { ImGuiContext& g = *GImGui; ImGuiWindow* window = g.CurrentWindow; - ImVec2 mx = window->ContentRegionRect.Max; - if (window->DC.CurrentColumns || g.CurrentTable) - mx.x = window->WorkRect.Max.x; + ImVec2 mx = (window->DC.CurrentColumns || g.CurrentTable) ? window->WorkRect.Max : window->ContentRegionRect.Max; return mx; } @@ -9692,6 +10924,7 @@ void ImGui::BeginGroup() ImGuiGroupData& group_data = g.GroupStack.back(); group_data.WindowID = window->ID; group_data.BackupCursorPos = window->DC.CursorPos; + group_data.BackupCursorPosPrevLine = window->DC.CursorPosPrevLine; group_data.BackupCursorMaxPos = window->DC.CursorMaxPos; group_data.BackupIndent = window->DC.Indent; group_data.BackupGroupOffset = window->DC.GroupOffset; @@ -9699,6 +10932,7 @@ void ImGui::BeginGroup() group_data.BackupCurrLineTextBaseOffset = window->DC.CurrLineTextBaseOffset; group_data.BackupActiveIdIsAlive = g.ActiveIdIsAlive; group_data.BackupHoveredIdIsAlive = g.HoveredId != 0; + group_data.BackupIsSameLine = window->DC.IsSameLine; group_data.BackupActiveIdPreviousFrameIsAlive = g.ActiveIdPreviousFrameIsAlive; group_data.EmitItem = true; @@ -9725,11 +10959,13 @@ void ImGui::EndGroup() ImRect group_bb(group_data.BackupCursorPos, ImMax(window->DC.CursorMaxPos, group_data.BackupCursorPos)); window->DC.CursorPos = group_data.BackupCursorPos; + window->DC.CursorPosPrevLine = group_data.BackupCursorPosPrevLine; window->DC.CursorMaxPos = ImMax(group_data.BackupCursorMaxPos, window->DC.CursorMaxPos); window->DC.Indent = group_data.BackupIndent; window->DC.GroupOffset = group_data.BackupGroupOffset; window->DC.CurrLineSize = group_data.BackupCurrLineSize; window->DC.CurrLineTextBaseOffset = group_data.BackupCurrLineTextBaseOffset; + window->DC.IsSameLine = group_data.BackupIsSameLine; if (g.LogEnabled) g.LogLinePosY = -FLT_MAX; // To enforce a carriage return @@ -9770,7 +11006,8 @@ void ImGui::EndGroup() g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_Deactivated; g.GroupStack.pop_back(); - //window->DrawList->AddRect(group_bb.Min, group_bb.Max, IM_COL32(255,0,255,255)); // [Debug] + if (g.DebugShowGroupRects) + window->DrawList->AddRect(group_bb.Min, group_bb.Max, IM_COL32(255,0,255,255)); // [Debug] } @@ -9809,7 +11046,7 @@ static ImVec2 CalcNextScrollFromScrollTargetAndClamp(ImGuiWindow* window) } scroll[axis] = scroll_target - center_ratio * (window->SizeFull[axis] - decoration_size[axis]); } - scroll[axis] = IM_FLOOR(ImMax(scroll[axis], 0.0f)); + scroll[axis] = IM_ROUND(ImMax(scroll[axis], 0.0f)); if (!window->Collapsed && !window->SkipItems) scroll[axis] = ImMin(scroll[axis], window->ScrollMax[axis]); } @@ -9864,7 +11101,7 @@ ImVec2 ImGui::ScrollToRectEx(ImGuiWindow* window, const ImRect& item_rect, ImGui else if (((flags & ImGuiScrollFlags_KeepVisibleCenterX) && !fully_visible_x) || (flags & ImGuiScrollFlags_AlwaysCenterX)) { if (can_be_fully_visible_x) - SetScrollFromPosX(window, ImFloor((item_rect.Min.x + item_rect.Max.x) * 0.5f) - window->Pos.x, 0.5f); + SetScrollFromPosX(window, ImTrunc((item_rect.Min.x + item_rect.Max.x) * 0.5f) - window->Pos.x, 0.5f); else SetScrollFromPosX(window, item_rect.Min.x - window->Pos.x, 0.0f); } @@ -9879,7 +11116,7 @@ ImVec2 ImGui::ScrollToRectEx(ImGuiWindow* window, const ImRect& item_rect, ImGui else if (((flags & ImGuiScrollFlags_KeepVisibleCenterY) && !fully_visible_y) || (flags & ImGuiScrollFlags_AlwaysCenterY)) { if (can_be_fully_visible_y) - SetScrollFromPosY(window, ImFloor((item_rect.Min.y + item_rect.Max.y) * 0.5f) - window->Pos.y, 0.5f); + SetScrollFromPosY(window, ImTrunc((item_rect.Min.y + item_rect.Max.y) * 0.5f) - window->Pos.y, 0.5f); else SetScrollFromPosY(window, item_rect.Min.y - window->Pos.y, 0.0f); } @@ -9964,7 +11201,7 @@ void ImGui::SetScrollY(float scroll_y) void ImGui::SetScrollFromPosX(ImGuiWindow* window, float local_x, float center_x_ratio) { IM_ASSERT(center_x_ratio >= 0.0f && center_x_ratio <= 1.0f); - window->ScrollTarget.x = IM_FLOOR(local_x - window->DecoOuterSizeX1 - window->DecoInnerSizeX1 + window->Scroll.x); // Convert local position to scroll offset + window->ScrollTarget.x = IM_TRUNC(local_x - window->DecoOuterSizeX1 - window->DecoInnerSizeX1 + window->Scroll.x); // Convert local position to scroll offset window->ScrollTargetCenterRatio.x = center_x_ratio; window->ScrollTargetEdgeSnapDist.x = 0.0f; } @@ -9972,7 +11209,7 @@ void ImGui::SetScrollFromPosX(ImGuiWindow* window, float local_x, float center_x void ImGui::SetScrollFromPosY(ImGuiWindow* window, float local_y, float center_y_ratio) { IM_ASSERT(center_y_ratio >= 0.0f && center_y_ratio <= 1.0f); - window->ScrollTarget.y = IM_FLOOR(local_y - window->DecoOuterSizeY1 - window->DecoInnerSizeY1 + window->Scroll.y); // Convert local position to scroll offset + window->ScrollTarget.y = IM_TRUNC(local_y - window->DecoOuterSizeY1 - window->DecoInnerSizeY1 + window->Scroll.y); // Convert local position to scroll offset window->ScrollTargetCenterRatio.y = center_y_ratio; window->ScrollTargetEdgeSnapDist.y = 0.0f; } @@ -10024,34 +11261,43 @@ bool ImGui::BeginTooltip() return BeginTooltipEx(ImGuiTooltipFlags_None, ImGuiWindowFlags_None); } +bool ImGui::BeginItemTooltip() +{ + if (!IsItemHovered(ImGuiHoveredFlags_ForTooltip)) + return false; + return BeginTooltipEx(ImGuiTooltipFlags_None, ImGuiWindowFlags_None); +} + bool ImGui::BeginTooltipEx(ImGuiTooltipFlags tooltip_flags, ImGuiWindowFlags extra_window_flags) { ImGuiContext& g = *GImGui; if (g.DragDropWithinSource || g.DragDropWithinTarget) { - // The default tooltip position is a little offset to give space to see the context menu (it's also clamped within the current viewport/monitor) - // In the context of a dragging tooltip we try to reduce that offset and we enforce following the cursor. - // Whatever we do we want to call SetNextWindowPos() to enforce a tooltip position and disable clipping the tooltip without our display area, like regular tooltip do. + // Drag and Drop tooltips are positioning differently than other tooltips: + // - offset visibility to increase visibility around mouse. + // - never clamp within outer viewport boundary. + // We call SetNextWindowPos() to enforce position and disable clamping. + // See FindBestWindowPosForPopup() for positionning logic of other tooltips (not drag and drop ones). //ImVec2 tooltip_pos = g.IO.MousePos - g.ActiveIdClickOffset - g.Style.WindowPadding; - ImVec2 tooltip_pos = g.IO.MousePos + ImVec2(16 * g.Style.MouseCursorScale, 8 * g.Style.MouseCursorScale); + ImVec2 tooltip_pos = g.IO.MousePos + TOOLTIP_DEFAULT_OFFSET * g.Style.MouseCursorScale; SetNextWindowPos(tooltip_pos); SetNextWindowBgAlpha(g.Style.Colors[ImGuiCol_PopupBg].w * 0.60f); //PushStyleVar(ImGuiStyleVar_Alpha, g.Style.Alpha * 0.60f); // This would be nice but e.g ColorButton with checkboard has issue with transparent colors :( - tooltip_flags |= ImGuiTooltipFlags_OverridePreviousTooltip; + tooltip_flags |= ImGuiTooltipFlags_OverridePrevious; } char window_name[16]; ImFormatString(window_name, IM_ARRAYSIZE(window_name), "##Tooltip_%02d", g.TooltipOverrideCount); - if (tooltip_flags & ImGuiTooltipFlags_OverridePreviousTooltip) + if (tooltip_flags & ImGuiTooltipFlags_OverridePrevious) if (ImGuiWindow* window = FindWindowByName(window_name)) if (window->Active) { // Hide previous tooltip from being displayed. We can't easily "reset" the content of a window so we create a new one. - SetWindowHiddendAndSkipItemsForCurrentFrame(window); + SetWindowHiddenAndSkipItemsForCurrentFrame(window); ImFormatString(window_name, IM_ARRAYSIZE(window_name), "##Tooltip_%02d", ++g.TooltipOverrideCount); } - ImGuiWindowFlags flags = ImGuiWindowFlags_Tooltip | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_AlwaysAutoResize; + ImGuiWindowFlags flags = ImGuiWindowFlags_Tooltip | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDocking; Begin(window_name, NULL, flags | extra_window_flags); // 2023-03-09: Added bool return value to the API, but currently always returning true. // If this ever returns false we need to update BeginDragDropSource() accordingly. @@ -10067,14 +11313,6 @@ void ImGui::EndTooltip() End(); } -void ImGui::SetTooltipV(const char* fmt, va_list args) -{ - if (!BeginTooltipEx(ImGuiTooltipFlags_OverridePreviousTooltip, ImGuiWindowFlags_None)) - return; - TextV(fmt, args); - EndTooltip(); -} - void ImGui::SetTooltip(const char* fmt, ...) { va_list args; @@ -10083,6 +11321,32 @@ void ImGui::SetTooltip(const char* fmt, ...) va_end(args); } +void ImGui::SetTooltipV(const char* fmt, va_list args) +{ + if (!BeginTooltipEx(ImGuiTooltipFlags_OverridePrevious, ImGuiWindowFlags_None)) + return; + TextV(fmt, args); + EndTooltip(); +} + +// Shortcut to use 'style.HoverFlagsForTooltipMouse' or 'style.HoverFlagsForTooltipNav'. +// Defaults to == ImGuiHoveredFlags_Stationary | ImGuiHoveredFlags_DelayShort when using the mouse. +void ImGui::SetItemTooltip(const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + if (IsItemHovered(ImGuiHoveredFlags_ForTooltip)) + SetTooltipV(fmt, args); + va_end(args); +} + +void ImGui::SetItemTooltipV(const char* fmt, va_list args) +{ + if (IsItemHovered(ImGuiHoveredFlags_ForTooltip)) + SetTooltipV(fmt, args); +} + + //----------------------------------------------------------------------------- // [SECTION] POPUPS //----------------------------------------------------------------------------- @@ -10106,8 +11370,8 @@ bool ImGui::IsPopupOpen(ImGuiID id, ImGuiPopupFlags popup_flags) if (popup_flags & ImGuiPopupFlags_AnyPopupLevel) { // Return true if the popup is open anywhere in the popup stack - for (int n = 0; n < g.OpenPopupStack.Size; n++) - if (g.OpenPopupStack[n].PopupId == id) + for (ImGuiPopupData& popup_data : g.OpenPopupStack) + if (popup_data.PopupId == id) return true; return false; } @@ -10193,17 +11457,23 @@ void ImGui::OpenPopupEx(ImGuiID id, ImGuiPopupFlags popup_flags) } else { - // Gently handle the user mistakenly calling OpenPopup() every frame. It is a programming mistake! However, if we were to run the regular code path, the ui - // would become completely unusable because the popup will always be in hidden-while-calculating-size state _while_ claiming focus. Which would be a very confusing - // situation for the programmer. Instead, we silently allow the popup to proceed, it will keep reappearing and the programming error will be more obvious to understand. - if (g.OpenPopupStack[current_stack_size].PopupId == id && g.OpenPopupStack[current_stack_size].OpenFrameCount == g.FrameCount - 1) + // Gently handle the user mistakenly calling OpenPopup() every frames: it is likely a programming mistake! + // However, if we were to run the regular code path, the ui would become completely unusable because the popup will always be + // in hidden-while-calculating-size state _while_ claiming focus. Which is extremely confusing situation for the programmer. + // Instead, for successive frames calls to OpenPopup(), we silently avoid reopening even if ImGuiPopupFlags_NoReopen is not specified. + bool keep_existing = false; + if (g.OpenPopupStack[current_stack_size].PopupId == id) + if ((g.OpenPopupStack[current_stack_size].OpenFrameCount == g.FrameCount - 1) || (popup_flags & ImGuiPopupFlags_NoReopen)) + keep_existing = true; + if (keep_existing) { + // No reopen g.OpenPopupStack[current_stack_size].OpenFrameCount = popup_ref.OpenFrameCount; } else { - // Close child popups if any, then flag popup for open/reopen - ClosePopupToLevel(current_stack_size, false); + // Reopen: close child popups if any, then flag popup for open/reopen (set position, focus, init navigation) + ClosePopupToLevel(current_stack_size, true); g.OpenPopupStack.push_back(popup_ref); } @@ -10233,17 +11503,19 @@ void ImGui::ClosePopupsOverWindow(ImGuiWindow* ref_window, bool restore_focus_to if (!popup.Window) continue; IM_ASSERT((popup.Window->Flags & ImGuiWindowFlags_Popup) != 0); - if (popup.Window->Flags & ImGuiWindowFlags_ChildWindow) - continue; // Trim the stack unless the popup is a direct parent of the reference window (the reference window is often the NavWindow) - // - With this stack of window, clicking/focusing Popup1 will close Popup2 and Popup3: - // Window -> Popup1 -> Popup2 -> Popup3 - // - Each popups may contain child windows, which is why we compare ->RootWindow! + // - Clicking/Focusing Window2 won't close Popup1: + // Window -> Popup1 -> Window2(Ref) + // - Clicking/focusing Popup1 will close Popup2 and Popup3: + // Window -> Popup1(Ref) -> Popup2 -> Popup3 + // - Each popups may contain child windows, which is why we compare ->RootWindowDockTree! // Window -> Popup1 -> Popup1_Child -> Popup2 -> Popup2_Child + // We step through every popup from bottom to top to validate their position relative to reference window. bool ref_window_is_descendent_of_popup = false; for (int n = popup_count_to_keep; n < g.OpenPopupStack.Size; n++) if (ImGuiWindow* popup_window = g.OpenPopupStack[n].Window) + //if (popup_window->RootWindowDockTree == ref_window->RootWindowDockTree) // FIXME-MERGE if (IsWindowWithinBeginStackOf(ref_window, popup_window)) { ref_window_is_descendent_of_popup = true; @@ -10339,15 +11611,17 @@ bool ImGui::BeginPopupEx(ImGuiID id, ImGuiWindowFlags flags) char name[20]; if (flags & ImGuiWindowFlags_ChildMenu) - ImFormatString(name, IM_ARRAYSIZE(name), "##Menu_%02d", g.BeginMenuCount); // Recycle windows based on depth + ImFormatString(name, IM_ARRAYSIZE(name), "##Menu_%02d", g.BeginMenuDepth); // Recycle windows based on depth else ImFormatString(name, IM_ARRAYSIZE(name), "##Popup_%08x", id); // Not recycling, so we can close/open during the same frame - flags |= ImGuiWindowFlags_Popup; + flags |= ImGuiWindowFlags_Popup | ImGuiWindowFlags_NoDocking; bool is_open = Begin(name, NULL, flags); if (!is_open) // NB: Begin can return false when the popup is completely clipped (e.g. zero size display) EndPopup(); + //g.CurrentWindow->FocusRouteParentWindow = g.CurrentWindow->ParentWindowInBeginStack; + return is_open; } @@ -10365,7 +11639,9 @@ bool ImGui::BeginPopup(const char* str_id, ImGuiWindowFlags flags) } // If 'p_open' is specified for a modal popup window, the popup will have a regular close button which will close the popup. -// Note that popup visibility status is owned by Dear ImGui (and manipulated with e.g. OpenPopup) so the actual value of *p_open is meaningless here. +// Note that popup visibility status is owned by Dear ImGui (and manipulated with e.g. OpenPopup). +// - *p_open set back to false in BeginPopupModal() when popup is not open. +// - if you set *p_open to false before calling BeginPopupModal(), it will close the popup. bool ImGui::BeginPopupModal(const char* name, bool* p_open, ImGuiWindowFlags flags) { ImGuiContext& g = *GImGui; @@ -10374,6 +11650,8 @@ bool ImGui::BeginPopupModal(const char* name, bool* p_open, ImGuiWindowFlags fla if (!IsPopupOpen(id, ImGuiPopupFlags_None)) { g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values + if (p_open && *p_open) + *p_open = false; return false; } @@ -10382,11 +11660,11 @@ bool ImGui::BeginPopupModal(const char* name, bool* p_open, ImGuiWindowFlags fla // FIXME: Should test for (PosCond & window->SetWindowPosAllowFlags) with the upcoming window. if ((g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasPos) == 0) { - const ImGuiViewport* viewport = GetMainViewport(); + const ImGuiViewport* viewport = window->WasActive ? window->Viewport : GetMainViewport(); // FIXME-VIEWPORT: What may be our reference viewport? SetNextWindowPos(viewport->GetCenter(), ImGuiCond_FirstUseEver, ImVec2(0.5f, 0.5f)); } - flags |= ImGuiWindowFlags_Popup | ImGuiWindowFlags_Modal | ImGuiWindowFlags_NoCollapse; + flags |= ImGuiWindowFlags_Popup | ImGuiWindowFlags_Modal | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoDocking; const bool is_open = Begin(name, p_open, flags); if (!is_open || (p_open && !*p_open)) // NB: is_open can be 'false' when the popup is completely clipped (e.g. zero size display) { @@ -10573,8 +11851,19 @@ ImVec2 ImGui::FindBestWindowPosForPopupEx(const ImVec2& ref_pos, const ImVec2& s ImRect ImGui::GetPopupAllowedExtentRect(ImGuiWindow* window) { ImGuiContext& g = *GImGui; - IM_UNUSED(window); - ImRect r_screen = ((ImGuiViewportP*)(void*)GetMainViewport())->GetMainRect(); + ImRect r_screen; + if (window->ViewportAllowPlatformMonitorExtend >= 0) + { + // Extent with be in the frame of reference of the given viewport (so Min is likely to be negative here) + const ImGuiPlatformMonitor& monitor = g.PlatformIO.Monitors[window->ViewportAllowPlatformMonitorExtend]; + r_screen.Min = monitor.WorkPos; + r_screen.Max = monitor.WorkPos + monitor.WorkSize; + } + else + { + // Use the full viewport area (not work area) for popups + r_screen = window->Viewport->GetMainRect(); + } ImVec2 padding = g.Style.DisplaySafeAreaPadding; r_screen.Expand(ImVec2((r_screen.GetWidth() > padding.x * 2) ? -padding.x : 0.0f, (r_screen.GetHeight() > padding.y * 2) ? -padding.y : 0.0f)); return r_screen; @@ -10589,8 +11878,7 @@ ImVec2 ImGui::FindBestWindowPosForPopup(ImGuiWindow* window) { // Child menus typically request _any_ position within the parent menu item, and then we move the new menu outside the parent bounds. // This is how we end up with child menus appearing (most-commonly) on the right of the parent menu. - IM_ASSERT(g.CurrentWindow == window); - ImGuiWindow* parent_window = g.CurrentWindowStack[g.CurrentWindowStack.Size - 2].Window; + ImGuiWindow* parent_window = window->ParentWindow; float horizontal_overlap = g.Style.ItemInnerSpacing.x; // We want some overlap to convey the relative depth of each menu (currently the amount of overlap is hard-coded to style.ItemSpacing.x). ImRect r_avoid; if (parent_window->DC.MenuBarAppending) @@ -10605,15 +11893,20 @@ ImVec2 ImGui::FindBestWindowPosForPopup(ImGuiWindow* window) } if (window->Flags & ImGuiWindowFlags_Tooltip) { - // Position tooltip (always follows mouse) - float sc = g.Style.MouseCursorScale; - ImVec2 ref_pos = NavCalcPreferredRefPos(); + // Position tooltip (always follows mouse + clamp within outer boundaries) + // Note that drag and drop tooltips are NOT using this path: BeginTooltipEx() manually sets their position. + // In theory we could handle both cases in same location, but requires a bit of shuffling as drag and drop tooltips are calling SetWindowPos() leading to 'window_pos_set_by_api' being set in Begin() + IM_ASSERT(g.CurrentWindow == window); + const float scale = g.Style.MouseCursorScale; + const ImVec2 ref_pos = NavCalcPreferredRefPos(); + const ImVec2 tooltip_pos = ref_pos + TOOLTIP_DEFAULT_OFFSET * scale; ImRect r_avoid; if (!g.NavDisableHighlight && g.NavDisableMouseHover && !(g.IO.ConfigFlags & ImGuiConfigFlags_NavEnableSetMousePos)) r_avoid = ImRect(ref_pos.x - 16, ref_pos.y - 8, ref_pos.x + 16, ref_pos.y + 8); else - r_avoid = ImRect(ref_pos.x - 16, ref_pos.y - 8, ref_pos.x + 24 * sc, ref_pos.y + 24 * sc); // FIXME: Hard-coded based on mouse cursor shape expectation. Exact dimension not very important. - return FindBestWindowPosForPopupEx(ref_pos, window->Size, &window->AutoPosLastDirection, r_outer, r_avoid, ImGuiPopupPositionPolicy_Tooltip); + r_avoid = ImRect(ref_pos.x - 16, ref_pos.y - 8, ref_pos.x + 24 * scale, ref_pos.y + 24 * scale); // FIXME: Hard-coded based on mouse cursor shape expectation. Exact dimension not very important. + //GetForegroundDrawList()->AddRect(r_avoid.Min, r_avoid.Max, IM_COL32(255, 0, 255, 255)); + return FindBestWindowPosForPopupEx(tooltip_pos, window->Size, &window->AutoPosLastDirection, r_outer, r_avoid, ImGuiPopupPositionPolicy_Tooltip); } IM_ASSERT(0); return window->Pos; @@ -10634,11 +11927,19 @@ void ImGui::SetNavWindow(ImGuiWindow* window) { IMGUI_DEBUG_LOG_FOCUS("[focus] SetNavWindow(\"%s\")\n", window ? window->Name : ""); g.NavWindow = window; + g.NavLastValidSelectionUserData = ImGuiSelectionUserData_Invalid; } g.NavInitRequest = g.NavMoveSubmitted = g.NavMoveScoringItems = false; NavUpdateAnyRequestFlag(); } +void ImGui::NavHighlightActivated(ImGuiID id) +{ + ImGuiContext& g = *GImGui; + g.NavHighlightActivatedId = id; + g.NavHighlightActivatedTimer = NAV_ACTIVATE_HIGHLIGHT_TIMER; +} + void ImGui::NavClearPreferredPosForAxis(ImGuiAxis axis) { ImGuiContext& g = *GImGui; @@ -10652,7 +11953,7 @@ void ImGui::SetNavID(ImGuiID id, ImGuiNavLayer nav_layer, ImGuiID focus_scope_id IM_ASSERT(nav_layer == ImGuiNavLayer_Main || nav_layer == ImGuiNavLayer_Menu); g.NavId = id; g.NavLayer = nav_layer; - g.NavFocusScopeId = focus_scope_id; + SetNavFocusScope(focus_scope_id); g.NavWindow->NavLastIds[nav_layer] = id; g.NavWindow->NavRectRel[nav_layer] = rect_rel; @@ -10674,7 +11975,7 @@ void ImGui::SetFocusID(ImGuiID id, ImGuiWindow* window) const ImGuiNavLayer nav_layer = window->DC.NavLayerCurrent; g.NavId = id; g.NavLayer = nav_layer; - g.NavFocusScopeId = g.CurrentFocusScopeId; + SetNavFocusScope(g.CurrentFocusScopeId); window->NavLastIds[nav_layer] = id; if (g.LastItemData.ID == id) window->NavRectRel[nav_layer] = WindowRectAbsToRel(window, g.LastItemData.NavRect); @@ -10853,6 +12154,11 @@ static void ImGui::NavApplyItemToResult(ImGuiNavItemData* result) result->FocusScopeId = g.CurrentFocusScopeId; result->InFlags = g.LastItemData.InFlags; result->RectRel = WindowRectAbsToRel(window, g.LastItemData.NavRect); + if (result->InFlags & ImGuiItemFlags_HasSelectionUserData) + { + IM_ASSERT(g.NextItemData.SelectionUserData != ImGuiSelectionUserData_Invalid); + result->SelectionUserData = g.NextItemData.SelectionUserData; // INTENTIONAL: At this point this field is not cleared in NextItemData. Avoid unnecessary copy to LastItemData. + } } // True when current work location may be scrolled horizontally when moving left / right. @@ -10865,7 +12171,7 @@ void ImGui::NavUpdateCurrentWindowIsScrollPushableX() } // We get there when either NavId == id, or when g.NavAnyRequest is set (which is updated by NavUpdateAnyRequestFlag above) -// This is called after LastItemData is set. +// This is called after LastItemData is set, but NextItemData is also still valid. static void ImGui::NavProcessItem() { ImGuiContext& g = *GImGui; @@ -10901,23 +12207,26 @@ static void ImGui::NavProcessItem() // FIXME-NAV: Consider policy for double scoring (scoring from NavScoringRect + scoring from a rect wrapped according to current wrapping policy) if (g.NavMoveScoringItems && (item_flags & ImGuiItemFlags_Disabled) == 0) { - const bool is_tabbing = (g.NavMoveFlags & ImGuiNavMoveFlags_Tabbing) != 0; - if (is_tabbing) + if ((g.NavMoveFlags & ImGuiNavMoveFlags_FocusApi) || (window->Flags & ImGuiWindowFlags_NoNavInputs) == 0) { - NavProcessItemForTabbingRequest(id, item_flags, g.NavMoveFlags); - } - else if (g.NavId != id || (g.NavMoveFlags & ImGuiNavMoveFlags_AllowCurrentNavId)) - { - ImGuiNavItemData* result = (window == g.NavWindow) ? &g.NavMoveResultLocal : &g.NavMoveResultOther; - if (NavScoreItem(result)) - NavApplyItemToResult(result); + const bool is_tabbing = (g.NavMoveFlags & ImGuiNavMoveFlags_IsTabbing) != 0; + if (is_tabbing) + { + NavProcessItemForTabbingRequest(id, item_flags, g.NavMoveFlags); + } + else if (g.NavId != id || (g.NavMoveFlags & ImGuiNavMoveFlags_AllowCurrentNavId)) + { + ImGuiNavItemData* result = (window == g.NavWindow) ? &g.NavMoveResultLocal : &g.NavMoveResultOther; + if (NavScoreItem(result)) + NavApplyItemToResult(result); - // Features like PageUp/PageDown need to maintain a separate score for the visible set of items. - const float VISIBLE_RATIO = 0.70f; - if ((g.NavMoveFlags & ImGuiNavMoveFlags_AlsoScoreVisibleSet) && window->ClipRect.Overlaps(nav_bb)) - if (ImClamp(nav_bb.Max.y, window->ClipRect.Min.y, window->ClipRect.Max.y) - ImClamp(nav_bb.Min.y, window->ClipRect.Min.y, window->ClipRect.Max.y) >= (nav_bb.Max.y - nav_bb.Min.y) * VISIBLE_RATIO) - if (NavScoreItem(&g.NavMoveResultLocalVisible)) - NavApplyItemToResult(&g.NavMoveResultLocalVisible); + // Features like PageUp/PageDown need to maintain a separate score for the visible set of items. + const float VISIBLE_RATIO = 0.70f; + if ((g.NavMoveFlags & ImGuiNavMoveFlags_AlsoScoreVisibleSet) && window->ClipRect.Overlaps(nav_bb)) + if (ImClamp(nav_bb.Max.y, window->ClipRect.Min.y, window->ClipRect.Max.y) - ImClamp(nav_bb.Min.y, window->ClipRect.Min.y, window->ClipRect.Max.y) >= (nav_bb.Max.y - nav_bb.Min.y) * VISIBLE_RATIO) + if (NavScoreItem(&g.NavMoveResultLocalVisible)) + NavApplyItemToResult(&g.NavMoveResultLocalVisible); + } } } @@ -10927,8 +12236,14 @@ static void ImGui::NavProcessItem() if (g.NavWindow != window) SetNavWindow(window); // Always refresh g.NavWindow, because some operations such as FocusItem() may not have a window. g.NavLayer = window->DC.NavLayerCurrent; + SetNavFocusScope(g.CurrentFocusScopeId); // Will set g.NavFocusScopeId AND store g.NavFocusScopePath g.NavFocusScopeId = g.CurrentFocusScopeId; g.NavIdIsAlive = true; + if (g.LastItemData.InFlags & ImGuiItemFlags_HasSelectionUserData) + { + IM_ASSERT(g.NextItemData.SelectionUserData != ImGuiSelectionUserData_Invalid); + g.NavLastValidSelectionUserData = g.NextItemData.SelectionUserData; // INTENTIONAL: At this point this field is not cleared in NextItemData. Avoid unnecessary copy to LastItemData. + } window->NavRectRel[window->DC.NavLayerCurrent] = WindowRectAbsToRel(window, nav_bb); // Store item bounding box (relative to window position) } } @@ -10945,8 +12260,12 @@ void ImGui::NavProcessItemForTabbingRequest(ImGuiID id, ImGuiItemFlags item_flag ImGuiContext& g = *GImGui; if ((move_flags & ImGuiNavMoveFlags_FocusApi) == 0) + { if (g.NavLayer != g.CurrentWindow->DC.NavLayerCurrent) return; + if (g.NavFocusScopeId != g.CurrentFocusScopeId) + return; + } // - Can always land on an item when using API call. // - Tabbing with _NavEnableKeyboard (space/enter/arrows): goes through every item. @@ -11007,7 +12326,7 @@ void ImGui::NavMoveRequestSubmit(ImGuiDir move_dir, ImGuiDir clip_dir, ImGuiNavM ImGuiContext& g = *GImGui; IM_ASSERT(g.NavWindow != NULL); - if (move_flags & ImGuiNavMoveFlags_Tabbing) + if (move_flags & ImGuiNavMoveFlags_IsTabbing) move_flags |= ImGuiNavMoveFlags_AllowCurrentNavId; g.NavMoveSubmitted = g.NavMoveScoringItems = true; @@ -11017,7 +12336,7 @@ void ImGui::NavMoveRequestSubmit(ImGuiDir move_dir, ImGuiDir clip_dir, ImGuiNavM g.NavMoveFlags = move_flags; g.NavMoveScrollFlags = scroll_flags; g.NavMoveForwardToNextFrame = false; - g.NavMoveKeyMods = g.IO.KeyMods; + g.NavMoveKeyMods = (move_flags & ImGuiNavMoveFlags_FocusApi) ? 0 : g.IO.KeyMods; g.NavMoveResultLocal.Clear(); g.NavMoveResultLocalVisible.Clear(); g.NavMoveResultOther.Clear(); @@ -11034,6 +12353,19 @@ void ImGui::NavMoveRequestResolveWithLastItem(ImGuiNavItemData* result) NavUpdateAnyRequestFlag(); } +// Called by TreePop() to implement ImGuiTreeNodeFlags_NavLeftJumpsBackHere +void ImGui::NavMoveRequestResolveWithPastTreeNode(ImGuiNavItemData* result, ImGuiNavTreeNodeData* tree_node_data) +{ + ImGuiContext& g = *GImGui; + g.NavMoveScoringItems = false; + g.LastItemData.ID = tree_node_data->ID; + g.LastItemData.InFlags = tree_node_data->InFlags & ~ImGuiItemFlags_HasSelectionUserData; // Losing SelectionUserData, recovered next-frame (cheaper). + g.LastItemData.NavRect = tree_node_data->NavRect; + NavApplyItemToResult(result); // Result this instead of implementing a NavApplyPastTreeNodeToResult() + NavClearPreferredPosForAxis(ImGuiAxis_Y); + NavUpdateAnyRequestFlag(); +} + void ImGui::NavMoveRequestCancel() { ImGuiContext& g = *GImGui; @@ -11084,6 +12416,9 @@ static ImGuiWindow* ImGui::NavRestoreLastChildNavWindow(ImGuiWindow* window) { if (window->NavLastChildNavWindow && window->NavLastChildNavWindow->WasActive) return window->NavLastChildNavWindow; + if (window->DockNodeAsHost && window->DockNodeAsHost->TabBar) + if (ImGuiTabItem* tab = TabBarFindMostRecentlySelectedTabForActiveWindow(window->DockNodeAsHost->TabBar)) + return tab->Window; return window; } @@ -11094,6 +12429,7 @@ void ImGui::NavRestoreLayer(ImGuiNavLayer layer) { ImGuiWindow* prev_nav_window = g.NavWindow; g.NavWindow = NavRestoreLastChildNavWindow(g.NavWindow); // FIXME-NAV: Should clear ongoing nav requests? + g.NavLastValidSelectionUserData = ImGuiSelectionUserData_Invalid; if (prev_nav_window) IMGUI_DEBUG_LOG_FOCUS("[focus] NavRestoreLayer: from \"%s\" to SetNavWindow(\"%s\")\n", prev_nav_window->Name, g.NavWindow->Name); } @@ -11127,13 +12463,14 @@ static inline void ImGui::NavUpdateAnyRequestFlag() // This needs to be called before we submit any widget (aka in or before Begin) void ImGui::NavInitWindow(ImGuiWindow* window, bool force_reinit) { + // FIXME: ChildWindow test here is wrong for docking ImGuiContext& g = *GImGui; IM_ASSERT(window == g.NavWindow); if (window->Flags & ImGuiWindowFlags_NoNavInputs) { g.NavId = 0; - g.NavFocusScopeId = window->NavRootFocusScopeId; + SetNavFocusScope(window->NavRootFocusScopeId); return; } @@ -11152,7 +12489,7 @@ void ImGui::NavInitWindow(ImGuiWindow* window, bool force_reinit) else { g.NavId = window->NavLastIds[0]; - g.NavFocusScopeId = window->NavRootFocusScopeId; + SetNavFocusScope(window->NavRootFocusScopeId); } } @@ -11179,8 +12516,8 @@ static ImVec2 ImGui::NavCalcPreferredRefPos() rect_rel.Translate(window->Scroll - next_scroll); } ImVec2 pos = ImVec2(rect_rel.Min.x + ImMin(g.Style.FramePadding.x * 4, rect_rel.GetWidth()), rect_rel.Max.y - ImMin(g.Style.FramePadding.y, rect_rel.GetHeight())); - ImGuiViewport* viewport = GetMainViewport(); - return ImFloor(ImClamp(pos, viewport->Pos, viewport->Pos + viewport->Size)); // ImFloor() is important because non-integer mouse position application in backend might be lossy and result in undesirable non-zero delta. + ImGuiViewport* viewport = window->Viewport; + return ImTrunc(ImClamp(pos, viewport->Pos, viewport->Pos + viewport->Size)); // ImTrunc() is important because non-integer mouse position application in backend might be lossy and result in undesirable non-zero delta. } } @@ -11273,10 +12610,10 @@ static void ImGui::NavUpdate() g.NavActivateFlags = ImGuiActivateFlags_None; if (g.NavId != 0 && !g.NavDisableHighlight && !g.NavWindowingTarget && g.NavWindow && !(g.NavWindow->Flags & ImGuiWindowFlags_NoNavInputs)) { - const bool activate_down = (nav_keyboard_active && IsKeyDown(ImGuiKey_Space)) || (nav_gamepad_active && IsKeyDown(ImGuiKey_NavGamepadActivate)); - const bool activate_pressed = activate_down && ((nav_keyboard_active && IsKeyPressed(ImGuiKey_Space, false)) || (nav_gamepad_active && IsKeyPressed(ImGuiKey_NavGamepadActivate, false))); - const bool input_down = (nav_keyboard_active && IsKeyDown(ImGuiKey_Enter)) || (nav_gamepad_active && IsKeyDown(ImGuiKey_NavGamepadInput)); - const bool input_pressed = input_down && ((nav_keyboard_active && IsKeyPressed(ImGuiKey_Enter, false)) || (nav_gamepad_active && IsKeyPressed(ImGuiKey_NavGamepadInput, false))); + const bool activate_down = (nav_keyboard_active && IsKeyDown(ImGuiKey_Space, ImGuiKeyOwner_None)) || (nav_gamepad_active && IsKeyDown(ImGuiKey_NavGamepadActivate, ImGuiKeyOwner_None)); + const bool activate_pressed = activate_down && ((nav_keyboard_active && IsKeyPressed(ImGuiKey_Space, ImGuiKeyOwner_None)) || (nav_gamepad_active && IsKeyPressed(ImGuiKey_NavGamepadActivate, ImGuiKeyOwner_None))); + const bool input_down = (nav_keyboard_active && (IsKeyDown(ImGuiKey_Enter, ImGuiKeyOwner_None) || IsKeyDown(ImGuiKey_KeypadEnter, ImGuiKeyOwner_None))) || (nav_gamepad_active && IsKeyDown(ImGuiKey_NavGamepadInput, ImGuiKeyOwner_None)); + const bool input_pressed = input_down && ((nav_keyboard_active && (IsKeyPressed(ImGuiKey_Enter, ImGuiKeyOwner_None) || IsKeyPressed(ImGuiKey_KeypadEnter, ImGuiKeyOwner_None))) || (nav_gamepad_active && IsKeyPressed(ImGuiKey_NavGamepadInput, ImGuiKeyOwner_None))); if (g.ActiveId == 0 && activate_pressed) { g.NavActivateId = g.NavId; @@ -11290,13 +12627,22 @@ static void ImGui::NavUpdate() if ((g.ActiveId == 0 || g.ActiveId == g.NavId) && (activate_down || input_down)) g.NavActivateDownId = g.NavId; if ((g.ActiveId == 0 || g.ActiveId == g.NavId) && (activate_pressed || input_pressed)) + { g.NavActivatePressedId = g.NavId; + NavHighlightActivated(g.NavId); + } } if (g.NavWindow && (g.NavWindow->Flags & ImGuiWindowFlags_NoNavInputs)) g.NavDisableHighlight = true; if (g.NavActivateId != 0) IM_ASSERT(g.NavActivateDownId == g.NavActivateId); + // Highlight + if (g.NavHighlightActivatedTimer > 0.0f) + g.NavHighlightActivatedTimer = ImMax(0.0f, g.NavHighlightActivatedTimer - io.DeltaTime); + if (g.NavHighlightActivatedTimer == 0.0f) + g.NavHighlightActivatedId = 0; + // Process programmatic activation request // FIXME-NAV: Those should eventually be queued (unlike focus they don't cancel each others) if (g.NavNextActivateId != 0) @@ -11323,9 +12669,9 @@ static void ImGui::NavUpdate() if (window->DC.NavLayersActiveMask == 0x00 && window->DC.NavWindowHasScrollY && move_dir != ImGuiDir_None) { if (move_dir == ImGuiDir_Left || move_dir == ImGuiDir_Right) - SetScrollX(window, ImFloor(window->Scroll.x + ((move_dir == ImGuiDir_Left) ? -1.0f : +1.0f) * scroll_speed)); + SetScrollX(window, ImTrunc(window->Scroll.x + ((move_dir == ImGuiDir_Left) ? -1.0f : +1.0f) * scroll_speed)); if (move_dir == ImGuiDir_Up || move_dir == ImGuiDir_Down) - SetScrollY(window, ImFloor(window->Scroll.y + ((move_dir == ImGuiDir_Up) ? -1.0f : +1.0f) * scroll_speed)); + SetScrollY(window, ImTrunc(window->Scroll.y + ((move_dir == ImGuiDir_Up) ? -1.0f : +1.0f) * scroll_speed)); } // *Normal* Manual scroll with LStick @@ -11335,9 +12681,9 @@ static void ImGui::NavUpdate() const ImVec2 scroll_dir = GetKeyMagnitude2d(ImGuiKey_GamepadLStickLeft, ImGuiKey_GamepadLStickRight, ImGuiKey_GamepadLStickUp, ImGuiKey_GamepadLStickDown); const float tweak_factor = IsKeyDown(ImGuiKey_NavGamepadTweakSlow) ? 1.0f / 10.0f : IsKeyDown(ImGuiKey_NavGamepadTweakFast) ? 10.0f : 1.0f; if (scroll_dir.x != 0.0f && window->ScrollbarX) - SetScrollX(window, ImFloor(window->Scroll.x + scroll_dir.x * scroll_speed * tweak_factor)); + SetScrollX(window, ImTrunc(window->Scroll.x + scroll_dir.x * scroll_speed * tweak_factor)); if (scroll_dir.y != 0.0f) - SetScrollY(window, ImFloor(window->Scroll.y + scroll_dir.y * scroll_speed * tweak_factor)); + SetScrollY(window, ImTrunc(window->Scroll.y + scroll_dir.y * scroll_speed * tweak_factor)); } } @@ -11351,11 +12697,7 @@ static void ImGui::NavUpdate() // Update mouse position if requested // (This will take into account the possibility that a Scroll was queued in the window to offset our absolute mouse position before scroll has been applied) if (set_mouse_pos && (io.ConfigFlags & ImGuiConfigFlags_NavEnableSetMousePos) && (io.BackendFlags & ImGuiBackendFlags_HasSetMousePos)) - { - io.MousePos = io.MousePosPrev = NavCalcPreferredRefPos(); - io.WantSetMousePos = true; - //IMGUI_DEBUG_LOG_IO("SetMousePos: (%.1f,%.1f)\n", io.MousePos.x, io.MousePos.y); - } + TeleportMousePos(NavCalcPreferredRefPos()); // [DEBUG] g.NavScoringDebugCount = 0; @@ -11389,6 +12731,8 @@ void ImGui::NavInitRequestApplyResult() IMGUI_DEBUG_LOG_NAV("[nav] NavInitRequest: ApplyResult: NavID 0x%08X in Layer %d Window \"%s\"\n", result->ID, g.NavLayer, g.NavWindow->Name); SetNavID(result->ID, g.NavLayer, result->FocusScopeId, result->RectRel); g.NavIdIsAlive = true; // Mark as alive from previous frame as we got a result + if (result->SelectionUserData != ImGuiSelectionUserData_Invalid) + g.NavLastValidSelectionUserData = result->SelectionUserData; if (g.NavInitRequestFromMove) NavRestoreHighlightAfterMove(); } @@ -11442,7 +12786,7 @@ void ImGui::NavUpdateCreateMoveRequest() g.NavMoveScrollFlags = ImGuiScrollFlags_None; if (window && !g.NavWindowingTarget && !(window->Flags & ImGuiWindowFlags_NoNavInputs)) { - const ImGuiInputFlags repeat_mode = ImGuiInputFlags_Repeat | (ImGuiInputFlags)ImGuiInputFlags_RepeatRateNavMove; + const ImGuiInputFlags repeat_mode = ImGuiInputFlags_Repeat | ImGuiInputFlags_RepeatRateNavMove; if (!IsActiveIdUsingNavDir(ImGuiDir_Left) && ((nav_gamepad_active && IsKeyPressed(ImGuiKey_GamepadDpadLeft, ImGuiKeyOwner_None, repeat_mode)) || (nav_keyboard_active && IsKeyPressed(ImGuiKey_LeftArrow, ImGuiKeyOwner_None, repeat_mode)))) { g.NavMoveDir = ImGuiDir_Left; } if (!IsActiveIdUsingNavDir(ImGuiDir_Right) && ((nav_gamepad_active && IsKeyPressed(ImGuiKey_GamepadDpadRight, ImGuiKeyOwner_None, repeat_mode)) || (nav_keyboard_active && IsKeyPressed(ImGuiKey_RightArrow, ImGuiKeyOwner_None, repeat_mode)))) { g.NavMoveDir = ImGuiDir_Right; } if (!IsActiveIdUsingNavDir(ImGuiDir_Up) && ((nav_gamepad_active && IsKeyPressed(ImGuiKey_GamepadDpadUp, ImGuiKeyOwner_None, repeat_mode)) || (nav_keyboard_active && IsKeyPressed(ImGuiKey_UpArrow, ImGuiKeyOwner_None, repeat_mode)))) { g.NavMoveDir = ImGuiDir_Up; } @@ -11526,7 +12870,7 @@ void ImGui::NavUpdateCreateMoveRequest() scoring_rect.TranslateY(scoring_rect_offset_y); if (g.NavMoveSubmitted) NavBiasScoringRect(scoring_rect, window->RootWindowForNav->NavPreferredScoringPosRel[g.NavLayer], g.NavMoveDir, g.NavMoveFlags); - IM_ASSERT(!scoring_rect.IsInverted()); // Ensure if we have a finite, non-inverted bounding box here will allow us to remove extraneous ImFabs() calls in NavScoreItem(). + IM_ASSERT(!scoring_rect.IsInverted()); // Ensure we have a non-inverted bounding box here will allow us to remove extraneous ImFabs() calls in NavScoreItem(). //GetForegroundDrawList()->AddRect(scoring_rect.Min, scoring_rect.Max, IM_COL32(255,200,0,255)); // [DEBUG] //if (!g.NavScoringNoClipRect.IsInverted()) { GetForegroundDrawList()->AddRect(g.NavScoringNoClipRect.Min, g.NavScoringNoClipRect.Max, IM_COL32(255, 200, 0, 255)); } // [DEBUG] } @@ -11548,16 +12892,16 @@ void ImGui::NavUpdateCreateTabbingRequest() // Initiate tabbing request // (this is ALWAYS ENABLED, regardless of ImGuiConfigFlags_NavEnableKeyboard flag!) - // Initially this was designed to use counters and modulo arithmetic, but that could not work with unsubmitted items (list clipper). Instead we use a strategy close to other move requests. // See NavProcessItemForTabbingRequest() for a description of the various forward/backward tabbing cases with and without wrapping. const bool nav_keyboard_active = (g.IO.ConfigFlags & ImGuiConfigFlags_NavEnableKeyboard) != 0; if (nav_keyboard_active) g.NavTabbingDir = g.IO.KeyShift ? -1 : (g.NavDisableHighlight == true && g.ActiveId == 0) ? 0 : +1; else g.NavTabbingDir = g.IO.KeyShift ? -1 : (g.ActiveId == 0) ? 0 : +1; + ImGuiNavMoveFlags move_flags = ImGuiNavMoveFlags_IsTabbing | ImGuiNavMoveFlags_Activate; ImGuiScrollFlags scroll_flags = window->Appearing ? ImGuiScrollFlags_KeepVisibleEdgeX | ImGuiScrollFlags_AlwaysCenterY : ImGuiScrollFlags_KeepVisibleEdgeX | ImGuiScrollFlags_KeepVisibleEdgeY; ImGuiDir clip_dir = (g.NavTabbingDir < 0) ? ImGuiDir_Up : ImGuiDir_Down; - NavMoveRequestSubmit(ImGuiDir_None, clip_dir, ImGuiNavMoveFlags_Tabbing, scroll_flags); // FIXME-NAV: Once we refactor tabbing, add LegacyApi flag to not activate non-inputable. + NavMoveRequestSubmit(ImGuiDir_None, clip_dir, move_flags, scroll_flags); // FIXME-NAV: Once we refactor tabbing, add LegacyApi flag to not activate non-inputable. g.NavTabbingCounter = -1; } @@ -11574,7 +12918,7 @@ void ImGui::NavMoveRequestApplyResult() ImGuiNavItemData* result = (g.NavMoveResultLocal.ID != 0) ? &g.NavMoveResultLocal : (g.NavMoveResultOther.ID != 0) ? &g.NavMoveResultOther : NULL; // Tabbing forward wrap - if ((g.NavMoveFlags & ImGuiNavMoveFlags_Tabbing) && result == NULL) + if ((g.NavMoveFlags & ImGuiNavMoveFlags_IsTabbing) && result == NULL) if ((g.NavTabbingCounter == 1 || g.NavTabbingDir == 0) && g.NavTabbingResultFirst.ID) result = &g.NavTabbingResultFirst; @@ -11582,9 +12926,9 @@ void ImGui::NavMoveRequestApplyResult() const ImGuiAxis axis = (g.NavMoveDir == ImGuiDir_Up || g.NavMoveDir == ImGuiDir_Down) ? ImGuiAxis_Y : ImGuiAxis_X; if (result == NULL) { - if (g.NavMoveFlags & ImGuiNavMoveFlags_Tabbing) - g.NavMoveFlags |= ImGuiNavMoveFlags_DontSetNavHighlight; - if (g.NavId != 0 && (g.NavMoveFlags & ImGuiNavMoveFlags_DontSetNavHighlight) == 0) + if (g.NavMoveFlags & ImGuiNavMoveFlags_IsTabbing) + g.NavMoveFlags |= ImGuiNavMoveFlags_NoSetNavHighlight; + if (g.NavId != 0 && (g.NavMoveFlags & ImGuiNavMoveFlags_NoSetNavHighlight) == 0) NavRestoreHighlightAfterMove(); NavClearPreferredPosForAxis(axis); // On a failed move, clear preferred pos for this axis. IMGUI_DEBUG_LOG_NAV("[nav] NavMoveSubmitted but not led to a result!\n"); @@ -11620,12 +12964,17 @@ void ImGui::NavMoveRequestApplyResult() { IMGUI_DEBUG_LOG_FOCUS("[focus] NavMoveRequest: SetNavWindow(\"%s\")\n", result->Window->Name); g.NavWindow = result->Window; + g.NavLastValidSelectionUserData = ImGuiSelectionUserData_Invalid; } + + // FIXME: Could become optional e.g. ImGuiNavMoveFlags_NoClearActiveId if we later want to apply navigation requests without altering active input. if (g.ActiveId != result->ID) ClearActiveID(); - if (g.NavId != result->ID) + + // Don't set NavJustMovedToId if just landed on the same spot (which may happen with ImGuiNavMoveFlags_AllowCurrentNavId) + // PageUp/PageDown however sets always set NavJustMovedTo (vs Home/End which doesn't) mimicking Windows behavior. + if ((g.NavId != result->ID || (g.NavMoveFlags & ImGuiNavMoveFlags_IsPageMove)) && (g.NavMoveFlags & ImGuiNavMoveFlags_NoSelect) == 0) { - // Don't set NavJustMovedToId if just landed on the same spot (which may happen with ImGuiNavMoveFlags_AllowCurrentNavId) g.NavJustMovedToId = result->ID; g.NavJustMovedToFocusScopeId = result->FocusScopeId; g.NavJustMovedToKeyMods = g.NavMoveKeyMods; @@ -11635,32 +12984,32 @@ void ImGui::NavMoveRequestApplyResult() IMGUI_DEBUG_LOG_NAV("[nav] NavMoveRequest: result NavID 0x%08X in Layer %d Window \"%s\"\n", result->ID, g.NavLayer, g.NavWindow->Name); ImVec2 preferred_scoring_pos_rel = g.NavWindow->RootWindowForNav->NavPreferredScoringPosRel[g.NavLayer]; SetNavID(result->ID, g.NavLayer, result->FocusScopeId, result->RectRel); + if (result->SelectionUserData != ImGuiSelectionUserData_Invalid) + g.NavLastValidSelectionUserData = result->SelectionUserData; // Restore last preferred position for current axis // (storing in RootWindowForNav-> as the info is desirable at the beginning of a Move Request. In theory all storage should use RootWindowForNav..) - if ((g.NavMoveFlags & ImGuiNavMoveFlags_Tabbing) == 0) + if ((g.NavMoveFlags & ImGuiNavMoveFlags_IsTabbing) == 0) { preferred_scoring_pos_rel[axis] = result->RectRel.GetCenter()[axis]; g.NavWindow->RootWindowForNav->NavPreferredScoringPosRel[g.NavLayer] = preferred_scoring_pos_rel; } - // Tabbing: Activates Inputable or Focus non-Inputable - if ((g.NavMoveFlags & ImGuiNavMoveFlags_Tabbing) && (result->InFlags & ImGuiItemFlags_Inputable)) - { - g.NavNextActivateId = result->ID; - g.NavNextActivateFlags = ImGuiActivateFlags_PreferInput | ImGuiActivateFlags_TryToPreserveState; - g.NavMoveFlags |= ImGuiNavMoveFlags_DontSetNavHighlight; - } + // Tabbing: Activates Inputable, otherwise only Focus + if ((g.NavMoveFlags & ImGuiNavMoveFlags_IsTabbing) && (result->InFlags & ImGuiItemFlags_Inputable) == 0) + g.NavMoveFlags &= ~ImGuiNavMoveFlags_Activate; // Activate if (g.NavMoveFlags & ImGuiNavMoveFlags_Activate) { g.NavNextActivateId = result->ID; g.NavNextActivateFlags = ImGuiActivateFlags_None; + if (g.NavMoveFlags & ImGuiNavMoveFlags_IsTabbing) + g.NavNextActivateFlags |= ImGuiActivateFlags_PreferInput | ImGuiActivateFlags_TryToPreserveState | ImGuiActivateFlags_FromTabbing; } // Enable nav highlight - if ((g.NavMoveFlags & ImGuiNavMoveFlags_DontSetNavHighlight) == 0) + if ((g.NavMoveFlags & ImGuiNavMoveFlags_NoSetNavHighlight) == 0) NavRestoreHighlightAfterMove(); } @@ -11687,15 +13036,14 @@ static void ImGui::NavUpdateCancelRequest() NavRestoreLayer(ImGuiNavLayer_Main); NavRestoreHighlightAfterMove(); } - else if (g.NavWindow && g.NavWindow != g.NavWindow->RootWindow && !(g.NavWindow->Flags & ImGuiWindowFlags_Popup) && g.NavWindow->ParentWindow) + else if (g.NavWindow && g.NavWindow != g.NavWindow->RootWindow && !(g.NavWindow->RootWindowForNav->Flags & ImGuiWindowFlags_Popup) && g.NavWindow->RootWindowForNav->ParentWindow) { // Exit child window - ImGuiWindow* child_window = g.NavWindow; - ImGuiWindow* parent_window = g.NavWindow->ParentWindow; + ImGuiWindow* child_window = g.NavWindow->RootWindowForNav; + ImGuiWindow* parent_window = child_window->ParentWindow; IM_ASSERT(child_window->ChildId != 0); - ImRect child_rect = child_window->Rect(); FocusWindow(parent_window); - SetNavID(child_window->ChildId, ImGuiNavLayer_Main, 0, WindowRectAbsToRel(parent_window, child_rect)); + SetNavID(child_window->ChildId, ImGuiNavLayer_Main, 0, WindowRectAbsToRel(parent_window, child_window->Rect())); NavRestoreHighlightAfterMove(); } else if (g.OpenPopupStack.Size > 0 && g.OpenPopupStack.back().Window != NULL && !(g.OpenPopupStack.back().Window->Flags & ImGuiWindowFlags_Modal)) @@ -11755,14 +13103,14 @@ static float ImGui::NavUpdatePageUpPageDown() nav_scoring_rect_offset_y = -page_offset_y; g.NavMoveDir = ImGuiDir_Down; // Because our scoring rect is offset up, we request the down direction (so we can always land on the last item) g.NavMoveClipDir = ImGuiDir_Up; - g.NavMoveFlags = ImGuiNavMoveFlags_AllowCurrentNavId | ImGuiNavMoveFlags_AlsoScoreVisibleSet; + g.NavMoveFlags = ImGuiNavMoveFlags_AllowCurrentNavId | ImGuiNavMoveFlags_AlsoScoreVisibleSet | ImGuiNavMoveFlags_IsPageMove; } else if (IsKeyPressed(ImGuiKey_PageDown, true)) { nav_scoring_rect_offset_y = +page_offset_y; g.NavMoveDir = ImGuiDir_Up; // Because our scoring rect is offset down, we request the up direction (so we can always land on the last item) g.NavMoveClipDir = ImGuiDir_Down; - g.NavMoveFlags = ImGuiNavMoveFlags_AllowCurrentNavId | ImGuiNavMoveFlags_AlsoScoreVisibleSet; + g.NavMoveFlags = ImGuiNavMoveFlags_AllowCurrentNavId | ImGuiNavMoveFlags_AlsoScoreVisibleSet | ImGuiNavMoveFlags_IsPageMove; } else if (home_pressed) { @@ -11927,6 +13275,7 @@ static void ImGui::NavUpdateWindowing() } // Start CTRL+Tab or Square+L/R window selection + // (g.ConfigNavWindowingKeyNext/g.ConfigNavWindowingKeyPrev defaults are ImGuiMod_Ctrl|ImGuiKey_Tab and ImGuiMod_Ctrl|ImGuiMod_Shift|ImGuiKey_Tab) const ImGuiID owner_id = ImHashStr("###NavUpdateWindowing"); const bool nav_gamepad_active = (io.ConfigFlags & ImGuiConfigFlags_NavEnableGamepad) != 0 && (io.BackendFlags & ImGuiBackendFlags_HasGamepad) != 0; const bool nav_keyboard_active = (io.ConfigFlags & ImGuiConfigFlags_NavEnableKeyboard) != 0; @@ -11989,28 +13338,33 @@ static void ImGui::NavUpdateWindowing() } // Keyboard: Press and Release ALT to toggle menu layer - // - Testing that only Alt is tested prevents Alt+Shift or AltGR from toggling menu layer. - // - AltGR is normally Alt+Ctrl but we can't reliably detect it (not all backends/systems/layout emit it as Alt+Ctrl). But even on keyboards without AltGR we don't want Alt+Ctrl to open menu anyway. - if (nav_keyboard_active && IsKeyPressed(ImGuiMod_Alt, ImGuiKeyOwner_None)) - { - g.NavWindowingToggleLayer = true; - g.NavInputSource = ImGuiInputSource_Keyboard; - } + const ImGuiKey windowing_toggle_keys[] = { ImGuiKey_LeftAlt, ImGuiKey_RightAlt }; + for (ImGuiKey windowing_toggle_key : windowing_toggle_keys) + if (nav_keyboard_active && IsKeyPressed(windowing_toggle_key, ImGuiKeyOwner_None)) + { + g.NavWindowingToggleLayer = true; + g.NavWindowingToggleKey = windowing_toggle_key; + g.NavInputSource = ImGuiInputSource_Keyboard; + break; + } if (g.NavWindowingToggleLayer && g.NavInputSource == ImGuiInputSource_Keyboard) { // We cancel toggling nav layer when any text has been typed (generally while holding Alt). (See #370) // We cancel toggling nav layer when other modifiers are pressed. (See #4439) + // - AltGR is Alt+Ctrl on some layout but we can't reliably detect it (not all backends/systems/layout emit it as Alt+Ctrl). // We cancel toggling nav layer if an owner has claimed the key. - if (io.InputQueueCharacters.Size > 0 || io.KeyCtrl || io.KeyShift || io.KeySuper || TestKeyOwner(ImGuiMod_Alt, ImGuiKeyOwner_None) == false) + if (io.InputQueueCharacters.Size > 0 || io.KeyCtrl || io.KeyShift || io.KeySuper) + g.NavWindowingToggleLayer = false; + if (TestKeyOwner(g.NavWindowingToggleKey, ImGuiKeyOwner_None) == false || TestKeyOwner(ImGuiMod_Alt, ImGuiKeyOwner_None) == false) g.NavWindowingToggleLayer = false; - // Apply layer toggle on release + // Apply layer toggle on Alt release // Important: as before version <18314 we lacked an explicit IO event for focus gain/loss, we also compare mouse validity to detect old backends clearing mouse pos on focus loss. - if (IsKeyReleased(ImGuiMod_Alt) && g.NavWindowingToggleLayer) + if (IsKeyReleased(g.NavWindowingToggleKey) && g.NavWindowingToggleLayer) if (g.ActiveId == 0 || g.ActiveIdAllowOverlap) if (IsMousePosValid(&io.MousePos) == IsMousePosValid(&io.MousePosPrev)) apply_toggle_layer = true; - if (!IsKeyDown(ImGuiMod_Alt)) + if (!IsKeyDown(g.NavWindowingToggleKey)) g.NavWindowingToggleLayer = false; } @@ -12028,10 +13382,10 @@ static void ImGui::NavUpdateWindowing() const float move_step = NAV_MOVE_SPEED * io.DeltaTime * ImMin(io.DisplayFramebufferScale.x, io.DisplayFramebufferScale.y); g.NavWindowingAccumDeltaPos += nav_move_dir * move_step; g.NavDisableMouseHover = true; - ImVec2 accum_floored = ImFloor(g.NavWindowingAccumDeltaPos); + ImVec2 accum_floored = ImTrunc(g.NavWindowingAccumDeltaPos); if (accum_floored.x != 0.0f || accum_floored.y != 0.0f) { - ImGuiWindow* moving_window = g.NavWindowingTarget->RootWindow; + ImGuiWindow* moving_window = g.NavWindowingTarget->RootWindowDockTree; SetWindowPos(moving_window, moving_window->Pos + accum_floored, ImGuiCond_Always); g.NavWindowingAccumDeltaPos -= accum_floored; } @@ -12041,6 +13395,9 @@ static void ImGui::NavUpdateWindowing() // Apply final focus if (apply_focus_window && (g.NavWindow == NULL || apply_focus_window != g.NavWindow->RootWindow)) { + // FIXME: Many actions here could be part of a higher-level/reused function. Why aren't they in FocusWindow() + // Investigate for each of them: ClearActiveID(), NavRestoreHighlightAfterMove(), NavRestoreLastChildNavWindow(), ClosePopupsOverWindow(), NavInitWindow() + ImGuiViewport* previous_viewport = g.NavWindow ? g.NavWindow->Viewport : NULL; ClearActiveID(); NavRestoreHighlightAfterMove(); ClosePopupsOverWindow(apply_focus_window, false); @@ -12058,6 +13415,10 @@ static void ImGui::NavUpdateWindowing() // won't be valid. if (apply_focus_window->DC.NavLayersActiveMaskNext == (1 << ImGuiNavLayer_Menu)) g.NavLayer = ImGuiNavLayer_Menu; + + // Request OS level focus + if (apply_focus_window->Viewport != previous_viewport && g.PlatformIO.Platform_SetWindowFocus) + g.PlatformIO.Platform_SetWindowFocus(apply_focus_window->Viewport); } if (apply_focus_window) g.NavWindowingTarget = NULL; @@ -12086,7 +13447,8 @@ static void ImGui::NavUpdateWindowing() if (new_nav_layer != g.NavLayer) { // Reinitialize navigation when entering menu bar with the Alt key (FIXME: could be a properly of the layer?) - if (new_nav_layer == ImGuiNavLayer_Menu) + const bool preserve_layer_1_nav_id = (new_nav_window->DockNodeAsHost != NULL); + if (new_nav_layer == ImGuiNavLayer_Menu && !preserve_layer_1_nav_id) g.NavWindow->NavLastIds[new_nav_layer] = 0; NavRestoreLayer(new_nav_layer); NavRestoreHighlightAfterMove(); @@ -12101,6 +13463,8 @@ static const char* GetFallbackWindowNameForWindowingList(ImGuiWindow* window) return ImGui::LocalizeGetMsg(ImGuiLocKey_WindowingPopup); if ((window->Flags & ImGuiWindowFlags_MenuBar) && strcmp(window->Name, "##MainMenuBar") == 0) return ImGui::LocalizeGetMsg(ImGuiLocKey_WindowingMainMenuBar); + if (window->DockNodeAsHost) + return "(Dock node)"; // Not normally shown to user. return ImGui::LocalizeGetMsg(ImGuiLocKey_WindowingUntitled); } @@ -12115,7 +13479,7 @@ void ImGui::NavUpdateWindowingOverlay() if (g.NavWindowingListWindow == NULL) g.NavWindowingListWindow = FindWindowByName("###NavWindowingList"); - const ImGuiViewport* viewport = GetMainViewport(); + const ImGuiViewport* viewport = /*g.NavWindow ? g.NavWindow->Viewport :*/ GetMainViewport(); SetNextWindowSizeConstraints(ImVec2(viewport->Size.x * 0.20f, viewport->Size.y * 0.20f), ImVec2(FLT_MAX, FLT_MAX)); SetNextWindowPos(viewport->GetCenter(), ImGuiCond_Always, ImVec2(0.5f, 0.5f)); PushStyleVar(ImGuiStyleVar_WindowPadding, g.Style.WindowPadding * 2.0f); @@ -12160,6 +13524,14 @@ void ImGui::ClearDragDrop() memset(&g.DragDropPayloadBufLocal, 0, sizeof(g.DragDropPayloadBufLocal)); } +bool ImGui::BeginTooltipHidden() +{ + ImGuiContext& g = *GImGui; + bool ret = Begin("##Tooltip_Hidden", NULL, ImGuiWindowFlags_Tooltip | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_AlwaysAutoResize); + SetWindowHiddenAndSkipItemsForCurrentFrame(g.CurrentWindow); + return ret; +} + // When this returns true you need to: a) call SetDragDropPayload() exactly once, b) you may render the payload visual/description, c) call EndDragDropSource() // If the item has an identifier: // - This assume/require the item to be activated (typically via ButtonBehavior). @@ -12216,7 +13588,7 @@ bool ImGui::BeginDragDropSource(ImGuiDragDropFlags flags) // Rely on keeping other window->LastItemXXX fields intact. source_id = g.LastItemData.ID = window->GetIDFromRectangle(g.LastItemData.Rect); KeepAliveID(source_id); - bool is_hovered = ItemHoverable(g.LastItemData.Rect, source_id); + bool is_hovered = ItemHoverable(g.LastItemData.Rect, source_id, g.LastItemData.InFlags); if (is_hovered && g.IO.MouseClicked[mouse_button]) { SetActiveID(source_id, window); @@ -12262,12 +13634,13 @@ bool ImGui::BeginDragDropSource(ImGuiDragDropFlags flags) { // Target can request the Source to not display its tooltip (we use a dedicated flag to make this request explicit) // We unfortunately can't just modify the source flags and skip the call to BeginTooltip, as caller may be emitting contents. - bool ret = BeginTooltip(); + bool ret; + if (g.DragDropAcceptIdPrev && (g.DragDropAcceptFlags & ImGuiDragDropFlags_AcceptNoPreviewTooltip)) + ret = BeginTooltipHidden(); + else + ret = BeginTooltip(); IM_ASSERT(ret); // FIXME-NEWBEGIN: If this ever becomes false, we need to Begin("##Hidden", NULL, ImGuiWindowFlags_NoSavedSettings) + SetWindowHiddendAndSkipItemsForCurrentFrame(). IM_UNUSED(ret); - - if (g.DragDropAcceptIdPrev && (g.DragDropAcceptFlags & ImGuiDragDropFlags_AcceptNoPreviewTooltip)) - SetWindowHiddendAndSkipItemsForCurrentFrame(g.CurrentWindow); } if (!(flags & ImGuiDragDropFlags_SourceNoDisableHover) && !(flags & ImGuiDragDropFlags_SourceExtern)) @@ -12346,7 +13719,7 @@ bool ImGui::BeginDragDropTargetCustom(const ImRect& bb, ImGuiID id) ImGuiWindow* window = g.CurrentWindow; ImGuiWindow* hovered_window = g.HoveredWindowUnderMovingWindow; - if (hovered_window == NULL || window->RootWindow != hovered_window->RootWindow) + if (hovered_window == NULL || window->RootWindowDockTree != hovered_window->RootWindowDockTree) return false; IM_ASSERT(id != 0); if (!IsMouseHoveringRect(bb.Min, bb.Max) || (id == g.DragDropPayload.SourceId)) @@ -12356,13 +13729,14 @@ bool ImGui::BeginDragDropTargetCustom(const ImRect& bb, ImGuiID id) IM_ASSERT(g.DragDropWithinTarget == false); g.DragDropTargetRect = bb; + g.DragDropTargetClipRect = window->ClipRect; // May want to be overriden by user depending on use case? g.DragDropTargetId = id; g.DragDropWithinTarget = true; return true; } // We don't use BeginDragDropTargetCustom() and duplicate its code because: -// 1) we use LastItemRectHoveredRect which handles items that push a temporarily clip rectangle in their code. Calling BeginDragDropTargetCustom(LastItemRect) would not handle them. +// 1) we use LastItemData's ImGuiItemStatusFlags_HoveredRect which handles items that push a temporarily clip rectangle in their code. Calling BeginDragDropTargetCustom(LastItemRect) would not handle them. // 2) and it's faster. as this code may be very frequently called, we want to early out as fast as we can. // Also note how the HoveredWindow test is positioned differently in both functions (in both functions we optimize for the cheapest early out case) bool ImGui::BeginDragDropTarget() @@ -12375,7 +13749,7 @@ bool ImGui::BeginDragDropTarget() if (!(g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredRect)) return false; ImGuiWindow* hovered_window = g.HoveredWindowUnderMovingWindow; - if (hovered_window == NULL || window->RootWindow != hovered_window->RootWindow || window->SkipItems) + if (hovered_window == NULL || window->RootWindowDockTree != hovered_window->RootWindowDockTree || window->SkipItems) return false; const ImRect& display_rect = (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HasDisplayRect) ? g.LastItemData.DisplayRect : g.LastItemData.Rect; @@ -12390,6 +13764,7 @@ bool ImGui::BeginDragDropTarget() IM_ASSERT(g.DragDropWithinTarget == false); g.DragDropTargetRect = display_rect; + g.DragDropTargetClipRect = (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HasClipRect) ? g.LastItemData.ClipRect : window->ClipRect; g.DragDropTargetId = id; g.DragDropWithinTarget = true; return true; @@ -12404,7 +13779,6 @@ bool ImGui::IsDragDropPayloadBeingAccepted() const ImGuiPayload* ImGui::AcceptDragDropPayload(const char* type, ImGuiDragDropFlags flags) { ImGuiContext& g = *GImGui; - ImGuiWindow* window = g.CurrentWindow; ImGuiPayload& payload = g.DragDropPayload; IM_ASSERT(g.DragDropActive); // Not called between BeginDragDropTarget() and EndDragDropTarget() ? IM_ASSERT(payload.DataFrameCount != -1); // Forgot to call EndDragDropTarget() ? @@ -12428,7 +13802,7 @@ const ImGuiPayload* ImGui::AcceptDragDropPayload(const char* type, ImGuiDragDrop payload.Preview = was_accepted_previously; flags |= (g.DragDropSourceFlags & ImGuiDragDropFlags_AcceptNoDrawDefaultRect); // Source can also inhibit the preview (useful for external sources that live for 1 frame) if (!(flags & ImGuiDragDropFlags_AcceptNoDrawDefaultRect) && payload.Preview) - window->DrawList->AddRect(r.Min - ImVec2(3.5f,3.5f), r.Max + ImVec2(3.5f, 3.5f), GetColorU32(ImGuiCol_DragDropTarget), 0.0f, 0, 2.0f); + RenderDragDropTargetRect(r, g.DragDropTargetClipRect); g.DragDropAcceptFrameCount = g.FrameCount; payload.Delivery = was_accepted_previously && !IsMouseDown(g.DragDropMouseButton); // For extern drag sources affecting OS window focus, it's easier to just test !IsMouseDown() instead of IsMouseReleased() @@ -12439,10 +13813,20 @@ const ImGuiPayload* ImGui::AcceptDragDropPayload(const char* type, ImGuiDragDrop return &payload; } -// FIXME-DRAGDROP: Settle on a proper default visuals for drop target. -void ImGui::RenderDragDropTargetRect(const ImRect& bb) +// FIXME-STYLE FIXME-DRAGDROP: Settle on a proper default visuals for drop target. +void ImGui::RenderDragDropTargetRect(const ImRect& bb, const ImRect& item_clip_rect) { - GetWindowDrawList()->AddRect(bb.Min - ImVec2(3.5f, 3.5f), bb.Max + ImVec2(3.5f, 3.5f), GetColorU32(ImGuiCol_DragDropTarget), 0.0f, 0, 2.0f); + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImRect bb_display = bb; + bb_display.ClipWith(item_clip_rect); // Clip THEN expand so we have a way to visualize that target is not entirely visible. + bb_display.Expand(3.5f); + bool push_clip_rect = !window->ClipRect.Contains(bb_display); + if (push_clip_rect) + window->DrawList->PushClipRectFullScreen(); + window->DrawList->AddRect(bb_display.Min, bb_display.Max, GetColorU32(ImGuiCol_DragDropTarget), 0.0f, 0, 2.0f); + if (push_clip_rect) + window->DrawList->PopClipRect(); } const ImGuiPayload* ImGui::GetDragDropPayload() @@ -12790,9 +14174,9 @@ ImGuiSettingsHandler* ImGui::FindSettingsHandler(const char* type_name) { ImGuiContext& g = *GImGui; const ImGuiID type_hash = ImHashStr(type_name); - for (int handler_n = 0; handler_n < g.SettingsHandlers.Size; handler_n++) - if (g.SettingsHandlers[handler_n].TypeHash == type_hash) - return &g.SettingsHandlers[handler_n]; + for (ImGuiSettingsHandler& handler : g.SettingsHandlers) + if (handler.TypeHash == type_hash) + return &handler; return NULL; } @@ -12801,9 +14185,9 @@ void ImGui::ClearIniSettings() { ImGuiContext& g = *GImGui; g.SettingsIniData.clear(); - for (int handler_n = 0; handler_n < g.SettingsHandlers.Size; handler_n++) - if (g.SettingsHandlers[handler_n].ClearAllFn) - g.SettingsHandlers[handler_n].ClearAllFn(&g, &g.SettingsHandlers[handler_n]); + for (ImGuiSettingsHandler& handler : g.SettingsHandlers) + if (handler.ClearAllFn != NULL) + handler.ClearAllFn(&g, &handler); } void ImGui::LoadIniSettingsFromDisk(const char* ini_filename) @@ -12838,9 +14222,9 @@ void ImGui::LoadIniSettingsFromMemory(const char* ini_data, size_t ini_size) // Call pre-read handlers // Some types will clear their data (e.g. dock information) some types will allow merge/override (window) - for (int handler_n = 0; handler_n < g.SettingsHandlers.Size; handler_n++) - if (g.SettingsHandlers[handler_n].ReadInitFn) - g.SettingsHandlers[handler_n].ReadInitFn(&g, &g.SettingsHandlers[handler_n]); + for (ImGuiSettingsHandler& handler : g.SettingsHandlers) + if (handler.ReadInitFn != NULL) + handler.ReadInitFn(&g, &handler); void* entry_data = NULL; ImGuiSettingsHandler* entry_handler = NULL; @@ -12884,9 +14268,9 @@ void ImGui::LoadIniSettingsFromMemory(const char* ini_data, size_t ini_size) memcpy(buf, ini_data, ini_size); // Call post-read handlers - for (int handler_n = 0; handler_n < g.SettingsHandlers.Size; handler_n++) - if (g.SettingsHandlers[handler_n].ApplyAllFn) - g.SettingsHandlers[handler_n].ApplyAllFn(&g, &g.SettingsHandlers[handler_n]); + for (ImGuiSettingsHandler& handler : g.SettingsHandlers) + if (handler.ApplyAllFn != NULL) + handler.ApplyAllFn(&g, &handler); } void ImGui::SaveIniSettingsToDisk(const char* ini_filename) @@ -12912,11 +14296,8 @@ const char* ImGui::SaveIniSettingsToMemory(size_t* out_size) g.SettingsDirtyTimer = 0.0f; g.SettingsIniData.Buf.resize(0); g.SettingsIniData.Buf.push_back(0); - for (int handler_n = 0; handler_n < g.SettingsHandlers.Size; handler_n++) - { - ImGuiSettingsHandler* handler = &g.SettingsHandlers[handler_n]; - handler->WriteAllFn(&g, handler, &g.SettingsIniData); - } + for (ImGuiSettingsHandler& handler : g.SettingsHandlers) + handler.WriteAllFn(&g, &handler, &g.SettingsIniData); if (out_size) *out_size = (size_t)g.SettingsIniData.size(); return g.SettingsIniData.c_str(); @@ -12926,12 +14307,13 @@ ImGuiWindowSettings* ImGui::CreateNewWindowSettings(const char* name) { ImGuiContext& g = *GImGui; -#if !IMGUI_DEBUG_INI_SETTINGS - // Skip to the "###" marker if any. We don't skip past to match the behavior of GetID() - // Preserve the full string when IMGUI_DEBUG_INI_SETTINGS is set to make .ini inspection easier. - if (const char* p = strstr(name, "###")) - name = p; -#endif + if (g.IO.ConfigDebugIniSettings == false) + { + // Skip to the "###" marker if any. We don't skip past to match the behavior of GetID() + // Preserve the full string when ConfigDebugVerboseIniSettings is set to make .ini inspection easier. + if (const char* p = strstr(name, "###")) + name = p; + } const size_t name_len = strlen(name); // Allocate chunk @@ -12968,11 +14350,14 @@ ImGuiWindowSettings* ImGui::FindWindowSettingsByWindow(ImGuiWindow* window) void ImGui::ClearWindowSettings(const char* name) { //IMGUI_DEBUG_LOG("ClearWindowSettings('%s')\n", name); + ImGuiContext& g = *GImGui; ImGuiWindow* window = FindWindowByName(name); if (window != NULL) { window->Flags |= ImGuiWindowFlags_NoSavedSettings; InitOrLoadWindowSettings(window, NULL); + if (window->DockId != 0) + DockContextProcessUndockWindow(&g, window, true); } if (ImGuiWindowSettings* settings = window ? FindWindowSettingsByWindow(window) : FindWindowSettingsByID(ImHashStr(name))) settings->WantDelete = true; @@ -12981,8 +14366,8 @@ void ImGui::ClearWindowSettings(const char* name) static void WindowSettingsHandler_ClearAll(ImGuiContext* ctx, ImGuiSettingsHandler*) { ImGuiContext& g = *ctx; - for (int i = 0; i != g.Windows.Size; i++) - g.Windows[i]->SettingsOffset = -1; + for (ImGuiWindow* window : g.Windows) + window->SettingsOffset = -1; g.SettingsWindows.clear(); } @@ -13004,9 +14389,16 @@ static void WindowSettingsHandler_ReadLine(ImGuiContext*, ImGuiSettingsHandler*, ImGuiWindowSettings* settings = (ImGuiWindowSettings*)entry; int x, y; int i; - if (sscanf(line, "Pos=%i,%i", &x, &y) == 2) { settings->Pos = ImVec2ih((short)x, (short)y); } - else if (sscanf(line, "Size=%i,%i", &x, &y) == 2) { settings->Size = ImVec2ih((short)x, (short)y); } - else if (sscanf(line, "Collapsed=%d", &i) == 1) { settings->Collapsed = (i != 0); } + ImU32 u1; + if (sscanf(line, "Pos=%i,%i", &x, &y) == 2) { settings->Pos = ImVec2ih((short)x, (short)y); } + else if (sscanf(line, "Size=%i,%i", &x, &y) == 2) { settings->Size = ImVec2ih((short)x, (short)y); } + else if (sscanf(line, "ViewportId=0x%08X", &u1) == 1) { settings->ViewportId = u1; } + else if (sscanf(line, "ViewportPos=%i,%i", &x, &y) == 2){ settings->ViewportPos = ImVec2ih((short)x, (short)y); } + else if (sscanf(line, "Collapsed=%d", &i) == 1) { settings->Collapsed = (i != 0); } + else if (sscanf(line, "IsChild=%d", &i) == 1) { settings->IsChild = (i != 0); } + else if (sscanf(line, "DockId=0x%X,%d", &u1, &i) == 2) { settings->DockId = u1; settings->DockOrder = (short)i; } + else if (sscanf(line, "DockId=0x%X", &u1) == 1) { settings->DockId = u1; settings->DockOrder = -1; } + else if (sscanf(line, "ClassId=0x%X", &u1) == 1) { settings->ClassId = u1; } } // Apply to existing windows (if any) @@ -13027,9 +14419,8 @@ static void WindowSettingsHandler_WriteAll(ImGuiContext* ctx, ImGuiSettingsHandl // Gather data from windows that were active during this session // (if a window wasn't opened in this session we preserve its settings) ImGuiContext& g = *ctx; - for (int i = 0; i != g.Windows.Size; i++) + for (ImGuiWindow* window : g.Windows) { - ImGuiWindow* window = g.Windows[i]; if (window->Flags & ImGuiWindowFlags_NoSavedSettings) continue; @@ -13040,10 +14431,16 @@ static void WindowSettingsHandler_WriteAll(ImGuiContext* ctx, ImGuiSettingsHandl window->SettingsOffset = g.SettingsWindows.offset_from_ptr(settings); } IM_ASSERT(settings->ID == window->ID); - settings->Pos = ImVec2ih(window->Pos); + settings->Pos = ImVec2ih(window->Pos - window->ViewportPos); settings->Size = ImVec2ih(window->SizeFull); - + settings->ViewportId = window->ViewportId; + settings->ViewportPos = ImVec2ih(window->ViewportPos); + IM_ASSERT(window->DockNode == NULL || window->DockNode->ID == window->DockId); + settings->DockId = window->DockId; + settings->ClassId = window->WindowClass.ClassId; + settings->DockOrder = window->DockOrder; settings->Collapsed = window->Collapsed; + settings->IsChild = (window->RootWindow != window); // Cannot rely on ImGuiWindowFlags_ChildWindow here as docked windows have this set. settings->WantDelete = false; } @@ -13055,9 +14452,34 @@ static void WindowSettingsHandler_WriteAll(ImGuiContext* ctx, ImGuiSettingsHandl continue; const char* settings_name = settings->GetName(); buf->appendf("[%s][%s]\n", handler->TypeName, settings_name); - buf->appendf("Pos=%d,%d\n", settings->Pos.x, settings->Pos.y); - buf->appendf("Size=%d,%d\n", settings->Size.x, settings->Size.y); - buf->appendf("Collapsed=%d\n", settings->Collapsed); + if (settings->IsChild) + { + buf->appendf("IsChild=1\n"); + buf->appendf("Size=%d,%d\n", settings->Size.x, settings->Size.y); + } + else + { + if (settings->ViewportId != 0 && settings->ViewportId != ImGui::IMGUI_VIEWPORT_DEFAULT_ID) + { + buf->appendf("ViewportPos=%d,%d\n", settings->ViewportPos.x, settings->ViewportPos.y); + buf->appendf("ViewportId=0x%08X\n", settings->ViewportId); + } + if (settings->Pos.x != 0 || settings->Pos.y != 0 || settings->ViewportId == ImGui::IMGUI_VIEWPORT_DEFAULT_ID) + buf->appendf("Pos=%d,%d\n", settings->Pos.x, settings->Pos.y); + if (settings->Size.x != 0 || settings->Size.y != 0) + buf->appendf("Size=%d,%d\n", settings->Size.x, settings->Size.y); + buf->appendf("Collapsed=%d\n", settings->Collapsed); + if (settings->DockId != 0) + { + //buf->appendf("TabId=0x%08X\n", ImHashStr("#TAB", 4, settings->ID)); // window->TabId: this is not read back but writing it makes "debugging" the .ini data easier. + if (settings->DockOrder == -1) + buf->appendf("DockId=0x%08X\n", settings->DockId); + else + buf->appendf("DockId=0x%08X,%d\n", settings->DockId, settings->DockOrder); + if (settings->ClassId != 0) + buf->appendf("ClassId=0x%08X\n", settings->ClassId); + } + } buf->append("\n"); } } @@ -13079,9 +14501,28 @@ void ImGui::LocalizeRegisterEntries(const ImGuiLocEntry* entries, int count) // [SECTION] VIEWPORTS, PLATFORM WINDOWS //----------------------------------------------------------------------------- // - GetMainViewport() +// - FindViewportByID() +// - FindViewportByPlatformHandle() +// - SetCurrentViewport() [Internal] // - SetWindowViewport() [Internal] +// - GetWindowAlwaysWantOwnViewport() [Internal] +// - UpdateTryMergeWindowIntoHostViewport() [Internal] +// - UpdateTryMergeWindowIntoHostViewports() [Internal] +// - TranslateWindowsInViewport() [Internal] +// - ScaleWindowsInViewport() [Internal] +// - FindHoveredViewportFromPlatformWindowStack() [Internal] // - UpdateViewportsNewFrame() [Internal] -// (this section is more complete in the 'docking' branch) +// - UpdateViewportsEndFrame() [Internal] +// - AddUpdateViewport() [Internal] +// - WindowSelectViewport() [Internal] +// - WindowSyncOwnedViewport() [Internal] +// - UpdatePlatformWindows() +// - RenderPlatformWindowsDefault() +// - FindPlatformMonitorForPos() [Internal] +// - FindPlatformMonitorForRect() [Internal] +// - UpdateViewportPlatformMonitor() [Internal] +// - DestroyPlatformWindow() [Internal] +// - DestroyPlatformWindows() //----------------------------------------------------------------------------- ImGuiViewport* ImGui::GetMainViewport() @@ -13090,41 +14531,4902 @@ ImGuiViewport* ImGui::GetMainViewport() return g.Viewports[0]; } +// FIXME: This leaks access to viewports not listed in PlatformIO.Viewports[]. Problematic? (#4236) +ImGuiViewport* ImGui::FindViewportByID(ImGuiID id) +{ + ImGuiContext& g = *GImGui; + for (ImGuiViewportP* viewport : g.Viewports) + if (viewport->ID == id) + return viewport; + return NULL; +} + +ImGuiViewport* ImGui::FindViewportByPlatformHandle(void* platform_handle) +{ + ImGuiContext& g = *GImGui; + for (ImGuiViewportP* viewport : g.Viewports) + if (viewport->PlatformHandle == platform_handle) + return viewport; + return NULL; +} + +void ImGui::SetCurrentViewport(ImGuiWindow* current_window, ImGuiViewportP* viewport) +{ + ImGuiContext& g = *GImGui; + (void)current_window; + + if (viewport) + viewport->LastFrameActive = g.FrameCount; + if (g.CurrentViewport == viewport) + return; + g.CurrentDpiScale = viewport ? viewport->DpiScale : 1.0f; + g.CurrentViewport = viewport; + //IMGUI_DEBUG_LOG_VIEWPORT("[viewport] SetCurrentViewport changed '%s' 0x%08X\n", current_window ? current_window->Name : NULL, viewport ? viewport->ID : 0); + + // Notify platform layer of viewport changes + // FIXME-DPI: This is only currently used for experimenting with handling of multiple DPI + if (g.CurrentViewport && g.PlatformIO.Platform_OnChangedViewport) + g.PlatformIO.Platform_OnChangedViewport(g.CurrentViewport); +} + void ImGui::SetWindowViewport(ImGuiWindow* window, ImGuiViewportP* viewport) { + // Abandon viewport + if (window->ViewportOwned && window->Viewport->Window == window) + window->Viewport->Size = ImVec2(0.0f, 0.0f); + window->Viewport = viewport; + window->ViewportId = viewport->ID; + window->ViewportOwned = (viewport->Window == window); +} + +static bool ImGui::GetWindowAlwaysWantOwnViewport(ImGuiWindow* window) +{ + // Tooltips and menus are not automatically forced into their own viewport when the NoMerge flag is set, however the multiplication of viewports makes them more likely to protrude and create their own. + ImGuiContext& g = *GImGui; + if (g.IO.ConfigViewportsNoAutoMerge || (window->WindowClass.ViewportFlagsOverrideSet & ImGuiViewportFlags_NoAutoMerge)) + if (g.ConfigFlagsCurrFrame & ImGuiConfigFlags_ViewportsEnable) + if (!window->DockIsActive) + if ((window->Flags & (ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_ChildMenu | ImGuiWindowFlags_Tooltip)) == 0) + if ((window->Flags & ImGuiWindowFlags_Popup) == 0 || (window->Flags & ImGuiWindowFlags_Modal) != 0) + return true; + return false; +} + +static bool ImGui::UpdateTryMergeWindowIntoHostViewport(ImGuiWindow* window, ImGuiViewportP* viewport) +{ + ImGuiContext& g = *GImGui; + if (window->Viewport == viewport) + return false; + if ((viewport->Flags & ImGuiViewportFlags_CanHostOtherWindows) == 0) + return false; + if ((viewport->Flags & ImGuiViewportFlags_IsMinimized) != 0) + return false; + if (!viewport->GetMainRect().Contains(window->Rect())) + return false; + if (GetWindowAlwaysWantOwnViewport(window)) + return false; + + // FIXME: Can't use g.WindowsFocusOrder[] for root windows only as we care about Z order. If we maintained a DisplayOrder along with FocusOrder we could.. + for (ImGuiWindow* window_behind : g.Windows) + { + if (window_behind == window) + break; + if (window_behind->WasActive && window_behind->ViewportOwned && !(window_behind->Flags & ImGuiWindowFlags_ChildWindow)) + if (window_behind->Viewport->GetMainRect().Overlaps(window->Rect())) + return false; + } + + // Move to the existing viewport, Move child/hosted windows as well (FIXME-OPT: iterate child) + ImGuiViewportP* old_viewport = window->Viewport; + if (window->ViewportOwned) + for (int n = 0; n < g.Windows.Size; n++) + if (g.Windows[n]->Viewport == old_viewport) + SetWindowViewport(g.Windows[n], viewport); + SetWindowViewport(window, viewport); + BringWindowToDisplayFront(window); + + return true; +} + +// FIXME: handle 0 to N host viewports +static bool ImGui::UpdateTryMergeWindowIntoHostViewports(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + return UpdateTryMergeWindowIntoHostViewport(window, g.Viewports[0]); +} + +// Translate Dear ImGui windows when a Host Viewport has been moved +// (This additionally keeps windows at the same place when ImGuiConfigFlags_ViewportsEnable is toggled!) +void ImGui::TranslateWindowsInViewport(ImGuiViewportP* viewport, const ImVec2& old_pos, const ImVec2& new_pos) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(viewport->Window == NULL && (viewport->Flags & ImGuiViewportFlags_CanHostOtherWindows)); + + // 1) We test if ImGuiConfigFlags_ViewportsEnable was just toggled, which allows us to conveniently + // translate imgui windows from OS-window-local to absolute coordinates or vice-versa. + // 2) If it's not going to fit into the new size, keep it at same absolute position. + // One problem with this is that most Win32 applications doesn't update their render while dragging, + // and so the window will appear to teleport when releasing the mouse. + const bool translate_all_windows = (g.ConfigFlagsCurrFrame & ImGuiConfigFlags_ViewportsEnable) != (g.ConfigFlagsLastFrame & ImGuiConfigFlags_ViewportsEnable); + ImRect test_still_fit_rect(old_pos, old_pos + viewport->Size); + ImVec2 delta_pos = new_pos - old_pos; + for (ImGuiWindow* window : g.Windows) // FIXME-OPT + if (translate_all_windows || (window->Viewport == viewport && test_still_fit_rect.Contains(window->Rect()))) + TranslateWindow(window, delta_pos); +} + +// Scale all windows (position, size). Use when e.g. changing DPI. (This is a lossy operation!) +void ImGui::ScaleWindowsInViewport(ImGuiViewportP* viewport, float scale) +{ + ImGuiContext& g = *GImGui; + if (viewport->Window) + { + ScaleWindow(viewport->Window, scale); + } + else + { + for (ImGuiWindow* window : g.Windows) + if (window->Viewport == viewport) + ScaleWindow(window, scale); + } +} + +// If the backend doesn't set MouseLastHoveredViewport or doesn't honor ImGuiViewportFlags_NoInputs, we do a search ourselves. +// A) It won't take account of the possibility that non-imgui windows may be in-between our dragged window and our target window. +// B) It requires Platform_GetWindowFocus to be implemented by backend. +ImGuiViewportP* ImGui::FindHoveredViewportFromPlatformWindowStack(const ImVec2& mouse_platform_pos) +{ + ImGuiContext& g = *GImGui; + ImGuiViewportP* best_candidate = NULL; + for (ImGuiViewportP* viewport : g.Viewports) + if (!(viewport->Flags & (ImGuiViewportFlags_NoInputs | ImGuiViewportFlags_IsMinimized)) && viewport->GetMainRect().Contains(mouse_platform_pos)) + if (best_candidate == NULL || best_candidate->LastFocusedStampCount < viewport->LastFocusedStampCount) + best_candidate = viewport; + return best_candidate; } // Update viewports and monitor infos +// Note that this is running even if 'ImGuiConfigFlags_ViewportsEnable' is not set, in order to clear unused viewports (if any) and update monitor info. static void ImGui::UpdateViewportsNewFrame() { ImGuiContext& g = *GImGui; - IM_ASSERT(g.Viewports.Size == 1); + IM_ASSERT(g.PlatformIO.Viewports.Size <= g.Viewports.Size); - // Update main viewport with current platform position. + // Update Minimized status (we need it first in order to decide if we'll apply Pos/Size of the main viewport) + // Update Focused status + const bool viewports_enabled = (g.ConfigFlagsCurrFrame & ImGuiConfigFlags_ViewportsEnable) != 0; + if (viewports_enabled) + { + ImGuiViewportP* focused_viewport = NULL; + for (ImGuiViewportP* viewport : g.Viewports) + { + const bool platform_funcs_available = viewport->PlatformWindowCreated; + if (g.PlatformIO.Platform_GetWindowMinimized && platform_funcs_available) + { + bool is_minimized = g.PlatformIO.Platform_GetWindowMinimized(viewport); + if (is_minimized) + viewport->Flags |= ImGuiViewportFlags_IsMinimized; + else + viewport->Flags &= ~ImGuiViewportFlags_IsMinimized; + } + + // Update our implicit z-order knowledge of platform windows, which is used when the backend cannot provide io.MouseHoveredViewport. + // When setting Platform_GetWindowFocus, it is expected that the platform backend can handle calls without crashing if it doesn't have data stored. + if (g.PlatformIO.Platform_GetWindowFocus && platform_funcs_available) + { + bool is_focused = g.PlatformIO.Platform_GetWindowFocus(viewport); + if (is_focused) + viewport->Flags |= ImGuiViewportFlags_IsFocused; + else + viewport->Flags &= ~ImGuiViewportFlags_IsFocused; + if (is_focused) + focused_viewport = viewport; + } + } + + // Focused viewport has changed? + if (focused_viewport && g.PlatformLastFocusedViewportId != focused_viewport->ID) + { + IMGUI_DEBUG_LOG_VIEWPORT("[viewport] Focused viewport changed %08X -> %08X, attempting to apply our focus.\n", g.PlatformLastFocusedViewportId, focused_viewport->ID); + const ImGuiViewport* prev_focused_viewport = FindViewportByID(g.PlatformLastFocusedViewportId); + const bool prev_focused_has_been_destroyed = (prev_focused_viewport == NULL) || (prev_focused_viewport->PlatformWindowCreated == false); + + // Store a tag so we can infer z-order easily from all our windows + // We compare PlatformLastFocusedViewportId so newly created viewports with _NoFocusOnAppearing flag + // will keep the front most stamp instead of losing it back to their parent viewport. + if (focused_viewport->LastFocusedStampCount != g.ViewportFocusedStampCount) + focused_viewport->LastFocusedStampCount = ++g.ViewportFocusedStampCount; + g.PlatformLastFocusedViewportId = focused_viewport->ID; + + // Focus associated dear imgui window + // - if focus didn't happen with a click within imgui boundaries, e.g. Clicking platform title bar. (#6299) + // - if focus didn't happen because we destroyed another window (#6462) + // FIXME: perhaps 'FocusTopMostWindowUnderOne()' can handle the 'focused_window->Window != NULL' case as well. + const bool apply_imgui_focus_on_focused_viewport = !IsAnyMouseDown() && !prev_focused_has_been_destroyed; + if (apply_imgui_focus_on_focused_viewport) + { + focused_viewport->LastFocusedHadNavWindow |= (g.NavWindow != NULL) && (g.NavWindow->Viewport == focused_viewport); // Update so a window changing viewport won't lose focus. + ImGuiFocusRequestFlags focus_request_flags = ImGuiFocusRequestFlags_UnlessBelowModal | ImGuiFocusRequestFlags_RestoreFocusedChild; + if (focused_viewport->Window != NULL) + FocusWindow(focused_viewport->Window, focus_request_flags); + else if (focused_viewport->LastFocusedHadNavWindow) + FocusTopMostWindowUnderOne(NULL, NULL, focused_viewport, focus_request_flags); // Focus top most in viewport + else + FocusWindow(NULL, focus_request_flags); // No window had focus last time viewport was focused + } + } + if (focused_viewport) + focused_viewport->LastFocusedHadNavWindow = (g.NavWindow != NULL) && (g.NavWindow->Viewport == focused_viewport); + } + + // Create/update main viewport with current platform position. // FIXME-VIEWPORT: Size is driven by backend/user code for backward-compatibility but we should aim to make this more consistent. ImGuiViewportP* main_viewport = g.Viewports[0]; - main_viewport->Flags = ImGuiViewportFlags_IsPlatformWindow | ImGuiViewportFlags_OwnedByApp; - main_viewport->Pos = ImVec2(0.0f, 0.0f); - main_viewport->Size = g.IO.DisplaySize; + IM_ASSERT(main_viewport->ID == IMGUI_VIEWPORT_DEFAULT_ID); + IM_ASSERT(main_viewport->Window == NULL); + ImVec2 main_viewport_pos = viewports_enabled ? g.PlatformIO.Platform_GetWindowPos(main_viewport) : ImVec2(0.0f, 0.0f); + ImVec2 main_viewport_size = g.IO.DisplaySize; + if (viewports_enabled && (main_viewport->Flags & ImGuiViewportFlags_IsMinimized)) + { + main_viewport_pos = main_viewport->Pos; // Preserve last pos/size when minimized (FIXME: We don't do the same for Size outside of the viewport path) + main_viewport_size = main_viewport->Size; + } + AddUpdateViewport(NULL, IMGUI_VIEWPORT_DEFAULT_ID, main_viewport_pos, main_viewport_size, ImGuiViewportFlags_OwnedByApp | ImGuiViewportFlags_CanHostOtherWindows); + g.CurrentDpiScale = 0.0f; + g.CurrentViewport = NULL; + g.MouseViewport = NULL; for (int n = 0; n < g.Viewports.Size; n++) { ImGuiViewportP* viewport = g.Viewports[n]; + viewport->Idx = n; - // Lock down space taken by menu bars and status bars, reset the offset for fucntions like BeginMainMenuBar() to alter them again. + // Erase unused viewports + if (n > 0 && viewport->LastFrameActive < g.FrameCount - 2) + { + DestroyViewport(viewport); + n--; + continue; + } + + const bool platform_funcs_available = viewport->PlatformWindowCreated; + if (viewports_enabled) + { + // Update Position and Size (from Platform Window to ImGui) if requested. + // We do it early in the frame instead of waiting for UpdatePlatformWindows() to avoid a frame of lag when moving/resizing using OS facilities. + if (!(viewport->Flags & ImGuiViewportFlags_IsMinimized) && platform_funcs_available) + { + // Viewport->WorkPos and WorkSize will be updated below + if (viewport->PlatformRequestMove) + viewport->Pos = viewport->LastPlatformPos = g.PlatformIO.Platform_GetWindowPos(viewport); + if (viewport->PlatformRequestResize) + viewport->Size = viewport->LastPlatformSize = g.PlatformIO.Platform_GetWindowSize(viewport); + } + } + + // Update/copy monitor info + UpdateViewportPlatformMonitor(viewport); + + // Lock down space taken by menu bars and status bars, reset the offset for functions like BeginMainMenuBar() to alter them again. viewport->WorkOffsetMin = viewport->BuildWorkOffsetMin; viewport->WorkOffsetMax = viewport->BuildWorkOffsetMax; viewport->BuildWorkOffsetMin = viewport->BuildWorkOffsetMax = ImVec2(0.0f, 0.0f); viewport->UpdateWorkRect(); + + // Reset alpha every frame. Users of transparency (docking) needs to request a lower alpha back. + viewport->Alpha = 1.0f; + + // Translate Dear ImGui windows when a Host Viewport has been moved + // (This additionally keeps windows at the same place when ImGuiConfigFlags_ViewportsEnable is toggled!) + const ImVec2 viewport_delta_pos = viewport->Pos - viewport->LastPos; + if ((viewport->Flags & ImGuiViewportFlags_CanHostOtherWindows) && (viewport_delta_pos.x != 0.0f || viewport_delta_pos.y != 0.0f)) + TranslateWindowsInViewport(viewport, viewport->LastPos, viewport->Pos); + + // Update DPI scale + float new_dpi_scale; + if (g.PlatformIO.Platform_GetWindowDpiScale && platform_funcs_available) + new_dpi_scale = g.PlatformIO.Platform_GetWindowDpiScale(viewport); + else if (viewport->PlatformMonitor != -1) + new_dpi_scale = g.PlatformIO.Monitors[viewport->PlatformMonitor].DpiScale; + else + new_dpi_scale = (viewport->DpiScale != 0.0f) ? viewport->DpiScale : 1.0f; + if (viewport->DpiScale != 0.0f && new_dpi_scale != viewport->DpiScale) + { + float scale_factor = new_dpi_scale / viewport->DpiScale; + if (g.IO.ConfigFlags & ImGuiConfigFlags_DpiEnableScaleViewports) + ScaleWindowsInViewport(viewport, scale_factor); + //if (viewport == GetMainViewport()) + // g.PlatformInterface.SetWindowSize(viewport, viewport->Size * scale_factor); + + // Scale our window moving pivot so that the window will rescale roughly around the mouse position. + // FIXME-VIEWPORT: This currently creates a resizing feedback loop when a window is straddling a DPI transition border. + // (Minor: since our sizes do not perfectly linearly scale, deferring the click offset scale until we know the actual window scale ratio may get us slightly more precise mouse positioning.) + //if (g.MovingWindow != NULL && g.MovingWindow->Viewport == viewport) + // g.ActiveIdClickOffset = ImTrunc(g.ActiveIdClickOffset * scale_factor); + } + viewport->DpiScale = new_dpi_scale; + } + + // Update fallback monitor + g.PlatformMonitorsFullWorkRect = ImRect(+FLT_MAX, +FLT_MAX, -FLT_MAX, -FLT_MAX); + if (g.PlatformIO.Monitors.Size == 0) + { + ImGuiPlatformMonitor* monitor = &g.FallbackMonitor; + monitor->MainPos = main_viewport->Pos; + monitor->MainSize = main_viewport->Size; + monitor->WorkPos = main_viewport->WorkPos; + monitor->WorkSize = main_viewport->WorkSize; + monitor->DpiScale = main_viewport->DpiScale; + g.PlatformMonitorsFullWorkRect.Add(monitor->WorkPos); + g.PlatformMonitorsFullWorkRect.Add(monitor->WorkPos + monitor->WorkSize); + } + for (ImGuiPlatformMonitor& monitor : g.PlatformIO.Monitors) + { + g.PlatformMonitorsFullWorkRect.Add(monitor.WorkPos); + g.PlatformMonitorsFullWorkRect.Add(monitor.WorkPos + monitor.WorkSize); + } + + if (!viewports_enabled) + { + g.MouseViewport = main_viewport; + return; + } + + // Mouse handling: decide on the actual mouse viewport for this frame between the active/focused viewport and the hovered viewport. + // Note that 'viewport_hovered' should skip over any viewport that has the ImGuiViewportFlags_NoInputs flags set. + ImGuiViewportP* viewport_hovered = NULL; + if (g.IO.BackendFlags & ImGuiBackendFlags_HasMouseHoveredViewport) + { + viewport_hovered = g.IO.MouseHoveredViewport ? (ImGuiViewportP*)FindViewportByID(g.IO.MouseHoveredViewport) : NULL; + if (viewport_hovered && (viewport_hovered->Flags & ImGuiViewportFlags_NoInputs)) + viewport_hovered = FindHoveredViewportFromPlatformWindowStack(g.IO.MousePos); // Backend failed to handle _NoInputs viewport: revert to our fallback. + } + else + { + // If the backend doesn't know how to honor ImGuiViewportFlags_NoInputs, we do a search ourselves. Note that this search: + // A) won't take account of the possibility that non-imgui windows may be in-between our dragged window and our target window. + // B) won't take account of how the backend apply parent<>child relationship to secondary viewports, which affects their Z order. + // C) uses LastFrameAsRefViewport as a flawed replacement for the last time a window was focused (we could/should fix that by introducing Focus functions in PlatformIO) + viewport_hovered = FindHoveredViewportFromPlatformWindowStack(g.IO.MousePos); + } + if (viewport_hovered != NULL) + g.MouseLastHoveredViewport = viewport_hovered; + else if (g.MouseLastHoveredViewport == NULL) + g.MouseLastHoveredViewport = g.Viewports[0]; + + // Update mouse reference viewport + // (when moving a window we aim at its viewport, but this will be overwritten below if we go in drag and drop mode) + // (MovingViewport->Viewport will be NULL in the rare situation where the window disappared while moving, set UpdateMouseMovingWindowNewFrame() for details) + if (g.MovingWindow && g.MovingWindow->Viewport) + g.MouseViewport = g.MovingWindow->Viewport; + else + g.MouseViewport = g.MouseLastHoveredViewport; + + // When dragging something, always refer to the last hovered viewport. + // - when releasing a moving window we will revert to aiming behind (at viewport_hovered) + // - when we are between viewports, our dragged preview will tend to show in the last viewport _even_ if we don't have tooltips in their viewports (when lacking monitor info) + // - consider the case of holding on a menu item to browse child menus: even thou a mouse button is held, there's no active id because menu items only react on mouse release. + // FIXME-VIEWPORT: This is essentially broken, when ImGuiBackendFlags_HasMouseHoveredViewport is set we want to trust when viewport_hovered==NULL and use that. + const bool is_mouse_dragging_with_an_expected_destination = g.DragDropActive; + if (is_mouse_dragging_with_an_expected_destination && viewport_hovered == NULL) + viewport_hovered = g.MouseLastHoveredViewport; + if (is_mouse_dragging_with_an_expected_destination || g.ActiveId == 0 || !IsAnyMouseDown()) + if (viewport_hovered != NULL && viewport_hovered != g.MouseViewport && !(viewport_hovered->Flags & ImGuiViewportFlags_NoInputs)) + g.MouseViewport = viewport_hovered; + + IM_ASSERT(g.MouseViewport != NULL); +} + +// Update user-facing viewport list (g.Viewports -> g.PlatformIO.Viewports after filtering out some) +static void ImGui::UpdateViewportsEndFrame() +{ + ImGuiContext& g = *GImGui; + g.PlatformIO.Viewports.resize(0); + for (int i = 0; i < g.Viewports.Size; i++) + { + ImGuiViewportP* viewport = g.Viewports[i]; + viewport->LastPos = viewport->Pos; + if (viewport->LastFrameActive < g.FrameCount || viewport->Size.x <= 0.0f || viewport->Size.y <= 0.0f) + if (i > 0) // Always include main viewport in the list + continue; + if (viewport->Window && !IsWindowActiveAndVisible(viewport->Window)) + continue; + if (i > 0) + IM_ASSERT(viewport->Window != NULL); + g.PlatformIO.Viewports.push_back(viewport); + } + g.Viewports[0]->ClearRequestFlags(); // Clear main viewport flags because UpdatePlatformWindows() won't do it and may not even be called +} + +// FIXME: We should ideally refactor the system to call this every frame (we currently don't) +ImGuiViewportP* ImGui::AddUpdateViewport(ImGuiWindow* window, ImGuiID id, const ImVec2& pos, const ImVec2& size, ImGuiViewportFlags flags) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(id != 0); + + flags |= ImGuiViewportFlags_IsPlatformWindow; + if (window != NULL) + { + if (g.MovingWindow && g.MovingWindow->RootWindowDockTree == window) + flags |= ImGuiViewportFlags_NoInputs | ImGuiViewportFlags_NoFocusOnAppearing; + if ((window->Flags & ImGuiWindowFlags_NoMouseInputs) && (window->Flags & ImGuiWindowFlags_NoNavInputs)) + flags |= ImGuiViewportFlags_NoInputs; + if (window->Flags & ImGuiWindowFlags_NoFocusOnAppearing) + flags |= ImGuiViewportFlags_NoFocusOnAppearing; + } + + ImGuiViewportP* viewport = (ImGuiViewportP*)FindViewportByID(id); + if (viewport) + { + // Always update for main viewport as we are already pulling correct platform pos/size (see #4900) + if (!viewport->PlatformRequestMove || viewport->ID == IMGUI_VIEWPORT_DEFAULT_ID) + viewport->Pos = pos; + if (!viewport->PlatformRequestResize || viewport->ID == IMGUI_VIEWPORT_DEFAULT_ID) + viewport->Size = size; + viewport->Flags = flags | (viewport->Flags & (ImGuiViewportFlags_IsMinimized | ImGuiViewportFlags_IsFocused)); // Preserve existing flags + } + else + { + // New viewport + viewport = IM_NEW(ImGuiViewportP)(); + viewport->ID = id; + viewport->Idx = g.Viewports.Size; + viewport->Pos = viewport->LastPos = pos; + viewport->Size = size; + viewport->Flags = flags; + UpdateViewportPlatformMonitor(viewport); + g.Viewports.push_back(viewport); + g.ViewportCreatedCount++; + IMGUI_DEBUG_LOG_VIEWPORT("[viewport] Add Viewport %08X '%s'\n", id, window ? window->Name : ""); + + // We normally setup for all viewports in NewFrame() but here need to handle the mid-frame creation of a new viewport. + // We need to extend the fullscreen clip rect so the OverlayDrawList clip is correct for that the first frame + g.DrawListSharedData.ClipRectFullscreen.x = ImMin(g.DrawListSharedData.ClipRectFullscreen.x, viewport->Pos.x); + g.DrawListSharedData.ClipRectFullscreen.y = ImMin(g.DrawListSharedData.ClipRectFullscreen.y, viewport->Pos.y); + g.DrawListSharedData.ClipRectFullscreen.z = ImMax(g.DrawListSharedData.ClipRectFullscreen.z, viewport->Pos.x + viewport->Size.x); + g.DrawListSharedData.ClipRectFullscreen.w = ImMax(g.DrawListSharedData.ClipRectFullscreen.w, viewport->Pos.y + viewport->Size.y); + + // Store initial DpiScale before the OS platform window creation, based on expected monitor data. + // This is so we can select an appropriate font size on the first frame of our window lifetime + if (viewport->PlatformMonitor != -1) + viewport->DpiScale = g.PlatformIO.Monitors[viewport->PlatformMonitor].DpiScale; + } + + viewport->Window = window; + viewport->LastFrameActive = g.FrameCount; + viewport->UpdateWorkRect(); + IM_ASSERT(window == NULL || viewport->ID == window->ID); + + if (window != NULL) + window->ViewportOwned = true; + + return viewport; +} + +static void ImGui::DestroyViewport(ImGuiViewportP* viewport) +{ + // Clear references to this viewport in windows (window->ViewportId becomes the master data) + ImGuiContext& g = *GImGui; + for (ImGuiWindow* window : g.Windows) + { + if (window->Viewport != viewport) + continue; + window->Viewport = NULL; + window->ViewportOwned = false; + } + if (viewport == g.MouseLastHoveredViewport) + g.MouseLastHoveredViewport = NULL; + + // Destroy + IMGUI_DEBUG_LOG_VIEWPORT("[viewport] Delete Viewport %08X '%s'\n", viewport->ID, viewport->Window ? viewport->Window->Name : "n/a"); + DestroyPlatformWindow(viewport); // In most circumstances the platform window will already be destroyed here. + IM_ASSERT(g.PlatformIO.Viewports.contains(viewport) == false); + IM_ASSERT(g.Viewports[viewport->Idx] == viewport); + g.Viewports.erase(g.Viewports.Data + viewport->Idx); + IM_DELETE(viewport); +} + +// FIXME-VIEWPORT: This is all super messy and ought to be clarified or rewritten. +static void ImGui::WindowSelectViewport(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + ImGuiWindowFlags flags = window->Flags; + window->ViewportAllowPlatformMonitorExtend = -1; + + // Restore main viewport if multi-viewport is not supported by the backend + ImGuiViewportP* main_viewport = (ImGuiViewportP*)(void*)GetMainViewport(); + if (!(g.ConfigFlagsCurrFrame & ImGuiConfigFlags_ViewportsEnable)) + { + SetWindowViewport(window, main_viewport); + return; + } + window->ViewportOwned = false; + + // Appearing popups reset their viewport so they can inherit again + if ((flags & (ImGuiWindowFlags_Popup | ImGuiWindowFlags_Tooltip)) && window->Appearing) + { + window->Viewport = NULL; + window->ViewportId = 0; + } + + if ((g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasViewport) == 0) + { + // By default inherit from parent window + if (window->Viewport == NULL && window->ParentWindow && (!window->ParentWindow->IsFallbackWindow || window->ParentWindow->WasActive)) + window->Viewport = window->ParentWindow->Viewport; + + // Attempt to restore saved viewport id (= window that hasn't been activated yet), try to restore the viewport based on saved 'window->ViewportPos' restored from .ini file + if (window->Viewport == NULL && window->ViewportId != 0) + { + window->Viewport = (ImGuiViewportP*)FindViewportByID(window->ViewportId); + if (window->Viewport == NULL && window->ViewportPos.x != FLT_MAX && window->ViewportPos.y != FLT_MAX) + window->Viewport = AddUpdateViewport(window, window->ID, window->ViewportPos, window->Size, ImGuiViewportFlags_None); + } + } + + bool lock_viewport = false; + if (g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasViewport) + { + // Code explicitly request a viewport + window->Viewport = (ImGuiViewportP*)FindViewportByID(g.NextWindowData.ViewportId); + window->ViewportId = g.NextWindowData.ViewportId; // Store ID even if Viewport isn't resolved yet. + if (window->Viewport && (window->Flags & ImGuiWindowFlags_DockNodeHost) != 0 && window->Viewport->Window != NULL) + { + window->Viewport->Window = window; + window->Viewport->ID = window->ViewportId = window->ID; // Overwrite ID (always owned by node) + } + lock_viewport = true; + } + else if ((flags & ImGuiWindowFlags_ChildWindow) || (flags & ImGuiWindowFlags_ChildMenu)) + { + // Always inherit viewport from parent window + if (window->DockNode && window->DockNode->HostWindow) + IM_ASSERT(window->DockNode->HostWindow->Viewport == window->ParentWindow->Viewport); + window->Viewport = window->ParentWindow->Viewport; + } + else if (window->DockNode && window->DockNode->HostWindow) + { + // This covers the "always inherit viewport from parent window" case for when a window reattach to a node that was just created mid-frame + window->Viewport = window->DockNode->HostWindow->Viewport; + } + else if (flags & ImGuiWindowFlags_Tooltip) + { + window->Viewport = g.MouseViewport; + } + else if (GetWindowAlwaysWantOwnViewport(window)) + { + window->Viewport = AddUpdateViewport(window, window->ID, window->Pos, window->Size, ImGuiViewportFlags_None); + } + else if (g.MovingWindow && g.MovingWindow->RootWindowDockTree == window && IsMousePosValid()) + { + if (window->Viewport != NULL && window->Viewport->Window == window) + window->Viewport = AddUpdateViewport(window, window->ID, window->Pos, window->Size, ImGuiViewportFlags_None); + } + else + { + // Merge into host viewport? + // We cannot test window->ViewportOwned as it set lower in the function. + // Testing (g.ActiveId == 0 || g.ActiveIdAllowOverlap) to avoid merging during a short-term widget interaction. Main intent was to avoid during resize (see #4212) + bool try_to_merge_into_host_viewport = (window->Viewport && window == window->Viewport->Window && (g.ActiveId == 0 || g.ActiveIdAllowOverlap)); + if (try_to_merge_into_host_viewport) + UpdateTryMergeWindowIntoHostViewports(window); + } + + // Fallback: merge in default viewport if z-order matches, otherwise create a new viewport + if (window->Viewport == NULL) + if (!UpdateTryMergeWindowIntoHostViewport(window, main_viewport)) + window->Viewport = AddUpdateViewport(window, window->ID, window->Pos, window->Size, ImGuiViewportFlags_None); + + // Mark window as allowed to protrude outside of its viewport and into the current monitor + if (!lock_viewport) + { + if (flags & (ImGuiWindowFlags_Tooltip | ImGuiWindowFlags_Popup)) + { + // We need to take account of the possibility that mouse may become invalid. + // Popups/Tooltip always set ViewportAllowPlatformMonitorExtend so GetWindowAllowedExtentRect() will return full monitor bounds. + ImVec2 mouse_ref = (flags & ImGuiWindowFlags_Tooltip) ? g.IO.MousePos : g.BeginPopupStack.back().OpenMousePos; + bool use_mouse_ref = (g.NavDisableHighlight || !g.NavDisableMouseHover || !g.NavWindow); + bool mouse_valid = IsMousePosValid(&mouse_ref); + if ((window->Appearing || (flags & (ImGuiWindowFlags_Tooltip | ImGuiWindowFlags_ChildMenu))) && (!use_mouse_ref || mouse_valid)) + window->ViewportAllowPlatformMonitorExtend = FindPlatformMonitorForPos((use_mouse_ref && mouse_valid) ? mouse_ref : NavCalcPreferredRefPos()); + else + window->ViewportAllowPlatformMonitorExtend = window->Viewport->PlatformMonitor; + } + else if (window->Viewport && window != window->Viewport->Window && window->Viewport->Window && !(flags & ImGuiWindowFlags_ChildWindow) && window->DockNode == NULL) + { + // When called from Begin() we don't have access to a proper version of the Hidden flag yet, so we replicate this code. + const bool will_be_visible = (window->DockIsActive && !window->DockTabIsVisible) ? false : true; + if ((window->Flags & ImGuiWindowFlags_DockNodeHost) && window->Viewport->LastFrameActive < g.FrameCount && will_be_visible) + { + // Steal/transfer ownership + IMGUI_DEBUG_LOG_VIEWPORT("[viewport] Window '%s' steal Viewport %08X from Window '%s'\n", window->Name, window->Viewport->ID, window->Viewport->Window->Name); + window->Viewport->Window = window; + window->Viewport->ID = window->ID; + window->Viewport->LastNameHash = 0; + } + else if (!UpdateTryMergeWindowIntoHostViewports(window)) // Merge? + { + // New viewport + window->Viewport = AddUpdateViewport(window, window->ID, window->Pos, window->Size, ImGuiViewportFlags_NoFocusOnAppearing); + } + } + else if (window->ViewportAllowPlatformMonitorExtend < 0 && (flags & ImGuiWindowFlags_ChildWindow) == 0) + { + // Regular (non-child, non-popup) windows by default are also allowed to protrude + // Child windows are kept contained within their parent. + window->ViewportAllowPlatformMonitorExtend = window->Viewport->PlatformMonitor; + } + } + + // Update flags + window->ViewportOwned = (window == window->Viewport->Window); + window->ViewportId = window->Viewport->ID; + + // If the OS window has a title bar, hide our imgui title bar + //if (window->ViewportOwned && !(window->Viewport->Flags & ImGuiViewportFlags_NoDecoration)) + // window->Flags |= ImGuiWindowFlags_NoTitleBar; +} + +void ImGui::WindowSyncOwnedViewport(ImGuiWindow* window, ImGuiWindow* parent_window_in_stack) +{ + ImGuiContext& g = *GImGui; + + bool viewport_rect_changed = false; + + // Synchronize window --> viewport in most situations + // Synchronize viewport -> window in case the platform window has been moved or resized from the OS/WM + if (window->Viewport->PlatformRequestMove) + { + window->Pos = window->Viewport->Pos; + MarkIniSettingsDirty(window); + } + else if (memcmp(&window->Viewport->Pos, &window->Pos, sizeof(window->Pos)) != 0) + { + viewport_rect_changed = true; + window->Viewport->Pos = window->Pos; + } + + if (window->Viewport->PlatformRequestResize) + { + window->Size = window->SizeFull = window->Viewport->Size; + MarkIniSettingsDirty(window); + } + else if (memcmp(&window->Viewport->Size, &window->Size, sizeof(window->Size)) != 0) + { + viewport_rect_changed = true; + window->Viewport->Size = window->Size; + } + window->Viewport->UpdateWorkRect(); + + // The viewport may have changed monitor since the global update in UpdateViewportsNewFrame() + // Either a SetNextWindowPos() call in the current frame or a SetWindowPos() call in the previous frame may have this effect. + if (viewport_rect_changed) + UpdateViewportPlatformMonitor(window->Viewport); + + // Update common viewport flags + const ImGuiViewportFlags viewport_flags_to_clear = ImGuiViewportFlags_TopMost | ImGuiViewportFlags_NoTaskBarIcon | ImGuiViewportFlags_NoDecoration | ImGuiViewportFlags_NoRendererClear; + ImGuiViewportFlags viewport_flags = window->Viewport->Flags & ~viewport_flags_to_clear; + ImGuiWindowFlags window_flags = window->Flags; + const bool is_modal = (window_flags & ImGuiWindowFlags_Modal) != 0; + const bool is_short_lived_floating_window = (window_flags & (ImGuiWindowFlags_ChildMenu | ImGuiWindowFlags_Tooltip | ImGuiWindowFlags_Popup)) != 0; + if (window_flags & ImGuiWindowFlags_Tooltip) + viewport_flags |= ImGuiViewportFlags_TopMost; + if ((g.IO.ConfigViewportsNoTaskBarIcon || is_short_lived_floating_window) && !is_modal) + viewport_flags |= ImGuiViewportFlags_NoTaskBarIcon; + if (g.IO.ConfigViewportsNoDecoration || is_short_lived_floating_window) + viewport_flags |= ImGuiViewportFlags_NoDecoration; + + // Not correct to set modal as topmost because: + // - Because other popups can be stacked above a modal (e.g. combo box in a modal) + // - ImGuiViewportFlags_TopMost is currently handled different in backends: in Win32 it is "appear top most" whereas in GLFW and SDL it is "stay topmost" + //if (flags & ImGuiWindowFlags_Modal) + // viewport_flags |= ImGuiViewportFlags_TopMost; + + // For popups and menus that may be protruding out of their parent viewport, we enable _NoFocusOnClick so that clicking on them + // won't steal the OS focus away from their parent window (which may be reflected in OS the title bar decoration). + // Setting _NoFocusOnClick would technically prevent us from bringing back to front in case they are being covered by an OS window from a different app, + // but it shouldn't be much of a problem considering those are already popups that are closed when clicking elsewhere. + if (is_short_lived_floating_window && !is_modal) + viewport_flags |= ImGuiViewportFlags_NoFocusOnAppearing | ImGuiViewportFlags_NoFocusOnClick; + + // We can overwrite viewport flags using ImGuiWindowClass (advanced users) + if (window->WindowClass.ViewportFlagsOverrideSet) + viewport_flags |= window->WindowClass.ViewportFlagsOverrideSet; + if (window->WindowClass.ViewportFlagsOverrideClear) + viewport_flags &= ~window->WindowClass.ViewportFlagsOverrideClear; + + // We can also tell the backend that clearing the platform window won't be necessary, + // as our window background is filling the viewport and we have disabled BgAlpha. + // FIXME: Work on support for per-viewport transparency (#2766) + if (!(window_flags & ImGuiWindowFlags_NoBackground)) + viewport_flags |= ImGuiViewportFlags_NoRendererClear; + + window->Viewport->Flags = viewport_flags; + + // Update parent viewport ID + // (the !IsFallbackWindow test mimic the one done in WindowSelectViewport()) + if (window->WindowClass.ParentViewportId != (ImGuiID)-1) + window->Viewport->ParentViewportId = window->WindowClass.ParentViewportId; + else if ((window_flags & (ImGuiWindowFlags_Popup | ImGuiWindowFlags_Tooltip)) && parent_window_in_stack && (!parent_window_in_stack->IsFallbackWindow || parent_window_in_stack->WasActive)) + window->Viewport->ParentViewportId = parent_window_in_stack->Viewport->ID; + else + window->Viewport->ParentViewportId = g.IO.ConfigViewportsNoDefaultParent ? 0 : IMGUI_VIEWPORT_DEFAULT_ID; +} + +// Called by user at the end of the main loop, after EndFrame() +// This will handle the creation/update of all OS windows via function defined in the ImGuiPlatformIO api. +void ImGui::UpdatePlatformWindows() +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.FrameCountEnded == g.FrameCount && "Forgot to call Render() or EndFrame() before UpdatePlatformWindows()?"); + IM_ASSERT(g.FrameCountPlatformEnded < g.FrameCount); + g.FrameCountPlatformEnded = g.FrameCount; + if (!(g.ConfigFlagsCurrFrame & ImGuiConfigFlags_ViewportsEnable)) + return; + + // Create/resize/destroy platform windows to match each active viewport. + // Skip the main viewport (index 0), which is always fully handled by the application! + for (int i = 1; i < g.Viewports.Size; i++) + { + ImGuiViewportP* viewport = g.Viewports[i]; + + // Destroy platform window if the viewport hasn't been submitted or if it is hosting a hidden window + // (the implicit/fallback Debug##Default window will be registering its viewport then be disabled, causing a dummy DestroyPlatformWindow to be made each frame) + bool destroy_platform_window = false; + destroy_platform_window |= (viewport->LastFrameActive < g.FrameCount - 1); + destroy_platform_window |= (viewport->Window && !IsWindowActiveAndVisible(viewport->Window)); + if (destroy_platform_window) + { + DestroyPlatformWindow(viewport); + continue; + } + + // New windows that appears directly in a new viewport won't always have a size on their first frame + if (viewport->LastFrameActive < g.FrameCount || viewport->Size.x <= 0 || viewport->Size.y <= 0) + continue; + + // Create window + const bool is_new_platform_window = (viewport->PlatformWindowCreated == false); + if (is_new_platform_window) + { + IMGUI_DEBUG_LOG_VIEWPORT("[viewport] Create Platform Window %08X '%s'\n", viewport->ID, viewport->Window ? viewport->Window->Name : "n/a"); + g.PlatformIO.Platform_CreateWindow(viewport); + if (g.PlatformIO.Renderer_CreateWindow != NULL) + g.PlatformIO.Renderer_CreateWindow(viewport); + g.PlatformWindowsCreatedCount++; + viewport->LastNameHash = 0; + viewport->LastPlatformPos = viewport->LastPlatformSize = ImVec2(FLT_MAX, FLT_MAX); // By clearing those we'll enforce a call to Platform_SetWindowPos/Size below, before Platform_ShowWindow (FIXME: Is that necessary?) + viewport->LastRendererSize = viewport->Size; // We don't need to call Renderer_SetWindowSize() as it is expected Renderer_CreateWindow() already did it. + viewport->PlatformWindowCreated = true; + } + + // Apply Position and Size (from ImGui to Platform/Renderer backends) + if ((viewport->LastPlatformPos.x != viewport->Pos.x || viewport->LastPlatformPos.y != viewport->Pos.y) && !viewport->PlatformRequestMove) + g.PlatformIO.Platform_SetWindowPos(viewport, viewport->Pos); + if ((viewport->LastPlatformSize.x != viewport->Size.x || viewport->LastPlatformSize.y != viewport->Size.y) && !viewport->PlatformRequestResize) + g.PlatformIO.Platform_SetWindowSize(viewport, viewport->Size); + if ((viewport->LastRendererSize.x != viewport->Size.x || viewport->LastRendererSize.y != viewport->Size.y) && g.PlatformIO.Renderer_SetWindowSize) + g.PlatformIO.Renderer_SetWindowSize(viewport, viewport->Size); + viewport->LastPlatformPos = viewport->Pos; + viewport->LastPlatformSize = viewport->LastRendererSize = viewport->Size; + + // Update title bar (if it changed) + if (ImGuiWindow* window_for_title = GetWindowForTitleDisplay(viewport->Window)) + { + const char* title_begin = window_for_title->Name; + char* title_end = (char*)(intptr_t)FindRenderedTextEnd(title_begin); + const ImGuiID title_hash = ImHashStr(title_begin, title_end - title_begin); + if (viewport->LastNameHash != title_hash) + { + char title_end_backup_c = *title_end; + *title_end = 0; // Cut existing buffer short instead of doing an alloc/free, no small gain. + g.PlatformIO.Platform_SetWindowTitle(viewport, title_begin); + *title_end = title_end_backup_c; + viewport->LastNameHash = title_hash; + } + } + + // Update alpha (if it changed) + if (viewport->LastAlpha != viewport->Alpha && g.PlatformIO.Platform_SetWindowAlpha) + g.PlatformIO.Platform_SetWindowAlpha(viewport, viewport->Alpha); + viewport->LastAlpha = viewport->Alpha; + + // Optional, general purpose call to allow the backend to perform general book-keeping even if things haven't changed. + if (g.PlatformIO.Platform_UpdateWindow) + g.PlatformIO.Platform_UpdateWindow(viewport); + + if (is_new_platform_window) + { + // On startup ensure new platform window don't steal focus (give it a few frames, as nested contents may lead to viewport being created a few frames late) + if (g.FrameCount < 3) + viewport->Flags |= ImGuiViewportFlags_NoFocusOnAppearing; + + // Show window + g.PlatformIO.Platform_ShowWindow(viewport); + + // Even without focus, we assume the window becomes front-most. + // This is useful for our platform z-order heuristic when io.MouseHoveredViewport is not available. + if (viewport->LastFocusedStampCount != g.ViewportFocusedStampCount) + viewport->LastFocusedStampCount = ++g.ViewportFocusedStampCount; + } + + // Clear request flags + viewport->ClearRequestFlags(); } } +// This is a default/basic function for performing the rendering/swap of multiple Platform Windows. +// Custom renderers may prefer to not call this function at all, and instead iterate the publicly exposed platform data and handle rendering/sync themselves. +// The Render/Swap functions stored in ImGuiPlatformIO are merely here to allow for this helper to exist, but you can do it yourself: +// +// ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO(); +// for (int i = 1; i < platform_io.Viewports.Size; i++) +// if ((platform_io.Viewports[i]->Flags & ImGuiViewportFlags_Minimized) == 0) +// MyRenderFunction(platform_io.Viewports[i], my_args); +// for (int i = 1; i < platform_io.Viewports.Size; i++) +// if ((platform_io.Viewports[i]->Flags & ImGuiViewportFlags_Minimized) == 0) +// MySwapBufferFunction(platform_io.Viewports[i], my_args); +// +void ImGui::RenderPlatformWindowsDefault(void* platform_render_arg, void* renderer_render_arg) +{ + // Skip the main viewport (index 0), which is always fully handled by the application! + ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO(); + for (int i = 1; i < platform_io.Viewports.Size; i++) + { + ImGuiViewport* viewport = platform_io.Viewports[i]; + if (viewport->Flags & ImGuiViewportFlags_IsMinimized) + continue; + if (platform_io.Platform_RenderWindow) platform_io.Platform_RenderWindow(viewport, platform_render_arg); + if (platform_io.Renderer_RenderWindow) platform_io.Renderer_RenderWindow(viewport, renderer_render_arg); + } + for (int i = 1; i < platform_io.Viewports.Size; i++) + { + ImGuiViewport* viewport = platform_io.Viewports[i]; + if (viewport->Flags & ImGuiViewportFlags_IsMinimized) + continue; + if (platform_io.Platform_SwapBuffers) platform_io.Platform_SwapBuffers(viewport, platform_render_arg); + if (platform_io.Renderer_SwapBuffers) platform_io.Renderer_SwapBuffers(viewport, renderer_render_arg); + } +} + +static int ImGui::FindPlatformMonitorForPos(const ImVec2& pos) +{ + ImGuiContext& g = *GImGui; + for (int monitor_n = 0; monitor_n < g.PlatformIO.Monitors.Size; monitor_n++) + { + const ImGuiPlatformMonitor& monitor = g.PlatformIO.Monitors[monitor_n]; + if (ImRect(monitor.MainPos, monitor.MainPos + monitor.MainSize).Contains(pos)) + return monitor_n; + } + return -1; +} + +// Search for the monitor with the largest intersection area with the given rectangle +// We generally try to avoid searching loops but the monitor count should be very small here +// FIXME-OPT: We could test the last monitor used for that viewport first, and early +static int ImGui::FindPlatformMonitorForRect(const ImRect& rect) +{ + ImGuiContext& g = *GImGui; + + const int monitor_count = g.PlatformIO.Monitors.Size; + if (monitor_count <= 1) + return monitor_count - 1; + + // Use a minimum threshold of 1.0f so a zero-sized rect won't false positive, and will still find the correct monitor given its position. + // This is necessary for tooltips which always resize down to zero at first. + const float surface_threshold = ImMax(rect.GetWidth() * rect.GetHeight() * 0.5f, 1.0f); + int best_monitor_n = -1; + float best_monitor_surface = 0.001f; + + for (int monitor_n = 0; monitor_n < g.PlatformIO.Monitors.Size && best_monitor_surface < surface_threshold; monitor_n++) + { + const ImGuiPlatformMonitor& monitor = g.PlatformIO.Monitors[monitor_n]; + const ImRect monitor_rect = ImRect(monitor.MainPos, monitor.MainPos + monitor.MainSize); + if (monitor_rect.Contains(rect)) + return monitor_n; + ImRect overlapping_rect = rect; + overlapping_rect.ClipWithFull(monitor_rect); + float overlapping_surface = overlapping_rect.GetWidth() * overlapping_rect.GetHeight(); + if (overlapping_surface < best_monitor_surface) + continue; + best_monitor_surface = overlapping_surface; + best_monitor_n = monitor_n; + } + return best_monitor_n; +} + +// Update monitor from viewport rectangle (we'll use this info to clamp windows and save windows lost in a removed monitor) +static void ImGui::UpdateViewportPlatformMonitor(ImGuiViewportP* viewport) +{ + viewport->PlatformMonitor = (short)FindPlatformMonitorForRect(viewport->GetMainRect()); +} + +// Return value is always != NULL, but don't hold on it across frames. +const ImGuiPlatformMonitor* ImGui::GetViewportPlatformMonitor(ImGuiViewport* viewport_p) +{ + ImGuiContext& g = *GImGui; + ImGuiViewportP* viewport = (ImGuiViewportP*)(void*)viewport_p; + int monitor_idx = viewport->PlatformMonitor; + if (monitor_idx >= 0 && monitor_idx < g.PlatformIO.Monitors.Size) + return &g.PlatformIO.Monitors[monitor_idx]; + return &g.FallbackMonitor; +} + +void ImGui::DestroyPlatformWindow(ImGuiViewportP* viewport) +{ + ImGuiContext& g = *GImGui; + if (viewport->PlatformWindowCreated) + { + IMGUI_DEBUG_LOG_VIEWPORT("[viewport] Destroy Platform Window %08X '%s'\n", viewport->ID, viewport->Window ? viewport->Window->Name : "n/a"); + if (g.PlatformIO.Renderer_DestroyWindow) + g.PlatformIO.Renderer_DestroyWindow(viewport); + if (g.PlatformIO.Platform_DestroyWindow) + g.PlatformIO.Platform_DestroyWindow(viewport); + IM_ASSERT(viewport->RendererUserData == NULL && viewport->PlatformUserData == NULL); + + // Don't clear PlatformWindowCreated for the main viewport, as we initially set that up to true in Initialize() + // The righter way may be to leave it to the backend to set this flag all-together, and made the flag public. + if (viewport->ID != IMGUI_VIEWPORT_DEFAULT_ID) + viewport->PlatformWindowCreated = false; + } + else + { + IM_ASSERT(viewport->RendererUserData == NULL && viewport->PlatformUserData == NULL && viewport->PlatformHandle == NULL); + } + viewport->RendererUserData = viewport->PlatformUserData = viewport->PlatformHandle = NULL; + viewport->ClearRequestFlags(); +} + +void ImGui::DestroyPlatformWindows() +{ + // We call the destroy window on every viewport (including the main viewport, index 0) to give a chance to the backend + // to clear any data they may have stored in e.g. PlatformUserData, RendererUserData. + // It is convenient for the platform backend code to store something in the main viewport, in order for e.g. the mouse handling + // code to operator a consistent manner. + // It is expected that the backend can handle calls to Renderer_DestroyWindow/Platform_DestroyWindow without + // crashing if it doesn't have data stored. + ImGuiContext& g = *GImGui; + for (ImGuiViewportP* viewport : g.Viewports) + DestroyPlatformWindow(viewport); +} + + //----------------------------------------------------------------------------- // [SECTION] DOCKING //----------------------------------------------------------------------------- +// Docking: Internal Types +// Docking: Forward Declarations +// Docking: ImGuiDockContext +// Docking: ImGuiDockContext Docking/Undocking functions +// Docking: ImGuiDockNode +// Docking: ImGuiDockNode Tree manipulation functions +// Docking: Public Functions (SetWindowDock, DockSpace, DockSpaceOverViewport) +// Docking: Builder Functions +// Docking: Begin/End Support Functions (called from Begin/End) +// Docking: Settings +//----------------------------------------------------------------------------- -// (this section is filled in the 'docking' branch) +//----------------------------------------------------------------------------- +// Typical Docking call flow: (root level is generally public API): +//----------------------------------------------------------------------------- +// - NewFrame() new dear imgui frame +// | DockContextNewFrameUpdateUndocking() - process queued undocking requests +// | - DockContextProcessUndockWindow() - process one window undocking request +// | - DockContextProcessUndockNode() - process one whole node undocking request +// | DockContextNewFrameUpdateUndocking() - process queue docking requests, create floating dock nodes +// | - update g.HoveredDockNode - [debug] update node hovered by mouse +// | - DockContextProcessDock() - process one docking request +// | - DockNodeUpdate() +// | - DockNodeUpdateForRootNode() +// | - DockNodeUpdateFlagsAndCollapse() +// | - DockNodeFindInfo() +// | - destroy unused node or tab bar +// | - create dock node host window +// | - Begin() etc. +// | - DockNodeStartMouseMovingWindow() +// | - DockNodeTreeUpdatePosSize() +// | - DockNodeTreeUpdateSplitter() +// | - draw node background +// | - DockNodeUpdateTabBar() - create/update tab bar for a docking node +// | - DockNodeAddTabBar() +// | - DockNodeWindowMenuUpdate() +// | - DockNodeCalcTabBarLayout() +// | - BeginTabBarEx() +// | - TabItemEx() calls +// | - EndTabBar() +// | - BeginDockableDragDropTarget() +// | - DockNodeUpdate() - recurse into child nodes... +//----------------------------------------------------------------------------- +// - DockSpace() user submit a dockspace into a window +// | Begin(Child) - create a child window +// | DockNodeUpdate() - call main dock node update function +// | End(Child) +// | ItemSize() +//----------------------------------------------------------------------------- +// - Begin() +// | BeginDocked() +// | BeginDockableDragDropSource() +// | BeginDockableDragDropTarget() +// | - DockNodePreviewDockRender() +//----------------------------------------------------------------------------- +// - EndFrame() +// | DockContextEndFrame() +//----------------------------------------------------------------------------- + +//----------------------------------------------------------------------------- +// Docking: Internal Types +//----------------------------------------------------------------------------- +// - ImGuiDockRequestType +// - ImGuiDockRequest +// - ImGuiDockPreviewData +// - ImGuiDockNodeSettings +// - ImGuiDockContext +//----------------------------------------------------------------------------- + +enum ImGuiDockRequestType +{ + ImGuiDockRequestType_None = 0, + ImGuiDockRequestType_Dock, + ImGuiDockRequestType_Undock, + ImGuiDockRequestType_Split // Split is the same as Dock but without a DockPayload +}; + +struct ImGuiDockRequest +{ + ImGuiDockRequestType Type; + ImGuiWindow* DockTargetWindow; // Destination/Target Window to dock into (may be a loose window or a DockNode, might be NULL in which case DockTargetNode cannot be NULL) + ImGuiDockNode* DockTargetNode; // Destination/Target Node to dock into + ImGuiWindow* DockPayload; // Source/Payload window to dock (may be a loose window or a DockNode), [Optional] + ImGuiDir DockSplitDir; + float DockSplitRatio; + bool DockSplitOuter; + ImGuiWindow* UndockTargetWindow; + ImGuiDockNode* UndockTargetNode; + + ImGuiDockRequest() + { + Type = ImGuiDockRequestType_None; + DockTargetWindow = DockPayload = UndockTargetWindow = NULL; + DockTargetNode = UndockTargetNode = NULL; + DockSplitDir = ImGuiDir_None; + DockSplitRatio = 0.5f; + DockSplitOuter = false; + } +}; + +struct ImGuiDockPreviewData +{ + ImGuiDockNode FutureNode; + bool IsDropAllowed; + bool IsCenterAvailable; + bool IsSidesAvailable; // Hold your breath, grammar freaks.. + bool IsSplitDirExplicit; // Set when hovered the drop rect (vs. implicit SplitDir==None when hovered the window) + ImGuiDockNode* SplitNode; + ImGuiDir SplitDir; + float SplitRatio; + ImRect DropRectsDraw[ImGuiDir_COUNT + 1]; // May be slightly different from hit-testing drop rects used in DockNodeCalcDropRects() + + ImGuiDockPreviewData() : FutureNode(0) { IsDropAllowed = IsCenterAvailable = IsSidesAvailable = IsSplitDirExplicit = false; SplitNode = NULL; SplitDir = ImGuiDir_None; SplitRatio = 0.f; for (int n = 0; n < IM_ARRAYSIZE(DropRectsDraw); n++) DropRectsDraw[n] = ImRect(+FLT_MAX, +FLT_MAX, -FLT_MAX, -FLT_MAX); } +}; + +// Persistent Settings data, stored contiguously in SettingsNodes (sizeof() ~32 bytes) +struct ImGuiDockNodeSettings +{ + ImGuiID ID; + ImGuiID ParentNodeId; + ImGuiID ParentWindowId; + ImGuiID SelectedTabId; + signed char SplitAxis; + char Depth; + ImGuiDockNodeFlags Flags; // NB: We save individual flags one by one in ascii format (ImGuiDockNodeFlags_SavedFlagsMask_) + ImVec2ih Pos; + ImVec2ih Size; + ImVec2ih SizeRef; + ImGuiDockNodeSettings() { memset(this, 0, sizeof(*this)); SplitAxis = ImGuiAxis_None; } +}; + +//----------------------------------------------------------------------------- +// Docking: Forward Declarations +//----------------------------------------------------------------------------- + +namespace ImGui +{ + // ImGuiDockContext + static ImGuiDockNode* DockContextAddNode(ImGuiContext* ctx, ImGuiID id); + static void DockContextRemoveNode(ImGuiContext* ctx, ImGuiDockNode* node, bool merge_sibling_into_parent_node); + static void DockContextQueueNotifyRemovedNode(ImGuiContext* ctx, ImGuiDockNode* node); + static void DockContextProcessDock(ImGuiContext* ctx, ImGuiDockRequest* req); + static void DockContextPruneUnusedSettingsNodes(ImGuiContext* ctx); + static ImGuiDockNode* DockContextBindNodeToWindow(ImGuiContext* ctx, ImGuiWindow* window); + static void DockContextBuildNodesFromSettings(ImGuiContext* ctx, ImGuiDockNodeSettings* node_settings_array, int node_settings_count); + static void DockContextBuildAddWindowsToNodes(ImGuiContext* ctx, ImGuiID root_id); // Use root_id==0 to add all + + // ImGuiDockNode + static void DockNodeAddWindow(ImGuiDockNode* node, ImGuiWindow* window, bool add_to_tab_bar); + static void DockNodeMoveWindows(ImGuiDockNode* dst_node, ImGuiDockNode* src_node); + static void DockNodeMoveChildNodes(ImGuiDockNode* dst_node, ImGuiDockNode* src_node); + static ImGuiWindow* DockNodeFindWindowByID(ImGuiDockNode* node, ImGuiID id); + static void DockNodeApplyPosSizeToWindows(ImGuiDockNode* node); + static void DockNodeRemoveWindow(ImGuiDockNode* node, ImGuiWindow* window, ImGuiID save_dock_id); + static void DockNodeHideHostWindow(ImGuiDockNode* node); + static void DockNodeUpdate(ImGuiDockNode* node); + static void DockNodeUpdateForRootNode(ImGuiDockNode* node); + static void DockNodeUpdateFlagsAndCollapse(ImGuiDockNode* node); + static void DockNodeUpdateHasCentralNodeChild(ImGuiDockNode* node); + static void DockNodeUpdateTabBar(ImGuiDockNode* node, ImGuiWindow* host_window); + static void DockNodeAddTabBar(ImGuiDockNode* node); + static void DockNodeRemoveTabBar(ImGuiDockNode* node); + static void DockNodeWindowMenuUpdate(ImGuiDockNode* node, ImGuiTabBar* tab_bar); + static void DockNodeUpdateVisibleFlag(ImGuiDockNode* node); + static void DockNodeStartMouseMovingWindow(ImGuiDockNode* node, ImGuiWindow* window); + static bool DockNodeIsDropAllowed(ImGuiWindow* host_window, ImGuiWindow* payload_window); + static void DockNodePreviewDockSetup(ImGuiWindow* host_window, ImGuiDockNode* host_node, ImGuiWindow* payload_window, ImGuiDockNode* payload_node, ImGuiDockPreviewData* preview_data, bool is_explicit_target, bool is_outer_docking); + static void DockNodePreviewDockRender(ImGuiWindow* host_window, ImGuiDockNode* host_node, ImGuiWindow* payload_window, const ImGuiDockPreviewData* preview_data); + static void DockNodeCalcTabBarLayout(const ImGuiDockNode* node, ImRect* out_title_rect, ImRect* out_tab_bar_rect, ImVec2* out_window_menu_button_pos, ImVec2* out_close_button_pos); + static void DockNodeCalcSplitRects(ImVec2& pos_old, ImVec2& size_old, ImVec2& pos_new, ImVec2& size_new, ImGuiDir dir, ImVec2 size_new_desired); + static bool DockNodeCalcDropRectsAndTestMousePos(const ImRect& parent, ImGuiDir dir, ImRect& out_draw, bool outer_docking, ImVec2* test_mouse_pos); + static const char* DockNodeGetHostWindowTitle(ImGuiDockNode* node, char* buf, int buf_size) { ImFormatString(buf, buf_size, "##DockNode_%02X", node->ID); return buf; } + static int DockNodeGetTabOrder(ImGuiWindow* window); + + // ImGuiDockNode tree manipulations + static void DockNodeTreeSplit(ImGuiContext* ctx, ImGuiDockNode* parent_node, ImGuiAxis split_axis, int split_first_child, float split_ratio, ImGuiDockNode* new_node); + static void DockNodeTreeMerge(ImGuiContext* ctx, ImGuiDockNode* parent_node, ImGuiDockNode* merge_lead_child); + static void DockNodeTreeUpdatePosSize(ImGuiDockNode* node, ImVec2 pos, ImVec2 size, ImGuiDockNode* only_write_to_single_node = NULL); + static void DockNodeTreeUpdateSplitter(ImGuiDockNode* node); + static ImGuiDockNode* DockNodeTreeFindVisibleNodeByPos(ImGuiDockNode* node, ImVec2 pos); + static ImGuiDockNode* DockNodeTreeFindFallbackLeafNode(ImGuiDockNode* node); + + // Settings + static void DockSettingsRenameNodeReferences(ImGuiID old_node_id, ImGuiID new_node_id); + static void DockSettingsRemoveNodeReferences(ImGuiID* node_ids, int node_ids_count); + static ImGuiDockNodeSettings* DockSettingsFindNodeSettings(ImGuiContext* ctx, ImGuiID node_id); + static void DockSettingsHandler_ClearAll(ImGuiContext*, ImGuiSettingsHandler*); + static void DockSettingsHandler_ApplyAll(ImGuiContext*, ImGuiSettingsHandler*); + static void* DockSettingsHandler_ReadOpen(ImGuiContext*, ImGuiSettingsHandler*, const char* name); + static void DockSettingsHandler_ReadLine(ImGuiContext*, ImGuiSettingsHandler*, void* entry, const char* line); + static void DockSettingsHandler_WriteAll(ImGuiContext* imgui_ctx, ImGuiSettingsHandler* handler, ImGuiTextBuffer* buf); +} + +//----------------------------------------------------------------------------- +// Docking: ImGuiDockContext +//----------------------------------------------------------------------------- +// The lifetime model is different from the one of regular windows: we always create a ImGuiDockNode for each ImGuiDockNodeSettings, +// or we always hold the entire docking node tree. Nodes are frequently hidden, e.g. if the window(s) or child nodes they host are not active. +// At boot time only, we run a simple GC to remove nodes that have no references. +// Because dock node settings (which are small, contiguous structures) are always mirrored by their corresponding dock nodes (more complete structures), +// we can also very easily recreate the nodes from scratch given the settings data (this is what DockContextRebuild() does). +// This is convenient as docking reconfiguration can be implemented by mostly poking at the simpler settings data. +//----------------------------------------------------------------------------- +// - DockContextInitialize() +// - DockContextShutdown() +// - DockContextClearNodes() +// - DockContextRebuildNodes() +// - DockContextNewFrameUpdateUndocking() +// - DockContextNewFrameUpdateDocking() +// - DockContextEndFrame() +// - DockContextFindNodeByID() +// - DockContextBindNodeToWindow() +// - DockContextGenNodeID() +// - DockContextAddNode() +// - DockContextRemoveNode() +// - ImGuiDockContextPruneNodeData +// - DockContextPruneUnusedSettingsNodes() +// - DockContextBuildNodesFromSettings() +// - DockContextBuildAddWindowsToNodes() +//----------------------------------------------------------------------------- + +void ImGui::DockContextInitialize(ImGuiContext* ctx) +{ + ImGuiContext& g = *ctx; + + // Add .ini handle for persistent docking data + ImGuiSettingsHandler ini_handler; + ini_handler.TypeName = "Docking"; + ini_handler.TypeHash = ImHashStr("Docking"); + ini_handler.ClearAllFn = DockSettingsHandler_ClearAll; + ini_handler.ReadInitFn = DockSettingsHandler_ClearAll; // Also clear on read + ini_handler.ReadOpenFn = DockSettingsHandler_ReadOpen; + ini_handler.ReadLineFn = DockSettingsHandler_ReadLine; + ini_handler.ApplyAllFn = DockSettingsHandler_ApplyAll; + ini_handler.WriteAllFn = DockSettingsHandler_WriteAll; + g.SettingsHandlers.push_back(ini_handler); + + g.DockNodeWindowMenuHandler = &DockNodeWindowMenuHandler_Default; +} + +void ImGui::DockContextShutdown(ImGuiContext* ctx) +{ + ImGuiDockContext* dc = &ctx->DockContext; + for (int n = 0; n < dc->Nodes.Data.Size; n++) + if (ImGuiDockNode* node = (ImGuiDockNode*)dc->Nodes.Data[n].val_p) + IM_DELETE(node); +} + +void ImGui::DockContextClearNodes(ImGuiContext* ctx, ImGuiID root_id, bool clear_settings_refs) +{ + IM_UNUSED(ctx); + IM_ASSERT(ctx == GImGui); + DockBuilderRemoveNodeDockedWindows(root_id, clear_settings_refs); + DockBuilderRemoveNodeChildNodes(root_id); +} + +// [DEBUG] This function also acts as a defacto test to make sure we can rebuild from scratch without a glitch +// (Different from DockSettingsHandler_ClearAll() + DockSettingsHandler_ApplyAll() because this reuses current settings!) +void ImGui::DockContextRebuildNodes(ImGuiContext* ctx) +{ + ImGuiContext& g = *ctx; + ImGuiDockContext* dc = &ctx->DockContext; + IMGUI_DEBUG_LOG_DOCKING("[docking] DockContextRebuildNodes\n"); + SaveIniSettingsToMemory(); + ImGuiID root_id = 0; // Rebuild all + DockContextClearNodes(ctx, root_id, false); + DockContextBuildNodesFromSettings(ctx, dc->NodesSettings.Data, dc->NodesSettings.Size); + DockContextBuildAddWindowsToNodes(ctx, root_id); +} + +// Docking context update function, called by NewFrame() +void ImGui::DockContextNewFrameUpdateUndocking(ImGuiContext* ctx) +{ + ImGuiContext& g = *ctx; + ImGuiDockContext* dc = &ctx->DockContext; + if (!(g.IO.ConfigFlags & ImGuiConfigFlags_DockingEnable)) + { + if (dc->Nodes.Data.Size > 0 || dc->Requests.Size > 0) + DockContextClearNodes(ctx, 0, true); + return; + } + + // Setting NoSplit at runtime merges all nodes + if (g.IO.ConfigDockingNoSplit) + for (int n = 0; n < dc->Nodes.Data.Size; n++) + if (ImGuiDockNode* node = (ImGuiDockNode*)dc->Nodes.Data[n].val_p) + if (node->IsRootNode() && node->IsSplitNode()) + { + DockBuilderRemoveNodeChildNodes(node->ID); + //dc->WantFullRebuild = true; + } + + // Process full rebuild +#if 0 + if (ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_C))) + dc->WantFullRebuild = true; +#endif + if (dc->WantFullRebuild) + { + DockContextRebuildNodes(ctx); + dc->WantFullRebuild = false; + } + + // Process Undocking requests (we need to process them _before_ the UpdateMouseMovingWindowNewFrame call in NewFrame) + for (ImGuiDockRequest& req : dc->Requests) + { + if (req.Type == ImGuiDockRequestType_Undock && req.UndockTargetWindow) + DockContextProcessUndockWindow(ctx, req.UndockTargetWindow); + else if (req.Type == ImGuiDockRequestType_Undock && req.UndockTargetNode) + DockContextProcessUndockNode(ctx, req.UndockTargetNode); + } +} + +// Docking context update function, called by NewFrame() +void ImGui::DockContextNewFrameUpdateDocking(ImGuiContext* ctx) +{ + ImGuiContext& g = *ctx; + ImGuiDockContext* dc = &ctx->DockContext; + if (!(g.IO.ConfigFlags & ImGuiConfigFlags_DockingEnable)) + return; + + // [DEBUG] Store hovered dock node. + // We could in theory use DockNodeTreeFindVisibleNodeByPos() on the root host dock node, but using ->DockNode is a good shortcut. + // Note this is mostly a debug thing and isn't actually used for docking target, because docking involve more detailed filtering. + g.DebugHoveredDockNode = NULL; + if (ImGuiWindow* hovered_window = g.HoveredWindowUnderMovingWindow) + { + if (hovered_window->DockNodeAsHost) + g.DebugHoveredDockNode = DockNodeTreeFindVisibleNodeByPos(hovered_window->DockNodeAsHost, g.IO.MousePos); + else if (hovered_window->RootWindow->DockNode) + g.DebugHoveredDockNode = hovered_window->RootWindow->DockNode; + } + + // Process Docking requests + for (ImGuiDockRequest& req : dc->Requests) + if (req.Type == ImGuiDockRequestType_Dock) + DockContextProcessDock(ctx, &req); + dc->Requests.resize(0); + + // Create windows for each automatic docking nodes + // We can have NULL pointers when we delete nodes, but because ID are recycled this should amortize nicely (and our node count will never be very high) + for (int n = 0; n < dc->Nodes.Data.Size; n++) + if (ImGuiDockNode* node = (ImGuiDockNode*)dc->Nodes.Data[n].val_p) + if (node->IsFloatingNode()) + DockNodeUpdate(node); +} + +void ImGui::DockContextEndFrame(ImGuiContext* ctx) +{ + // Draw backgrounds of node missing their window + ImGuiContext& g = *ctx; + ImGuiDockContext* dc = &g.DockContext; + for (int n = 0; n < dc->Nodes.Data.Size; n++) + if (ImGuiDockNode* node = (ImGuiDockNode*)dc->Nodes.Data[n].val_p) + if (node->LastFrameActive == g.FrameCount && node->IsVisible && node->HostWindow && node->IsLeafNode() && !node->IsBgDrawnThisFrame) + { + ImRect bg_rect(node->Pos + ImVec2(0.0f, GetFrameHeight()), node->Pos + node->Size); + ImDrawFlags bg_rounding_flags = CalcRoundingFlagsForRectInRect(bg_rect, node->HostWindow->Rect(), g.Style.DockingSeparatorSize); + node->HostWindow->DrawList->ChannelsSetCurrent(DOCKING_HOST_DRAW_CHANNEL_BG); + node->HostWindow->DrawList->AddRectFilled(bg_rect.Min, bg_rect.Max, node->LastBgColor, node->HostWindow->WindowRounding, bg_rounding_flags); + } +} + +ImGuiDockNode* ImGui::DockContextFindNodeByID(ImGuiContext* ctx, ImGuiID id) +{ + return (ImGuiDockNode*)ctx->DockContext.Nodes.GetVoidPtr(id); +} + +ImGuiID ImGui::DockContextGenNodeID(ImGuiContext* ctx) +{ + // Generate an ID for new node (the exact ID value doesn't matter as long as it is not already used) + // FIXME-OPT FIXME-DOCK: This is suboptimal, even if the node count is small enough not to be a worry.0 + // We should poke in ctx->Nodes to find a suitable ID faster. Even more so trivial that ctx->Nodes lookup is already sorted. + ImGuiID id = 0x0001; + while (DockContextFindNodeByID(ctx, id) != NULL) + id++; + return id; +} + +static ImGuiDockNode* ImGui::DockContextAddNode(ImGuiContext* ctx, ImGuiID id) +{ + // Generate an ID for the new node (the exact ID value doesn't matter as long as it is not already used) and add the first window. + ImGuiContext& g = *ctx; + if (id == 0) + id = DockContextGenNodeID(ctx); + else + IM_ASSERT(DockContextFindNodeByID(ctx, id) == NULL); + + // We don't set node->LastFrameAlive on construction. Nodes are always created at all time to reflect .ini settings! + IMGUI_DEBUG_LOG_DOCKING("[docking] DockContextAddNode 0x%08X\n", id); + ImGuiDockNode* node = IM_NEW(ImGuiDockNode)(id); + ctx->DockContext.Nodes.SetVoidPtr(node->ID, node); + return node; +} + +static void ImGui::DockContextRemoveNode(ImGuiContext* ctx, ImGuiDockNode* node, bool merge_sibling_into_parent_node) +{ + ImGuiContext& g = *ctx; + ImGuiDockContext* dc = &ctx->DockContext; + + IMGUI_DEBUG_LOG_DOCKING("[docking] DockContextRemoveNode 0x%08X\n", node->ID); + IM_ASSERT(DockContextFindNodeByID(ctx, node->ID) == node); + IM_ASSERT(node->ChildNodes[0] == NULL && node->ChildNodes[1] == NULL); + IM_ASSERT(node->Windows.Size == 0); + + if (node->HostWindow) + node->HostWindow->DockNodeAsHost = NULL; + + ImGuiDockNode* parent_node = node->ParentNode; + const bool merge = (merge_sibling_into_parent_node && parent_node != NULL); + if (merge) + { + IM_ASSERT(parent_node->ChildNodes[0] == node || parent_node->ChildNodes[1] == node); + ImGuiDockNode* sibling_node = (parent_node->ChildNodes[0] == node ? parent_node->ChildNodes[1] : parent_node->ChildNodes[0]); + DockNodeTreeMerge(&g, parent_node, sibling_node); + } + else + { + for (int n = 0; parent_node && n < IM_ARRAYSIZE(parent_node->ChildNodes); n++) + if (parent_node->ChildNodes[n] == node) + node->ParentNode->ChildNodes[n] = NULL; + dc->Nodes.SetVoidPtr(node->ID, NULL); + IM_DELETE(node); + } +} + +static int IMGUI_CDECL DockNodeComparerDepthMostFirst(const void* lhs, const void* rhs) +{ + const ImGuiDockNode* a = *(const ImGuiDockNode* const*)lhs; + const ImGuiDockNode* b = *(const ImGuiDockNode* const*)rhs; + return ImGui::DockNodeGetDepth(b) - ImGui::DockNodeGetDepth(a); +} + +// Pre C++0x doesn't allow us to use a function-local type (without linkage) as template parameter, so we moved this here. +struct ImGuiDockContextPruneNodeData +{ + int CountWindows, CountChildWindows, CountChildNodes; + ImGuiID RootId; + ImGuiDockContextPruneNodeData() { CountWindows = CountChildWindows = CountChildNodes = 0; RootId = 0; } +}; + +// Garbage collect unused nodes (run once at init time) +static void ImGui::DockContextPruneUnusedSettingsNodes(ImGuiContext* ctx) +{ + ImGuiContext& g = *ctx; + ImGuiDockContext* dc = &ctx->DockContext; + IM_ASSERT(g.Windows.Size == 0); + + ImPool pool; + pool.Reserve(dc->NodesSettings.Size); + + // Count child nodes and compute RootID + for (int settings_n = 0; settings_n < dc->NodesSettings.Size; settings_n++) + { + ImGuiDockNodeSettings* settings = &dc->NodesSettings[settings_n]; + ImGuiDockContextPruneNodeData* parent_data = settings->ParentNodeId ? pool.GetByKey(settings->ParentNodeId) : 0; + pool.GetOrAddByKey(settings->ID)->RootId = parent_data ? parent_data->RootId : settings->ID; + if (settings->ParentNodeId) + pool.GetOrAddByKey(settings->ParentNodeId)->CountChildNodes++; + } + + // Count reference to dock ids from dockspaces + // We track the 'auto-DockNode <- manual-Window <- manual-DockSpace' in order to avoid 'auto-DockNode' being ditched by DockContextPruneUnusedSettingsNodes() + for (int settings_n = 0; settings_n < dc->NodesSettings.Size; settings_n++) + { + ImGuiDockNodeSettings* settings = &dc->NodesSettings[settings_n]; + if (settings->ParentWindowId != 0) + if (ImGuiWindowSettings* window_settings = FindWindowSettingsByID(settings->ParentWindowId)) + if (window_settings->DockId) + if (ImGuiDockContextPruneNodeData* data = pool.GetByKey(window_settings->DockId)) + data->CountChildNodes++; + } + + // Count reference to dock ids from window settings + // We guard against the possibility of an invalid .ini file (RootID may point to a missing node) + for (ImGuiWindowSettings* settings = g.SettingsWindows.begin(); settings != NULL; settings = g.SettingsWindows.next_chunk(settings)) + if (ImGuiID dock_id = settings->DockId) + if (ImGuiDockContextPruneNodeData* data = pool.GetByKey(dock_id)) + { + data->CountWindows++; + if (ImGuiDockContextPruneNodeData* data_root = (data->RootId == dock_id) ? data : pool.GetByKey(data->RootId)) + data_root->CountChildWindows++; + } + + // Prune + for (int settings_n = 0; settings_n < dc->NodesSettings.Size; settings_n++) + { + ImGuiDockNodeSettings* settings = &dc->NodesSettings[settings_n]; + ImGuiDockContextPruneNodeData* data = pool.GetByKey(settings->ID); + if (data->CountWindows > 1) + continue; + ImGuiDockContextPruneNodeData* data_root = (data->RootId == settings->ID) ? data : pool.GetByKey(data->RootId); + + bool remove = false; + remove |= (data->CountWindows == 1 && settings->ParentNodeId == 0 && data->CountChildNodes == 0 && !(settings->Flags & ImGuiDockNodeFlags_CentralNode)); // Floating root node with only 1 window + remove |= (data->CountWindows == 0 && settings->ParentNodeId == 0 && data->CountChildNodes == 0); // Leaf nodes with 0 window + remove |= (data_root->CountChildWindows == 0); + if (remove) + { + IMGUI_DEBUG_LOG_DOCKING("[docking] DockContextPruneUnusedSettingsNodes: Prune 0x%08X\n", settings->ID); + DockSettingsRemoveNodeReferences(&settings->ID, 1); + settings->ID = 0; + } + } +} + +static void ImGui::DockContextBuildNodesFromSettings(ImGuiContext* ctx, ImGuiDockNodeSettings* node_settings_array, int node_settings_count) +{ + // Build nodes + for (int node_n = 0; node_n < node_settings_count; node_n++) + { + ImGuiDockNodeSettings* settings = &node_settings_array[node_n]; + if (settings->ID == 0) + continue; + ImGuiDockNode* node = DockContextAddNode(ctx, settings->ID); + node->ParentNode = settings->ParentNodeId ? DockContextFindNodeByID(ctx, settings->ParentNodeId) : NULL; + node->Pos = ImVec2(settings->Pos.x, settings->Pos.y); + node->Size = ImVec2(settings->Size.x, settings->Size.y); + node->SizeRef = ImVec2(settings->SizeRef.x, settings->SizeRef.y); + node->AuthorityForPos = node->AuthorityForSize = node->AuthorityForViewport = ImGuiDataAuthority_DockNode; + if (node->ParentNode && node->ParentNode->ChildNodes[0] == NULL) + node->ParentNode->ChildNodes[0] = node; + else if (node->ParentNode && node->ParentNode->ChildNodes[1] == NULL) + node->ParentNode->ChildNodes[1] = node; + node->SelectedTabId = settings->SelectedTabId; + node->SplitAxis = (ImGuiAxis)settings->SplitAxis; + node->SetLocalFlags(settings->Flags & ImGuiDockNodeFlags_SavedFlagsMask_); + + // Bind host window immediately if it already exist (in case of a rebuild) + // This is useful as the RootWindowForTitleBarHighlight links necessary to highlight the currently focused node requires node->HostWindow to be set. + char host_window_title[20]; + ImGuiDockNode* root_node = DockNodeGetRootNode(node); + node->HostWindow = FindWindowByName(DockNodeGetHostWindowTitle(root_node, host_window_title, IM_ARRAYSIZE(host_window_title))); + } +} + +void ImGui::DockContextBuildAddWindowsToNodes(ImGuiContext* ctx, ImGuiID root_id) +{ + // Rebind all windows to nodes (they can also lazily rebind but we'll have a visible glitch during the first frame) + ImGuiContext& g = *ctx; + for (ImGuiWindow* window : g.Windows) + { + if (window->DockId == 0 || window->LastFrameActive < g.FrameCount - 1) + continue; + if (window->DockNode != NULL) + continue; + + ImGuiDockNode* node = DockContextFindNodeByID(ctx, window->DockId); + IM_ASSERT(node != NULL); // This should have been called after DockContextBuildNodesFromSettings() + if (root_id == 0 || DockNodeGetRootNode(node)->ID == root_id) + DockNodeAddWindow(node, window, true); + } +} + +//----------------------------------------------------------------------------- +// Docking: ImGuiDockContext Docking/Undocking functions +//----------------------------------------------------------------------------- +// - DockContextQueueDock() +// - DockContextQueueUndockWindow() +// - DockContextQueueUndockNode() +// - DockContextQueueNotifyRemovedNode() +// - DockContextProcessDock() +// - DockContextProcessUndockWindow() +// - DockContextProcessUndockNode() +// - DockContextCalcDropPosForDocking() +//----------------------------------------------------------------------------- + +void ImGui::DockContextQueueDock(ImGuiContext* ctx, ImGuiWindow* target, ImGuiDockNode* target_node, ImGuiWindow* payload, ImGuiDir split_dir, float split_ratio, bool split_outer) +{ + IM_ASSERT(target != payload); + ImGuiDockRequest req; + req.Type = ImGuiDockRequestType_Dock; + req.DockTargetWindow = target; + req.DockTargetNode = target_node; + req.DockPayload = payload; + req.DockSplitDir = split_dir; + req.DockSplitRatio = split_ratio; + req.DockSplitOuter = split_outer; + ctx->DockContext.Requests.push_back(req); +} + +void ImGui::DockContextQueueUndockWindow(ImGuiContext* ctx, ImGuiWindow* window) +{ + ImGuiDockRequest req; + req.Type = ImGuiDockRequestType_Undock; + req.UndockTargetWindow = window; + ctx->DockContext.Requests.push_back(req); +} + +void ImGui::DockContextQueueUndockNode(ImGuiContext* ctx, ImGuiDockNode* node) +{ + ImGuiDockRequest req; + req.Type = ImGuiDockRequestType_Undock; + req.UndockTargetNode = node; + ctx->DockContext.Requests.push_back(req); +} + +void ImGui::DockContextQueueNotifyRemovedNode(ImGuiContext* ctx, ImGuiDockNode* node) +{ + ImGuiDockContext* dc = &ctx->DockContext; + for (ImGuiDockRequest& req : dc->Requests) + if (req.DockTargetNode == node) + req.Type = ImGuiDockRequestType_None; +} + +void ImGui::DockContextProcessDock(ImGuiContext* ctx, ImGuiDockRequest* req) +{ + IM_ASSERT((req->Type == ImGuiDockRequestType_Dock && req->DockPayload != NULL) || (req->Type == ImGuiDockRequestType_Split && req->DockPayload == NULL)); + IM_ASSERT(req->DockTargetWindow != NULL || req->DockTargetNode != NULL); + + ImGuiContext& g = *ctx; + IM_UNUSED(g); + + ImGuiWindow* payload_window = req->DockPayload; // Optional + ImGuiWindow* target_window = req->DockTargetWindow; + ImGuiDockNode* node = req->DockTargetNode; + if (payload_window) + IMGUI_DEBUG_LOG_DOCKING("[docking] DockContextProcessDock node 0x%08X target '%s' dock window '%s', split_dir %d\n", node ? node->ID : 0, target_window ? target_window->Name : "NULL", payload_window->Name, req->DockSplitDir); + else + IMGUI_DEBUG_LOG_DOCKING("[docking] DockContextProcessDock node 0x%08X, split_dir %d\n", node ? node->ID : 0, req->DockSplitDir); + + // Decide which Tab will be selected at the end of the operation + ImGuiID next_selected_id = 0; + ImGuiDockNode* payload_node = NULL; + if (payload_window) + { + payload_node = payload_window->DockNodeAsHost; + payload_window->DockNodeAsHost = NULL; // Important to clear this as the node will have its life as a child which might be merged/deleted later. + if (payload_node && payload_node->IsLeafNode()) + next_selected_id = payload_node->TabBar->NextSelectedTabId ? payload_node->TabBar->NextSelectedTabId : payload_node->TabBar->SelectedTabId; + if (payload_node == NULL) + next_selected_id = payload_window->TabId; + } + + // FIXME-DOCK: When we are trying to dock an existing single-window node into a loose window, transfer Node ID as well + // When processing an interactive split, usually LastFrameAlive will be < g.FrameCount. But DockBuilder operations can make it ==. + if (node) + IM_ASSERT(node->LastFrameAlive <= g.FrameCount); + if (node && target_window && node == target_window->DockNodeAsHost) + IM_ASSERT(node->Windows.Size > 0 || node->IsSplitNode() || node->IsCentralNode()); + + // Create new node and add existing window to it + if (node == NULL) + { + node = DockContextAddNode(ctx, 0); + node->Pos = target_window->Pos; + node->Size = target_window->Size; + if (target_window->DockNodeAsHost == NULL) + { + DockNodeAddWindow(node, target_window, true); + node->TabBar->Tabs[0].Flags &= ~ImGuiTabItemFlags_Unsorted; + target_window->DockIsActive = true; + } + } + + ImGuiDir split_dir = req->DockSplitDir; + if (split_dir != ImGuiDir_None) + { + // Split into two, one side will be our payload node unless we are dropping a loose window + const ImGuiAxis split_axis = (split_dir == ImGuiDir_Left || split_dir == ImGuiDir_Right) ? ImGuiAxis_X : ImGuiAxis_Y; + const int split_inheritor_child_idx = (split_dir == ImGuiDir_Left || split_dir == ImGuiDir_Up) ? 1 : 0; // Current contents will be moved to the opposite side + const float split_ratio = req->DockSplitRatio; + DockNodeTreeSplit(ctx, node, split_axis, split_inheritor_child_idx, split_ratio, payload_node); // payload_node may be NULL here! + ImGuiDockNode* new_node = node->ChildNodes[split_inheritor_child_idx ^ 1]; + new_node->HostWindow = node->HostWindow; + node = new_node; + } + node->SetLocalFlags(node->LocalFlags & ~ImGuiDockNodeFlags_HiddenTabBar); + + if (node != payload_node) + { + // Create tab bar before we call DockNodeMoveWindows (which would attempt to move the old tab-bar, which would lead us to payload tabs wrongly appearing before target tabs!) + if (node->Windows.Size > 0 && node->TabBar == NULL) + { + DockNodeAddTabBar(node); + for (int n = 0; n < node->Windows.Size; n++) + TabBarAddTab(node->TabBar, ImGuiTabItemFlags_None, node->Windows[n]); + } + + if (payload_node != NULL) + { + // Transfer full payload node (with 1+ child windows or child nodes) + if (payload_node->IsSplitNode()) + { + if (node->Windows.Size > 0) + { + // We can dock a split payload into a node that already has windows _only_ if our payload is a node tree with a single visible node. + // In this situation, we move the windows of the target node into the currently visible node of the payload. + // This allows us to preserve some of the underlying dock tree settings nicely. + IM_ASSERT(payload_node->OnlyNodeWithWindows != NULL); // The docking should have been blocked by DockNodePreviewDockSetup() early on and never submitted. + ImGuiDockNode* visible_node = payload_node->OnlyNodeWithWindows; + if (visible_node->TabBar) + IM_ASSERT(visible_node->TabBar->Tabs.Size > 0); + DockNodeMoveWindows(node, visible_node); + DockNodeMoveWindows(visible_node, node); + DockSettingsRenameNodeReferences(node->ID, visible_node->ID); + } + if (node->IsCentralNode()) + { + // Central node property needs to be moved to a leaf node, pick the last focused one. + // FIXME-DOCK: If we had to transfer other flags here, what would the policy be? + ImGuiDockNode* last_focused_node = DockContextFindNodeByID(ctx, payload_node->LastFocusedNodeId); + IM_ASSERT(last_focused_node != NULL); + ImGuiDockNode* last_focused_root_node = DockNodeGetRootNode(last_focused_node); + IM_ASSERT(last_focused_root_node == DockNodeGetRootNode(payload_node)); + last_focused_node->SetLocalFlags(last_focused_node->LocalFlags | ImGuiDockNodeFlags_CentralNode); + node->SetLocalFlags(node->LocalFlags & ~ImGuiDockNodeFlags_CentralNode); + last_focused_root_node->CentralNode = last_focused_node; + } + + IM_ASSERT(node->Windows.Size == 0); + DockNodeMoveChildNodes(node, payload_node); + } + else + { + const ImGuiID payload_dock_id = payload_node->ID; + DockNodeMoveWindows(node, payload_node); + DockSettingsRenameNodeReferences(payload_dock_id, node->ID); + } + DockContextRemoveNode(ctx, payload_node, true); + } + else if (payload_window) + { + // Transfer single window + const ImGuiID payload_dock_id = payload_window->DockId; + node->VisibleWindow = payload_window; + DockNodeAddWindow(node, payload_window, true); + if (payload_dock_id != 0) + DockSettingsRenameNodeReferences(payload_dock_id, node->ID); + } + } + else + { + // When docking a floating single window node we want to reevaluate auto-hiding of the tab bar + node->WantHiddenTabBarUpdate = true; + } + + // Update selection immediately + if (ImGuiTabBar* tab_bar = node->TabBar) + tab_bar->NextSelectedTabId = next_selected_id; + MarkIniSettingsDirty(); +} + +// Problem: +// Undocking a large (~full screen) window would leave it so large that the bottom right sizing corner would more +// than likely be off the screen and the window would be hard to resize to fit on screen. This can be particularly problematic +// with 'ConfigWindowsMoveFromTitleBarOnly=true' and/or with 'ConfigWindowsResizeFromEdges=false' as well (the later can be +// due to missing ImGuiBackendFlags_HasMouseCursors backend flag). +// Solution: +// When undocking a window we currently force its maximum size to 90% of the host viewport or monitor. +// Reevaluate this when we implement preserving docked/undocked size ("docking_wip/undocked_size" branch). +static ImVec2 FixLargeWindowsWhenUndocking(const ImVec2& size, ImGuiViewport* ref_viewport) +{ + if (ref_viewport == NULL) + return size; + + ImGuiContext& g = *GImGui; + ImVec2 max_size = ImTrunc(ref_viewport->WorkSize * 0.90f); + if (g.ConfigFlagsCurrFrame & ImGuiConfigFlags_ViewportsEnable) + { + const ImGuiPlatformMonitor* monitor = ImGui::GetViewportPlatformMonitor(ref_viewport); + max_size = ImTrunc(monitor->WorkSize * 0.90f); + } + return ImMin(size, max_size); +} + +void ImGui::DockContextProcessUndockWindow(ImGuiContext* ctx, ImGuiWindow* window, bool clear_persistent_docking_ref) +{ + ImGuiContext& g = *ctx; + IMGUI_DEBUG_LOG_DOCKING("[docking] DockContextProcessUndockWindow window '%s', clear_persistent_docking_ref = %d\n", window->Name, clear_persistent_docking_ref); + if (window->DockNode) + DockNodeRemoveWindow(window->DockNode, window, clear_persistent_docking_ref ? 0 : window->DockId); + else + window->DockId = 0; + window->Collapsed = false; + window->DockIsActive = false; + window->DockNodeIsVisible = window->DockTabIsVisible = false; + window->Size = window->SizeFull = FixLargeWindowsWhenUndocking(window->SizeFull, window->Viewport); + + MarkIniSettingsDirty(); +} + +void ImGui::DockContextProcessUndockNode(ImGuiContext* ctx, ImGuiDockNode* node) +{ + ImGuiContext& g = *ctx; + IMGUI_DEBUG_LOG_DOCKING("[docking] DockContextProcessUndockNode node %08X\n", node->ID); + IM_ASSERT(node->IsLeafNode()); + IM_ASSERT(node->Windows.Size >= 1); + + if (node->IsRootNode() || node->IsCentralNode()) + { + // In the case of a root node or central node, the node will have to stay in place. Create a new node to receive the payload. + ImGuiDockNode* new_node = DockContextAddNode(ctx, 0); + new_node->Pos = node->Pos; + new_node->Size = node->Size; + new_node->SizeRef = node->SizeRef; + DockNodeMoveWindows(new_node, node); + DockSettingsRenameNodeReferences(node->ID, new_node->ID); + node = new_node; + } + else + { + // Otherwise extract our node and merge our sibling back into the parent node. + IM_ASSERT(node->ParentNode->ChildNodes[0] == node || node->ParentNode->ChildNodes[1] == node); + int index_in_parent = (node->ParentNode->ChildNodes[0] == node) ? 0 : 1; + node->ParentNode->ChildNodes[index_in_parent] = NULL; + DockNodeTreeMerge(ctx, node->ParentNode, node->ParentNode->ChildNodes[index_in_parent ^ 1]); + node->ParentNode->AuthorityForViewport = ImGuiDataAuthority_Window; // The node that stays in place keeps the viewport, so our newly dragged out node will create a new viewport + node->ParentNode = NULL; + } + for (ImGuiWindow* window : node->Windows) + { + window->Flags &= ~ImGuiWindowFlags_ChildWindow; + if (window->ParentWindow) + window->ParentWindow->DC.ChildWindows.find_erase(window); + UpdateWindowParentAndRootLinks(window, window->Flags, NULL); + } + node->AuthorityForPos = node->AuthorityForSize = ImGuiDataAuthority_DockNode; + node->Size = FixLargeWindowsWhenUndocking(node->Size, node->Windows[0]->Viewport); + node->WantMouseMove = true; + MarkIniSettingsDirty(); +} + +// This is mostly used for automation. +bool ImGui::DockContextCalcDropPosForDocking(ImGuiWindow* target, ImGuiDockNode* target_node, ImGuiWindow* payload_window, ImGuiDockNode* payload_node, ImGuiDir split_dir, bool split_outer, ImVec2* out_pos) +{ + if (target != NULL && target_node == NULL) + target_node = target->DockNode; + + // In DockNodePreviewDockSetup() for a root central node instead of showing both "inner" and "outer" drop rects + // (which would be functionally identical) we only show the outer one. Reflect this here. + if (target_node && target_node->ParentNode == NULL && target_node->IsCentralNode() && split_dir != ImGuiDir_None) + split_outer = true; + ImGuiDockPreviewData split_data; + DockNodePreviewDockSetup(target, target_node, payload_window, payload_node, &split_data, false, split_outer); + if (split_data.DropRectsDraw[split_dir+1].IsInverted()) + return false; + *out_pos = split_data.DropRectsDraw[split_dir+1].GetCenter(); + return true; +} + +//----------------------------------------------------------------------------- +// Docking: ImGuiDockNode +//----------------------------------------------------------------------------- +// - DockNodeGetTabOrder() +// - DockNodeAddWindow() +// - DockNodeRemoveWindow() +// - DockNodeMoveChildNodes() +// - DockNodeMoveWindows() +// - DockNodeApplyPosSizeToWindows() +// - DockNodeHideHostWindow() +// - ImGuiDockNodeFindInfoResults +// - DockNodeFindInfo() +// - DockNodeFindWindowByID() +// - DockNodeUpdateFlagsAndCollapse() +// - DockNodeUpdateHasCentralNodeFlag() +// - DockNodeUpdateVisibleFlag() +// - DockNodeStartMouseMovingWindow() +// - DockNodeUpdate() +// - DockNodeUpdateWindowMenu() +// - DockNodeBeginAmendTabBar() +// - DockNodeEndAmendTabBar() +// - DockNodeUpdateTabBar() +// - DockNodeAddTabBar() +// - DockNodeRemoveTabBar() +// - DockNodeIsDropAllowedOne() +// - DockNodeIsDropAllowed() +// - DockNodeCalcTabBarLayout() +// - DockNodeCalcSplitRects() +// - DockNodeCalcDropRectsAndTestMousePos() +// - DockNodePreviewDockSetup() +// - DockNodePreviewDockRender() +//----------------------------------------------------------------------------- + +ImGuiDockNode::ImGuiDockNode(ImGuiID id) +{ + ID = id; + SharedFlags = LocalFlags = LocalFlagsInWindows = MergedFlags = ImGuiDockNodeFlags_None; + ParentNode = ChildNodes[0] = ChildNodes[1] = NULL; + TabBar = NULL; + SplitAxis = ImGuiAxis_None; + + State = ImGuiDockNodeState_Unknown; + LastBgColor = IM_COL32_WHITE; + HostWindow = VisibleWindow = NULL; + CentralNode = OnlyNodeWithWindows = NULL; + CountNodeWithWindows = 0; + LastFrameAlive = LastFrameActive = LastFrameFocused = -1; + LastFocusedNodeId = 0; + SelectedTabId = 0; + WantCloseTabId = 0; + RefViewportId = 0; + AuthorityForPos = AuthorityForSize = ImGuiDataAuthority_DockNode; + AuthorityForViewport = ImGuiDataAuthority_Auto; + IsVisible = true; + IsFocused = HasCloseButton = HasWindowMenuButton = HasCentralNodeChild = false; + IsBgDrawnThisFrame = false; + WantCloseAll = WantLockSizeOnce = WantMouseMove = WantHiddenTabBarUpdate = WantHiddenTabBarToggle = false; +} + +ImGuiDockNode::~ImGuiDockNode() +{ + IM_DELETE(TabBar); + TabBar = NULL; + ChildNodes[0] = ChildNodes[1] = NULL; +} + +int ImGui::DockNodeGetTabOrder(ImGuiWindow* window) +{ + ImGuiTabBar* tab_bar = window->DockNode->TabBar; + if (tab_bar == NULL) + return -1; + ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, window->TabId); + return tab ? TabBarGetTabOrder(tab_bar, tab) : -1; +} + +static void DockNodeHideWindowDuringHostWindowCreation(ImGuiWindow* window) +{ + window->Hidden = true; + window->HiddenFramesCanSkipItems = window->Active ? 1 : 2; +} + +static void ImGui::DockNodeAddWindow(ImGuiDockNode* node, ImGuiWindow* window, bool add_to_tab_bar) +{ + ImGuiContext& g = *GImGui; (void)g; + if (window->DockNode) + { + // Can overwrite an existing window->DockNode (e.g. pointing to a disabled DockSpace node) + IM_ASSERT(window->DockNode->ID != node->ID); + DockNodeRemoveWindow(window->DockNode, window, 0); + } + IM_ASSERT(window->DockNode == NULL || window->DockNodeAsHost == NULL); + IMGUI_DEBUG_LOG_DOCKING("[docking] DockNodeAddWindow node 0x%08X window '%s'\n", node->ID, window->Name); + + // If more than 2 windows appeared on the same frame leading to the creation of a new hosting window, + // we'll hide windows until the host window is ready. Hide the 1st window after its been output (so it is not visible for one frame). + // We will call DockNodeHideWindowDuringHostWindowCreation() on ourselves in Begin() + if (node->HostWindow == NULL && node->Windows.Size == 1 && node->Windows[0]->WasActive == false) + DockNodeHideWindowDuringHostWindowCreation(node->Windows[0]); + + node->Windows.push_back(window); + node->WantHiddenTabBarUpdate = true; + window->DockNode = node; + window->DockId = node->ID; + window->DockIsActive = (node->Windows.Size > 1); + window->DockTabWantClose = false; + + // When reactivating a node with one or two loose window, the window pos/size/viewport are authoritative over the node storage. + // In particular it is important we init the viewport from the first window so we don't create two viewports and drop one. + if (node->HostWindow == NULL && node->IsFloatingNode()) + { + if (node->AuthorityForPos == ImGuiDataAuthority_Auto) + node->AuthorityForPos = ImGuiDataAuthority_Window; + if (node->AuthorityForSize == ImGuiDataAuthority_Auto) + node->AuthorityForSize = ImGuiDataAuthority_Window; + if (node->AuthorityForViewport == ImGuiDataAuthority_Auto) + node->AuthorityForViewport = ImGuiDataAuthority_Window; + } + + // Add to tab bar if requested + if (add_to_tab_bar) + { + if (node->TabBar == NULL) + { + DockNodeAddTabBar(node); + node->TabBar->SelectedTabId = node->TabBar->NextSelectedTabId = node->SelectedTabId; + + // Add existing windows + for (int n = 0; n < node->Windows.Size - 1; n++) + TabBarAddTab(node->TabBar, ImGuiTabItemFlags_None, node->Windows[n]); + } + TabBarAddTab(node->TabBar, ImGuiTabItemFlags_Unsorted, window); + } + + DockNodeUpdateVisibleFlag(node); + + // Update this without waiting for the next time we Begin() in the window, so our host window will have the proper title bar color on its first frame. + if (node->HostWindow) + UpdateWindowParentAndRootLinks(window, window->Flags | ImGuiWindowFlags_ChildWindow, node->HostWindow); +} + +static void ImGui::DockNodeRemoveWindow(ImGuiDockNode* node, ImGuiWindow* window, ImGuiID save_dock_id) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(window->DockNode == node); + //IM_ASSERT(window->RootWindowDockTree == node->HostWindow); + //IM_ASSERT(window->LastFrameActive < g.FrameCount); // We may call this from Begin() + IM_ASSERT(save_dock_id == 0 || save_dock_id == node->ID); + IMGUI_DEBUG_LOG_DOCKING("[docking] DockNodeRemoveWindow node 0x%08X window '%s'\n", node->ID, window->Name); + + window->DockNode = NULL; + window->DockIsActive = window->DockTabWantClose = false; + window->DockId = save_dock_id; + window->Flags &= ~ImGuiWindowFlags_ChildWindow; + if (window->ParentWindow) + window->ParentWindow->DC.ChildWindows.find_erase(window); + UpdateWindowParentAndRootLinks(window, window->Flags, NULL); // Update immediately + + if (node->HostWindow && node->HostWindow->ViewportOwned) + { + // When undocking from a user interaction this will always run in NewFrame() and have not much effect. + // But mid-frame, if we clear viewport we need to mark window as hidden as well. + window->Viewport = NULL; + window->ViewportId = 0; + window->ViewportOwned = false; + window->Hidden = true; + } + + // Remove window + bool erased = false; + for (int n = 0; n < node->Windows.Size; n++) + if (node->Windows[n] == window) + { + node->Windows.erase(node->Windows.Data + n); + erased = true; + break; + } + if (!erased) + IM_ASSERT(erased); + if (node->VisibleWindow == window) + node->VisibleWindow = NULL; + + // Remove tab and possibly tab bar + node->WantHiddenTabBarUpdate = true; + if (node->TabBar) + { + TabBarRemoveTab(node->TabBar, window->TabId); + const int tab_count_threshold_for_tab_bar = node->IsCentralNode() ? 1 : 2; + if (node->Windows.Size < tab_count_threshold_for_tab_bar) + DockNodeRemoveTabBar(node); + } + + if (node->Windows.Size == 0 && !node->IsCentralNode() && !node->IsDockSpace() && window->DockId != node->ID) + { + // Automatic dock node delete themselves if they are not holding at least one tab + DockContextRemoveNode(&g, node, true); + return; + } + + if (node->Windows.Size == 1 && !node->IsCentralNode() && node->HostWindow) + { + ImGuiWindow* remaining_window = node->Windows[0]; + // Note: we used to transport viewport ownership here. + remaining_window->Collapsed = node->HostWindow->Collapsed; + } + + // Update visibility immediately is required so the DockNodeUpdateRemoveInactiveChilds() processing can reflect changes up the tree + DockNodeUpdateVisibleFlag(node); +} + +static void ImGui::DockNodeMoveChildNodes(ImGuiDockNode* dst_node, ImGuiDockNode* src_node) +{ + IM_ASSERT(dst_node->Windows.Size == 0); + dst_node->ChildNodes[0] = src_node->ChildNodes[0]; + dst_node->ChildNodes[1] = src_node->ChildNodes[1]; + if (dst_node->ChildNodes[0]) + dst_node->ChildNodes[0]->ParentNode = dst_node; + if (dst_node->ChildNodes[1]) + dst_node->ChildNodes[1]->ParentNode = dst_node; + dst_node->SplitAxis = src_node->SplitAxis; + dst_node->SizeRef = src_node->SizeRef; + src_node->ChildNodes[0] = src_node->ChildNodes[1] = NULL; +} + +static void ImGui::DockNodeMoveWindows(ImGuiDockNode* dst_node, ImGuiDockNode* src_node) +{ + // Insert tabs in the same orders as currently ordered (node->Windows isn't ordered) + IM_ASSERT(src_node && dst_node && dst_node != src_node); + ImGuiTabBar* src_tab_bar = src_node->TabBar; + if (src_tab_bar != NULL) + IM_ASSERT(src_node->Windows.Size <= src_node->TabBar->Tabs.Size); + + // If the dst_node is empty we can just move the entire tab bar (to preserve selection, scrolling, etc.) + bool move_tab_bar = (src_tab_bar != NULL) && (dst_node->TabBar == NULL); + if (move_tab_bar) + { + dst_node->TabBar = src_node->TabBar; + src_node->TabBar = NULL; + } + + // Tab order is not important here, it is preserved by sorting in DockNodeUpdateTabBar(). + for (ImGuiWindow* window : src_node->Windows) + { + window->DockNode = NULL; + window->DockIsActive = false; + DockNodeAddWindow(dst_node, window, !move_tab_bar); + } + src_node->Windows.clear(); + + if (!move_tab_bar && src_node->TabBar) + { + if (dst_node->TabBar) + dst_node->TabBar->SelectedTabId = src_node->TabBar->SelectedTabId; + DockNodeRemoveTabBar(src_node); + } +} + +static void ImGui::DockNodeApplyPosSizeToWindows(ImGuiDockNode* node) +{ + for (ImGuiWindow* window : node->Windows) + { + SetWindowPos(window, node->Pos, ImGuiCond_Always); // We don't assign directly to Pos because it can break the calculation of SizeContents on next frame + SetWindowSize(window, node->Size, ImGuiCond_Always); + } +} + +static void ImGui::DockNodeHideHostWindow(ImGuiDockNode* node) +{ + if (node->HostWindow) + { + if (node->HostWindow->DockNodeAsHost == node) + node->HostWindow->DockNodeAsHost = NULL; + node->HostWindow = NULL; + } + + if (node->Windows.Size == 1) + { + node->VisibleWindow = node->Windows[0]; + node->Windows[0]->DockIsActive = false; + } + + if (node->TabBar) + DockNodeRemoveTabBar(node); +} + +// Search function called once by root node in DockNodeUpdate() +struct ImGuiDockNodeTreeInfo +{ + ImGuiDockNode* CentralNode; + ImGuiDockNode* FirstNodeWithWindows; + int CountNodesWithWindows; + //ImGuiWindowClass WindowClassForMerges; + + ImGuiDockNodeTreeInfo() { memset(this, 0, sizeof(*this)); } +}; + +static void DockNodeFindInfo(ImGuiDockNode* node, ImGuiDockNodeTreeInfo* info) +{ + if (node->Windows.Size > 0) + { + if (info->FirstNodeWithWindows == NULL) + info->FirstNodeWithWindows = node; + info->CountNodesWithWindows++; + } + if (node->IsCentralNode()) + { + IM_ASSERT(info->CentralNode == NULL); // Should be only one + IM_ASSERT(node->IsLeafNode() && "If you get this assert: please submit .ini file + repro of actions leading to this."); + info->CentralNode = node; + } + if (info->CountNodesWithWindows > 1 && info->CentralNode != NULL) + return; + if (node->ChildNodes[0]) + DockNodeFindInfo(node->ChildNodes[0], info); + if (node->ChildNodes[1]) + DockNodeFindInfo(node->ChildNodes[1], info); +} + +static ImGuiWindow* ImGui::DockNodeFindWindowByID(ImGuiDockNode* node, ImGuiID id) +{ + IM_ASSERT(id != 0); + for (ImGuiWindow* window : node->Windows) + if (window->ID == id) + return window; + return NULL; +} + +// - Remove inactive windows/nodes. +// - Update visibility flag. +static void ImGui::DockNodeUpdateFlagsAndCollapse(ImGuiDockNode* node) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(node->ParentNode == NULL || node->ParentNode->ChildNodes[0] == node || node->ParentNode->ChildNodes[1] == node); + + // Inherit most flags + if (node->ParentNode) + node->SharedFlags = node->ParentNode->SharedFlags & ImGuiDockNodeFlags_SharedFlagsInheritMask_; + + // Recurse into children + // There is the possibility that one of our child becoming empty will delete itself and moving its sibling contents into 'node'. + // If 'node->ChildNode[0]' delete itself, then 'node->ChildNode[1]->Windows' will be moved into 'node' + // If 'node->ChildNode[1]' delete itself, then 'node->ChildNode[0]->Windows' will be moved into 'node' and the "remove inactive windows" loop will have run twice on those windows (harmless) + node->HasCentralNodeChild = false; + if (node->ChildNodes[0]) + DockNodeUpdateFlagsAndCollapse(node->ChildNodes[0]); + if (node->ChildNodes[1]) + DockNodeUpdateFlagsAndCollapse(node->ChildNodes[1]); + + // Remove inactive windows, collapse nodes + // Merge node flags overrides stored in windows + node->LocalFlagsInWindows = ImGuiDockNodeFlags_None; + for (int window_n = 0; window_n < node->Windows.Size; window_n++) + { + ImGuiWindow* window = node->Windows[window_n]; + IM_ASSERT(window->DockNode == node); + + bool node_was_active = (node->LastFrameActive + 1 == g.FrameCount); + bool remove = false; + remove |= node_was_active && (window->LastFrameActive + 1 < g.FrameCount); + remove |= node_was_active && (node->WantCloseAll || node->WantCloseTabId == window->TabId) && window->HasCloseButton && !(window->Flags & ImGuiWindowFlags_UnsavedDocument); // Submit all _expected_ closure from last frame + remove |= (window->DockTabWantClose); + if (remove) + { + window->DockTabWantClose = false; + if (node->Windows.Size == 1 && !node->IsCentralNode()) + { + DockNodeHideHostWindow(node); + node->State = ImGuiDockNodeState_HostWindowHiddenBecauseSingleWindow; + DockNodeRemoveWindow(node, window, node->ID); // Will delete the node so it'll be invalid on return + return; + } + DockNodeRemoveWindow(node, window, node->ID); + window_n--; + continue; + } + + // FIXME-DOCKING: Missing policies for conflict resolution, hence the "Experimental" tag on this. + //node->LocalFlagsInWindow &= ~window->WindowClass.DockNodeFlagsOverrideClear; + node->LocalFlagsInWindows |= window->WindowClass.DockNodeFlagsOverrideSet; + } + node->UpdateMergedFlags(); + + // Auto-hide tab bar option + ImGuiDockNodeFlags node_flags = node->MergedFlags; + if (node->WantHiddenTabBarUpdate && node->Windows.Size == 1 && (node_flags & ImGuiDockNodeFlags_AutoHideTabBar) && !node->IsHiddenTabBar()) + node->WantHiddenTabBarToggle = true; + node->WantHiddenTabBarUpdate = false; + + // Cancel toggling if we know our tab bar is enforced to be hidden at all times + if (node->WantHiddenTabBarToggle && node->VisibleWindow && (node->VisibleWindow->WindowClass.DockNodeFlagsOverrideSet & ImGuiDockNodeFlags_HiddenTabBar)) + node->WantHiddenTabBarToggle = false; + + // Apply toggles at a single point of the frame (here!) + if (node->Windows.Size > 1) + node->SetLocalFlags(node->LocalFlags & ~ImGuiDockNodeFlags_HiddenTabBar); + else if (node->WantHiddenTabBarToggle) + node->SetLocalFlags(node->LocalFlags ^ ImGuiDockNodeFlags_HiddenTabBar); + node->WantHiddenTabBarToggle = false; + + DockNodeUpdateVisibleFlag(node); +} + +// This is rarely called as DockNodeUpdateForRootNode() generally does it most frames. +static void ImGui::DockNodeUpdateHasCentralNodeChild(ImGuiDockNode* node) +{ + node->HasCentralNodeChild = false; + if (node->ChildNodes[0]) + DockNodeUpdateHasCentralNodeChild(node->ChildNodes[0]); + if (node->ChildNodes[1]) + DockNodeUpdateHasCentralNodeChild(node->ChildNodes[1]); + if (node->IsRootNode()) + { + ImGuiDockNode* mark_node = node->CentralNode; + while (mark_node) + { + mark_node->HasCentralNodeChild = true; + mark_node = mark_node->ParentNode; + } + } +} + +static void ImGui::DockNodeUpdateVisibleFlag(ImGuiDockNode* node) +{ + // Update visibility flag + bool is_visible = (node->ParentNode == NULL) ? node->IsDockSpace() : node->IsCentralNode(); + is_visible |= (node->Windows.Size > 0); + is_visible |= (node->ChildNodes[0] && node->ChildNodes[0]->IsVisible); + is_visible |= (node->ChildNodes[1] && node->ChildNodes[1]->IsVisible); + node->IsVisible = is_visible; +} + +static void ImGui::DockNodeStartMouseMovingWindow(ImGuiDockNode* node, ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(node->WantMouseMove == true); + StartMouseMovingWindow(window); + g.ActiveIdClickOffset = g.IO.MouseClickedPos[0] - node->Pos; + g.MovingWindow = window; // If we are docked into a non moveable root window, StartMouseMovingWindow() won't set g.MovingWindow. Override that decision. + node->WantMouseMove = false; +} + +// Update CentralNode, OnlyNodeWithWindows, LastFocusedNodeID. Copy window class. +static void ImGui::DockNodeUpdateForRootNode(ImGuiDockNode* node) +{ + DockNodeUpdateFlagsAndCollapse(node); + + // - Setup central node pointers + // - Find if there's only a single visible window in the hierarchy (in which case we need to display a regular title bar -> FIXME-DOCK: that last part is not done yet!) + // Cannot merge this with DockNodeUpdateFlagsAndCollapse() because FirstNodeWithWindows is found after window removal and child collapsing + ImGuiDockNodeTreeInfo info; + DockNodeFindInfo(node, &info); + node->CentralNode = info.CentralNode; + node->OnlyNodeWithWindows = (info.CountNodesWithWindows == 1) ? info.FirstNodeWithWindows : NULL; + node->CountNodeWithWindows = info.CountNodesWithWindows; + if (node->LastFocusedNodeId == 0 && info.FirstNodeWithWindows != NULL) + node->LastFocusedNodeId = info.FirstNodeWithWindows->ID; + + // Copy the window class from of our first window so it can be used for proper dock filtering. + // When node has mixed windows, prioritize the class with the most constraint (DockingAllowUnclassed = false) as the reference to copy. + // FIXME-DOCK: We don't recurse properly, this code could be reworked to work from DockNodeUpdateScanRec. + if (ImGuiDockNode* first_node_with_windows = info.FirstNodeWithWindows) + { + node->WindowClass = first_node_with_windows->Windows[0]->WindowClass; + for (int n = 1; n < first_node_with_windows->Windows.Size; n++) + if (first_node_with_windows->Windows[n]->WindowClass.DockingAllowUnclassed == false) + { + node->WindowClass = first_node_with_windows->Windows[n]->WindowClass; + break; + } + } + + ImGuiDockNode* mark_node = node->CentralNode; + while (mark_node) + { + mark_node->HasCentralNodeChild = true; + mark_node = mark_node->ParentNode; + } +} + +static void DockNodeSetupHostWindow(ImGuiDockNode* node, ImGuiWindow* host_window) +{ + // Remove ourselves from any previous different host window + // This can happen if a user mistakenly does (see #4295 for details): + // - N+0: DockBuilderAddNode(id, 0) // missing ImGuiDockNodeFlags_DockSpace + // - N+1: NewFrame() // will create floating host window for that node + // - N+1: DockSpace(id) // requalify node as dockspace, moving host window + if (node->HostWindow && node->HostWindow != host_window && node->HostWindow->DockNodeAsHost == node) + node->HostWindow->DockNodeAsHost = NULL; + + host_window->DockNodeAsHost = node; + node->HostWindow = host_window; +} + +static void ImGui::DockNodeUpdate(ImGuiDockNode* node) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(node->LastFrameActive != g.FrameCount); + node->LastFrameAlive = g.FrameCount; + node->IsBgDrawnThisFrame = false; + + node->CentralNode = node->OnlyNodeWithWindows = NULL; + if (node->IsRootNode()) + DockNodeUpdateForRootNode(node); + + // Remove tab bar if not needed + if (node->TabBar && node->IsNoTabBar()) + DockNodeRemoveTabBar(node); + + // Early out for hidden root dock nodes (when all DockId references are in inactive windows, or there is only 1 floating window holding on the DockId) + bool want_to_hide_host_window = false; + if (node->IsFloatingNode()) + { + if (node->Windows.Size <= 1 && node->IsLeafNode()) + if (!g.IO.ConfigDockingAlwaysTabBar && (node->Windows.Size == 0 || !node->Windows[0]->WindowClass.DockingAlwaysTabBar)) + want_to_hide_host_window = true; + if (node->CountNodeWithWindows == 0) + want_to_hide_host_window = true; + } + if (want_to_hide_host_window) + { + if (node->Windows.Size == 1) + { + // Floating window pos/size is authoritative + ImGuiWindow* single_window = node->Windows[0]; + node->Pos = single_window->Pos; + node->Size = single_window->SizeFull; + node->AuthorityForPos = node->AuthorityForSize = node->AuthorityForViewport = ImGuiDataAuthority_Window; + + // Transfer focus immediately so when we revert to a regular window it is immediately selected + if (node->HostWindow && g.NavWindow == node->HostWindow) + FocusWindow(single_window); + if (node->HostWindow) + { + IMGUI_DEBUG_LOG_VIEWPORT("[viewport] Node %08X transfer Viewport %08X->%08X to Window '%s'\n", node->ID, node->HostWindow->Viewport->ID, single_window->ID, single_window->Name); + single_window->Viewport = node->HostWindow->Viewport; + single_window->ViewportId = node->HostWindow->ViewportId; + if (node->HostWindow->ViewportOwned) + { + single_window->Viewport->ID = single_window->ID; + single_window->Viewport->Window = single_window; + single_window->ViewportOwned = true; + } + } + node->RefViewportId = single_window->ViewportId; + } + + DockNodeHideHostWindow(node); + node->State = ImGuiDockNodeState_HostWindowHiddenBecauseSingleWindow; + node->WantCloseAll = false; + node->WantCloseTabId = 0; + node->HasCloseButton = node->HasWindowMenuButton = false; + node->LastFrameActive = g.FrameCount; + + if (node->WantMouseMove && node->Windows.Size == 1) + DockNodeStartMouseMovingWindow(node, node->Windows[0]); + return; + } + + // In some circumstance we will defer creating the host window (so everything will be kept hidden), + // while the expected visible window is resizing itself. + // This is important for first-time (no ini settings restored) single window when io.ConfigDockingAlwaysTabBar is enabled, + // otherwise the node ends up using the minimum window size. Effectively those windows will take an extra frame to show up: + // N+0: Begin(): window created (with no known size), node is created + // N+1: DockNodeUpdate(): node skip creating host window / Begin(): window size applied, not visible + // N+2: DockNodeUpdate(): node can create host window / Begin(): window becomes visible + // We could remove this frame if we could reliably calculate the expected window size during node update, before the Begin() code. + // It would require a generalization of CalcWindowExpectedSize(), probably extracting code away from Begin(). + // In reality it isn't very important as user quickly ends up with size data in .ini file. + if (node->IsVisible && node->HostWindow == NULL && node->IsFloatingNode() && node->IsLeafNode()) + { + IM_ASSERT(node->Windows.Size > 0); + ImGuiWindow* ref_window = NULL; + if (node->SelectedTabId != 0) // Note that we prune single-window-node settings on .ini loading, so this is generally 0 for them! + ref_window = DockNodeFindWindowByID(node, node->SelectedTabId); + if (ref_window == NULL) + ref_window = node->Windows[0]; + if (ref_window->AutoFitFramesX > 0 || ref_window->AutoFitFramesY > 0) + { + node->State = ImGuiDockNodeState_HostWindowHiddenBecauseWindowsAreResizing; + return; + } + } + + const ImGuiDockNodeFlags node_flags = node->MergedFlags; + + // Decide if the node will have a close button and a window menu button + node->HasWindowMenuButton = (node->Windows.Size > 0) && (node_flags & ImGuiDockNodeFlags_NoWindowMenuButton) == 0; + node->HasCloseButton = false; + for (ImGuiWindow* window : node->Windows) + { + // FIXME-DOCK: Setting DockIsActive here means that for single active window in a leaf node, DockIsActive will be cleared until the next Begin() call. + node->HasCloseButton |= window->HasCloseButton; + window->DockIsActive = (node->Windows.Size > 1); + } + if (node_flags & ImGuiDockNodeFlags_NoCloseButton) + node->HasCloseButton = false; + + // Bind or create host window + ImGuiWindow* host_window = NULL; + bool beginned_into_host_window = false; + if (node->IsDockSpace()) + { + // [Explicit root dockspace node] + IM_ASSERT(node->HostWindow); + host_window = node->HostWindow; + } + else + { + // [Automatic root or child nodes] + if (node->IsRootNode() && node->IsVisible) + { + ImGuiWindow* ref_window = (node->Windows.Size > 0) ? node->Windows[0] : NULL; + + // Sync Pos + if (node->AuthorityForPos == ImGuiDataAuthority_Window && ref_window) + SetNextWindowPos(ref_window->Pos); + else if (node->AuthorityForPos == ImGuiDataAuthority_DockNode) + SetNextWindowPos(node->Pos); + + // Sync Size + if (node->AuthorityForSize == ImGuiDataAuthority_Window && ref_window) + SetNextWindowSize(ref_window->SizeFull); + else if (node->AuthorityForSize == ImGuiDataAuthority_DockNode) + SetNextWindowSize(node->Size); + + // Sync Collapsed + if (node->AuthorityForSize == ImGuiDataAuthority_Window && ref_window) + SetNextWindowCollapsed(ref_window->Collapsed); + + // Sync Viewport + if (node->AuthorityForViewport == ImGuiDataAuthority_Window && ref_window) + SetNextWindowViewport(ref_window->ViewportId); + else if (node->AuthorityForViewport == ImGuiDataAuthority_Window && node->RefViewportId != 0) + SetNextWindowViewport(node->RefViewportId); + + SetNextWindowClass(&node->WindowClass); + + // Begin into the host window + char window_label[20]; + DockNodeGetHostWindowTitle(node, window_label, IM_ARRAYSIZE(window_label)); + ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_DockNodeHost; + window_flags |= ImGuiWindowFlags_NoFocusOnAppearing; + window_flags |= ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoNavFocus | ImGuiWindowFlags_NoCollapse; + window_flags |= ImGuiWindowFlags_NoTitleBar; + + SetNextWindowBgAlpha(0.0f); // Don't set ImGuiWindowFlags_NoBackground because it disables borders + PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); + Begin(window_label, NULL, window_flags); + PopStyleVar(); + beginned_into_host_window = true; + + host_window = g.CurrentWindow; + DockNodeSetupHostWindow(node, host_window); + host_window->DC.CursorPos = host_window->Pos; + node->Pos = host_window->Pos; + node->Size = host_window->Size; + + // We set ImGuiWindowFlags_NoFocusOnAppearing because we don't want the host window to take full focus (e.g. steal NavWindow) + // But we still it bring it to the front of display. There's no way to choose this precise behavior via window flags. + // One simple case to ponder if: window A has a toggle to create windows B/C/D. Dock B/C/D together, clear the toggle and enable it again. + // When reappearing B/C/D will request focus and be moved to the top of the display pile, but they are not linked to the dock host window + // during the frame they appear. The dock host window would keep its old display order, and the sorting in EndFrame would move B/C/D back + // after the dock host window, losing their top-most status. + if (node->HostWindow->Appearing) + BringWindowToDisplayFront(node->HostWindow); + + node->AuthorityForPos = node->AuthorityForSize = node->AuthorityForViewport = ImGuiDataAuthority_Auto; + } + else if (node->ParentNode) + { + node->HostWindow = host_window = node->ParentNode->HostWindow; + node->AuthorityForPos = node->AuthorityForSize = node->AuthorityForViewport = ImGuiDataAuthority_Auto; + } + if (node->WantMouseMove && node->HostWindow) + DockNodeStartMouseMovingWindow(node, node->HostWindow); + } + node->RefViewportId = 0; // Clear when we have a host window + + // Update focused node (the one whose title bar is highlight) within a node tree + if (node->IsSplitNode()) + IM_ASSERT(node->TabBar == NULL); + if (node->IsRootNode()) + if (ImGuiWindow* p_window = g.NavWindow ? g.NavWindow->RootWindow : NULL) + while (p_window != NULL && p_window->DockNode != NULL) + { + ImGuiDockNode* p_node = DockNodeGetRootNode(p_window->DockNode); + if (p_node == node) + { + node->LastFocusedNodeId = p_window->DockNode->ID; // Note: not using root node ID! + break; + } + p_window = p_node->HostWindow ? p_node->HostWindow->RootWindow : NULL; + } + + // Register a hit-test hole in the window unless we are currently dragging a window that is compatible with our dockspace + ImGuiDockNode* central_node = node->CentralNode; + const bool central_node_hole = node->IsRootNode() && host_window && (node_flags & ImGuiDockNodeFlags_PassthruCentralNode) != 0 && central_node != NULL && central_node->IsEmpty(); + bool central_node_hole_register_hit_test_hole = central_node_hole; + if (central_node_hole) + if (const ImGuiPayload* payload = ImGui::GetDragDropPayload()) + if (payload->IsDataType(IMGUI_PAYLOAD_TYPE_WINDOW) && DockNodeIsDropAllowed(host_window, *(ImGuiWindow**)payload->Data)) + central_node_hole_register_hit_test_hole = false; + if (central_node_hole_register_hit_test_hole) + { + // We add a little padding to match the "resize from edges" behavior and allow grabbing the splitter easily. + // (But we only add it if there's something else on the other side of the hole, otherwise for e.g. fullscreen + // covering passthru node we'd have a gap on the edge not covered by the hole) + IM_ASSERT(node->IsDockSpace()); // We cannot pass this flag without the DockSpace() api. Testing this because we also setup the hole in host_window->ParentNode + ImGuiDockNode* root_node = DockNodeGetRootNode(central_node); + ImRect root_rect(root_node->Pos, root_node->Pos + root_node->Size); + ImRect hole_rect(central_node->Pos, central_node->Pos + central_node->Size); + if (hole_rect.Min.x > root_rect.Min.x) { hole_rect.Min.x += WINDOWS_HOVER_PADDING; } + if (hole_rect.Max.x < root_rect.Max.x) { hole_rect.Max.x -= WINDOWS_HOVER_PADDING; } + if (hole_rect.Min.y > root_rect.Min.y) { hole_rect.Min.y += WINDOWS_HOVER_PADDING; } + if (hole_rect.Max.y < root_rect.Max.y) { hole_rect.Max.y -= WINDOWS_HOVER_PADDING; } + //GetForegroundDrawList()->AddRect(hole_rect.Min, hole_rect.Max, IM_COL32(255, 0, 0, 255)); + if (central_node_hole && !hole_rect.IsInverted()) + { + SetWindowHitTestHole(host_window, hole_rect.Min, hole_rect.Max - hole_rect.Min); + if (host_window->ParentWindow) + SetWindowHitTestHole(host_window->ParentWindow, hole_rect.Min, hole_rect.Max - hole_rect.Min); + } + } + + // Update position/size, process and draw resizing splitters + if (node->IsRootNode() && host_window) + { + DockNodeTreeUpdatePosSize(node, host_window->Pos, host_window->Size); + PushStyleColor(ImGuiCol_Separator, g.Style.Colors[ImGuiCol_Border]); + PushStyleColor(ImGuiCol_SeparatorActive, g.Style.Colors[ImGuiCol_ResizeGripActive]); + PushStyleColor(ImGuiCol_SeparatorHovered, g.Style.Colors[ImGuiCol_ResizeGripHovered]); + DockNodeTreeUpdateSplitter(node); + PopStyleColor(3); + } + + // Draw empty node background (currently can only be the Central Node) + if (host_window && node->IsEmpty() && node->IsVisible) + { + host_window->DrawList->ChannelsSetCurrent(DOCKING_HOST_DRAW_CHANNEL_BG); + node->LastBgColor = (node_flags & ImGuiDockNodeFlags_PassthruCentralNode) ? 0 : GetColorU32(ImGuiCol_DockingEmptyBg); + if (node->LastBgColor != 0) + host_window->DrawList->AddRectFilled(node->Pos, node->Pos + node->Size, node->LastBgColor); + node->IsBgDrawnThisFrame = true; + } + + // Draw whole dockspace background if ImGuiDockNodeFlags_PassthruCentralNode if set. + // We need to draw a background at the root level if requested by ImGuiDockNodeFlags_PassthruCentralNode, but we will only know the correct pos/size + // _after_ processing the resizing splitters. So we are using the DrawList channel splitting facility to submit drawing primitives out of order! + const bool render_dockspace_bg = node->IsRootNode() && host_window && (node_flags & ImGuiDockNodeFlags_PassthruCentralNode) != 0; + if (render_dockspace_bg && node->IsVisible) + { + host_window->DrawList->ChannelsSetCurrent(DOCKING_HOST_DRAW_CHANNEL_BG); + if (central_node_hole) + RenderRectFilledWithHole(host_window->DrawList, node->Rect(), central_node->Rect(), GetColorU32(ImGuiCol_WindowBg), 0.0f); + else + host_window->DrawList->AddRectFilled(node->Pos, node->Pos + node->Size, GetColorU32(ImGuiCol_WindowBg), 0.0f); + } + + // Draw and populate Tab Bar + if (host_window) + host_window->DrawList->ChannelsSetCurrent(DOCKING_HOST_DRAW_CHANNEL_FG); + if (host_window && node->Windows.Size > 0) + { + DockNodeUpdateTabBar(node, host_window); + } + else + { + node->WantCloseAll = false; + node->WantCloseTabId = 0; + node->IsFocused = false; + } + if (node->TabBar && node->TabBar->SelectedTabId) + node->SelectedTabId = node->TabBar->SelectedTabId; + else if (node->Windows.Size > 0) + node->SelectedTabId = node->Windows[0]->TabId; + + // Draw payload drop target + if (host_window && node->IsVisible) + if (node->IsRootNode() && (g.MovingWindow == NULL || g.MovingWindow->RootWindowDockTree != host_window)) + BeginDockableDragDropTarget(host_window); + + // We update this after DockNodeUpdateTabBar() + node->LastFrameActive = g.FrameCount; + + // Recurse into children + // FIXME-DOCK FIXME-OPT: Should not need to recurse into children + if (host_window) + { + if (node->ChildNodes[0]) + DockNodeUpdate(node->ChildNodes[0]); + if (node->ChildNodes[1]) + DockNodeUpdate(node->ChildNodes[1]); + + // Render outer borders last (after the tab bar) + if (node->IsRootNode()) + RenderWindowOuterBorders(host_window); + } + + // End host window + if (beginned_into_host_window) //-V1020 + End(); +} + +// Compare TabItem nodes given the last known DockOrder (will persist in .ini file as hint), used to sort tabs when multiple tabs are added on the same frame. +static int IMGUI_CDECL TabItemComparerByDockOrder(const void* lhs, const void* rhs) +{ + ImGuiWindow* a = ((const ImGuiTabItem*)lhs)->Window; + ImGuiWindow* b = ((const ImGuiTabItem*)rhs)->Window; + if (int d = ((a->DockOrder == -1) ? INT_MAX : a->DockOrder) - ((b->DockOrder == -1) ? INT_MAX : b->DockOrder)) + return d; + return (a->BeginOrderWithinContext - b->BeginOrderWithinContext); +} + +// Default handler for g.DockNodeWindowMenuHandler(): display the list of windows for a given dock-node. +// This is exceptionally stored in a function pointer to also user applications to tweak this menu (undocumented) +// Custom overrides may want to decorate, group, sort entries. +// Please note those are internal structures: if you copy this expect occasional breakage. +// (if you don't need to modify the "Tabs.Size == 1" behavior/path it is recommend you call this function in your handler) +void ImGui::DockNodeWindowMenuHandler_Default(ImGuiContext* ctx, ImGuiDockNode* node, ImGuiTabBar* tab_bar) +{ + IM_UNUSED(ctx); + if (tab_bar->Tabs.Size == 1) + { + // "Hide tab bar" option. Being one of our rare user-facing string we pull it from a table. + if (MenuItem(LocalizeGetMsg(ImGuiLocKey_DockingHideTabBar), NULL, node->IsHiddenTabBar())) + node->WantHiddenTabBarToggle = true; + } + else + { + // Display a selectable list of windows in this docking node + for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++) + { + ImGuiTabItem* tab = &tab_bar->Tabs[tab_n]; + if (tab->Flags & ImGuiTabItemFlags_Button) + continue; + if (Selectable(TabBarGetTabName(tab_bar, tab), tab->ID == tab_bar->SelectedTabId)) + TabBarQueueFocus(tab_bar, tab); + SameLine(); + Text(" "); + } + } +} + +static void ImGui::DockNodeWindowMenuUpdate(ImGuiDockNode* node, ImGuiTabBar* tab_bar) +{ + // Try to position the menu so it is more likely to stays within the same viewport + ImGuiContext& g = *GImGui; + if (g.Style.WindowMenuButtonPosition == ImGuiDir_Left) + SetNextWindowPos(ImVec2(node->Pos.x, node->Pos.y + GetFrameHeight()), ImGuiCond_Always, ImVec2(0.0f, 0.0f)); + else + SetNextWindowPos(ImVec2(node->Pos.x + node->Size.x, node->Pos.y + GetFrameHeight()), ImGuiCond_Always, ImVec2(1.0f, 0.0f)); + if (BeginPopup("#WindowMenu")) + { + node->IsFocused = true; + g.DockNodeWindowMenuHandler(&g, node, tab_bar); + EndPopup(); + } +} + +// User helper to append/amend into a dock node tab bar. Most commonly used to add e.g. a "+" button. +bool ImGui::DockNodeBeginAmendTabBar(ImGuiDockNode* node) +{ + if (node->TabBar == NULL || node->HostWindow == NULL) + return false; + if (node->MergedFlags & ImGuiDockNodeFlags_KeepAliveOnly) + return false; + if (node->TabBar->ID == 0) + return false; + Begin(node->HostWindow->Name); + PushOverrideID(node->ID); + bool ret = BeginTabBarEx(node->TabBar, node->TabBar->BarRect, node->TabBar->Flags); + IM_UNUSED(ret); + IM_ASSERT(ret); + return true; +} + +void ImGui::DockNodeEndAmendTabBar() +{ + EndTabBar(); + PopID(); + End(); +} + +static bool IsDockNodeTitleBarHighlighted(ImGuiDockNode* node, ImGuiDockNode* root_node) +{ + // CTRL+Tab highlight (only highlighting leaf node, not whole hierarchy) + ImGuiContext& g = *GImGui; + if (g.NavWindowingTarget) + return (g.NavWindowingTarget->DockNode == node); + + // FIXME-DOCKING: May want alternative to treat central node void differently? e.g. if (g.NavWindow == host_window) + if (g.NavWindow && root_node->LastFocusedNodeId == node->ID) + { + // FIXME: This could all be backed in RootWindowForTitleBarHighlight? Probably need to reorganize for both dock nodes + other RootWindowForTitleBarHighlight users (not-node) + ImGuiWindow* parent_window = g.NavWindow->RootWindow; + while (parent_window->Flags & ImGuiWindowFlags_ChildMenu) + parent_window = parent_window->ParentWindow->RootWindow; + ImGuiDockNode* start_parent_node = parent_window->DockNodeAsHost ? parent_window->DockNodeAsHost : parent_window->DockNode; + for (ImGuiDockNode* parent_node = start_parent_node; parent_node != NULL; parent_node = parent_node->HostWindow ? parent_node->HostWindow->RootWindow->DockNode : NULL) + if ((parent_node = ImGui::DockNodeGetRootNode(parent_node)) == root_node) + return true; + } + return false; +} + +// Submit the tab bar corresponding to a dock node and various housekeeping details. +static void ImGui::DockNodeUpdateTabBar(ImGuiDockNode* node, ImGuiWindow* host_window) +{ + ImGuiContext& g = *GImGui; + ImGuiStyle& style = g.Style; + + const bool node_was_active = (node->LastFrameActive + 1 == g.FrameCount); + const bool closed_all = node->WantCloseAll && node_was_active; + const ImGuiID closed_one = node->WantCloseTabId && node_was_active; + node->WantCloseAll = false; + node->WantCloseTabId = 0; + + // Decide if we should use a focused title bar color + bool is_focused = false; + ImGuiDockNode* root_node = DockNodeGetRootNode(node); + if (IsDockNodeTitleBarHighlighted(node, root_node)) + is_focused = true; + + // Hidden tab bar will show a triangle on the upper-left (in Begin) + if (node->IsHiddenTabBar() || node->IsNoTabBar()) + { + node->VisibleWindow = (node->Windows.Size > 0) ? node->Windows[0] : NULL; + node->IsFocused = is_focused; + if (is_focused) + node->LastFrameFocused = g.FrameCount; + if (node->VisibleWindow) + { + // Notify root of visible window (used to display title in OS task bar) + if (is_focused || root_node->VisibleWindow == NULL) + root_node->VisibleWindow = node->VisibleWindow; + if (node->TabBar) + node->TabBar->VisibleTabId = node->VisibleWindow->TabId; + } + return; + } + + // Move ourselves to the Menu layer (so we can be accessed by tapping Alt) + undo SkipItems flag in order to draw over the title bar even if the window is collapsed + bool backup_skip_item = host_window->SkipItems; + if (!node->IsDockSpace()) + { + host_window->SkipItems = false; + host_window->DC.NavLayerCurrent = ImGuiNavLayer_Menu; + } + + // Use PushOverrideID() instead of PushID() to use the node id _without_ the host window ID. + // This is to facilitate computing those ID from the outside, and will affect more or less only the ID of the collapse button, popup and tabs, + // as docked windows themselves will override the stack with their own root ID. + PushOverrideID(node->ID); + ImGuiTabBar* tab_bar = node->TabBar; + bool tab_bar_is_recreated = (tab_bar == NULL); // Tab bar are automatically destroyed when a node gets hidden + if (tab_bar == NULL) + { + DockNodeAddTabBar(node); + tab_bar = node->TabBar; + } + + ImGuiID focus_tab_id = 0; + node->IsFocused = is_focused; + + const ImGuiDockNodeFlags node_flags = node->MergedFlags; + const bool has_window_menu_button = (node_flags & ImGuiDockNodeFlags_NoWindowMenuButton) == 0 && (style.WindowMenuButtonPosition != ImGuiDir_None); + + // In a dock node, the Collapse Button turns into the Window Menu button. + // FIXME-DOCK FIXME-OPT: Could we recycle popups id across multiple dock nodes? + if (has_window_menu_button && IsPopupOpen("#WindowMenu")) + { + ImGuiID next_selected_tab_id = tab_bar->NextSelectedTabId; + DockNodeWindowMenuUpdate(node, tab_bar); + if (tab_bar->NextSelectedTabId != 0 && tab_bar->NextSelectedTabId != next_selected_tab_id) + focus_tab_id = tab_bar->NextSelectedTabId; + is_focused |= node->IsFocused; + } + + // Layout + ImRect title_bar_rect, tab_bar_rect; + ImVec2 window_menu_button_pos; + ImVec2 close_button_pos; + DockNodeCalcTabBarLayout(node, &title_bar_rect, &tab_bar_rect, &window_menu_button_pos, &close_button_pos); + + // Submit new tabs, they will be added as Unsorted and sorted below based on relative DockOrder value. + const int tabs_count_old = tab_bar->Tabs.Size; + for (int window_n = 0; window_n < node->Windows.Size; window_n++) + { + ImGuiWindow* window = node->Windows[window_n]; + if (TabBarFindTabByID(tab_bar, window->TabId) == NULL) + TabBarAddTab(tab_bar, ImGuiTabItemFlags_Unsorted, window); + } + + // Title bar + if (is_focused) + node->LastFrameFocused = g.FrameCount; + ImU32 title_bar_col = GetColorU32(host_window->Collapsed ? ImGuiCol_TitleBgCollapsed : is_focused ? ImGuiCol_TitleBgActive : ImGuiCol_TitleBg); + ImDrawFlags rounding_flags = CalcRoundingFlagsForRectInRect(title_bar_rect, host_window->Rect(), g.Style.DockingSeparatorSize); + host_window->DrawList->AddRectFilled(title_bar_rect.Min, title_bar_rect.Max, title_bar_col, host_window->WindowRounding, rounding_flags); + + // Docking/Collapse button + if (has_window_menu_button) + { + if (CollapseButton(host_window->GetID("#COLLAPSE"), window_menu_button_pos, node)) // == DockNodeGetWindowMenuButtonId(node) + OpenPopup("#WindowMenu"); + if (IsItemActive()) + focus_tab_id = tab_bar->SelectedTabId; + if (IsItemHovered(ImGuiHoveredFlags_ForTooltip | ImGuiHoveredFlags_DelayNormal) && g.HoveredIdTimer > 0.5f) + SetTooltip("%s", LocalizeGetMsg(ImGuiLocKey_DockingDragToUndockOrMoveNode)); + } + + // If multiple tabs are appearing on the same frame, sort them based on their persistent DockOrder value + int tabs_unsorted_start = tab_bar->Tabs.Size; + for (int tab_n = tab_bar->Tabs.Size - 1; tab_n >= 0 && (tab_bar->Tabs[tab_n].Flags & ImGuiTabItemFlags_Unsorted); tab_n--) + { + // FIXME-DOCK: Consider only clearing the flag after the tab has been alive for a few consecutive frames, allowing late comers to not break sorting? + tab_bar->Tabs[tab_n].Flags &= ~ImGuiTabItemFlags_Unsorted; + tabs_unsorted_start = tab_n; + } + if (tab_bar->Tabs.Size > tabs_unsorted_start) + { + IMGUI_DEBUG_LOG_DOCKING("[docking] In node 0x%08X: %d new appearing tabs:%s\n", node->ID, tab_bar->Tabs.Size - tabs_unsorted_start, (tab_bar->Tabs.Size > tabs_unsorted_start + 1) ? " (will sort)" : ""); + for (int tab_n = tabs_unsorted_start; tab_n < tab_bar->Tabs.Size; tab_n++) + { + ImGuiTabItem* tab = &tab_bar->Tabs[tab_n]; + IMGUI_DEBUG_LOG_DOCKING("[docking] - Tab 0x%08X '%s' Order %d\n", tab->ID, TabBarGetTabName(tab_bar, tab), tab->Window ? tab->Window->DockOrder : -1); + } + IMGUI_DEBUG_LOG_DOCKING("[docking] SelectedTabId = 0x%08X, NavWindow->TabId = 0x%08X\n", node->SelectedTabId, g.NavWindow ? g.NavWindow->TabId : -1); + if (tab_bar->Tabs.Size > tabs_unsorted_start + 1) + ImQsort(tab_bar->Tabs.Data + tabs_unsorted_start, tab_bar->Tabs.Size - tabs_unsorted_start, sizeof(ImGuiTabItem), TabItemComparerByDockOrder); + } + + // Apply NavWindow focus back to the tab bar + if (g.NavWindow && g.NavWindow->RootWindow->DockNode == node) + tab_bar->SelectedTabId = g.NavWindow->RootWindow->TabId; + + // Selected newly added tabs, or persistent tab ID if the tab bar was just recreated + if (tab_bar_is_recreated && TabBarFindTabByID(tab_bar, node->SelectedTabId) != NULL) + tab_bar->SelectedTabId = tab_bar->NextSelectedTabId = node->SelectedTabId; + else if (tab_bar->Tabs.Size > tabs_count_old) + tab_bar->SelectedTabId = tab_bar->NextSelectedTabId = tab_bar->Tabs.back().Window->TabId; + + // Begin tab bar + ImGuiTabBarFlags tab_bar_flags = ImGuiTabBarFlags_Reorderable | ImGuiTabBarFlags_AutoSelectNewTabs; // | ImGuiTabBarFlags_NoTabListScrollingButtons); + tab_bar_flags |= ImGuiTabBarFlags_SaveSettings | ImGuiTabBarFlags_DockNode;// | ImGuiTabBarFlags_FittingPolicyScroll; + if (!host_window->Collapsed && is_focused) + tab_bar_flags |= ImGuiTabBarFlags_IsFocused; + tab_bar->ID = GetID("#TabBar"); + tab_bar->SeparatorMinX = node->Pos.x + host_window->WindowBorderSize; // Separator cover the whole node width + tab_bar->SeparatorMaxX = node->Pos.x + node->Size.x - host_window->WindowBorderSize; + BeginTabBarEx(tab_bar, tab_bar_rect, tab_bar_flags); + //host_window->DrawList->AddRect(tab_bar_rect.Min, tab_bar_rect.Max, IM_COL32(255,0,255,255)); + + // Backup style colors + ImVec4 backup_style_cols[ImGuiWindowDockStyleCol_COUNT]; + for (int color_n = 0; color_n < ImGuiWindowDockStyleCol_COUNT; color_n++) + backup_style_cols[color_n] = g.Style.Colors[GWindowDockStyleColors[color_n]]; + + // Submit actual tabs + node->VisibleWindow = NULL; + for (int window_n = 0; window_n < node->Windows.Size; window_n++) + { + ImGuiWindow* window = node->Windows[window_n]; + if ((closed_all || closed_one == window->TabId) && window->HasCloseButton && !(window->Flags & ImGuiWindowFlags_UnsavedDocument)) + continue; + if (window->LastFrameActive + 1 >= g.FrameCount || !node_was_active) + { + ImGuiTabItemFlags tab_item_flags = 0; + tab_item_flags |= window->WindowClass.TabItemFlagsOverrideSet; + if (window->Flags & ImGuiWindowFlags_UnsavedDocument) + tab_item_flags |= ImGuiTabItemFlags_UnsavedDocument; + if (tab_bar->Flags & ImGuiTabBarFlags_NoCloseWithMiddleMouseButton) + tab_item_flags |= ImGuiTabItemFlags_NoCloseWithMiddleMouseButton; + + // Apply stored style overrides for the window + for (int color_n = 0; color_n < ImGuiWindowDockStyleCol_COUNT; color_n++) + g.Style.Colors[GWindowDockStyleColors[color_n]] = ColorConvertU32ToFloat4(window->DockStyle.Colors[color_n]); + + // Note that TabItemEx() calls TabBarCalcTabID() so our tab item ID will ignore the current ID stack (rightly so) + bool tab_open = true; + TabItemEx(tab_bar, window->Name, window->HasCloseButton ? &tab_open : NULL, tab_item_flags, window); + if (!tab_open) + node->WantCloseTabId = window->TabId; + if (tab_bar->VisibleTabId == window->TabId) + node->VisibleWindow = window; + + // Store last item data so it can be queried with IsItemXXX functions after the user Begin() call + window->DockTabItemStatusFlags = g.LastItemData.StatusFlags; + window->DockTabItemRect = g.LastItemData.Rect; + + // Update navigation ID on menu layer + if (g.NavWindow && g.NavWindow->RootWindow == window && (window->DC.NavLayersActiveMask & (1 << ImGuiNavLayer_Menu)) == 0) + host_window->NavLastIds[1] = window->TabId; + } + } + + // Restore style colors + for (int color_n = 0; color_n < ImGuiWindowDockStyleCol_COUNT; color_n++) + g.Style.Colors[GWindowDockStyleColors[color_n]] = backup_style_cols[color_n]; + + // Notify root of visible window (used to display title in OS task bar) + if (node->VisibleWindow) + if (is_focused || root_node->VisibleWindow == NULL) + root_node->VisibleWindow = node->VisibleWindow; + + // Close button (after VisibleWindow was updated) + // Note that VisibleWindow may have been overrided by CTRL+Tabbing, so VisibleWindow->TabId may be != from tab_bar->SelectedTabId + const bool close_button_is_enabled = node->HasCloseButton && node->VisibleWindow && node->VisibleWindow->HasCloseButton; + const bool close_button_is_visible = node->HasCloseButton; + //const bool close_button_is_visible = close_button_is_enabled; // Most people would expect this behavior of not even showing the button (leaving a hole since we can't claim that space as other windows in the tba bar have one) + if (close_button_is_visible) + { + if (!close_button_is_enabled) + { + PushItemFlag(ImGuiItemFlags_Disabled, true); + PushStyleColor(ImGuiCol_Text, style.Colors[ImGuiCol_Text] * ImVec4(1.0f,1.0f,1.0f,0.4f)); + } + if (CloseButton(host_window->GetID("#CLOSE"), close_button_pos)) + { + node->WantCloseAll = true; + for (int n = 0; n < tab_bar->Tabs.Size; n++) + TabBarCloseTab(tab_bar, &tab_bar->Tabs[n]); + } + //if (IsItemActive()) + // focus_tab_id = tab_bar->SelectedTabId; + if (!close_button_is_enabled) + { + PopStyleColor(); + PopItemFlag(); + } + } + + // When clicking on the title bar outside of tabs, we still focus the selected tab for that node + // FIXME: TabItems submitted earlier use AllowItemOverlap so we manually perform a more specific test for now (hovered || held) in order to not cover them. + ImGuiID title_bar_id = host_window->GetID("#TITLEBAR"); + if (g.HoveredId == 0 || g.HoveredId == title_bar_id || g.ActiveId == title_bar_id) + { + // AllowOverlap mode required for appending into dock node tab bar, + // otherwise dragging window will steal HoveredId and amended tabs cannot get them. + bool held; + KeepAliveID(title_bar_id); + ButtonBehavior(title_bar_rect, title_bar_id, NULL, &held, ImGuiButtonFlags_AllowOverlap); + if (g.HoveredId == title_bar_id) + { + g.LastItemData.ID = title_bar_id; + } + if (held) + { + if (IsMouseClicked(0)) + focus_tab_id = tab_bar->SelectedTabId; + + // Forward moving request to selected window + if (ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_bar->SelectedTabId)) + StartMouseMovingWindowOrNode(tab->Window ? tab->Window : node->HostWindow, node, false); // Undock from tab bar empty space + } + } + + // Forward focus from host node to selected window + //if (is_focused && g.NavWindow == host_window && !g.NavWindowingTarget) + // focus_tab_id = tab_bar->SelectedTabId; + + // When clicked on a tab we requested focus to the docked child + // This overrides the value set by "forward focus from host node to selected window". + if (tab_bar->NextSelectedTabId) + focus_tab_id = tab_bar->NextSelectedTabId; + + // Apply navigation focus + if (focus_tab_id != 0) + if (ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, focus_tab_id)) + if (tab->Window) + { + FocusWindow(tab->Window); + NavInitWindow(tab->Window, false); + } + + EndTabBar(); + PopID(); + + // Restore SkipItems flag + if (!node->IsDockSpace()) + { + host_window->DC.NavLayerCurrent = ImGuiNavLayer_Main; + host_window->SkipItems = backup_skip_item; + } +} + +static void ImGui::DockNodeAddTabBar(ImGuiDockNode* node) +{ + IM_ASSERT(node->TabBar == NULL); + node->TabBar = IM_NEW(ImGuiTabBar); +} + +static void ImGui::DockNodeRemoveTabBar(ImGuiDockNode* node) +{ + if (node->TabBar == NULL) + return; + IM_DELETE(node->TabBar); + node->TabBar = NULL; +} + +static bool DockNodeIsDropAllowedOne(ImGuiWindow* payload, ImGuiWindow* host_window) +{ + if (host_window->DockNodeAsHost && host_window->DockNodeAsHost->IsDockSpace() && payload->BeginOrderWithinContext < host_window->BeginOrderWithinContext) + return false; + + ImGuiWindowClass* host_class = host_window->DockNodeAsHost ? &host_window->DockNodeAsHost->WindowClass : &host_window->WindowClass; + ImGuiWindowClass* payload_class = &payload->WindowClass; + if (host_class->ClassId != payload_class->ClassId) + { + bool pass = false; + if (host_class->ClassId != 0 && host_class->DockingAllowUnclassed && payload_class->ClassId == 0) + pass = true; + if (payload_class->ClassId != 0 && payload_class->DockingAllowUnclassed && host_class->ClassId == 0) + pass = true; + if (!pass) + return false; + } + + // Prevent docking any window created above a popup + // Technically we should support it (e.g. in the case of a long-lived modal window that had fancy docking features), + // by e.g. adding a 'if (!ImGui::IsWindowWithinBeginStackOf(host_window, popup_window))' test. + // But it would requires more work on our end because the dock host windows is technically created in NewFrame() + // and our ->ParentXXX and ->RootXXX pointers inside windows are currently mislading or lacking. + ImGuiContext& g = *GImGui; + for (int i = g.OpenPopupStack.Size - 1; i >= 0; i--) + if (ImGuiWindow* popup_window = g.OpenPopupStack[i].Window) + if (ImGui::IsWindowWithinBeginStackOf(payload, popup_window)) // Payload is created from within a popup begin stack. + return false; + + return true; +} + +static bool ImGui::DockNodeIsDropAllowed(ImGuiWindow* host_window, ImGuiWindow* root_payload) +{ + if (root_payload->DockNodeAsHost && root_payload->DockNodeAsHost->IsSplitNode()) // FIXME-DOCK: Missing filtering + return true; + + const int payload_count = root_payload->DockNodeAsHost ? root_payload->DockNodeAsHost->Windows.Size : 1; + for (int payload_n = 0; payload_n < payload_count; payload_n++) + { + ImGuiWindow* payload = root_payload->DockNodeAsHost ? root_payload->DockNodeAsHost->Windows[payload_n] : root_payload; + if (DockNodeIsDropAllowedOne(payload, host_window)) + return true; + } + return false; +} + +// window menu button == collapse button when not in a dock node. +// FIXME: This is similar to RenderWindowTitleBarContents(), may want to share code. +static void ImGui::DockNodeCalcTabBarLayout(const ImGuiDockNode* node, ImRect* out_title_rect, ImRect* out_tab_bar_rect, ImVec2* out_window_menu_button_pos, ImVec2* out_close_button_pos) +{ + ImGuiContext& g = *GImGui; + ImGuiStyle& style = g.Style; + + ImRect r = ImRect(node->Pos.x, node->Pos.y, node->Pos.x + node->Size.x, node->Pos.y + g.FontSize + g.Style.FramePadding.y * 2.0f); + if (out_title_rect) { *out_title_rect = r; } + + r.Min.x += style.WindowBorderSize; + r.Max.x -= style.WindowBorderSize; + + float button_sz = g.FontSize; + r.Min.x += style.FramePadding.x; + r.Max.x -= style.FramePadding.x; + ImVec2 window_menu_button_pos = ImVec2(r.Min.x, r.Min.y + style.FramePadding.y); + if (node->HasCloseButton) + { + if (out_close_button_pos) *out_close_button_pos = ImVec2(r.Max.x - button_sz, r.Min.y + style.FramePadding.y); + r.Max.x -= button_sz + style.ItemInnerSpacing.x; + } + if (node->HasWindowMenuButton && style.WindowMenuButtonPosition == ImGuiDir_Left) + { + r.Min.x += button_sz + style.ItemInnerSpacing.x; + } + else if (node->HasWindowMenuButton && style.WindowMenuButtonPosition == ImGuiDir_Right) + { + window_menu_button_pos = ImVec2(r.Max.x - button_sz, r.Min.y + style.FramePadding.y); + r.Max.x -= button_sz + style.ItemInnerSpacing.x; + } + if (out_tab_bar_rect) { *out_tab_bar_rect = r; } + if (out_window_menu_button_pos) { *out_window_menu_button_pos = window_menu_button_pos; } +} + +void ImGui::DockNodeCalcSplitRects(ImVec2& pos_old, ImVec2& size_old, ImVec2& pos_new, ImVec2& size_new, ImGuiDir dir, ImVec2 size_new_desired) +{ + ImGuiContext& g = *GImGui; + const float dock_spacing = g.Style.ItemInnerSpacing.x; + const ImGuiAxis axis = (dir == ImGuiDir_Left || dir == ImGuiDir_Right) ? ImGuiAxis_X : ImGuiAxis_Y; + pos_new[axis ^ 1] = pos_old[axis ^ 1]; + size_new[axis ^ 1] = size_old[axis ^ 1]; + + // Distribute size on given axis (with a desired size or equally) + const float w_avail = size_old[axis] - dock_spacing; + if (size_new_desired[axis] > 0.0f && size_new_desired[axis] <= w_avail * 0.5f) + { + size_new[axis] = size_new_desired[axis]; + size_old[axis] = IM_TRUNC(w_avail - size_new[axis]); + } + else + { + size_new[axis] = IM_TRUNC(w_avail * 0.5f); + size_old[axis] = IM_TRUNC(w_avail - size_new[axis]); + } + + // Position each node + if (dir == ImGuiDir_Right || dir == ImGuiDir_Down) + { + pos_new[axis] = pos_old[axis] + size_old[axis] + dock_spacing; + } + else if (dir == ImGuiDir_Left || dir == ImGuiDir_Up) + { + pos_new[axis] = pos_old[axis]; + pos_old[axis] = pos_new[axis] + size_new[axis] + dock_spacing; + } +} + +// Retrieve the drop rectangles for a given direction or for the center + perform hit testing. +bool ImGui::DockNodeCalcDropRectsAndTestMousePos(const ImRect& parent, ImGuiDir dir, ImRect& out_r, bool outer_docking, ImVec2* test_mouse_pos) +{ + ImGuiContext& g = *GImGui; + + const float parent_smaller_axis = ImMin(parent.GetWidth(), parent.GetHeight()); + const float hs_for_central_nodes = ImMin(g.FontSize * 1.5f, ImMax(g.FontSize * 0.5f, parent_smaller_axis / 8.0f)); + float hs_w; // Half-size, longer axis + float hs_h; // Half-size, smaller axis + ImVec2 off; // Distance from edge or center + if (outer_docking) + { + //hs_w = ImTrunc(ImClamp(parent_smaller_axis - hs_for_central_nodes * 4.0f, g.FontSize * 0.5f, g.FontSize * 8.0f)); + //hs_h = ImTrunc(hs_w * 0.15f); + //off = ImVec2(ImTrunc(parent.GetWidth() * 0.5f - GetFrameHeightWithSpacing() * 1.4f - hs_h), ImTrunc(parent.GetHeight() * 0.5f - GetFrameHeightWithSpacing() * 1.4f - hs_h)); + hs_w = ImTrunc(hs_for_central_nodes * 1.50f); + hs_h = ImTrunc(hs_for_central_nodes * 0.80f); + off = ImTrunc(ImVec2(parent.GetWidth() * 0.5f - hs_h, parent.GetHeight() * 0.5f - hs_h)); + } + else + { + hs_w = ImTrunc(hs_for_central_nodes); + hs_h = ImTrunc(hs_for_central_nodes * 0.90f); + off = ImTrunc(ImVec2(hs_w * 2.40f, hs_w * 2.40f)); + } + + ImVec2 c = ImTrunc(parent.GetCenter()); + if (dir == ImGuiDir_None) { out_r = ImRect(c.x - hs_w, c.y - hs_w, c.x + hs_w, c.y + hs_w); } + else if (dir == ImGuiDir_Up) { out_r = ImRect(c.x - hs_w, c.y - off.y - hs_h, c.x + hs_w, c.y - off.y + hs_h); } + else if (dir == ImGuiDir_Down) { out_r = ImRect(c.x - hs_w, c.y + off.y - hs_h, c.x + hs_w, c.y + off.y + hs_h); } + else if (dir == ImGuiDir_Left) { out_r = ImRect(c.x - off.x - hs_h, c.y - hs_w, c.x - off.x + hs_h, c.y + hs_w); } + else if (dir == ImGuiDir_Right) { out_r = ImRect(c.x + off.x - hs_h, c.y - hs_w, c.x + off.x + hs_h, c.y + hs_w); } + + if (test_mouse_pos == NULL) + return false; + + ImRect hit_r = out_r; + if (!outer_docking) + { + // Custom hit testing for the 5-way selection, designed to reduce flickering when moving diagonally between sides + hit_r.Expand(ImTrunc(hs_w * 0.30f)); + ImVec2 mouse_delta = (*test_mouse_pos - c); + float mouse_delta_len2 = ImLengthSqr(mouse_delta); + float r_threshold_center = hs_w * 1.4f; + float r_threshold_sides = hs_w * (1.4f + 1.2f); + if (mouse_delta_len2 < r_threshold_center * r_threshold_center) + return (dir == ImGuiDir_None); + if (mouse_delta_len2 < r_threshold_sides * r_threshold_sides) + return (dir == ImGetDirQuadrantFromDelta(mouse_delta.x, mouse_delta.y)); + } + return hit_r.Contains(*test_mouse_pos); +} + +// host_node may be NULL if the window doesn't have a DockNode already. +// FIXME-DOCK: This is misnamed since it's also doing the filtering. +static void ImGui::DockNodePreviewDockSetup(ImGuiWindow* host_window, ImGuiDockNode* host_node, ImGuiWindow* payload_window, ImGuiDockNode* payload_node, ImGuiDockPreviewData* data, bool is_explicit_target, bool is_outer_docking) +{ + ImGuiContext& g = *GImGui; + + // There is an edge case when docking into a dockspace which only has inactive nodes. + // In this case DockNodeTreeFindNodeByPos() will have selected a leaf node which is inactive. + // Because the inactive leaf node doesn't have proper pos/size yet, we'll use the root node as reference. + if (payload_node == NULL) + payload_node = payload_window->DockNodeAsHost; + ImGuiDockNode* ref_node_for_rect = (host_node && !host_node->IsVisible) ? DockNodeGetRootNode(host_node) : host_node; + if (ref_node_for_rect) + IM_ASSERT(ref_node_for_rect->IsVisible == true); + + // Filter, figure out where we are allowed to dock + ImGuiDockNodeFlags src_node_flags = payload_node ? payload_node->MergedFlags : payload_window->WindowClass.DockNodeFlagsOverrideSet; + ImGuiDockNodeFlags dst_node_flags = host_node ? host_node->MergedFlags : host_window->WindowClass.DockNodeFlagsOverrideSet; + data->IsCenterAvailable = true; + if (is_outer_docking) + data->IsCenterAvailable = false; + else if (dst_node_flags & ImGuiDockNodeFlags_NoDockingOverMe) + data->IsCenterAvailable = false; + else if (host_node && (dst_node_flags & ImGuiDockNodeFlags_NoDockingOverCentralNode) && host_node->IsCentralNode()) + data->IsCenterAvailable = false; + else if ((!host_node || !host_node->IsEmpty()) && payload_node && payload_node->IsSplitNode() && (payload_node->OnlyNodeWithWindows == NULL)) // Is _visibly_ split? + data->IsCenterAvailable = false; + else if ((src_node_flags & ImGuiDockNodeFlags_NoDockingOverOther) && (!host_node || !host_node->IsEmpty())) + data->IsCenterAvailable = false; + else if ((src_node_flags & ImGuiDockNodeFlags_NoDockingOverEmpty) && host_node && host_node->IsEmpty()) + data->IsCenterAvailable = false; + + data->IsSidesAvailable = true; + if ((dst_node_flags & ImGuiDockNodeFlags_NoDockingSplit) || g.IO.ConfigDockingNoSplit) + data->IsSidesAvailable = false; + else if (!is_outer_docking && host_node && host_node->ParentNode == NULL && host_node->IsCentralNode()) + data->IsSidesAvailable = false; + else if (src_node_flags & ImGuiDockNodeFlags_NoDockingSplitOther) + data->IsSidesAvailable = false; + + // Build a tentative future node (reuse same structure because it is practical. Shape will be readjusted when previewing a split) + data->FutureNode.HasCloseButton = (host_node ? host_node->HasCloseButton : host_window->HasCloseButton) || (payload_window->HasCloseButton); + data->FutureNode.HasWindowMenuButton = host_node ? true : ((host_window->Flags & ImGuiWindowFlags_NoCollapse) == 0); + data->FutureNode.Pos = ref_node_for_rect ? ref_node_for_rect->Pos : host_window->Pos; + data->FutureNode.Size = ref_node_for_rect ? ref_node_for_rect->Size : host_window->Size; + + // Calculate drop shapes geometry for allowed splitting directions + IM_ASSERT(ImGuiDir_None == -1); + data->SplitNode = host_node; + data->SplitDir = ImGuiDir_None; + data->IsSplitDirExplicit = false; + if (!host_window->Collapsed) + for (int dir = ImGuiDir_None; dir < ImGuiDir_COUNT; dir++) + { + if (dir == ImGuiDir_None && !data->IsCenterAvailable) + continue; + if (dir != ImGuiDir_None && !data->IsSidesAvailable) + continue; + if (DockNodeCalcDropRectsAndTestMousePos(data->FutureNode.Rect(), (ImGuiDir)dir, data->DropRectsDraw[dir+1], is_outer_docking, &g.IO.MousePos)) + { + data->SplitDir = (ImGuiDir)dir; + data->IsSplitDirExplicit = true; + } + } + + // When docking without holding Shift, we only allow and preview docking when hovering over a drop rect or over the title bar + data->IsDropAllowed = (data->SplitDir != ImGuiDir_None) || (data->IsCenterAvailable); + if (!is_explicit_target && !data->IsSplitDirExplicit && !g.IO.ConfigDockingWithShift) + data->IsDropAllowed = false; + + // Calculate split area + data->SplitRatio = 0.0f; + if (data->SplitDir != ImGuiDir_None) + { + ImGuiDir split_dir = data->SplitDir; + ImGuiAxis split_axis = (split_dir == ImGuiDir_Left || split_dir == ImGuiDir_Right) ? ImGuiAxis_X : ImGuiAxis_Y; + ImVec2 pos_new, pos_old = data->FutureNode.Pos; + ImVec2 size_new, size_old = data->FutureNode.Size; + DockNodeCalcSplitRects(pos_old, size_old, pos_new, size_new, split_dir, payload_window->Size); + + // Calculate split ratio so we can pass it down the docking request + float split_ratio = ImSaturate(size_new[split_axis] / data->FutureNode.Size[split_axis]); + data->FutureNode.Pos = pos_new; + data->FutureNode.Size = size_new; + data->SplitRatio = (split_dir == ImGuiDir_Right || split_dir == ImGuiDir_Down) ? (1.0f - split_ratio) : (split_ratio); + } +} + +static void ImGui::DockNodePreviewDockRender(ImGuiWindow* host_window, ImGuiDockNode* host_node, ImGuiWindow* root_payload, const ImGuiDockPreviewData* data) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.CurrentWindow == host_window); // Because we rely on font size to calculate tab sizes + + // With this option, we only display the preview on the target viewport, and the payload viewport is made transparent. + // To compensate for the single layer obstructed by the payload, we'll increase the alpha of the preview nodes. + const bool is_transparent_payload = g.IO.ConfigDockingTransparentPayload; + + // In case the two windows involved are on different viewports, we will draw the overlay on each of them. + int overlay_draw_lists_count = 0; + ImDrawList* overlay_draw_lists[2]; + overlay_draw_lists[overlay_draw_lists_count++] = GetForegroundDrawList(host_window->Viewport); + if (host_window->Viewport != root_payload->Viewport && !is_transparent_payload) + overlay_draw_lists[overlay_draw_lists_count++] = GetForegroundDrawList(root_payload->Viewport); + + // Draw main preview rectangle + const ImU32 overlay_col_main = GetColorU32(ImGuiCol_DockingPreview, is_transparent_payload ? 0.60f : 0.40f); + const ImU32 overlay_col_drop = GetColorU32(ImGuiCol_DockingPreview, is_transparent_payload ? 0.90f : 0.70f); + const ImU32 overlay_col_drop_hovered = GetColorU32(ImGuiCol_DockingPreview, is_transparent_payload ? 1.20f : 1.00f); + const ImU32 overlay_col_lines = GetColorU32(ImGuiCol_NavWindowingHighlight, is_transparent_payload ? 0.80f : 0.60f); + + // Display area preview + const bool can_preview_tabs = (root_payload->DockNodeAsHost == NULL || root_payload->DockNodeAsHost->Windows.Size > 0); + if (data->IsDropAllowed) + { + ImRect overlay_rect = data->FutureNode.Rect(); + if (data->SplitDir == ImGuiDir_None && can_preview_tabs) + overlay_rect.Min.y += GetFrameHeight(); + if (data->SplitDir != ImGuiDir_None || data->IsCenterAvailable) + for (int overlay_n = 0; overlay_n < overlay_draw_lists_count; overlay_n++) + overlay_draw_lists[overlay_n]->AddRectFilled(overlay_rect.Min, overlay_rect.Max, overlay_col_main, host_window->WindowRounding, CalcRoundingFlagsForRectInRect(overlay_rect, host_window->Rect(), g.Style.DockingSeparatorSize)); + } + + // Display tab shape/label preview unless we are splitting node (it generally makes the situation harder to read) + if (data->IsDropAllowed && can_preview_tabs && data->SplitDir == ImGuiDir_None && data->IsCenterAvailable) + { + // Compute target tab bar geometry so we can locate our preview tabs + ImRect tab_bar_rect; + DockNodeCalcTabBarLayout(&data->FutureNode, NULL, &tab_bar_rect, NULL, NULL); + ImVec2 tab_pos = tab_bar_rect.Min; + if (host_node && host_node->TabBar) + { + if (!host_node->IsHiddenTabBar() && !host_node->IsNoTabBar()) + tab_pos.x += host_node->TabBar->WidthAllTabs + g.Style.ItemInnerSpacing.x; // We don't use OffsetNewTab because when using non-persistent-order tab bar it is incremented with each Tab submission. + else + tab_pos.x += g.Style.ItemInnerSpacing.x + TabItemCalcSize(host_node->Windows[0]).x; + } + else if (!(host_window->Flags & ImGuiWindowFlags_DockNodeHost)) + { + tab_pos.x += g.Style.ItemInnerSpacing.x + TabItemCalcSize(host_window).x; // Account for slight offset which will be added when changing from title bar to tab bar + } + + // Draw tab shape/label preview (payload may be a loose window or a host window carrying multiple tabbed windows) + if (root_payload->DockNodeAsHost) + IM_ASSERT(root_payload->DockNodeAsHost->Windows.Size <= root_payload->DockNodeAsHost->TabBar->Tabs.Size); + ImGuiTabBar* tab_bar_with_payload = root_payload->DockNodeAsHost ? root_payload->DockNodeAsHost->TabBar : NULL; + const int payload_count = tab_bar_with_payload ? tab_bar_with_payload->Tabs.Size : 1; + for (int payload_n = 0; payload_n < payload_count; payload_n++) + { + // DockNode's TabBar may have non-window Tabs manually appended by user + ImGuiWindow* payload_window = tab_bar_with_payload ? tab_bar_with_payload->Tabs[payload_n].Window : root_payload; + if (tab_bar_with_payload && payload_window == NULL) + continue; + if (!DockNodeIsDropAllowedOne(payload_window, host_window)) + continue; + + // Calculate the tab bounding box for each payload window + ImVec2 tab_size = TabItemCalcSize(payload_window); + ImRect tab_bb(tab_pos.x, tab_pos.y, tab_pos.x + tab_size.x, tab_pos.y + tab_size.y); + tab_pos.x += tab_size.x + g.Style.ItemInnerSpacing.x; + const ImU32 overlay_col_text = GetColorU32(payload_window->DockStyle.Colors[ImGuiWindowDockStyleCol_Text]); + const ImU32 overlay_col_tabs = GetColorU32(payload_window->DockStyle.Colors[ImGuiWindowDockStyleCol_TabActive]); + PushStyleColor(ImGuiCol_Text, overlay_col_text); + for (int overlay_n = 0; overlay_n < overlay_draw_lists_count; overlay_n++) + { + ImGuiTabItemFlags tab_flags = (payload_window->Flags & ImGuiWindowFlags_UnsavedDocument) ? ImGuiTabItemFlags_UnsavedDocument : 0; + if (!tab_bar_rect.Contains(tab_bb)) + overlay_draw_lists[overlay_n]->PushClipRect(tab_bar_rect.Min, tab_bar_rect.Max); + TabItemBackground(overlay_draw_lists[overlay_n], tab_bb, tab_flags, overlay_col_tabs); + TabItemLabelAndCloseButton(overlay_draw_lists[overlay_n], tab_bb, tab_flags, g.Style.FramePadding, payload_window->Name, 0, 0, false, NULL, NULL); + if (!tab_bar_rect.Contains(tab_bb)) + overlay_draw_lists[overlay_n]->PopClipRect(); + } + PopStyleColor(); + } + } + + // Display drop boxes + const float overlay_rounding = ImMax(3.0f, g.Style.FrameRounding); + for (int dir = ImGuiDir_None; dir < ImGuiDir_COUNT; dir++) + { + if (!data->DropRectsDraw[dir + 1].IsInverted()) + { + ImRect draw_r = data->DropRectsDraw[dir + 1]; + ImRect draw_r_in = draw_r; + draw_r_in.Expand(-2.0f); + ImU32 overlay_col = (data->SplitDir == (ImGuiDir)dir && data->IsSplitDirExplicit) ? overlay_col_drop_hovered : overlay_col_drop; + for (int overlay_n = 0; overlay_n < overlay_draw_lists_count; overlay_n++) + { + ImVec2 center = ImFloor(draw_r_in.GetCenter()); + overlay_draw_lists[overlay_n]->AddRectFilled(draw_r.Min, draw_r.Max, overlay_col, overlay_rounding); + overlay_draw_lists[overlay_n]->AddRect(draw_r_in.Min, draw_r_in.Max, overlay_col_lines, overlay_rounding); + if (dir == ImGuiDir_Left || dir == ImGuiDir_Right) + overlay_draw_lists[overlay_n]->AddLine(ImVec2(center.x, draw_r_in.Min.y), ImVec2(center.x, draw_r_in.Max.y), overlay_col_lines); + if (dir == ImGuiDir_Up || dir == ImGuiDir_Down) + overlay_draw_lists[overlay_n]->AddLine(ImVec2(draw_r_in.Min.x, center.y), ImVec2(draw_r_in.Max.x, center.y), overlay_col_lines); + } + } + + // Stop after ImGuiDir_None + if ((host_node && (host_node->MergedFlags & ImGuiDockNodeFlags_NoDockingSplit)) || g.IO.ConfigDockingNoSplit) + return; + } +} + +//----------------------------------------------------------------------------- +// Docking: ImGuiDockNode Tree manipulation functions +//----------------------------------------------------------------------------- +// - DockNodeTreeSplit() +// - DockNodeTreeMerge() +// - DockNodeTreeUpdatePosSize() +// - DockNodeTreeUpdateSplitterFindTouchingNode() +// - DockNodeTreeUpdateSplitter() +// - DockNodeTreeFindFallbackLeafNode() +// - DockNodeTreeFindNodeByPos() +//----------------------------------------------------------------------------- + +void ImGui::DockNodeTreeSplit(ImGuiContext* ctx, ImGuiDockNode* parent_node, ImGuiAxis split_axis, int split_inheritor_child_idx, float split_ratio, ImGuiDockNode* new_node) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(split_axis != ImGuiAxis_None); + + ImGuiDockNode* child_0 = (new_node && split_inheritor_child_idx != 0) ? new_node : DockContextAddNode(ctx, 0); + child_0->ParentNode = parent_node; + + ImGuiDockNode* child_1 = (new_node && split_inheritor_child_idx != 1) ? new_node : DockContextAddNode(ctx, 0); + child_1->ParentNode = parent_node; + + ImGuiDockNode* child_inheritor = (split_inheritor_child_idx == 0) ? child_0 : child_1; + DockNodeMoveChildNodes(child_inheritor, parent_node); + parent_node->ChildNodes[0] = child_0; + parent_node->ChildNodes[1] = child_1; + parent_node->ChildNodes[split_inheritor_child_idx]->VisibleWindow = parent_node->VisibleWindow; + parent_node->SplitAxis = split_axis; + parent_node->VisibleWindow = NULL; + parent_node->AuthorityForPos = parent_node->AuthorityForSize = ImGuiDataAuthority_DockNode; + + float size_avail = (parent_node->Size[split_axis] - g.Style.DockingSeparatorSize); + size_avail = ImMax(size_avail, g.Style.WindowMinSize[split_axis] * 2.0f); + IM_ASSERT(size_avail > 0.0f); // If you created a node manually with DockBuilderAddNode(), you need to also call DockBuilderSetNodeSize() before splitting. + child_0->SizeRef = child_1->SizeRef = parent_node->Size; + child_0->SizeRef[split_axis] = ImTrunc(size_avail * split_ratio); + child_1->SizeRef[split_axis] = ImTrunc(size_avail - child_0->SizeRef[split_axis]); + + DockNodeMoveWindows(parent_node->ChildNodes[split_inheritor_child_idx], parent_node); + DockSettingsRenameNodeReferences(parent_node->ID, parent_node->ChildNodes[split_inheritor_child_idx]->ID); + DockNodeUpdateHasCentralNodeChild(DockNodeGetRootNode(parent_node)); + DockNodeTreeUpdatePosSize(parent_node, parent_node->Pos, parent_node->Size); + + // Flags transfer (e.g. this is where we transfer the ImGuiDockNodeFlags_CentralNode property) + child_0->SharedFlags = parent_node->SharedFlags & ImGuiDockNodeFlags_SharedFlagsInheritMask_; + child_1->SharedFlags = parent_node->SharedFlags & ImGuiDockNodeFlags_SharedFlagsInheritMask_; + child_inheritor->LocalFlags = parent_node->LocalFlags & ImGuiDockNodeFlags_LocalFlagsTransferMask_; + parent_node->LocalFlags &= ~ImGuiDockNodeFlags_LocalFlagsTransferMask_; + child_0->UpdateMergedFlags(); + child_1->UpdateMergedFlags(); + parent_node->UpdateMergedFlags(); + if (child_inheritor->IsCentralNode()) + DockNodeGetRootNode(parent_node)->CentralNode = child_inheritor; +} + +void ImGui::DockNodeTreeMerge(ImGuiContext* ctx, ImGuiDockNode* parent_node, ImGuiDockNode* merge_lead_child) +{ + // When called from DockContextProcessUndockNode() it is possible that one of the child is NULL. + ImGuiContext& g = *GImGui; + ImGuiDockNode* child_0 = parent_node->ChildNodes[0]; + ImGuiDockNode* child_1 = parent_node->ChildNodes[1]; + IM_ASSERT(child_0 || child_1); + IM_ASSERT(merge_lead_child == child_0 || merge_lead_child == child_1); + if ((child_0 && child_0->Windows.Size > 0) || (child_1 && child_1->Windows.Size > 0)) + { + IM_ASSERT(parent_node->TabBar == NULL); + IM_ASSERT(parent_node->Windows.Size == 0); + } + IMGUI_DEBUG_LOG_DOCKING("[docking] DockNodeTreeMerge: 0x%08X + 0x%08X back into parent 0x%08X\n", child_0 ? child_0->ID : 0, child_1 ? child_1->ID : 0, parent_node->ID); + + ImVec2 backup_last_explicit_size = parent_node->SizeRef; + DockNodeMoveChildNodes(parent_node, merge_lead_child); + if (child_0) + { + DockNodeMoveWindows(parent_node, child_0); // Generally only 1 of the 2 child node will have windows + DockSettingsRenameNodeReferences(child_0->ID, parent_node->ID); + } + if (child_1) + { + DockNodeMoveWindows(parent_node, child_1); + DockSettingsRenameNodeReferences(child_1->ID, parent_node->ID); + } + DockNodeApplyPosSizeToWindows(parent_node); + parent_node->AuthorityForPos = parent_node->AuthorityForSize = parent_node->AuthorityForViewport = ImGuiDataAuthority_Auto; + parent_node->VisibleWindow = merge_lead_child->VisibleWindow; + parent_node->SizeRef = backup_last_explicit_size; + + // Flags transfer + parent_node->LocalFlags &= ~ImGuiDockNodeFlags_LocalFlagsTransferMask_; // Preserve Dockspace flag + parent_node->LocalFlags |= (child_0 ? child_0->LocalFlags : 0) & ImGuiDockNodeFlags_LocalFlagsTransferMask_; + parent_node->LocalFlags |= (child_1 ? child_1->LocalFlags : 0) & ImGuiDockNodeFlags_LocalFlagsTransferMask_; + parent_node->LocalFlagsInWindows = (child_0 ? child_0->LocalFlagsInWindows : 0) | (child_1 ? child_1->LocalFlagsInWindows : 0); // FIXME: Would be more consistent to update from actual windows + parent_node->UpdateMergedFlags(); + + if (child_0) + { + ctx->DockContext.Nodes.SetVoidPtr(child_0->ID, NULL); + IM_DELETE(child_0); + } + if (child_1) + { + ctx->DockContext.Nodes.SetVoidPtr(child_1->ID, NULL); + IM_DELETE(child_1); + } +} + +// Update Pos/Size for a node hierarchy (don't affect child Windows yet) +// (Depth-first, Pre-Order) +void ImGui::DockNodeTreeUpdatePosSize(ImGuiDockNode* node, ImVec2 pos, ImVec2 size, ImGuiDockNode* only_write_to_single_node) +{ + // During the regular dock node update we write to all nodes. + // 'only_write_to_single_node' is only set when turning a node visible mid-frame and we need its size right-away. + ImGuiContext& g = *GImGui; + const bool write_to_node = only_write_to_single_node == NULL || only_write_to_single_node == node; + if (write_to_node) + { + node->Pos = pos; + node->Size = size; + } + + if (node->IsLeafNode()) + return; + + ImGuiDockNode* child_0 = node->ChildNodes[0]; + ImGuiDockNode* child_1 = node->ChildNodes[1]; + ImVec2 child_0_pos = pos, child_1_pos = pos; + ImVec2 child_0_size = size, child_1_size = size; + + const bool child_0_is_toward_single_node = (only_write_to_single_node != NULL && DockNodeIsInHierarchyOf(only_write_to_single_node, child_0)); + const bool child_1_is_toward_single_node = (only_write_to_single_node != NULL && DockNodeIsInHierarchyOf(only_write_to_single_node, child_1)); + const bool child_0_is_or_will_be_visible = child_0->IsVisible || child_0_is_toward_single_node; + const bool child_1_is_or_will_be_visible = child_1->IsVisible || child_1_is_toward_single_node; + + if (child_0_is_or_will_be_visible && child_1_is_or_will_be_visible) + { + const float spacing = g.Style.DockingSeparatorSize; + const ImGuiAxis axis = (ImGuiAxis)node->SplitAxis; + const float size_avail = ImMax(size[axis] - spacing, 0.0f); + + // Size allocation policy + // 1) The first 0..WindowMinSize[axis]*2 are allocated evenly to both windows. + const float size_min_each = ImTrunc(ImMin(size_avail, g.Style.WindowMinSize[axis] * 2.0f) * 0.5f); + + // FIXME: Blocks 2) and 3) are essentially doing nearly the same thing. + // Difference are: write-back to SizeRef; application of a minimum size; rounding before ImTrunc() + // Clarify and rework differences between Size & SizeRef and purpose of WantLockSizeOnce + + // 2) Process locked absolute size (during a splitter resize we preserve the child of nodes not touching the splitter edge) + if (child_0->WantLockSizeOnce && !child_1->WantLockSizeOnce) + { + child_0_size[axis] = child_0->SizeRef[axis] = ImMin(size_avail - 1.0f, child_0->Size[axis]); + child_1_size[axis] = child_1->SizeRef[axis] = (size_avail - child_0_size[axis]); + IM_ASSERT(child_0->SizeRef[axis] > 0.0f && child_1->SizeRef[axis] > 0.0f); + } + else if (child_1->WantLockSizeOnce && !child_0->WantLockSizeOnce) + { + child_1_size[axis] = child_1->SizeRef[axis] = ImMin(size_avail - 1.0f, child_1->Size[axis]); + child_0_size[axis] = child_0->SizeRef[axis] = (size_avail - child_1_size[axis]); + IM_ASSERT(child_0->SizeRef[axis] > 0.0f && child_1->SizeRef[axis] > 0.0f); + } + else if (child_0->WantLockSizeOnce && child_1->WantLockSizeOnce) + { + // FIXME-DOCK: We cannot honor the requested size, so apply ratio. + // Currently this path will only be taken if code programmatically sets WantLockSizeOnce + float split_ratio = child_0_size[axis] / (child_0_size[axis] + child_1_size[axis]); + child_0_size[axis] = child_0->SizeRef[axis] = ImTrunc(size_avail * split_ratio); + child_1_size[axis] = child_1->SizeRef[axis] = (size_avail - child_0_size[axis]); + IM_ASSERT(child_0->SizeRef[axis] > 0.0f && child_1->SizeRef[axis] > 0.0f); + } + + // 3) If one window is the central node (~ use remaining space, should be made explicit!), use explicit size from the other, and remainder for the central node + else if (child_0->SizeRef[axis] != 0.0f && child_1->HasCentralNodeChild) + { + child_0_size[axis] = ImMin(size_avail - size_min_each, child_0->SizeRef[axis]); + child_1_size[axis] = (size_avail - child_0_size[axis]); + } + else if (child_1->SizeRef[axis] != 0.0f && child_0->HasCentralNodeChild) + { + child_1_size[axis] = ImMin(size_avail - size_min_each, child_1->SizeRef[axis]); + child_0_size[axis] = (size_avail - child_1_size[axis]); + } + else + { + // 4) Otherwise distribute according to the relative ratio of each SizeRef value + float split_ratio = child_0->SizeRef[axis] / (child_0->SizeRef[axis] + child_1->SizeRef[axis]); + child_0_size[axis] = ImMax(size_min_each, ImTrunc(size_avail * split_ratio + 0.5f)); + child_1_size[axis] = (size_avail - child_0_size[axis]); + } + + child_1_pos[axis] += spacing + child_0_size[axis]; + } + + if (only_write_to_single_node == NULL) + child_0->WantLockSizeOnce = child_1->WantLockSizeOnce = false; + + const bool child_0_recurse = only_write_to_single_node ? child_0_is_toward_single_node : child_0->IsVisible; + const bool child_1_recurse = only_write_to_single_node ? child_1_is_toward_single_node : child_1->IsVisible; + if (child_0_recurse) + DockNodeTreeUpdatePosSize(child_0, child_0_pos, child_0_size); + if (child_1_recurse) + DockNodeTreeUpdatePosSize(child_1, child_1_pos, child_1_size); +} + +static void DockNodeTreeUpdateSplitterFindTouchingNode(ImGuiDockNode* node, ImGuiAxis axis, int side, ImVector* touching_nodes) +{ + if (node->IsLeafNode()) + { + touching_nodes->push_back(node); + return; + } + if (node->ChildNodes[0]->IsVisible) + if (node->SplitAxis != axis || side == 0 || !node->ChildNodes[1]->IsVisible) + DockNodeTreeUpdateSplitterFindTouchingNode(node->ChildNodes[0], axis, side, touching_nodes); + if (node->ChildNodes[1]->IsVisible) + if (node->SplitAxis != axis || side == 1 || !node->ChildNodes[0]->IsVisible) + DockNodeTreeUpdateSplitterFindTouchingNode(node->ChildNodes[1], axis, side, touching_nodes); +} + +// (Depth-First, Pre-Order) +void ImGui::DockNodeTreeUpdateSplitter(ImGuiDockNode* node) +{ + if (node->IsLeafNode()) + return; + + ImGuiContext& g = *GImGui; + + ImGuiDockNode* child_0 = node->ChildNodes[0]; + ImGuiDockNode* child_1 = node->ChildNodes[1]; + if (child_0->IsVisible && child_1->IsVisible) + { + // Bounding box of the splitter cover the space between both nodes (w = Spacing, h = Size[xy^1] for when splitting horizontally) + const ImGuiAxis axis = (ImGuiAxis)node->SplitAxis; + IM_ASSERT(axis != ImGuiAxis_None); + ImRect bb; + bb.Min = child_0->Pos; + bb.Max = child_1->Pos; + bb.Min[axis] += child_0->Size[axis]; + bb.Max[axis ^ 1] += child_1->Size[axis ^ 1]; + //if (g.IO.KeyCtrl) GetForegroundDrawList(g.CurrentWindow->Viewport)->AddRect(bb.Min, bb.Max, IM_COL32(255,0,255,255)); + + const ImGuiDockNodeFlags merged_flags = child_0->MergedFlags | child_1->MergedFlags; // Merged flags for BOTH childs + const ImGuiDockNodeFlags no_resize_axis_flag = (axis == ImGuiAxis_X) ? ImGuiDockNodeFlags_NoResizeX : ImGuiDockNodeFlags_NoResizeY; + if ((merged_flags & ImGuiDockNodeFlags_NoResize) || (merged_flags & no_resize_axis_flag)) + { + ImGuiWindow* window = g.CurrentWindow; + window->DrawList->AddRectFilled(bb.Min, bb.Max, GetColorU32(ImGuiCol_Separator), g.Style.FrameRounding); + } + else + { + //bb.Min[axis] += 1; // Display a little inward so highlight doesn't connect with nearby tabs on the neighbor node. + //bb.Max[axis] -= 1; + PushID(node->ID); + + // Find resizing limits by gathering list of nodes that are touching the splitter line. + ImVector touching_nodes[2]; + float min_size = g.Style.WindowMinSize[axis]; + float resize_limits[2]; + resize_limits[0] = node->ChildNodes[0]->Pos[axis] + min_size; + resize_limits[1] = node->ChildNodes[1]->Pos[axis] + node->ChildNodes[1]->Size[axis] - min_size; + + ImGuiID splitter_id = GetID("##Splitter"); + if (g.ActiveId == splitter_id) // Only process when splitter is active + { + DockNodeTreeUpdateSplitterFindTouchingNode(child_0, axis, 1, &touching_nodes[0]); + DockNodeTreeUpdateSplitterFindTouchingNode(child_1, axis, 0, &touching_nodes[1]); + for (int touching_node_n = 0; touching_node_n < touching_nodes[0].Size; touching_node_n++) + resize_limits[0] = ImMax(resize_limits[0], touching_nodes[0][touching_node_n]->Rect().Min[axis] + min_size); + for (int touching_node_n = 0; touching_node_n < touching_nodes[1].Size; touching_node_n++) + resize_limits[1] = ImMin(resize_limits[1], touching_nodes[1][touching_node_n]->Rect().Max[axis] - min_size); + + // [DEBUG] Render touching nodes & limits + /* + ImDrawList* draw_list = node->HostWindow ? GetForegroundDrawList(node->HostWindow) : GetForegroundDrawList(GetMainViewport()); + for (int n = 0; n < 2; n++) + { + for (int touching_node_n = 0; touching_node_n < touching_nodes[n].Size; touching_node_n++) + draw_list->AddRect(touching_nodes[n][touching_node_n]->Pos, touching_nodes[n][touching_node_n]->Pos + touching_nodes[n][touching_node_n]->Size, IM_COL32(0, 255, 0, 255)); + if (axis == ImGuiAxis_X) + draw_list->AddLine(ImVec2(resize_limits[n], node->ChildNodes[n]->Pos.y), ImVec2(resize_limits[n], node->ChildNodes[n]->Pos.y + node->ChildNodes[n]->Size.y), IM_COL32(255, 0, 255, 255), 3.0f); + else + draw_list->AddLine(ImVec2(node->ChildNodes[n]->Pos.x, resize_limits[n]), ImVec2(node->ChildNodes[n]->Pos.x + node->ChildNodes[n]->Size.x, resize_limits[n]), IM_COL32(255, 0, 255, 255), 3.0f); + } + */ + } + + // Use a short delay before highlighting the splitter (and changing the mouse cursor) in order for regular mouse movement to not highlight many splitters + float cur_size_0 = child_0->Size[axis]; + float cur_size_1 = child_1->Size[axis]; + float min_size_0 = resize_limits[0] - child_0->Pos[axis]; + float min_size_1 = child_1->Pos[axis] + child_1->Size[axis] - resize_limits[1]; + ImU32 bg_col = GetColorU32(ImGuiCol_WindowBg); + if (SplitterBehavior(bb, GetID("##Splitter"), axis, &cur_size_0, &cur_size_1, min_size_0, min_size_1, WINDOWS_HOVER_PADDING, WINDOWS_RESIZE_FROM_EDGES_FEEDBACK_TIMER, bg_col)) + { + if (touching_nodes[0].Size > 0 && touching_nodes[1].Size > 0) + { + child_0->Size[axis] = child_0->SizeRef[axis] = cur_size_0; + child_1->Pos[axis] -= cur_size_1 - child_1->Size[axis]; + child_1->Size[axis] = child_1->SizeRef[axis] = cur_size_1; + + // Lock the size of every node that is a sibling of the node we are touching + // This might be less desirable if we can merge sibling of a same axis into the same parental level. + for (int side_n = 0; side_n < 2; side_n++) + for (int touching_node_n = 0; touching_node_n < touching_nodes[side_n].Size; touching_node_n++) + { + ImGuiDockNode* touching_node = touching_nodes[side_n][touching_node_n]; + //ImDrawList* draw_list = node->HostWindow ? GetForegroundDrawList(node->HostWindow) : GetForegroundDrawList(GetMainViewport()); + //draw_list->AddRect(touching_node->Pos, touching_node->Pos + touching_node->Size, IM_COL32(255, 128, 0, 255)); + while (touching_node->ParentNode != node) + { + if (touching_node->ParentNode->SplitAxis == axis) + { + // Mark other node so its size will be preserved during the upcoming call to DockNodeTreeUpdatePosSize(). + ImGuiDockNode* node_to_preserve = touching_node->ParentNode->ChildNodes[side_n]; + node_to_preserve->WantLockSizeOnce = true; + //draw_list->AddRect(touching_node->Pos, touching_node->Rect().Max, IM_COL32(255, 0, 0, 255)); + //draw_list->AddRectFilled(node_to_preserve->Pos, node_to_preserve->Rect().Max, IM_COL32(0, 255, 0, 100)); + } + touching_node = touching_node->ParentNode; + } + } + + DockNodeTreeUpdatePosSize(child_0, child_0->Pos, child_0->Size); + DockNodeTreeUpdatePosSize(child_1, child_1->Pos, child_1->Size); + MarkIniSettingsDirty(); + } + } + PopID(); + } + } + + if (child_0->IsVisible) + DockNodeTreeUpdateSplitter(child_0); + if (child_1->IsVisible) + DockNodeTreeUpdateSplitter(child_1); +} + +ImGuiDockNode* ImGui::DockNodeTreeFindFallbackLeafNode(ImGuiDockNode* node) +{ + if (node->IsLeafNode()) + return node; + if (ImGuiDockNode* leaf_node = DockNodeTreeFindFallbackLeafNode(node->ChildNodes[0])) + return leaf_node; + if (ImGuiDockNode* leaf_node = DockNodeTreeFindFallbackLeafNode(node->ChildNodes[1])) + return leaf_node; + return NULL; +} + +ImGuiDockNode* ImGui::DockNodeTreeFindVisibleNodeByPos(ImGuiDockNode* node, ImVec2 pos) +{ + if (!node->IsVisible) + return NULL; + + const float dock_spacing = 0.0f;// g.Style.ItemInnerSpacing.x; // FIXME: Relation to DOCKING_SPLITTER_SIZE? + ImRect r(node->Pos, node->Pos + node->Size); + r.Expand(dock_spacing * 0.5f); + bool inside = r.Contains(pos); + if (!inside) + return NULL; + + if (node->IsLeafNode()) + return node; + if (ImGuiDockNode* hovered_node = DockNodeTreeFindVisibleNodeByPos(node->ChildNodes[0], pos)) + return hovered_node; + if (ImGuiDockNode* hovered_node = DockNodeTreeFindVisibleNodeByPos(node->ChildNodes[1], pos)) + return hovered_node; + + // This means we are hovering over the splitter/spacing of a parent node + return node; +} + +//----------------------------------------------------------------------------- +// Docking: Public Functions (SetWindowDock, DockSpace, DockSpaceOverViewport) +//----------------------------------------------------------------------------- +// - SetWindowDock() [Internal] +// - DockSpace() +// - DockSpaceOverViewport() +//----------------------------------------------------------------------------- + +// [Internal] Called via SetNextWindowDockID() +void ImGui::SetWindowDock(ImGuiWindow* window, ImGuiID dock_id, ImGuiCond cond) +{ + // Test condition (NB: bit 0 is always true) and clear flags for next time + if (cond && (window->SetWindowDockAllowFlags & cond) == 0) + return; + window->SetWindowDockAllowFlags &= ~(ImGuiCond_Once | ImGuiCond_FirstUseEver | ImGuiCond_Appearing); + + if (window->DockId == dock_id) + return; + + // If the user attempt to set a dock id that is a split node, we'll dig within to find a suitable docking spot + ImGuiContext& g = *GImGui; + if (ImGuiDockNode* new_node = DockContextFindNodeByID(&g, dock_id)) + if (new_node->IsSplitNode()) + { + // Policy: Find central node or latest focused node. We first move back to our root node. + new_node = DockNodeGetRootNode(new_node); + if (new_node->CentralNode) + { + IM_ASSERT(new_node->CentralNode->IsCentralNode()); + dock_id = new_node->CentralNode->ID; + } + else + { + dock_id = new_node->LastFocusedNodeId; + } + } + + if (window->DockId == dock_id) + return; + + if (window->DockNode) + DockNodeRemoveWindow(window->DockNode, window, 0); + window->DockId = dock_id; +} + +// Create an explicit dockspace node within an existing window. Also expose dock node flags and creates a CentralNode by default. +// The Central Node is always displayed even when empty and shrink/extend according to the requested size of its neighbors. +// DockSpace() needs to be submitted _before_ any window they can host. If you use a dockspace, submit it early in your app. +// When ImGuiDockNodeFlags_KeepAliveOnly is set, nothing is submitted in the current window (function may be called from any location). +ImGuiID ImGui::DockSpace(ImGuiID id, const ImVec2& size_arg, ImGuiDockNodeFlags flags, const ImGuiWindowClass* window_class) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = GetCurrentWindowRead(); + if (!(g.IO.ConfigFlags & ImGuiConfigFlags_DockingEnable)) + return 0; + + // Early out if parent window is hidden/collapsed + // This is faster but also DockNodeUpdateTabBar() relies on TabBarLayout() running (which won't if SkipItems=true) to set NextSelectedTabId = 0). See #2960. + // If for whichever reason this is causing problem we would need to ensure that DockNodeUpdateTabBar() ends up clearing NextSelectedTabId even if SkipItems=true. + if (window->SkipItems) + flags |= ImGuiDockNodeFlags_KeepAliveOnly; + if ((flags & ImGuiDockNodeFlags_KeepAliveOnly) == 0) + window = GetCurrentWindow(); // call to set window->WriteAccessed = true; + + IM_ASSERT((flags & ImGuiDockNodeFlags_DockSpace) == 0); + IM_ASSERT(id != 0); + ImGuiDockNode* node = DockContextFindNodeByID(&g, id); + if (!node) + { + IMGUI_DEBUG_LOG_DOCKING("[docking] DockSpace: dockspace node 0x%08X created\n", id); + node = DockContextAddNode(&g, id); + node->SetLocalFlags(ImGuiDockNodeFlags_CentralNode); + } + if (window_class && window_class->ClassId != node->WindowClass.ClassId) + IMGUI_DEBUG_LOG_DOCKING("[docking] DockSpace: dockspace node 0x%08X: setup WindowClass 0x%08X -> 0x%08X\n", id, node->WindowClass.ClassId, window_class->ClassId); + node->SharedFlags = flags; + node->WindowClass = window_class ? *window_class : ImGuiWindowClass(); + + // When a DockSpace transitioned form implicit to explicit this may be called a second time + // It is possible that the node has already been claimed by a docked window which appeared before the DockSpace() node, so we overwrite IsDockSpace again. + if (node->LastFrameActive == g.FrameCount && !(flags & ImGuiDockNodeFlags_KeepAliveOnly)) + { + IM_ASSERT(node->IsDockSpace() == false && "Cannot call DockSpace() twice a frame with the same ID"); + node->SetLocalFlags(node->LocalFlags | ImGuiDockNodeFlags_DockSpace); + return id; + } + node->SetLocalFlags(node->LocalFlags | ImGuiDockNodeFlags_DockSpace); + + // Keep alive mode, this is allow windows docked into this node so stay docked even if they are not visible + if (flags & ImGuiDockNodeFlags_KeepAliveOnly) + { + node->LastFrameAlive = g.FrameCount; + return id; + } + + const ImVec2 content_avail = GetContentRegionAvail(); + ImVec2 size = ImTrunc(size_arg); + if (size.x <= 0.0f) + size.x = ImMax(content_avail.x + size.x, 4.0f); // Arbitrary minimum child size (0.0f causing too much issues) + if (size.y <= 0.0f) + size.y = ImMax(content_avail.y + size.y, 4.0f); + IM_ASSERT(size.x > 0.0f && size.y > 0.0f); + + node->Pos = window->DC.CursorPos; + node->Size = node->SizeRef = size; + SetNextWindowPos(node->Pos); + SetNextWindowSize(node->Size); + g.NextWindowData.PosUndock = false; + + // FIXME-DOCK: Why do we need a child window to host a dockspace, could we host it in the existing window? + // FIXME-DOCK: What is the reason for not simply calling BeginChild()? (OK to have a reason but should be commented) + ImGuiWindowFlags window_flags = ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_DockNodeHost; + window_flags |= ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar; + window_flags |= ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse; + window_flags |= ImGuiWindowFlags_NoBackground; + + char title[256]; + ImFormatString(title, IM_ARRAYSIZE(title), "%s/DockSpace_%08X", window->Name, id); + + PushStyleVar(ImGuiStyleVar_ChildBorderSize, 0.0f); + Begin(title, NULL, window_flags); + PopStyleVar(); + + ImGuiWindow* host_window = g.CurrentWindow; + DockNodeSetupHostWindow(node, host_window); + host_window->ChildId = window->GetID(title); + node->OnlyNodeWithWindows = NULL; + + IM_ASSERT(node->IsRootNode()); + + // We need to handle the rare case were a central node is missing. + // This can happen if the node was first created manually with DockBuilderAddNode() but _without_ the ImGuiDockNodeFlags_Dockspace. + // Doing it correctly would set the _CentralNode flags, which would then propagate according to subsequent split. + // It would also be ambiguous to attempt to assign a central node while there are split nodes, so we wait until there's a single node remaining. + // The specific sub-property of _CentralNode we are interested in recovering here is the "Don't delete when empty" property, + // as it doesn't make sense for an empty dockspace to not have this property. + if (node->IsLeafNode() && !node->IsCentralNode()) + node->SetLocalFlags(node->LocalFlags | ImGuiDockNodeFlags_CentralNode); + + // Update the node + DockNodeUpdate(node); + + End(); + + ImRect bb(node->Pos, node->Pos + size); + ItemSize(size); + ItemAdd(bb, id, NULL, ImGuiItemFlags_NoNav); // Not a nav point (could be, would need to draw the nav rect and replicate/refactor activation from BeginChild(), but seems like CTRL+Tab works better here?) + if ((g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredRect) && IsWindowChildOf(g.HoveredWindow, host_window, false, true)) // To fullfill IsItemHovered(), similar to EndChild() + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredWindow; + + return id; +} + +// Tips: Use with ImGuiDockNodeFlags_PassthruCentralNode! +// The limitation with this call is that your window won't have a menu bar. +// Even though we could pass window flags, it would also require the user to be able to call BeginMenuBar() somehow meaning we can't Begin/End in a single function. +// But you can also use BeginMainMenuBar(). If you really want a menu bar inside the same window as the one hosting the dockspace, you will need to copy this code somewhere and tweak it. +ImGuiID ImGui::DockSpaceOverViewport(const ImGuiViewport* viewport, ImGuiDockNodeFlags dockspace_flags, const ImGuiWindowClass* window_class) +{ + if (viewport == NULL) + viewport = GetMainViewport(); + + SetNextWindowPos(viewport->WorkPos); + SetNextWindowSize(viewport->WorkSize); + SetNextWindowViewport(viewport->ID); + + ImGuiWindowFlags host_window_flags = 0; + host_window_flags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDocking; + host_window_flags |= ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoNavFocus; + if (dockspace_flags & ImGuiDockNodeFlags_PassthruCentralNode) + host_window_flags |= ImGuiWindowFlags_NoBackground; + + char label[32]; + ImFormatString(label, IM_ARRAYSIZE(label), "DockSpaceViewport_%08X", viewport->ID); + + PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); + PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); + PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); + Begin(label, NULL, host_window_flags); + PopStyleVar(3); + + ImGuiID dockspace_id = GetID("DockSpace"); + DockSpace(dockspace_id, ImVec2(0.0f, 0.0f), dockspace_flags, window_class); + End(); + + return dockspace_id; +} + +//----------------------------------------------------------------------------- +// Docking: Builder Functions +//----------------------------------------------------------------------------- +// Very early end-user API to manipulate dock nodes. +// Only available in imgui_internal.h. Expect this API to change/break! +// It is expected that those functions are all called _before_ the dockspace node submission. +//----------------------------------------------------------------------------- +// - DockBuilderDockWindow() +// - DockBuilderGetNode() +// - DockBuilderSetNodePos() +// - DockBuilderSetNodeSize() +// - DockBuilderAddNode() +// - DockBuilderRemoveNode() +// - DockBuilderRemoveNodeChildNodes() +// - DockBuilderRemoveNodeDockedWindows() +// - DockBuilderSplitNode() +// - DockBuilderCopyNodeRec() +// - DockBuilderCopyNode() +// - DockBuilderCopyWindowSettings() +// - DockBuilderCopyDockSpace() +// - DockBuilderFinish() +//----------------------------------------------------------------------------- + +void ImGui::DockBuilderDockWindow(const char* window_name, ImGuiID node_id) +{ + // We don't preserve relative order of multiple docked windows (by clearing DockOrder back to -1) + ImGuiContext& g = *GImGui; IM_UNUSED(g); + IMGUI_DEBUG_LOG_DOCKING("[docking] DockBuilderDockWindow '%s' to node 0x%08X\n", window_name, node_id); + ImGuiID window_id = ImHashStr(window_name); + if (ImGuiWindow* window = FindWindowByID(window_id)) + { + // Apply to created window + ImGuiID prev_node_id = window->DockId; + SetWindowDock(window, node_id, ImGuiCond_Always); + if (window->DockId != prev_node_id) + window->DockOrder = -1; + } + else + { + // Apply to settings + ImGuiWindowSettings* settings = FindWindowSettingsByID(window_id); + if (settings == NULL) + settings = CreateNewWindowSettings(window_name); + if (settings->DockId != node_id) + settings->DockOrder = -1; + settings->DockId = node_id; + } +} + +ImGuiDockNode* ImGui::DockBuilderGetNode(ImGuiID node_id) +{ + ImGuiContext& g = *GImGui; + return DockContextFindNodeByID(&g, node_id); +} + +void ImGui::DockBuilderSetNodePos(ImGuiID node_id, ImVec2 pos) +{ + ImGuiContext& g = *GImGui; + ImGuiDockNode* node = DockContextFindNodeByID(&g, node_id); + if (node == NULL) + return; + node->Pos = pos; + node->AuthorityForPos = ImGuiDataAuthority_DockNode; +} + +void ImGui::DockBuilderSetNodeSize(ImGuiID node_id, ImVec2 size) +{ + ImGuiContext& g = *GImGui; + ImGuiDockNode* node = DockContextFindNodeByID(&g, node_id); + if (node == NULL) + return; + IM_ASSERT(size.x > 0.0f && size.y > 0.0f); + node->Size = node->SizeRef = size; + node->AuthorityForSize = ImGuiDataAuthority_DockNode; +} + +// Make sure to use the ImGuiDockNodeFlags_DockSpace flag to create a dockspace node! Otherwise this will create a floating node! +// - Floating node: you can then call DockBuilderSetNodePos()/DockBuilderSetNodeSize() to position and size the floating node. +// - Dockspace node: calling DockBuilderSetNodePos() is unnecessary. +// - If you intend to split a node immediately after creation using DockBuilderSplitNode(), make sure to call DockBuilderSetNodeSize() beforehand! +// For various reason, the splitting code currently needs a base size otherwise space may not be allocated as precisely as you would expect. +// - Use (id == 0) to let the system allocate a node identifier. +// - Existing node with a same id will be removed. +ImGuiID ImGui::DockBuilderAddNode(ImGuiID node_id, ImGuiDockNodeFlags flags) +{ + ImGuiContext& g = *GImGui; IM_UNUSED(g); + IMGUI_DEBUG_LOG_DOCKING("[docking] DockBuilderAddNode 0x%08X flags=%08X\n", node_id, flags); + + if (node_id != 0) + DockBuilderRemoveNode(node_id); + + ImGuiDockNode* node = NULL; + if (flags & ImGuiDockNodeFlags_DockSpace) + { + DockSpace(node_id, ImVec2(0, 0), (flags & ~ImGuiDockNodeFlags_DockSpace) | ImGuiDockNodeFlags_KeepAliveOnly); + node = DockContextFindNodeByID(&g, node_id); + } + else + { + node = DockContextAddNode(&g, node_id); + node->SetLocalFlags(flags); + } + node->LastFrameAlive = g.FrameCount; // Set this otherwise BeginDocked will undock during the same frame. + return node->ID; +} + +void ImGui::DockBuilderRemoveNode(ImGuiID node_id) +{ + ImGuiContext& g = *GImGui; IM_UNUSED(g); + IMGUI_DEBUG_LOG_DOCKING("[docking] DockBuilderRemoveNode 0x%08X\n", node_id); + + ImGuiDockNode* node = DockContextFindNodeByID(&g, node_id); + if (node == NULL) + return; + DockBuilderRemoveNodeDockedWindows(node_id, true); + DockBuilderRemoveNodeChildNodes(node_id); + // Node may have moved or deleted if e.g. any merge happened + node = DockContextFindNodeByID(&g, node_id); + if (node == NULL) + return; + if (node->IsCentralNode() && node->ParentNode) + node->ParentNode->SetLocalFlags(node->ParentNode->LocalFlags | ImGuiDockNodeFlags_CentralNode); + DockContextRemoveNode(&g, node, true); +} + +// root_id = 0 to remove all, root_id != 0 to remove child of given node. +void ImGui::DockBuilderRemoveNodeChildNodes(ImGuiID root_id) +{ + ImGuiContext& g = *GImGui; + ImGuiDockContext* dc = &g.DockContext; + + ImGuiDockNode* root_node = root_id ? DockContextFindNodeByID(&g, root_id) : NULL; + if (root_id && root_node == NULL) + return; + bool has_central_node = false; + + ImGuiDataAuthority backup_root_node_authority_for_pos = root_node ? root_node->AuthorityForPos : ImGuiDataAuthority_Auto; + ImGuiDataAuthority backup_root_node_authority_for_size = root_node ? root_node->AuthorityForSize : ImGuiDataAuthority_Auto; + + // Process active windows + ImVector nodes_to_remove; + for (int n = 0; n < dc->Nodes.Data.Size; n++) + if (ImGuiDockNode* node = (ImGuiDockNode*)dc->Nodes.Data[n].val_p) + { + bool want_removal = (root_id == 0) || (node->ID != root_id && DockNodeGetRootNode(node)->ID == root_id); + if (want_removal) + { + if (node->IsCentralNode()) + has_central_node = true; + if (root_id != 0) + DockContextQueueNotifyRemovedNode(&g, node); + if (root_node) + { + DockNodeMoveWindows(root_node, node); + DockSettingsRenameNodeReferences(node->ID, root_node->ID); + } + nodes_to_remove.push_back(node); + } + } + + // DockNodeMoveWindows->DockNodeAddWindow will normally set those when reaching two windows (which is only adequate during interactive merge) + // Make sure we don't lose our current pos/size. (FIXME-DOCK: Consider tidying up that code in DockNodeAddWindow instead) + if (root_node) + { + root_node->AuthorityForPos = backup_root_node_authority_for_pos; + root_node->AuthorityForSize = backup_root_node_authority_for_size; + } + + // Apply to settings + for (ImGuiWindowSettings* settings = g.SettingsWindows.begin(); settings != NULL; settings = g.SettingsWindows.next_chunk(settings)) + if (ImGuiID window_settings_dock_id = settings->DockId) + for (int n = 0; n < nodes_to_remove.Size; n++) + if (nodes_to_remove[n]->ID == window_settings_dock_id) + { + settings->DockId = root_id; + break; + } + + // Not really efficient, but easier to destroy a whole hierarchy considering DockContextRemoveNode is attempting to merge nodes + if (nodes_to_remove.Size > 1) + ImQsort(nodes_to_remove.Data, nodes_to_remove.Size, sizeof(ImGuiDockNode*), DockNodeComparerDepthMostFirst); + for (int n = 0; n < nodes_to_remove.Size; n++) + DockContextRemoveNode(&g, nodes_to_remove[n], false); + + if (root_id == 0) + { + dc->Nodes.Clear(); + dc->Requests.clear(); + } + else if (has_central_node) + { + root_node->CentralNode = root_node; + root_node->SetLocalFlags(root_node->LocalFlags | ImGuiDockNodeFlags_CentralNode); + } +} + +void ImGui::DockBuilderRemoveNodeDockedWindows(ImGuiID root_id, bool clear_settings_refs) +{ + // Clear references in settings + ImGuiContext& g = *GImGui; + if (clear_settings_refs) + { + for (ImGuiWindowSettings* settings = g.SettingsWindows.begin(); settings != NULL; settings = g.SettingsWindows.next_chunk(settings)) + { + bool want_removal = (root_id == 0) || (settings->DockId == root_id); + if (!want_removal && settings->DockId != 0) + if (ImGuiDockNode* node = DockContextFindNodeByID(&g, settings->DockId)) + if (DockNodeGetRootNode(node)->ID == root_id) + want_removal = true; + if (want_removal) + settings->DockId = 0; + } + } + + // Clear references in windows + for (int n = 0; n < g.Windows.Size; n++) + { + ImGuiWindow* window = g.Windows[n]; + bool want_removal = (root_id == 0) || (window->DockNode && DockNodeGetRootNode(window->DockNode)->ID == root_id) || (window->DockNodeAsHost && window->DockNodeAsHost->ID == root_id); + if (want_removal) + { + const ImGuiID backup_dock_id = window->DockId; + IM_UNUSED(backup_dock_id); + DockContextProcessUndockWindow(&g, window, clear_settings_refs); + if (!clear_settings_refs) + IM_ASSERT(window->DockId == backup_dock_id); + } + } +} + +// If 'out_id_at_dir' or 'out_id_at_opposite_dir' are non NULL, the function will write out the ID of the two new nodes created. +// Return value is ID of the node at the specified direction, so same as (*out_id_at_dir) if that pointer is set. +// FIXME-DOCK: We are not exposing nor using split_outer. +ImGuiID ImGui::DockBuilderSplitNode(ImGuiID id, ImGuiDir split_dir, float size_ratio_for_node_at_dir, ImGuiID* out_id_at_dir, ImGuiID* out_id_at_opposite_dir) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(split_dir != ImGuiDir_None); + IMGUI_DEBUG_LOG_DOCKING("[docking] DockBuilderSplitNode: node 0x%08X, split_dir %d\n", id, split_dir); + + ImGuiDockNode* node = DockContextFindNodeByID(&g, id); + if (node == NULL) + { + IM_ASSERT(node != NULL); + return 0; + } + + IM_ASSERT(!node->IsSplitNode()); // Assert if already Split + + ImGuiDockRequest req; + req.Type = ImGuiDockRequestType_Split; + req.DockTargetWindow = NULL; + req.DockTargetNode = node; + req.DockPayload = NULL; + req.DockSplitDir = split_dir; + req.DockSplitRatio = ImSaturate((split_dir == ImGuiDir_Left || split_dir == ImGuiDir_Up) ? size_ratio_for_node_at_dir : 1.0f - size_ratio_for_node_at_dir); + req.DockSplitOuter = false; + DockContextProcessDock(&g, &req); + + ImGuiID id_at_dir = node->ChildNodes[(split_dir == ImGuiDir_Left || split_dir == ImGuiDir_Up) ? 0 : 1]->ID; + ImGuiID id_at_opposite_dir = node->ChildNodes[(split_dir == ImGuiDir_Left || split_dir == ImGuiDir_Up) ? 1 : 0]->ID; + if (out_id_at_dir) + *out_id_at_dir = id_at_dir; + if (out_id_at_opposite_dir) + *out_id_at_opposite_dir = id_at_opposite_dir; + return id_at_dir; +} + +static ImGuiDockNode* DockBuilderCopyNodeRec(ImGuiDockNode* src_node, ImGuiID dst_node_id_if_known, ImVector* out_node_remap_pairs) +{ + ImGuiContext& g = *GImGui; + ImGuiDockNode* dst_node = ImGui::DockContextAddNode(&g, dst_node_id_if_known); + dst_node->SharedFlags = src_node->SharedFlags; + dst_node->LocalFlags = src_node->LocalFlags; + dst_node->LocalFlagsInWindows = ImGuiDockNodeFlags_None; + dst_node->Pos = src_node->Pos; + dst_node->Size = src_node->Size; + dst_node->SizeRef = src_node->SizeRef; + dst_node->SplitAxis = src_node->SplitAxis; + dst_node->UpdateMergedFlags(); + + out_node_remap_pairs->push_back(src_node->ID); + out_node_remap_pairs->push_back(dst_node->ID); + + for (int child_n = 0; child_n < IM_ARRAYSIZE(src_node->ChildNodes); child_n++) + if (src_node->ChildNodes[child_n]) + { + dst_node->ChildNodes[child_n] = DockBuilderCopyNodeRec(src_node->ChildNodes[child_n], 0, out_node_remap_pairs); + dst_node->ChildNodes[child_n]->ParentNode = dst_node; + } + + IMGUI_DEBUG_LOG_DOCKING("[docking] Fork node %08X -> %08X (%d childs)\n", src_node->ID, dst_node->ID, dst_node->IsSplitNode() ? 2 : 0); + return dst_node; +} + +void ImGui::DockBuilderCopyNode(ImGuiID src_node_id, ImGuiID dst_node_id, ImVector* out_node_remap_pairs) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(src_node_id != 0); + IM_ASSERT(dst_node_id != 0); + IM_ASSERT(out_node_remap_pairs != NULL); + + DockBuilderRemoveNode(dst_node_id); + + ImGuiDockNode* src_node = DockContextFindNodeByID(&g, src_node_id); + IM_ASSERT(src_node != NULL); + + out_node_remap_pairs->clear(); + DockBuilderCopyNodeRec(src_node, dst_node_id, out_node_remap_pairs); + + IM_ASSERT((out_node_remap_pairs->Size % 2) == 0); +} + +void ImGui::DockBuilderCopyWindowSettings(const char* src_name, const char* dst_name) +{ + ImGuiWindow* src_window = FindWindowByName(src_name); + if (src_window == NULL) + return; + if (ImGuiWindow* dst_window = FindWindowByName(dst_name)) + { + dst_window->Pos = src_window->Pos; + dst_window->Size = src_window->Size; + dst_window->SizeFull = src_window->SizeFull; + dst_window->Collapsed = src_window->Collapsed; + } + else + { + ImGuiWindowSettings* dst_settings = FindWindowSettingsByID(ImHashStr(dst_name)); + if (!dst_settings) + dst_settings = CreateNewWindowSettings(dst_name); + ImVec2ih window_pos_2ih = ImVec2ih(src_window->Pos); + if (src_window->ViewportId != 0 && src_window->ViewportId != IMGUI_VIEWPORT_DEFAULT_ID) + { + dst_settings->ViewportPos = window_pos_2ih; + dst_settings->ViewportId = src_window->ViewportId; + dst_settings->Pos = ImVec2ih(0, 0); + } + else + { + dst_settings->Pos = window_pos_2ih; + } + dst_settings->Size = ImVec2ih(src_window->SizeFull); + dst_settings->Collapsed = src_window->Collapsed; + } +} + +// FIXME: Will probably want to change this signature, in particular how the window remapping pairs are passed. +void ImGui::DockBuilderCopyDockSpace(ImGuiID src_dockspace_id, ImGuiID dst_dockspace_id, ImVector* in_window_remap_pairs) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(src_dockspace_id != 0); + IM_ASSERT(dst_dockspace_id != 0); + IM_ASSERT(in_window_remap_pairs != NULL); + IM_ASSERT((in_window_remap_pairs->Size % 2) == 0); + + // Duplicate entire dock + // FIXME: When overwriting dst_dockspace_id, windows that aren't part of our dockspace window class but that are docked in a same node will be split apart, + // whereas we could attempt to at least keep them together in a new, same floating node. + ImVector node_remap_pairs; + DockBuilderCopyNode(src_dockspace_id, dst_dockspace_id, &node_remap_pairs); + + // Attempt to transition all the upcoming windows associated to dst_dockspace_id into the newly created hierarchy of dock nodes + // (The windows associated to src_dockspace_id are staying in place) + ImVector src_windows; + for (int remap_window_n = 0; remap_window_n < in_window_remap_pairs->Size; remap_window_n += 2) + { + const char* src_window_name = (*in_window_remap_pairs)[remap_window_n]; + const char* dst_window_name = (*in_window_remap_pairs)[remap_window_n + 1]; + ImGuiID src_window_id = ImHashStr(src_window_name); + src_windows.push_back(src_window_id); + + // Search in the remapping tables + ImGuiID src_dock_id = 0; + if (ImGuiWindow* src_window = FindWindowByID(src_window_id)) + src_dock_id = src_window->DockId; + else if (ImGuiWindowSettings* src_window_settings = FindWindowSettingsByID(src_window_id)) + src_dock_id = src_window_settings->DockId; + ImGuiID dst_dock_id = 0; + for (int dock_remap_n = 0; dock_remap_n < node_remap_pairs.Size; dock_remap_n += 2) + if (node_remap_pairs[dock_remap_n] == src_dock_id) + { + dst_dock_id = node_remap_pairs[dock_remap_n + 1]; + //node_remap_pairs[dock_remap_n] = node_remap_pairs[dock_remap_n + 1] = 0; // Clear + break; + } + + if (dst_dock_id != 0) + { + // Docked windows gets redocked into the new node hierarchy. + IMGUI_DEBUG_LOG_DOCKING("[docking] Remap live window '%s' 0x%08X -> '%s' 0x%08X\n", src_window_name, src_dock_id, dst_window_name, dst_dock_id); + DockBuilderDockWindow(dst_window_name, dst_dock_id); + } + else + { + // Floating windows gets their settings transferred (regardless of whether the new window already exist or not) + // When this is leading to a Copy and not a Move, we would get two overlapping floating windows. Could we possibly dock them together? + IMGUI_DEBUG_LOG_DOCKING("[docking] Remap window settings '%s' -> '%s'\n", src_window_name, dst_window_name); + DockBuilderCopyWindowSettings(src_window_name, dst_window_name); + } + } + + // Anything else in the source nodes of 'node_remap_pairs' are windows that are not included in the remapping list. + // Find those windows and move to them to the cloned dock node. This may be optional? + // Dock those are a second step as undocking would invalidate source dock nodes. + struct DockRemainingWindowTask { ImGuiWindow* Window; ImGuiID DockId; DockRemainingWindowTask(ImGuiWindow* window, ImGuiID dock_id) { Window = window; DockId = dock_id; } }; + ImVector dock_remaining_windows; + for (int dock_remap_n = 0; dock_remap_n < node_remap_pairs.Size; dock_remap_n += 2) + if (ImGuiID src_dock_id = node_remap_pairs[dock_remap_n]) + { + ImGuiID dst_dock_id = node_remap_pairs[dock_remap_n + 1]; + ImGuiDockNode* node = DockBuilderGetNode(src_dock_id); + for (int window_n = 0; window_n < node->Windows.Size; window_n++) + { + ImGuiWindow* window = node->Windows[window_n]; + if (src_windows.contains(window->ID)) + continue; + + // Docked windows gets redocked into the new node hierarchy. + IMGUI_DEBUG_LOG_DOCKING("[docking] Remap window '%s' %08X -> %08X\n", window->Name, src_dock_id, dst_dock_id); + dock_remaining_windows.push_back(DockRemainingWindowTask(window, dst_dock_id)); + } + } + for (const DockRemainingWindowTask& task : dock_remaining_windows) + DockBuilderDockWindow(task.Window->Name, task.DockId); +} + +// FIXME-DOCK: This is awkward because in series of split user is likely to loose access to its root node. +void ImGui::DockBuilderFinish(ImGuiID root_id) +{ + ImGuiContext& g = *GImGui; + //DockContextRebuild(&g); + DockContextBuildAddWindowsToNodes(&g, root_id); +} + +//----------------------------------------------------------------------------- +// Docking: Begin/End Support Functions (called from Begin/End) +//----------------------------------------------------------------------------- +// - GetWindowAlwaysWantOwnTabBar() +// - DockContextBindNodeToWindow() +// - BeginDocked() +// - BeginDockableDragDropSource() +// - BeginDockableDragDropTarget() +//----------------------------------------------------------------------------- + +bool ImGui::GetWindowAlwaysWantOwnTabBar(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + if (g.IO.ConfigDockingAlwaysTabBar || window->WindowClass.DockingAlwaysTabBar) + if ((window->Flags & (ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoDocking)) == 0) + if (!window->IsFallbackWindow) // We don't support AlwaysTabBar on the fallback/implicit window to avoid unused dock-node overhead/noise + return true; + return false; +} + +static ImGuiDockNode* ImGui::DockContextBindNodeToWindow(ImGuiContext* ctx, ImGuiWindow* window) +{ + ImGuiContext& g = *ctx; + ImGuiDockNode* node = DockContextFindNodeByID(ctx, window->DockId); + IM_ASSERT(window->DockNode == NULL); + + // We should not be docking into a split node (SetWindowDock should avoid this) + if (node && node->IsSplitNode()) + { + DockContextProcessUndockWindow(ctx, window); + return NULL; + } + + // Create node + if (node == NULL) + { + node = DockContextAddNode(ctx, window->DockId); + node->AuthorityForPos = node->AuthorityForSize = node->AuthorityForViewport = ImGuiDataAuthority_Window; + node->LastFrameAlive = g.FrameCount; + } + + // If the node just turned visible and is part of a hierarchy, it doesn't have a Size assigned by DockNodeTreeUpdatePosSize() yet, + // so we're forcing a Pos/Size update from the first ancestor that is already visible (often it will be the root node). + // If we don't do this, the window will be assigned a zero-size on its first frame, which won't ideally warm up the layout. + // This is a little wonky because we don't normally update the Pos/Size of visible node mid-frame. + if (!node->IsVisible) + { + ImGuiDockNode* ancestor_node = node; + while (!ancestor_node->IsVisible && ancestor_node->ParentNode) + ancestor_node = ancestor_node->ParentNode; + IM_ASSERT(ancestor_node->Size.x > 0.0f && ancestor_node->Size.y > 0.0f); + DockNodeUpdateHasCentralNodeChild(DockNodeGetRootNode(ancestor_node)); + DockNodeTreeUpdatePosSize(ancestor_node, ancestor_node->Pos, ancestor_node->Size, node); + } + + // Add window to node + bool node_was_visible = node->IsVisible; + DockNodeAddWindow(node, window, true); + node->IsVisible = node_was_visible; // Don't mark visible right away (so DockContextEndFrame() doesn't render it, maybe other side effects? will see) + IM_ASSERT(node == window->DockNode); + return node; +} + +void ImGui::BeginDocked(ImGuiWindow* window, bool* p_open) +{ + ImGuiContext& g = *GImGui; + + // Clear fields ahead so most early-out paths don't have to do it + window->DockIsActive = window->DockNodeIsVisible = window->DockTabIsVisible = false; + + const bool auto_dock_node = GetWindowAlwaysWantOwnTabBar(window); + if (auto_dock_node) + { + if (window->DockId == 0) + { + IM_ASSERT(window->DockNode == NULL); + window->DockId = DockContextGenNodeID(&g); + } + } + else + { + // Calling SetNextWindowPos() undock windows by default (by setting PosUndock) + bool want_undock = false; + want_undock |= (window->Flags & ImGuiWindowFlags_NoDocking) != 0; + want_undock |= (g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasPos) && (window->SetWindowPosAllowFlags & g.NextWindowData.PosCond) && g.NextWindowData.PosUndock; + if (want_undock) + { + DockContextProcessUndockWindow(&g, window); + return; + } + } + + // Bind to our dock node + ImGuiDockNode* node = window->DockNode; + if (node != NULL) + IM_ASSERT(window->DockId == node->ID); + if (window->DockId != 0 && node == NULL) + { + node = DockContextBindNodeToWindow(&g, window); + if (node == NULL) + return; + } + +#if 0 + // Undock if the ImGuiDockNodeFlags_NoDockingInCentralNode got set + if (node->IsCentralNode && (node->Flags & ImGuiDockNodeFlags_NoDockingInCentralNode)) + { + DockContextProcessUndockWindow(ctx, window); + return; + } +#endif + + // Undock if our dockspace node disappeared + // Note how we are testing for LastFrameAlive and NOT LastFrameActive. A DockSpace node can be maintained alive while being inactive with ImGuiDockNodeFlags_KeepAliveOnly. + if (node->LastFrameAlive < g.FrameCount) + { + // If the window has been orphaned, transition the docknode to an implicit node processed in DockContextNewFrameUpdateDocking() + ImGuiDockNode* root_node = DockNodeGetRootNode(node); + if (root_node->LastFrameAlive < g.FrameCount) + DockContextProcessUndockWindow(&g, window); + else + window->DockIsActive = true; + return; + } + + // Store style overrides + for (int color_n = 0; color_n < ImGuiWindowDockStyleCol_COUNT; color_n++) + window->DockStyle.Colors[color_n] = ColorConvertFloat4ToU32(g.Style.Colors[GWindowDockStyleColors[color_n]]); + + // Fast path return. It is common for windows to hold on a persistent DockId but be the only visible window, + // and never create neither a host window neither a tab bar. + // FIXME-DOCK: replace ->HostWindow NULL compare with something more explicit (~was initially intended as a first frame test) + if (node->HostWindow == NULL) + { + if (node->State == ImGuiDockNodeState_HostWindowHiddenBecauseWindowsAreResizing) + window->DockIsActive = true; + if (node->Windows.Size > 1 && window->Appearing) // Only hide appearing window + DockNodeHideWindowDuringHostWindowCreation(window); + return; + } + + // We can have zero-sized nodes (e.g. children of a small-size dockspace) + IM_ASSERT(node->HostWindow); + IM_ASSERT(node->IsLeafNode()); + IM_ASSERT(node->Size.x >= 0.0f && node->Size.y >= 0.0f); + node->State = ImGuiDockNodeState_HostWindowVisible; + + // Undock if we are submitted earlier than the host window + if (!(node->MergedFlags & ImGuiDockNodeFlags_KeepAliveOnly) && window->BeginOrderWithinContext < node->HostWindow->BeginOrderWithinContext) + { + DockContextProcessUndockWindow(&g, window); + return; + } + + // Position/Size window + SetNextWindowPos(node->Pos); + SetNextWindowSize(node->Size); + g.NextWindowData.PosUndock = false; // Cancel implicit undocking of SetNextWindowPos() + window->DockIsActive = true; + window->DockNodeIsVisible = true; + window->DockTabIsVisible = false; + if (node->MergedFlags & ImGuiDockNodeFlags_KeepAliveOnly) + return; + + // When the window is selected we mark it as visible. + if (node->VisibleWindow == window) + window->DockTabIsVisible = true; + + // Update window flag + IM_ASSERT((window->Flags & ImGuiWindowFlags_ChildWindow) == 0); + window->Flags |= ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_NoResize; + window->ChildFlags |= ImGuiChildFlags_AlwaysUseWindowPadding; + if (node->IsHiddenTabBar() || node->IsNoTabBar()) + window->Flags |= ImGuiWindowFlags_NoTitleBar; + else + window->Flags &= ~ImGuiWindowFlags_NoTitleBar; // Clear the NoTitleBar flag in case the user set it: confusingly enough we need a title bar height so we are correctly offset, but it won't be displayed! + + // Save new dock order only if the window has been visible once already + // This allows multiple windows to be created in the same frame and have their respective dock orders preserved. + if (node->TabBar && window->WasActive) + window->DockOrder = (short)DockNodeGetTabOrder(window); + + if ((node->WantCloseAll || node->WantCloseTabId == window->TabId) && p_open != NULL) + *p_open = false; + + // Update ChildId to allow returning from Child to Parent with Escape + ImGuiWindow* parent_window = window->DockNode->HostWindow; + window->ChildId = parent_window->GetID(window->Name); +} + +void ImGui::BeginDockableDragDropSource(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.ActiveId == window->MoveId); + IM_ASSERT(g.MovingWindow == window); + IM_ASSERT(g.CurrentWindow == window); + + // 0: Hold SHIFT to disable docking, 1: Hold SHIFT to enable docking. + if (g.IO.ConfigDockingWithShift != g.IO.KeyShift) + { + // When ConfigDockingWithShift is set, display a tooltip to increase UI affordance. + // We cannot set for HoveredWindowUnderMovingWindow != NULL here, as it is only valid/useful when drag and drop is already active + // (because of the 'is_mouse_dragging_with_an_expected_destination' logic in UpdateViewportsNewFrame() function) + if (g.IO.ConfigDockingWithShift && g.MouseStationaryTimer >= 1.0f && g.ActiveId >= 1.0f) + SetTooltip("%s", LocalizeGetMsg(ImGuiLocKey_DockingHoldShiftToDock)); + return; + } + + g.LastItemData.ID = window->MoveId; + window = window->RootWindowDockTree; + IM_ASSERT((window->Flags & ImGuiWindowFlags_NoDocking) == 0); + bool is_drag_docking = (g.IO.ConfigDockingWithShift) || ImRect(0, 0, window->SizeFull.x, GetFrameHeight()).Contains(g.ActiveIdClickOffset); // FIXME-DOCKING: Need to make this stateful and explicit + if (is_drag_docking && BeginDragDropSource(ImGuiDragDropFlags_SourceNoPreviewTooltip | ImGuiDragDropFlags_SourceNoHoldToOpenOthers | ImGuiDragDropFlags_SourceAutoExpirePayload)) + { + SetDragDropPayload(IMGUI_PAYLOAD_TYPE_WINDOW, &window, sizeof(window)); + EndDragDropSource(); + + // Store style overrides + for (int color_n = 0; color_n < ImGuiWindowDockStyleCol_COUNT; color_n++) + window->DockStyle.Colors[color_n] = ColorConvertFloat4ToU32(g.Style.Colors[GWindowDockStyleColors[color_n]]); + } +} + +void ImGui::BeginDockableDragDropTarget(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + + //IM_ASSERT(window->RootWindowDockTree == window); // May also be a DockSpace + IM_ASSERT((window->Flags & ImGuiWindowFlags_NoDocking) == 0); + if (!g.DragDropActive) + return; + //GetForegroundDrawList(window)->AddRect(window->Pos, window->Pos + window->Size, IM_COL32(255, 255, 0, 255)); + if (!BeginDragDropTargetCustom(window->Rect(), window->ID)) + return; + + // Peek into the payload before calling AcceptDragDropPayload() so we can handle overlapping dock nodes with filtering + // (this is a little unusual pattern, normally most code would call AcceptDragDropPayload directly) + const ImGuiPayload* payload = &g.DragDropPayload; + if (!payload->IsDataType(IMGUI_PAYLOAD_TYPE_WINDOW) || !DockNodeIsDropAllowed(window, *(ImGuiWindow**)payload->Data)) + { + EndDragDropTarget(); + return; + } + + ImGuiWindow* payload_window = *(ImGuiWindow**)payload->Data; + if (AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_WINDOW, ImGuiDragDropFlags_AcceptBeforeDelivery | ImGuiDragDropFlags_AcceptNoDrawDefaultRect)) + { + // Select target node + // (Important: we cannot use g.HoveredDockNode here! Because each of our target node have filters based on payload, each candidate drop target will do its own evaluation) + bool dock_into_floating_window = false; + ImGuiDockNode* node = NULL; + if (window->DockNodeAsHost) + { + // Cannot assume that node will != NULL even though we passed the rectangle test: it depends on padding/spacing handled by DockNodeTreeFindVisibleNodeByPos(). + node = DockNodeTreeFindVisibleNodeByPos(window->DockNodeAsHost, g.IO.MousePos); + + // There is an edge case when docking into a dockspace which only has _inactive_ nodes (because none of the windows are active) + // In this case we need to fallback into any leaf mode, possibly the central node. + // FIXME-20181220: We should not have to test for IsLeafNode() here but we have another bug to fix first. + if (node && node->IsDockSpace() && node->IsRootNode()) + node = (node->CentralNode && node->IsLeafNode()) ? node->CentralNode : DockNodeTreeFindFallbackLeafNode(node); + } + else + { + if (window->DockNode) + node = window->DockNode; + else + dock_into_floating_window = true; // Dock into a regular window + } + + const ImRect explicit_target_rect = (node && node->TabBar && !node->IsHiddenTabBar() && !node->IsNoTabBar()) ? node->TabBar->BarRect : ImRect(window->Pos, window->Pos + ImVec2(window->Size.x, GetFrameHeight())); + const bool is_explicit_target = g.IO.ConfigDockingWithShift || IsMouseHoveringRect(explicit_target_rect.Min, explicit_target_rect.Max); + + // Preview docking request and find out split direction/ratio + //const bool do_preview = true; // Ignore testing for payload->IsPreview() which removes one frame of delay, but breaks overlapping drop targets within the same window. + const bool do_preview = payload->IsPreview() || payload->IsDelivery(); + if (do_preview && (node != NULL || dock_into_floating_window)) + { + // If we have a non-leaf node it means we are hovering the border of a parent node, in which case only outer markers will appear. + ImGuiDockPreviewData split_inner; + ImGuiDockPreviewData split_outer; + ImGuiDockPreviewData* split_data = &split_inner; + if (node && (node->ParentNode || node->IsCentralNode() || !node->IsLeafNode())) + if (ImGuiDockNode* root_node = DockNodeGetRootNode(node)) + { + DockNodePreviewDockSetup(window, root_node, payload_window, NULL, &split_outer, is_explicit_target, true); + if (split_outer.IsSplitDirExplicit) + split_data = &split_outer; + } + if (!node || node->IsLeafNode()) + DockNodePreviewDockSetup(window, node, payload_window, NULL, &split_inner, is_explicit_target, false); + if (split_data == &split_outer) + split_inner.IsDropAllowed = false; + + // Draw inner then outer, so that previewed tab (in inner data) will be behind the outer drop boxes + DockNodePreviewDockRender(window, node, payload_window, &split_inner); + DockNodePreviewDockRender(window, node, payload_window, &split_outer); + + // Queue docking request + if (split_data->IsDropAllowed && payload->IsDelivery()) + DockContextQueueDock(&g, window, split_data->SplitNode, payload_window, split_data->SplitDir, split_data->SplitRatio, split_data == &split_outer); + } + } + EndDragDropTarget(); +} + +//----------------------------------------------------------------------------- +// Docking: Settings +//----------------------------------------------------------------------------- +// - DockSettingsRenameNodeReferences() +// - DockSettingsRemoveNodeReferences() +// - DockSettingsFindNodeSettings() +// - DockSettingsHandler_ApplyAll() +// - DockSettingsHandler_ReadOpen() +// - DockSettingsHandler_ReadLine() +// - DockSettingsHandler_DockNodeToSettings() +// - DockSettingsHandler_WriteAll() +//----------------------------------------------------------------------------- + +static void ImGui::DockSettingsRenameNodeReferences(ImGuiID old_node_id, ImGuiID new_node_id) +{ + ImGuiContext& g = *GImGui; + IMGUI_DEBUG_LOG_DOCKING("[docking] DockSettingsRenameNodeReferences: from 0x%08X -> to 0x%08X\n", old_node_id, new_node_id); + for (int window_n = 0; window_n < g.Windows.Size; window_n++) + { + ImGuiWindow* window = g.Windows[window_n]; + if (window->DockId == old_node_id && window->DockNode == NULL) + window->DockId = new_node_id; + } + //// FIXME-OPT: We could remove this loop by storing the index in the map + for (ImGuiWindowSettings* settings = g.SettingsWindows.begin(); settings != NULL; settings = g.SettingsWindows.next_chunk(settings)) + if (settings->DockId == old_node_id) + settings->DockId = new_node_id; +} + +// Remove references stored in ImGuiWindowSettings to the given ImGuiDockNodeSettings +static void ImGui::DockSettingsRemoveNodeReferences(ImGuiID* node_ids, int node_ids_count) +{ + ImGuiContext& g = *GImGui; + int found = 0; + //// FIXME-OPT: We could remove this loop by storing the index in the map + for (ImGuiWindowSettings* settings = g.SettingsWindows.begin(); settings != NULL; settings = g.SettingsWindows.next_chunk(settings)) + for (int node_n = 0; node_n < node_ids_count; node_n++) + if (settings->DockId == node_ids[node_n]) + { + settings->DockId = 0; + settings->DockOrder = -1; + if (++found < node_ids_count) + break; + return; + } +} + +static ImGuiDockNodeSettings* ImGui::DockSettingsFindNodeSettings(ImGuiContext* ctx, ImGuiID id) +{ + // FIXME-OPT + ImGuiDockContext* dc = &ctx->DockContext; + for (int n = 0; n < dc->NodesSettings.Size; n++) + if (dc->NodesSettings[n].ID == id) + return &dc->NodesSettings[n]; + return NULL; +} + +// Clear settings data +static void ImGui::DockSettingsHandler_ClearAll(ImGuiContext* ctx, ImGuiSettingsHandler*) +{ + ImGuiDockContext* dc = &ctx->DockContext; + dc->NodesSettings.clear(); + DockContextClearNodes(ctx, 0, true); +} + +// Recreate nodes based on settings data +static void ImGui::DockSettingsHandler_ApplyAll(ImGuiContext* ctx, ImGuiSettingsHandler*) +{ + // Prune settings at boot time only + ImGuiDockContext* dc = &ctx->DockContext; + if (ctx->Windows.Size == 0) + DockContextPruneUnusedSettingsNodes(ctx); + DockContextBuildNodesFromSettings(ctx, dc->NodesSettings.Data, dc->NodesSettings.Size); + DockContextBuildAddWindowsToNodes(ctx, 0); +} + +static void* ImGui::DockSettingsHandler_ReadOpen(ImGuiContext*, ImGuiSettingsHandler*, const char* name) +{ + if (strcmp(name, "Data") != 0) + return NULL; + return (void*)1; +} + +static void ImGui::DockSettingsHandler_ReadLine(ImGuiContext* ctx, ImGuiSettingsHandler*, void*, const char* line) +{ + char c = 0; + int x = 0, y = 0; + int r = 0; + + // Parsing, e.g. + // " DockNode ID=0x00000001 Pos=383,193 Size=201,322 Split=Y,0.506 " + // " DockNode ID=0x00000002 Parent=0x00000001 " + // Important: this code expect currently fields in a fixed order. + ImGuiDockNodeSettings node; + line = ImStrSkipBlank(line); + if (strncmp(line, "DockNode", 8) == 0) { line = ImStrSkipBlank(line + strlen("DockNode")); } + else if (strncmp(line, "DockSpace", 9) == 0) { line = ImStrSkipBlank(line + strlen("DockSpace")); node.Flags |= ImGuiDockNodeFlags_DockSpace; } + else return; + if (sscanf(line, "ID=0x%08X%n", &node.ID, &r) == 1) { line += r; } else return; + if (sscanf(line, " Parent=0x%08X%n", &node.ParentNodeId, &r) == 1) { line += r; if (node.ParentNodeId == 0) return; } + if (sscanf(line, " Window=0x%08X%n", &node.ParentWindowId, &r) ==1) { line += r; if (node.ParentWindowId == 0) return; } + if (node.ParentNodeId == 0) + { + if (sscanf(line, " Pos=%i,%i%n", &x, &y, &r) == 2) { line += r; node.Pos = ImVec2ih((short)x, (short)y); } else return; + if (sscanf(line, " Size=%i,%i%n", &x, &y, &r) == 2) { line += r; node.Size = ImVec2ih((short)x, (short)y); } else return; + } + else + { + if (sscanf(line, " SizeRef=%i,%i%n", &x, &y, &r) == 2) { line += r; node.SizeRef = ImVec2ih((short)x, (short)y); } + } + if (sscanf(line, " Split=%c%n", &c, &r) == 1) { line += r; if (c == 'X') node.SplitAxis = ImGuiAxis_X; else if (c == 'Y') node.SplitAxis = ImGuiAxis_Y; } + if (sscanf(line, " NoResize=%d%n", &x, &r) == 1) { line += r; if (x != 0) node.Flags |= ImGuiDockNodeFlags_NoResize; } + if (sscanf(line, " CentralNode=%d%n", &x, &r) == 1) { line += r; if (x != 0) node.Flags |= ImGuiDockNodeFlags_CentralNode; } + if (sscanf(line, " NoTabBar=%d%n", &x, &r) == 1) { line += r; if (x != 0) node.Flags |= ImGuiDockNodeFlags_NoTabBar; } + if (sscanf(line, " HiddenTabBar=%d%n", &x, &r) == 1) { line += r; if (x != 0) node.Flags |= ImGuiDockNodeFlags_HiddenTabBar; } + if (sscanf(line, " NoWindowMenuButton=%d%n", &x, &r) == 1) { line += r; if (x != 0) node.Flags |= ImGuiDockNodeFlags_NoWindowMenuButton; } + if (sscanf(line, " NoCloseButton=%d%n", &x, &r) == 1) { line += r; if (x != 0) node.Flags |= ImGuiDockNodeFlags_NoCloseButton; } + if (sscanf(line, " Selected=0x%08X%n", &node.SelectedTabId,&r) == 1) { line += r; } + if (node.ParentNodeId != 0) + if (ImGuiDockNodeSettings* parent_settings = DockSettingsFindNodeSettings(ctx, node.ParentNodeId)) + node.Depth = parent_settings->Depth + 1; + ctx->DockContext.NodesSettings.push_back(node); +} + +static void DockSettingsHandler_DockNodeToSettings(ImGuiDockContext* dc, ImGuiDockNode* node, int depth) +{ + ImGuiDockNodeSettings node_settings; + IM_ASSERT(depth < (1 << (sizeof(node_settings.Depth) << 3))); + node_settings.ID = node->ID; + node_settings.ParentNodeId = node->ParentNode ? node->ParentNode->ID : 0; + node_settings.ParentWindowId = (node->IsDockSpace() && node->HostWindow && node->HostWindow->ParentWindow) ? node->HostWindow->ParentWindow->ID : 0; + node_settings.SelectedTabId = node->SelectedTabId; + node_settings.SplitAxis = (signed char)(node->IsSplitNode() ? node->SplitAxis : ImGuiAxis_None); + node_settings.Depth = (char)depth; + node_settings.Flags = (node->LocalFlags & ImGuiDockNodeFlags_SavedFlagsMask_); + node_settings.Pos = ImVec2ih(node->Pos); + node_settings.Size = ImVec2ih(node->Size); + node_settings.SizeRef = ImVec2ih(node->SizeRef); + dc->NodesSettings.push_back(node_settings); + if (node->ChildNodes[0]) + DockSettingsHandler_DockNodeToSettings(dc, node->ChildNodes[0], depth + 1); + if (node->ChildNodes[1]) + DockSettingsHandler_DockNodeToSettings(dc, node->ChildNodes[1], depth + 1); +} + +static void ImGui::DockSettingsHandler_WriteAll(ImGuiContext* ctx, ImGuiSettingsHandler* handler, ImGuiTextBuffer* buf) +{ + ImGuiContext& g = *ctx; + ImGuiDockContext* dc = &ctx->DockContext; + if (!(g.IO.ConfigFlags & ImGuiConfigFlags_DockingEnable)) + return; + + // Gather settings data + // (unlike our windows settings, because nodes are always built we can do a full rewrite of the SettingsNode buffer) + dc->NodesSettings.resize(0); + dc->NodesSettings.reserve(dc->Nodes.Data.Size); + for (int n = 0; n < dc->Nodes.Data.Size; n++) + if (ImGuiDockNode* node = (ImGuiDockNode*)dc->Nodes.Data[n].val_p) + if (node->IsRootNode()) + DockSettingsHandler_DockNodeToSettings(dc, node, 0); + + int max_depth = 0; + for (int node_n = 0; node_n < dc->NodesSettings.Size; node_n++) + max_depth = ImMax((int)dc->NodesSettings[node_n].Depth, max_depth); + + // Write to text buffer + buf->appendf("[%s][Data]\n", handler->TypeName); + for (int node_n = 0; node_n < dc->NodesSettings.Size; node_n++) + { + const int line_start_pos = buf->size(); (void)line_start_pos; + const ImGuiDockNodeSettings* node_settings = &dc->NodesSettings[node_n]; + buf->appendf("%*s%s%*s", node_settings->Depth * 2, "", (node_settings->Flags & ImGuiDockNodeFlags_DockSpace) ? "DockSpace" : "DockNode ", (max_depth - node_settings->Depth) * 2, ""); // Text align nodes to facilitate looking at .ini file + buf->appendf(" ID=0x%08X", node_settings->ID); + if (node_settings->ParentNodeId) + { + buf->appendf(" Parent=0x%08X SizeRef=%d,%d", node_settings->ParentNodeId, node_settings->SizeRef.x, node_settings->SizeRef.y); + } + else + { + if (node_settings->ParentWindowId) + buf->appendf(" Window=0x%08X", node_settings->ParentWindowId); + buf->appendf(" Pos=%d,%d Size=%d,%d", node_settings->Pos.x, node_settings->Pos.y, node_settings->Size.x, node_settings->Size.y); + } + if (node_settings->SplitAxis != ImGuiAxis_None) + buf->appendf(" Split=%c", (node_settings->SplitAxis == ImGuiAxis_X) ? 'X' : 'Y'); + if (node_settings->Flags & ImGuiDockNodeFlags_NoResize) + buf->appendf(" NoResize=1"); + if (node_settings->Flags & ImGuiDockNodeFlags_CentralNode) + buf->appendf(" CentralNode=1"); + if (node_settings->Flags & ImGuiDockNodeFlags_NoTabBar) + buf->appendf(" NoTabBar=1"); + if (node_settings->Flags & ImGuiDockNodeFlags_HiddenTabBar) + buf->appendf(" HiddenTabBar=1"); + if (node_settings->Flags & ImGuiDockNodeFlags_NoWindowMenuButton) + buf->appendf(" NoWindowMenuButton=1"); + if (node_settings->Flags & ImGuiDockNodeFlags_NoCloseButton) + buf->appendf(" NoCloseButton=1"); + if (node_settings->SelectedTabId) + buf->appendf(" Selected=0x%08X", node_settings->SelectedTabId); + + // [DEBUG] Include comments in the .ini file to ease debugging (this makes saving slower!) + if (g.IO.ConfigDebugIniSettings) + if (ImGuiDockNode* node = DockContextFindNodeByID(ctx, node_settings->ID)) + { + buf->appendf("%*s", ImMax(2, (line_start_pos + 92) - buf->size()), ""); // Align everything + if (node->IsDockSpace() && node->HostWindow && node->HostWindow->ParentWindow) + buf->appendf(" ; in '%s'", node->HostWindow->ParentWindow->Name); + // Iterate settings so we can give info about windows that didn't exist during the session. + int contains_window = 0; + for (ImGuiWindowSettings* settings = g.SettingsWindows.begin(); settings != NULL; settings = g.SettingsWindows.next_chunk(settings)) + if (settings->DockId == node_settings->ID) + { + if (contains_window++ == 0) + buf->appendf(" ; contains "); + buf->appendf("'%s' ", settings->GetName()); + } + } + + buf->appendf("\n"); + } + buf->appendf("\n"); +} //----------------------------------------------------------------------------- @@ -13276,14 +19578,14 @@ static void SetPlatformImeDataFn_DefaultImpl(ImGuiViewport* viewport, ImGuiPlatf if (HIMC himc = ::ImmGetContext(hwnd)) { COMPOSITIONFORM composition_form = {}; - composition_form.ptCurrentPos.x = (LONG)data->InputPos.x; - composition_form.ptCurrentPos.y = (LONG)data->InputPos.y; + composition_form.ptCurrentPos.x = (LONG)(data->InputPos.x - viewport->Pos.x); + composition_form.ptCurrentPos.y = (LONG)(data->InputPos.y - viewport->Pos.y); composition_form.dwStyle = CFS_FORCE_POSITION; ::ImmSetCompositionWindow(himc, &composition_form); CANDIDATEFORM candidate_form = {}; candidate_form.dwStyle = CFS_CANDIDATEPOS; - candidate_form.ptCurrentPos.x = (LONG)data->InputPos.x; - candidate_form.ptCurrentPos.y = (LONG)data->InputPos.y; + candidate_form.ptCurrentPos.x = (LONG)(data->InputPos.x - viewport->Pos.x); + candidate_form.ptCurrentPos.y = (LONG)(data->InputPos.y - viewport->Pos.y); ::ImmSetCandidateWindow(himc, &candidate_form); ::ImmReleaseContext(hwnd, himc); } @@ -13298,13 +19600,14 @@ static void SetPlatformImeDataFn_DefaultImpl(ImGuiViewport*, ImGuiPlatformImeDat //----------------------------------------------------------------------------- // [SECTION] METRICS/DEBUGGER WINDOW //----------------------------------------------------------------------------- -// - RenderViewportThumbnail() [Internal] +// - DebugRenderViewportThumbnail() [Internal] // - RenderViewportsThumbnails() [Internal] // - DebugTextEncoding() // - MetricsHelpMarker() [Internal] // - ShowFontAtlas() [Internal] // - ShowMetricsWindow() // - DebugNodeColumns() [Internal] +// - DebugNodeDockNode() [Internal] // - DebugNodeDrawList() [Internal] // - DebugNodeDrawCmdShowMeshAndBoundingBox() [Internal] // - DebugNodeFont() [Internal] @@ -13327,18 +19630,19 @@ void ImGui::DebugRenderViewportThumbnail(ImDrawList* draw_list, ImGuiViewportP* ImVec2 scale = bb.GetSize() / viewport->Size; ImVec2 off = bb.Min - viewport->Pos * scale; - float alpha_mul = 1.0f; + float alpha_mul = (viewport->Flags & ImGuiViewportFlags_IsMinimized) ? 0.30f : 1.00f; window->DrawList->AddRectFilled(bb.Min, bb.Max, GetColorU32(ImGuiCol_Border, alpha_mul * 0.40f)); - for (int i = 0; i != g.Windows.Size; i++) + for (ImGuiWindow* thumb_window : g.Windows) { - ImGuiWindow* thumb_window = g.Windows[i]; if (!thumb_window->WasActive || (thumb_window->Flags & ImGuiWindowFlags_ChildWindow)) continue; + if (thumb_window->Viewport != viewport) + continue; ImRect thumb_r = thumb_window->Rect(); ImRect title_r = thumb_window->TitleBarRect(); - thumb_r = ImRect(ImFloor(off + thumb_r.Min * scale), ImFloor(off + thumb_r.Max * scale)); - title_r = ImRect(ImFloor(off + title_r.Min * scale), ImFloor(off + ImVec2(title_r.Max.x, title_r.Min.y) * scale) + ImVec2(0,5)); // Exaggerate title bar height + thumb_r = ImRect(ImTrunc(off + thumb_r.Min * scale), ImTrunc(off + thumb_r.Max * scale)); + title_r = ImRect(ImTrunc(off + title_r.Min * scale), ImTrunc(off + ImVec2(title_r.Max.x, title_r.Min.y + title_r.GetHeight() * 3.0f) * scale)); // Exaggerate title bar height thumb_r.ClipWithFull(bb); title_r.ClipWithFull(bb); const bool window_is_focused = (g.NavWindow && thumb_window->RootWindowForTitleBarHighlight == g.NavWindow->RootWindowForTitleBarHighlight); @@ -13348,6 +19652,8 @@ void ImGui::DebugRenderViewportThumbnail(ImDrawList* draw_list, ImGuiViewportP* window->DrawList->AddText(g.Font, g.FontSize * 1.0f, title_r.Min, GetColorU32(ImGuiCol_Text, alpha_mul), thumb_window->Name, FindRenderedTextEnd(thumb_window->Name)); } draw_list->AddRect(bb.Min, bb.Max, GetColorU32(ImGuiCol_Border, alpha_mul)); + if (viewport->ID == g.DebugMetricsConfig.HighlightViewportID) + window->DrawList->AddRect(bb.Min, bb.Max, IM_COL32(255, 255, 0, 255)); } static void RenderViewportsThumbnails() @@ -13355,33 +19661,48 @@ static void RenderViewportsThumbnails() ImGuiContext& g = *GImGui; ImGuiWindow* window = g.CurrentWindow; - // We don't display full monitor bounds (we could, but it often looks awkward), instead we display just enough to cover all of our viewports. + // Draw monitor and calculate their boundaries float SCALE = 1.0f / 8.0f; ImRect bb_full(FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX); - for (int n = 0; n < g.Viewports.Size; n++) - bb_full.Add(g.Viewports[n]->GetMainRect()); + for (ImGuiPlatformMonitor& monitor : g.PlatformIO.Monitors) + bb_full.Add(ImRect(monitor.MainPos, monitor.MainPos + monitor.MainSize)); ImVec2 p = window->DC.CursorPos; ImVec2 off = p - bb_full.Min * SCALE; - for (int n = 0; n < g.Viewports.Size; n++) + for (ImGuiPlatformMonitor& monitor : g.PlatformIO.Monitors) + { + ImRect monitor_draw_bb(off + (monitor.MainPos) * SCALE, off + (monitor.MainPos + monitor.MainSize) * SCALE); + window->DrawList->AddRect(monitor_draw_bb.Min, monitor_draw_bb.Max, (g.DebugMetricsConfig.HighlightMonitorIdx == g.PlatformIO.Monitors.index_from_ptr(&monitor)) ? IM_COL32(255, 255, 0, 255) : ImGui::GetColorU32(ImGuiCol_Border), 4.0f); + window->DrawList->AddRectFilled(monitor_draw_bb.Min, monitor_draw_bb.Max, ImGui::GetColorU32(ImGuiCol_Border, 0.10f), 4.0f); + } + + // Draw viewports + for (ImGuiViewportP* viewport : g.Viewports) { - ImGuiViewportP* viewport = g.Viewports[n]; ImRect viewport_draw_bb(off + (viewport->Pos) * SCALE, off + (viewport->Pos + viewport->Size) * SCALE); ImGui::DebugRenderViewportThumbnail(window->DrawList, viewport, viewport_draw_bb); } ImGui::Dummy(bb_full.GetSize() * SCALE); } +static int IMGUI_CDECL ViewportComparerByLastFocusedStampCount(const void* lhs, const void* rhs) +{ + const ImGuiViewportP* a = *(const ImGuiViewportP* const*)lhs; + const ImGuiViewportP* b = *(const ImGuiViewportP* const*)rhs; + return b->LastFocusedStampCount - a->LastFocusedStampCount; +} + // Draw an arbitrary US keyboard layout to visualize translated keys void ImGui::DebugRenderKeyboardPreview(ImDrawList* draw_list) { - const ImVec2 key_size = ImVec2(35.0f, 35.0f); - const float key_rounding = 3.0f; - const ImVec2 key_face_size = ImVec2(25.0f, 25.0f); - const ImVec2 key_face_pos = ImVec2(5.0f, 3.0f); - const float key_face_rounding = 2.0f; - const ImVec2 key_label_pos = ImVec2(7.0f, 4.0f); + const float scale = ImGui::GetFontSize() / 13.0f; + const ImVec2 key_size = ImVec2(35.0f, 35.0f) * scale; + const float key_rounding = 3.0f * scale; + const ImVec2 key_face_size = ImVec2(25.0f, 25.0f) * scale; + const ImVec2 key_face_pos = ImVec2(5.0f, 3.0f) * scale; + const float key_face_rounding = 2.0f * scale; + const ImVec2 key_label_pos = ImVec2(7.0f, 4.0f) * scale; const ImVec2 key_step = ImVec2(key_size.x - 1.0f, key_size.y - 1.0f); - const float key_row_offset = 9.0f; + const float key_row_offset = 9.0f * scale; ImVec2 board_min = GetCursorScreenPos(); ImVec2 board_max = ImVec2(board_min.x + 3 * key_step.x + 2 * key_row_offset + 10.0f, board_min.y + 3 * key_step.y + 10.0f); @@ -13456,11 +19777,40 @@ void ImGui::DebugTextEncoding(const char* str) EndTable(); } +static void DebugFlashStyleColorStop() +{ + ImGuiContext& g = *GImGui; + if (g.DebugFlashStyleColorIdx != ImGuiCol_COUNT) + g.Style.Colors[g.DebugFlashStyleColorIdx] = g.DebugFlashStyleColorBackup; + g.DebugFlashStyleColorIdx = ImGuiCol_COUNT; +} + +// Flash a given style color for some + inhibit modifications of this color via PushStyleColor() calls. +void ImGui::DebugFlashStyleColor(ImGuiCol idx) +{ + ImGuiContext& g = *GImGui; + DebugFlashStyleColorStop(); + g.DebugFlashStyleColorTime = 0.5f; + g.DebugFlashStyleColorIdx = idx; + g.DebugFlashStyleColorBackup = g.Style.Colors[idx]; +} + +void ImGui::UpdateDebugToolFlashStyleColor() +{ + ImGuiContext& g = *GImGui; + if (g.DebugFlashStyleColorTime <= 0.0f) + return; + ColorConvertHSVtoRGB(cosf(g.DebugFlashStyleColorTime * 6.0f) * 0.5f + 0.5f, 0.5f, 0.5f, g.Style.Colors[g.DebugFlashStyleColorIdx].x, g.Style.Colors[g.DebugFlashStyleColorIdx].y, g.Style.Colors[g.DebugFlashStyleColorIdx].z); + g.Style.Colors[g.DebugFlashStyleColorIdx].w = 1.0f; + if ((g.DebugFlashStyleColorTime -= g.IO.DeltaTime) <= 0.0f) + DebugFlashStyleColorStop(); +} + // Avoid naming collision with imgui_demo.cpp's HelpMarker() for unity builds. static void MetricsHelpMarker(const char* desc) { ImGui::TextDisabled("(?)"); - if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort) && ImGui::BeginTooltip()) + if (ImGui::BeginItemTooltip()) { ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f); ImGui::TextUnformatted(desc); @@ -13472,9 +19822,8 @@ static void MetricsHelpMarker(const char* desc) // [DEBUG] List fonts in a font atlas and display its texture void ImGui::ShowFontAtlas(ImFontAtlas* atlas) { - for (int i = 0; i < atlas->Fonts.Size; i++) + for (ImFont* font : atlas->Fonts) { - ImFont* font = atlas->Fonts[i]; PushID(font); DebugNodeFont(font); PopID(); @@ -13498,8 +19847,8 @@ void ImGui::ShowMetricsWindow(bool* p_open) ImGuiMetricsConfig* cfg = &g.DebugMetricsConfig; if (cfg->ShowDebugLog) ShowDebugLogWindow(&cfg->ShowDebugLog); - if (cfg->ShowStackTool) - ShowStackToolWindow(&cfg->ShowStackTool); + if (cfg->ShowIDStackTool) + ShowIDStackToolWindow(&cfg->ShowIDStackTool); if (!Begin("Dear ImGui Metrics/Debugger", p_open) || GetCurrentWindow()->BeginCount > 1) { @@ -13507,11 +19856,14 @@ void ImGui::ShowMetricsWindow(bool* p_open) return; } + // [DEBUG] Clear debug breaks hooks after exactly one cycle. + DebugBreakClearData(); + // Basic info Text("Dear ImGui %s", GetVersion()); Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0f / io.Framerate, io.Framerate); Text("%d vertices, %d indices (%d triangles)", io.MetricsRenderVertices, io.MetricsRenderIndices, io.MetricsRenderIndices / 3); - Text("%d visible windows, %d active allocations", io.MetricsRenderWindows, io.MetricsActiveAllocations); + Text("%d visible windows, %d current allocations", io.MetricsRenderWindows, g.DebugAllocInfo.TotalAllocCount - g.DebugAllocInfo.TotalFreeCount); //SameLine(); if (SmallButton("GC")) { g.GcCompactAll = true; } Separator(); @@ -13540,8 +19892,8 @@ void ImGui::ShowMetricsWindow(bool* p_open) else if (rect_type == TRT_ColumnsRect) { ImGuiTableColumn* c = &table->Columns[n]; return ImRect(c->MinX, table->InnerClipRect.Min.y, c->MaxX, table->InnerClipRect.Min.y + table_instance->LastOuterHeight); } else if (rect_type == TRT_ColumnsWorkRect) { ImGuiTableColumn* c = &table->Columns[n]; return ImRect(c->WorkMinX, table->WorkRect.Min.y, c->WorkMaxX, table->WorkRect.Max.y); } else if (rect_type == TRT_ColumnsClipRect) { ImGuiTableColumn* c = &table->Columns[n]; return c->ClipRect; } - else if (rect_type == TRT_ColumnsContentHeadersUsed){ ImGuiTableColumn* c = &table->Columns[n]; return ImRect(c->WorkMinX, table->InnerClipRect.Min.y, c->ContentMaxXHeadersUsed, table->InnerClipRect.Min.y + table_instance->LastFirstRowHeight); } // Note: y1/y2 not always accurate - else if (rect_type == TRT_ColumnsContentHeadersIdeal){ImGuiTableColumn* c = &table->Columns[n]; return ImRect(c->WorkMinX, table->InnerClipRect.Min.y, c->ContentMaxXHeadersIdeal, table->InnerClipRect.Min.y + table_instance->LastFirstRowHeight); } + else if (rect_type == TRT_ColumnsContentHeadersUsed){ ImGuiTableColumn* c = &table->Columns[n]; return ImRect(c->WorkMinX, table->InnerClipRect.Min.y, c->ContentMaxXHeadersUsed, table->InnerClipRect.Min.y + table_instance->LastTopHeadersRowHeight); } // Note: y1/y2 not always accurate + else if (rect_type == TRT_ColumnsContentHeadersIdeal){ImGuiTableColumn* c = &table->Columns[n]; return ImRect(c->WorkMinX, table->InnerClipRect.Min.y, c->ContentMaxXHeadersIdeal, table->InnerClipRect.Min.y + table_instance->LastTopHeadersRowHeight); } else if (rect_type == TRT_ColumnsContentFrozen) { ImGuiTableColumn* c = &table->Columns[n]; return ImRect(c->WorkMinX, table->InnerClipRect.Min.y, c->ContentMaxXFrozen, table->InnerClipRect.Min.y + table_instance->LastFrozenHeight); } else if (rect_type == TRT_ColumnsContentUnfrozen) { ImGuiTableColumn* c = &table->Columns[n]; return ImRect(c->WorkMinX, table->InnerClipRect.Min.y + table_instance->LastFrozenHeight, c->ContentMaxXUnfrozen, table->InnerClipRect.Max.y); } IM_ASSERT(0); @@ -13555,7 +19907,7 @@ void ImGui::ShowMetricsWindow(bool* p_open) else if (rect_type == WRT_InnerRect) { return window->InnerRect; } else if (rect_type == WRT_InnerClipRect) { return window->InnerClipRect; } else if (rect_type == WRT_WorkRect) { return window->WorkRect; } - else if (rect_type == WRT_Content) { ImVec2 min = window->InnerRect.Min - window->Scroll + window->WindowPadding; return ImRect(min, min + window->ContentSize); } + else if (rect_type == WRT_Content) { ImVec2 min = window->InnerRect.Min - window->Scroll + window->WindowPadding; return ImRect(min, min + window->ContentSize); } else if (rect_type == WRT_ContentIdeal) { ImVec2 min = window->InnerRect.Min - window->Scroll + window->WindowPadding; return ImRect(min, min + window->ContentSizeIdeal); } else if (rect_type == WRT_ContentRegionRect) { return window->ContentRegionRect; } IM_ASSERT(0); @@ -13566,34 +19918,24 @@ void ImGui::ShowMetricsWindow(bool* p_open) // Tools if (TreeNode("Tools")) { - bool show_encoding_viewer = TreeNode("UTF-8 Encoding viewer"); - SameLine(); - MetricsHelpMarker("You can also call ImGui::DebugTextEncoding() from your code with a given string to test that your UTF-8 encoding settings are correct."); - if (show_encoding_viewer) - { - static char buf[100] = ""; - SetNextItemWidth(-FLT_MIN); - InputText("##Text", buf, IM_ARRAYSIZE(buf)); - if (buf[0] != 0) - DebugTextEncoding(buf); - TreePop(); - } - + // Debug Break features // The Item Picker tool is super useful to visually select an item and break into the call-stack of where it was submitted. - if (Checkbox("Show Item Picker", &g.DebugItemPickerActive) && g.DebugItemPickerActive) - DebugStartItemPicker(); + SeparatorTextEx(0, "Debug breaks", NULL, CalcTextSize("(?)").x + g.Style.SeparatorTextPadding.x); SameLine(); MetricsHelpMarker("Will call the IM_DEBUG_BREAK() macro to break in debugger.\nWarning: If you don't have a debugger attached, this will probably crash."); + if (Checkbox("Show Item Picker", &g.DebugItemPickerActive) && g.DebugItemPickerActive) + DebugStartItemPicker(); + Checkbox("Show \"Debug Break\" buttons in other sections (io.ConfigDebugIsDebuggerPresent)", &g.IO.ConfigDebugIsDebuggerPresent); + + SeparatorText("Visualize"); - // Stack Tool is your best friend! Checkbox("Show Debug Log", &cfg->ShowDebugLog); SameLine(); MetricsHelpMarker("You can also call ImGui::ShowDebugLogWindow() from your code."); - // Stack Tool is your best friend! - Checkbox("Show Stack Tool", &cfg->ShowStackTool); + Checkbox("Show ID Stack Tool", &cfg->ShowIDStackTool); SameLine(); - MetricsHelpMarker("You can also call ImGui::ShowStackToolWindow() from your code."); + MetricsHelpMarker("You can also call ImGui::ShowIDStackToolWindow() from your code."); Checkbox("Show windows begin order", &cfg->ShowWindowsBeginOrder); Checkbox("Show windows rectangles", &cfg->ShowWindowsRects); @@ -13656,11 +19998,26 @@ void ImGui::ShowMetricsWindow(bool* p_open) Unindent(); } } + Checkbox("Show groups rectangles", &g.DebugShowGroupRects); // Storing in context as this is used by group code and prefers to be in hot-data + + SeparatorText("Validate"); Checkbox("Debug Begin/BeginChild return value", &io.ConfigDebugBeginReturnValueLoop); SameLine(); MetricsHelpMarker("Some calls to Begin()/BeginChild() will return false.\n\nWill cycle through window depths then repeat. Windows should be flickering while running."); + Checkbox("UTF-8 Encoding viewer", &cfg->ShowTextEncodingViewer); + SameLine(); + MetricsHelpMarker("You can also call ImGui::DebugTextEncoding() from your code with a given string to test that your UTF-8 encoding settings are correct."); + if (cfg->ShowTextEncodingViewer) + { + static char buf[64] = ""; + SetNextItemWidth(-FLT_MIN); + InputText("##DebugTextEncodingBuf", buf, IM_ARRAYSIZE(buf)); + if (buf[0] != 0) + DebugTextEncoding(buf); + } + TreePop(); } @@ -13675,9 +20032,9 @@ void ImGui::ShowMetricsWindow(bool* p_open) // Here we display windows in their submitted order/hierarchy, however note that the Begin stack doesn't constitute a Parent<>Child relationship! ImVector& temp_buffer = g.WindowsTempSortBuffer; temp_buffer.resize(0); - for (int i = 0; i < g.Windows.Size; i++) - if (g.Windows[i]->LastFrameActive + 1 >= g.FrameCount) - temp_buffer.push_back(g.Windows[i]); + for (ImGuiWindow* window : g.Windows) + if (window->LastFrameActive + 1 >= g.FrameCount) + temp_buffer.push_back(window); struct Func { static int IMGUI_CDECL WindowComparerByBeginOrder(const void* lhs, const void* rhs) { return ((int)(*(const ImGuiWindow* const *)lhs)->BeginOrderWithinContext - (*(const ImGuiWindow* const*)rhs)->BeginOrderWithinContext); } }; ImQsort(temp_buffer.Data, (size_t)temp_buffer.Size, sizeof(ImGuiWindow*), Func::WindowComparerByBeginOrder); DebugNodeWindowsListByBeginStackParent(temp_buffer.Data, temp_buffer.Size, NULL); @@ -13689,18 +20046,22 @@ void ImGui::ShowMetricsWindow(bool* p_open) // DrawLists int drawlist_count = 0; - for (int viewport_i = 0; viewport_i < g.Viewports.Size; viewport_i++) - drawlist_count += g.Viewports[viewport_i]->DrawDataBuilder.GetDrawListCount(); + for (ImGuiViewportP* viewport : g.Viewports) + drawlist_count += viewport->DrawDataP.CmdLists.Size; if (TreeNode("DrawLists", "DrawLists (%d)", drawlist_count)) { Checkbox("Show ImDrawCmd mesh when hovering", &cfg->ShowDrawCmdMesh); Checkbox("Show ImDrawCmd bounding boxes when hovering", &cfg->ShowDrawCmdBoundingBoxes); - for (int viewport_i = 0; viewport_i < g.Viewports.Size; viewport_i++) + for (ImGuiViewportP* viewport : g.Viewports) { - ImGuiViewportP* viewport = g.Viewports[viewport_i]; - for (int layer_i = 0; layer_i < IM_ARRAYSIZE(viewport->DrawDataBuilder.Layers); layer_i++) - for (int draw_list_i = 0; draw_list_i < viewport->DrawDataBuilder.Layers[layer_i].Size; draw_list_i++) - DebugNodeDrawList(NULL, viewport->DrawDataBuilder.Layers[layer_i][draw_list_i], "DrawList"); + bool viewport_has_drawlist = false; + for (ImDrawList* draw_list : viewport->DrawDataP.CmdLists) + { + if (!viewport_has_drawlist) + Text("Active DrawLists in Viewport #%d, ID: 0x%08X", viewport->Idx, viewport->ID); + viewport_has_drawlist = true; + DebugNodeDrawList(NULL, viewport, draw_list, "DrawList"); + } } TreePop(); } @@ -13708,25 +20069,68 @@ void ImGui::ShowMetricsWindow(bool* p_open) // Viewports if (TreeNode("Viewports", "Viewports (%d)", g.Viewports.Size)) { - Indent(GetTreeNodeToLabelSpacing()); - RenderViewportsThumbnails(); - Unindent(GetTreeNodeToLabelSpacing()); - for (int i = 0; i < g.Viewports.Size; i++) - DebugNodeViewport(g.Viewports[i]); + cfg->HighlightMonitorIdx = -1; + bool open = TreeNode("Monitors", "Monitors (%d)", g.PlatformIO.Monitors.Size); + SameLine(); + MetricsHelpMarker("Dear ImGui uses monitor data:\n- to query DPI settings on a per monitor basis\n- to position popup/tooltips so they don't straddle monitors."); + if (open) + { + for (int i = 0; i < g.PlatformIO.Monitors.Size; i++) + { + const ImGuiPlatformMonitor& mon = g.PlatformIO.Monitors[i]; + BulletText("Monitor #%d: DPI %.0f%%\n MainMin (%.0f,%.0f), MainMax (%.0f,%.0f), MainSize (%.0f,%.0f)\n WorkMin (%.0f,%.0f), WorkMax (%.0f,%.0f), WorkSize (%.0f,%.0f)", + i, mon.DpiScale * 100.0f, + mon.MainPos.x, mon.MainPos.y, mon.MainPos.x + mon.MainSize.x, mon.MainPos.y + mon.MainSize.y, mon.MainSize.x, mon.MainSize.y, + mon.WorkPos.x, mon.WorkPos.y, mon.WorkPos.x + mon.WorkSize.x, mon.WorkPos.y + mon.WorkSize.y, mon.WorkSize.x, mon.WorkSize.y); + if (IsItemHovered()) + cfg->HighlightMonitorIdx = i; + } + TreePop(); + } + + SetNextItemOpen(true, ImGuiCond_Once); + if (TreeNode("Windows Minimap")) + { + RenderViewportsThumbnails(); + TreePop(); + } + cfg->HighlightViewportID = 0; + + BulletText("MouseViewport: 0x%08X (UserHovered 0x%08X, LastHovered 0x%08X)", g.MouseViewport ? g.MouseViewport->ID : 0, g.IO.MouseHoveredViewport, g.MouseLastHoveredViewport ? g.MouseLastHoveredViewport->ID : 0); + if (TreeNode("Inferred Z order (front-to-back)")) + { + static ImVector viewports; + viewports.resize(g.Viewports.Size); + memcpy(viewports.Data, g.Viewports.Data, g.Viewports.size_in_bytes()); + if (viewports.Size > 1) + ImQsort(viewports.Data, viewports.Size, sizeof(ImGuiViewport*), ViewportComparerByLastFocusedStampCount); + for (ImGuiViewportP* viewport : viewports) + { + BulletText("Viewport #%d, ID: 0x%08X, LastFocused = %08d, PlatformFocused = %s, Window: \"%s\"", + viewport->Idx, viewport->ID, viewport->LastFocusedStampCount, + (g.PlatformIO.Platform_GetWindowFocus && viewport->PlatformWindowCreated) ? (g.PlatformIO.Platform_GetWindowFocus(viewport) ? "1" : "0") : "N/A", + viewport->Window ? viewport->Window->Name : "N/A"); + if (IsItemHovered()) + cfg->HighlightViewportID = viewport->ID; + } + TreePop(); + } + + for (ImGuiViewportP* viewport : g.Viewports) + DebugNodeViewport(viewport); TreePop(); } // Details for Popups if (TreeNode("Popups", "Popups (%d)", g.OpenPopupStack.Size)) { - for (int i = 0; i < g.OpenPopupStack.Size; i++) + for (const ImGuiPopupData& popup_data : g.OpenPopupStack) { // As it's difficult to interact with tree nodes while popups are open, we display everything inline. - const ImGuiPopupData* popup_data = &g.OpenPopupStack[i]; - ImGuiWindow* window = popup_data->Window; + ImGuiWindow* window = popup_data.Window; BulletText("PopupID: %08x, Window: '%s' (%s%s), BackupNavWindow '%s', ParentWindow '%s'", - popup_data->PopupId, window ? window->Name : "NULL", window && (window->Flags & ImGuiWindowFlags_ChildWindow) ? "Child;" : "", window && (window->Flags & ImGuiWindowFlags_ChildMenu) ? "Menu;" : "", - popup_data->BackupNavWindow ? popup_data->BackupNavWindow->Name : "NULL", window && window->ParentWindow ? window->ParentWindow->Name : "NULL"); + popup_data.PopupId, window ? window->Name : "NULL", window && (window->Flags & ImGuiWindowFlags_ChildWindow) ? "Child;" : "", window && (window->Flags & ImGuiWindowFlags_ChildMenu) ? "Menu;" : "", + popup_data.BackupNavWindow ? popup_data.BackupNavWindow->Name : "NULL", window && window->ParentWindow ? window->ParentWindow->Name : "NULL"); } TreePop(); } @@ -13768,10 +20172,28 @@ void ImGui::ShowMetricsWindow(bool* p_open) TreePop(); } + // Details for TypingSelect + if (TreeNode("TypingSelect", "TypingSelect (%d)", g.TypingSelectState.SearchBuffer[0] != 0 ? 1 : 0)) + { + DebugNodeTypingSelectState(&g.TypingSelectState); + TreePop(); + } + // Details for Docking #ifdef IMGUI_HAS_DOCK if (TreeNode("Docking")) { + static bool root_nodes_only = true; + ImGuiDockContext* dc = &g.DockContext; + Checkbox("List root nodes", &root_nodes_only); + Checkbox("Ctrl shows window dock info", &cfg->ShowDockingNodes); + if (SmallButton("Clear nodes")) { DockContextClearNodes(&g, 0, true); } + SameLine(); + if (SmallButton("Rebuild all")) { dc->WantFullRebuild = true; } + for (int n = 0; n < dc->Nodes.Data.Size; n++) + if (ImGuiDockNode* node = (ImGuiDockNode*)dc->Nodes.Data[n].val_p) + if (!root_nodes_only || node->IsRootNode()) + DebugNodeDockNode(node, "Node"); TreePop(); } #endif // #ifdef IMGUI_HAS_DOCK @@ -13792,11 +20214,12 @@ void ImGui::ShowMetricsWindow(bool* p_open) Text("\"%s\"", g.IO.IniFilename); else TextUnformatted(""); + Checkbox("io.ConfigDebugIniSettings", &io.ConfigDebugIniSettings); Text("SettingsDirtyTimer %.2f", g.SettingsDirtyTimer); if (TreeNode("SettingsHandlers", "Settings handlers: (%d)", g.SettingsHandlers.Size)) { - for (int n = 0; n < g.SettingsHandlers.Size; n++) - BulletText("%s", g.SettingsHandlers[n].TypeName); + for (ImGuiSettingsHandler& handler : g.SettingsHandlers) + BulletText("\"%s\"", handler.TypeName); TreePop(); } if (TreeNode("SettingsWindows", "Settings packed data: Windows: %d bytes", g.SettingsWindows.size())) @@ -13814,6 +20237,29 @@ void ImGui::ShowMetricsWindow(bool* p_open) } #ifdef IMGUI_HAS_DOCK + if (TreeNode("SettingsDocking", "Settings packed data: Docking")) + { + ImGuiDockContext* dc = &g.DockContext; + Text("In SettingsWindows:"); + for (ImGuiWindowSettings* settings = g.SettingsWindows.begin(); settings != NULL; settings = g.SettingsWindows.next_chunk(settings)) + if (settings->DockId != 0) + BulletText("Window '%s' -> DockId %08X DockOrder=%d", settings->GetName(), settings->DockId, settings->DockOrder); + Text("In SettingsNodes:"); + for (int n = 0; n < dc->NodesSettings.Size; n++) + { + ImGuiDockNodeSettings* settings = &dc->NodesSettings[n]; + const char* selected_tab_name = NULL; + if (settings->SelectedTabId) + { + if (ImGuiWindow* window = FindWindowByID(settings->SelectedTabId)) + selected_tab_name = window->Name; + else if (ImGuiWindowSettings* window_settings = FindWindowSettingsByID(settings->SelectedTabId)) + selected_tab_name = window_settings->GetName(); + } + BulletText("Node %08X, Parent %08X, SelectedTab %08X ('%s')", settings->ID, settings->ParentNodeId, settings->SelectedTabId, selected_tab_name ? selected_tab_name : settings->SelectedTabId ? "N/A" : ""); + } + TreePop(); + } #endif // #ifdef IMGUI_HAS_DOCK if (TreeNode("SettingsIniData", "Settings unpacked data (.ini): %d bytes", g.SettingsIniData.size())) @@ -13824,6 +20270,22 @@ void ImGui::ShowMetricsWindow(bool* p_open) TreePop(); } + // Settings + if (TreeNode("Memory allocations")) + { + ImGuiDebugAllocInfo* info = &g.DebugAllocInfo; + Text("%d current allocations", info->TotalAllocCount - info->TotalFreeCount); + if (SmallButton("GC now")) { g.GcCompactAll = true; } + Text("Recent frames with allocations:"); + int buf_size = IM_ARRAYSIZE(info->LastEntriesBuf); + for (int n = buf_size - 1; n >= 0; n--) + { + ImGuiDebugAllocEntry* entry = &info->LastEntriesBuf[(info->LastEntriesIdx - n + buf_size) % buf_size]; + BulletText("Frame %06d: %+3d ( %2d malloc, %2d free )%s", entry->FrameCount, entry->AllocCount - entry->FreeCount, entry->AllocCount, entry->FreeCount, (n == 0) ? " (most recent)" : ""); + } + TreePop(); + } + if (TreeNode("Inputs")) { Text("KEYBOARD/GAMEPAD/MOUSE KEYS"); @@ -13834,7 +20296,7 @@ void ImGui::ShowMetricsWindow(bool* p_open) #ifdef IMGUI_DISABLE_OBSOLETE_KEYIO struct funcs { static bool IsLegacyNativeDupe(ImGuiKey) { return false; } }; #else - struct funcs { static bool IsLegacyNativeDupe(ImGuiKey key) { return key < 512 && GetIO().KeyMap[key] != -1; } }; // Hide Native<>ImGuiKey duplicates when both exists in the array + struct funcs { static bool IsLegacyNativeDupe(ImGuiKey key) { return key >= 0 && key < 512 && GetIO().KeyMap[key] != -1; } }; // Hide Native<>ImGuiKey duplicates when both exists in the array //Text("Legacy raw:"); for (ImGuiKey key = ImGuiKey_KeysData_OFFSET; key < ImGuiKey_COUNT; key++) { if (io.KeysDown[key]) { SameLine(); Text("\"%s\" %d", GetKeyName(key), key); } } #endif Text("Keys down:"); for (ImGuiKey key = ImGuiKey_KeysData_OFFSET; key < ImGuiKey_COUNT; key = (ImGuiKey)(key + 1)) { if (funcs::IsLegacyNativeDupe(key) || !IsKeyDown(key)) continue; SameLine(); Text(IsNamedKey(key) ? "\"%s\"" : "\"%s\" %d", GetKeyName(key), key); SameLine(); Text("(%.02f)", GetKeyData(key)->DownDuration); } @@ -13859,6 +20321,7 @@ void ImGui::ShowMetricsWindow(bool* p_open) Text("Mouse clicked:"); for (int i = 0; i < count; i++) if (IsMouseClicked(i)) { SameLine(); Text("b%d (%d)", i, io.MouseClickedCount[i]); } Text("Mouse released:"); for (int i = 0; i < count; i++) if (IsMouseReleased(i)) { SameLine(); Text("b%d", i); } Text("Mouse wheel: %.1f", io.MouseWheel); + Text("MouseStationaryTimer: %.2f", g.MouseStationaryTimer); Text("Mouse source: %s", GetMouseSourceName(io.MouseSource)); Text("Pen Pressure: %.1f", io.PenPressure); // Note: currently unused Unindent(); @@ -13876,8 +20339,7 @@ void ImGui::ShowMetricsWindow(bool* p_open) Text("KEY OWNERS"); { Indent(); - if (BeginListBox("##owners", ImVec2(-FLT_MIN, GetTextLineHeightWithSpacing() * 6))) - { + if (BeginChild("##owners", ImVec2(-FLT_MIN, GetTextLineHeightWithSpacing() * 8), ImGuiChildFlags_FrameStyle | ImGuiChildFlags_ResizeY, ImGuiWindowFlags_NoSavedSettings)) for (ImGuiKey key = ImGuiKey_NamedKey_BEGIN; key < ImGuiKey_NamedKey_END; key = (ImGuiKey)(key + 1)) { ImGuiKeyOwnerData* owner_data = GetKeyOwnerData(&g, key); @@ -13887,30 +20349,34 @@ void ImGui::ShowMetricsWindow(bool* p_open) owner_data->LockUntilRelease ? " LockUntilRelease" : owner_data->LockThisFrame ? " LockThisFrame" : ""); DebugLocateItemOnHover(owner_data->OwnerCurr); } - EndListBox(); - } + EndChild(); Unindent(); } Text("SHORTCUT ROUTING"); + SameLine(); + MetricsHelpMarker("Declared shortcut routes automatically set key owner when mods matches."); { Indent(); - if (BeginListBox("##routes", ImVec2(-FLT_MIN, GetTextLineHeightWithSpacing() * 6))) - { + if (BeginChild("##routes", ImVec2(-FLT_MIN, GetTextLineHeightWithSpacing() * 8), ImGuiChildFlags_FrameStyle | ImGuiChildFlags_ResizeY, ImGuiWindowFlags_NoSavedSettings)) for (ImGuiKey key = ImGuiKey_NamedKey_BEGIN; key < ImGuiKey_NamedKey_END; key = (ImGuiKey)(key + 1)) { ImGuiKeyRoutingTable* rt = &g.KeysRoutingTable; for (ImGuiKeyRoutingIndex idx = rt->Index[key - ImGuiKey_NamedKey_BEGIN]; idx != -1; ) { - char key_chord_name[64]; ImGuiKeyRoutingData* routing_data = &rt->Entries[idx]; - GetKeyChordName(key | routing_data->Mods, key_chord_name, IM_ARRAYSIZE(key_chord_name)); - Text("%s: 0x%08X", key_chord_name, routing_data->RoutingCurr); + ImGuiKeyChord key_chord = key | routing_data->Mods; + Text("%s: 0x%08X (scored %d)", GetKeyChordName(key_chord), routing_data->RoutingCurr, routing_data->RoutingCurrScore); DebugLocateItemOnHover(routing_data->RoutingCurr); + if (g.IO.ConfigDebugIsDebuggerPresent) + { + SameLine(); + if (DebugBreakButton("**DebugBreak**", "in SetShortcutRouting() for this KeyChord")) + g.DebugBreakInShortcutRouting = key_chord; + } idx = routing_data->NextEntryIndex; } } - EndListBox(); - } + EndChild(); Text("(ActiveIdUsing: AllKeyboardKeys: %d, NavDirMask: 0x%X)", g.ActiveIdUsingAllKeyboardKeys, g.ActiveIdUsingNavDirMask); Unindent(); } @@ -13922,9 +20388,11 @@ void ImGui::ShowMetricsWindow(bool* p_open) Text("WINDOWING"); Indent(); Text("HoveredWindow: '%s'", g.HoveredWindow ? g.HoveredWindow->Name : "NULL"); - Text("HoveredWindow->Root: '%s'", g.HoveredWindow ? g.HoveredWindow->RootWindow->Name : "NULL"); + Text("HoveredWindow->Root: '%s'", g.HoveredWindow ? g.HoveredWindow->RootWindowDockTree->Name : "NULL"); Text("HoveredWindowUnderMovingWindow: '%s'", g.HoveredWindowUnderMovingWindow ? g.HoveredWindowUnderMovingWindow->Name : "NULL"); + Text("HoveredDockNode: 0x%08X", g.DebugHoveredDockNode ? g.DebugHoveredDockNode->ID : 0); Text("MovingWindow: '%s'", g.MovingWindow ? g.MovingWindow->Name : "NULL"); + Text("MouseViewport: 0x%08X (UserHovered 0x%08X, LastHovered 0x%08X)", g.MouseViewport->ID, g.IO.MouseHoveredViewport, g.MouseLastHoveredViewport ? g.MouseLastHoveredViewport->ID : 0); Unindent(); Text("ITEMS"); @@ -13934,7 +20402,7 @@ void ImGui::ShowMetricsWindow(bool* p_open) Text("ActiveIdWindow: '%s'", g.ActiveIdWindow ? g.ActiveIdWindow->Name : "NULL"); Text("ActiveIdUsing: AllKeyboardKeys: %d, NavDirMask: %X", g.ActiveIdUsingAllKeyboardKeys, g.ActiveIdUsingNavDirMask); Text("HoveredId: 0x%08X (%.2f sec), AllowOverlap: %d", g.HoveredIdPreviousFrame, g.HoveredIdTimer, g.HoveredIdAllowOverlap); // Not displaying g.HoveredId as it is update mid-frame - Text("HoverDelayId: 0x%08X, Timer: %.2f, ClearTimer: %.2f", g.HoverDelayId, g.HoverDelayTimer, g.HoverDelayClearTimer); + Text("HoverItemDelayId: 0x%08X, Timer: %.2f, ClearTimer: %.2f", g.HoverItemDelayId, g.HoverItemDelayTimer, g.HoverItemDelayClearTimer); Text("DragDrop: %d, SourceId = 0x%08X, Payload \"%s\" (%d bytes)", g.DragDropActive, g.DragDropPayload.SourceId, g.DragDropPayload.DataType, g.DragDropPayload.DataSize); DebugLocateItemOnHover(g.DragDropPayload.SourceId); Unindent(); @@ -13945,11 +20413,20 @@ void ImGui::ShowMetricsWindow(bool* p_open) Text("NavId: 0x%08X, NavLayer: %d", g.NavId, g.NavLayer); DebugLocateItemOnHover(g.NavId); Text("NavInputSource: %s", GetInputSourceName(g.NavInputSource)); + Text("NavLastValidSelectionUserData = %" IM_PRId64 " (0x%" IM_PRIX64 ")", g.NavLastValidSelectionUserData, g.NavLastValidSelectionUserData); Text("NavActive: %d, NavVisible: %d", g.IO.NavActive, g.IO.NavVisible); Text("NavActivateId/DownId/PressedId: %08X/%08X/%08X", g.NavActivateId, g.NavActivateDownId, g.NavActivatePressedId); Text("NavActivateFlags: %04X", g.NavActivateFlags); Text("NavDisableHighlight: %d, NavDisableMouseHover: %d", g.NavDisableHighlight, g.NavDisableMouseHover); Text("NavFocusScopeId = 0x%08X", g.NavFocusScopeId); + Text("NavFocusRoute[] = "); + for (int path_n = g.NavFocusRoute.Size - 1; path_n >= 0; path_n--) + { + const ImGuiFocusScopeData& focus_scope = g.NavFocusRoute[path_n]; + SameLine(0.0f, 0.0f); + Text("0x%08X/", focus_scope.ID); + SetItemTooltip("In window \"%s\"", FindWindowByID(focus_scope.WindowID)->Name); + } Text("NavWindowingTarget: '%s'", g.NavWindowingTarget ? g.NavWindowingTarget->Name : "NULL"); Unindent(); @@ -13959,9 +20436,8 @@ void ImGui::ShowMetricsWindow(bool* p_open) // Overlay: Display windows Rectangles and Begin Order if (cfg->ShowWindowsRects || cfg->ShowWindowsBeginOrder) { - for (int n = 0; n < g.Windows.Size; n++) + for (ImGuiWindow* window : g.Windows) { - ImGuiWindow* window = g.Windows[n]; if (!window->WasActive) continue; ImDrawList* draw_list = GetForegroundDrawList(window); @@ -14010,27 +20486,196 @@ void ImGui::ShowMetricsWindow(bool* p_open) #ifdef IMGUI_HAS_DOCK // Overlay: Display Docking info - if (show_docking_nodes && g.IO.KeyCtrl) + if (cfg->ShowDockingNodes && g.IO.KeyCtrl && g.DebugHoveredDockNode) { + char buf[64] = ""; + char* p = buf; + ImGuiDockNode* node = g.DebugHoveredDockNode; + ImDrawList* overlay_draw_list = node->HostWindow ? GetForegroundDrawList(node->HostWindow) : GetForegroundDrawList(GetMainViewport()); + p += ImFormatString(p, buf + IM_ARRAYSIZE(buf) - p, "DockId: %X%s\n", node->ID, node->IsCentralNode() ? " *CentralNode*" : ""); + p += ImFormatString(p, buf + IM_ARRAYSIZE(buf) - p, "WindowClass: %08X\n", node->WindowClass.ClassId); + p += ImFormatString(p, buf + IM_ARRAYSIZE(buf) - p, "Size: (%.0f, %.0f)\n", node->Size.x, node->Size.y); + p += ImFormatString(p, buf + IM_ARRAYSIZE(buf) - p, "SizeRef: (%.0f, %.0f)\n", node->SizeRef.x, node->SizeRef.y); + int depth = DockNodeGetDepth(node); + overlay_draw_list->AddRect(node->Pos + ImVec2(3, 3) * (float)depth, node->Pos + node->Size - ImVec2(3, 3) * (float)depth, IM_COL32(200, 100, 100, 255)); + ImVec2 pos = node->Pos + ImVec2(3, 3) * (float)depth; + overlay_draw_list->AddRectFilled(pos - ImVec2(1, 1), pos + CalcTextSize(buf) + ImVec2(1, 1), IM_COL32(200, 100, 100, 255)); + overlay_draw_list->AddText(NULL, 0.0f, pos, IM_COL32(255, 255, 255, 255), buf); } #endif // #ifdef IMGUI_HAS_DOCK End(); } +void ImGui::DebugBreakClearData() +{ + // Those fields are scattered in their respective subsystem to stay in hot-data locations + ImGuiContext& g = *GImGui; + g.DebugBreakInWindow = 0; + g.DebugBreakInTable = 0; + g.DebugBreakInShortcutRouting = ImGuiKey_None; +} + +void ImGui::DebugBreakButtonTooltip(bool keyboard_only, const char* description_of_location) +{ + if (!BeginItemTooltip()) + return; + Text("To call IM_DEBUG_BREAK() %s:", description_of_location); + Separator(); + TextUnformatted(keyboard_only ? "- Press 'Pause/Break' on keyboard." : "- Press 'Pause/Break' on keyboard.\n- or Click (may alter focus/active id).\n- or navigate using keyboard and press space."); + Separator(); + TextUnformatted("Choose one way that doesn't interfere with what you are trying to debug!\nYou need a debugger attached or this will crash!"); + EndTooltip(); +} + +// Special button that doesn't take focus, doesn't take input owner, and can be activated without a click etc. +// In order to reduce interferences with the contents we are trying to debug into. +bool ImGui::DebugBreakButton(const char* label, const char* description_of_location) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const ImGuiID id = window->GetID(label); + const ImVec2 label_size = CalcTextSize(label, NULL, true); + ImVec2 pos = window->DC.CursorPos + ImVec2(0.0f, window->DC.CurrLineTextBaseOffset); + ImVec2 size = ImVec2(label_size.x + g.Style.FramePadding.x * 2.0f, label_size.y); + + const ImRect bb(pos, pos + size); + ItemSize(size, 0.0f); + if (!ItemAdd(bb, id)) + return false; + + // WE DO NOT USE ButtonEx() or ButtonBehavior() in order to reduce our side-effects. + bool hovered = ItemHoverable(bb, id, g.CurrentItemFlags); + bool pressed = hovered && (IsKeyChordPressed(g.DebugBreakKeyChord) || IsMouseClicked(0) || g.NavActivateId == id); + DebugBreakButtonTooltip(false, description_of_location); + + ImVec4 col4f = GetStyleColorVec4(hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button); + ImVec4 hsv; + ColorConvertRGBtoHSV(col4f.x, col4f.y, col4f.z, hsv.x, hsv.y, hsv.z); + ColorConvertHSVtoRGB(hsv.x + 0.20f, hsv.y, hsv.z, col4f.x, col4f.y, col4f.z); + + RenderNavHighlight(bb, id); + RenderFrame(bb.Min, bb.Max, GetColorU32(col4f), true, g.Style.FrameRounding); + RenderTextClipped(bb.Min, bb.Max, label, NULL, &label_size, g.Style.ButtonTextAlign, &bb); + + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags); + return pressed; +} + // [DEBUG] Display contents of Columns void ImGui::DebugNodeColumns(ImGuiOldColumns* columns) { if (!TreeNode((void*)(uintptr_t)columns->ID, "Columns Id: 0x%08X, Count: %d, Flags: 0x%04X", columns->ID, columns->Count, columns->Flags)) return; BulletText("Width: %.1f (MinX: %.1f, MaxX: %.1f)", columns->OffMaxX - columns->OffMinX, columns->OffMinX, columns->OffMaxX); - for (int column_n = 0; column_n < columns->Columns.Size; column_n++) - BulletText("Column %02d: OffsetNorm %.3f (= %.1f px)", column_n, columns->Columns[column_n].OffsetNorm, GetColumnOffsetFromNorm(columns, columns->Columns[column_n].OffsetNorm)); + for (ImGuiOldColumnData& column : columns->Columns) + BulletText("Column %02d: OffsetNorm %.3f (= %.1f px)", (int)columns->Columns.index_from_ptr(&column), column.OffsetNorm, GetColumnOffsetFromNorm(columns, column.OffsetNorm)); TreePop(); } +static void DebugNodeDockNodeFlags(ImGuiDockNodeFlags* p_flags, const char* label, bool enabled) +{ + using namespace ImGui; + PushID(label); + PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0.0f, 0.0f)); + Text("%s:", label); + if (!enabled) + BeginDisabled(); + CheckboxFlags("NoResize", p_flags, ImGuiDockNodeFlags_NoResize); + CheckboxFlags("NoResizeX", p_flags, ImGuiDockNodeFlags_NoResizeX); + CheckboxFlags("NoResizeY",p_flags, ImGuiDockNodeFlags_NoResizeY); + CheckboxFlags("NoTabBar", p_flags, ImGuiDockNodeFlags_NoTabBar); + CheckboxFlags("HiddenTabBar", p_flags, ImGuiDockNodeFlags_HiddenTabBar); + CheckboxFlags("NoWindowMenuButton", p_flags, ImGuiDockNodeFlags_NoWindowMenuButton); + CheckboxFlags("NoCloseButton", p_flags, ImGuiDockNodeFlags_NoCloseButton); + CheckboxFlags("DockedWindowsInFocusRoute", p_flags, ImGuiDockNodeFlags_DockedWindowsInFocusRoute); + CheckboxFlags("NoDocking", p_flags, ImGuiDockNodeFlags_NoDocking); // Multiple flags + CheckboxFlags("NoDockingSplit", p_flags, ImGuiDockNodeFlags_NoDockingSplit); + CheckboxFlags("NoDockingSplitOther", p_flags, ImGuiDockNodeFlags_NoDockingSplitOther); + CheckboxFlags("NoDockingOver", p_flags, ImGuiDockNodeFlags_NoDockingOverMe); + CheckboxFlags("NoDockingOverOther", p_flags, ImGuiDockNodeFlags_NoDockingOverOther); + CheckboxFlags("NoDockingOverEmpty", p_flags, ImGuiDockNodeFlags_NoDockingOverEmpty); + CheckboxFlags("NoUndocking", p_flags, ImGuiDockNodeFlags_NoUndocking); + if (!enabled) + EndDisabled(); + PopStyleVar(); + PopID(); +} + +// [DEBUG] Display contents of ImDockNode +void ImGui::DebugNodeDockNode(ImGuiDockNode* node, const char* label) +{ + ImGuiContext& g = *GImGui; + const bool is_alive = (g.FrameCount - node->LastFrameAlive < 2); // Submitted with ImGuiDockNodeFlags_KeepAliveOnly + const bool is_active = (g.FrameCount - node->LastFrameActive < 2); // Submitted + if (!is_alive) { PushStyleColor(ImGuiCol_Text, GetStyleColorVec4(ImGuiCol_TextDisabled)); } + bool open; + ImGuiTreeNodeFlags tree_node_flags = node->IsFocused ? ImGuiTreeNodeFlags_Selected : ImGuiTreeNodeFlags_None; + if (node->Windows.Size > 0) + open = TreeNodeEx((void*)(intptr_t)node->ID, tree_node_flags, "%s 0x%04X%s: %d windows (vis: '%s')", label, node->ID, node->IsVisible ? "" : " (hidden)", node->Windows.Size, node->VisibleWindow ? node->VisibleWindow->Name : "NULL"); + else + open = TreeNodeEx((void*)(intptr_t)node->ID, tree_node_flags, "%s 0x%04X%s: %s (vis: '%s')", label, node->ID, node->IsVisible ? "" : " (hidden)", (node->SplitAxis == ImGuiAxis_X) ? "horizontal split" : (node->SplitAxis == ImGuiAxis_Y) ? "vertical split" : "empty", node->VisibleWindow ? node->VisibleWindow->Name : "NULL"); + if (!is_alive) { PopStyleColor(); } + if (is_active && IsItemHovered()) + if (ImGuiWindow* window = node->HostWindow ? node->HostWindow : node->VisibleWindow) + GetForegroundDrawList(window)->AddRect(node->Pos, node->Pos + node->Size, IM_COL32(255, 255, 0, 255)); + if (open) + { + IM_ASSERT(node->ChildNodes[0] == NULL || node->ChildNodes[0]->ParentNode == node); + IM_ASSERT(node->ChildNodes[1] == NULL || node->ChildNodes[1]->ParentNode == node); + BulletText("Pos (%.0f,%.0f), Size (%.0f, %.0f) Ref (%.0f, %.0f)", + node->Pos.x, node->Pos.y, node->Size.x, node->Size.y, node->SizeRef.x, node->SizeRef.y); + DebugNodeWindow(node->HostWindow, "HostWindow"); + DebugNodeWindow(node->VisibleWindow, "VisibleWindow"); + BulletText("SelectedTabID: 0x%08X, LastFocusedNodeID: 0x%08X", node->SelectedTabId, node->LastFocusedNodeId); + BulletText("Misc:%s%s%s%s%s%s%s", + node->IsDockSpace() ? " IsDockSpace" : "", + node->IsCentralNode() ? " IsCentralNode" : "", + is_alive ? " IsAlive" : "", is_active ? " IsActive" : "", node->IsFocused ? " IsFocused" : "", + node->WantLockSizeOnce ? " WantLockSizeOnce" : "", + node->HasCentralNodeChild ? " HasCentralNodeChild" : ""); + if (TreeNode("flags", "Flags Merged: 0x%04X, Local: 0x%04X, InWindows: 0x%04X, Shared: 0x%04X", node->MergedFlags, node->LocalFlags, node->LocalFlagsInWindows, node->SharedFlags)) + { + if (BeginTable("flags", 4)) + { + TableNextColumn(); DebugNodeDockNodeFlags(&node->MergedFlags, "MergedFlags", false); + TableNextColumn(); DebugNodeDockNodeFlags(&node->LocalFlags, "LocalFlags", true); + TableNextColumn(); DebugNodeDockNodeFlags(&node->LocalFlagsInWindows, "LocalFlagsInWindows", false); + TableNextColumn(); DebugNodeDockNodeFlags(&node->SharedFlags, "SharedFlags", true); + EndTable(); + } + TreePop(); + } + if (node->ParentNode) + DebugNodeDockNode(node->ParentNode, "ParentNode"); + if (node->ChildNodes[0]) + DebugNodeDockNode(node->ChildNodes[0], "Child[0]"); + if (node->ChildNodes[1]) + DebugNodeDockNode(node->ChildNodes[1], "Child[1]"); + if (node->TabBar) + DebugNodeTabBar(node->TabBar, "TabBar"); + DebugNodeWindowsList(&node->Windows, "Windows"); + + TreePop(); + } +} + +static void FormatTextureIDForDebugDisplay(char* buf, int buf_size, ImTextureID tex_id) +{ + union { void* ptr; int integer; } tex_id_opaque; + memcpy(&tex_id_opaque, &tex_id, ImMin(sizeof(void*), sizeof(tex_id))); + if (sizeof(tex_id) >= sizeof(void*)) + ImFormatString(buf, buf_size, "0x%p", tex_id_opaque.ptr); + else + ImFormatString(buf, buf_size, "0x%04X", tex_id_opaque.integer); +} + // [DEBUG] Display contents of ImDrawList -void ImGui::DebugNodeDrawList(ImGuiWindow* window, const ImDrawList* draw_list, const char* label) +// Note that both 'window' and 'viewport' may be NULL here. Viewport is generally null of destroyed popups which previously owned a viewport. +void ImGui::DebugNodeDrawList(ImGuiWindow* window, ImGuiViewportP* viewport, const ImDrawList* draw_list, const char* label) { ImGuiContext& g = *GImGui; ImGuiMetricsConfig* cfg = &g.DebugMetricsConfig; @@ -14047,7 +20692,7 @@ void ImGui::DebugNodeDrawList(ImGuiWindow* window, const ImDrawList* draw_list, return; } - ImDrawList* fg_draw_list = GetForegroundDrawList(window); // Render additional visuals into the top-most draw list + ImDrawList* fg_draw_list = viewport ? GetForegroundDrawList(viewport) : NULL; // Render additional visuals into the top-most draw list if (window && IsItemHovered() && fg_draw_list) fg_draw_list->AddRect(window->Pos, window->Pos + window->Size, IM_COL32(255, 255, 0, 255)); if (!node_open) @@ -14064,10 +20709,11 @@ void ImGui::DebugNodeDrawList(ImGuiWindow* window, const ImDrawList* draw_list, continue; } + char texid_desc[20]; + FormatTextureIDForDebugDisplay(texid_desc, IM_ARRAYSIZE(texid_desc), pcmd->TextureId); char buf[300]; - ImFormatString(buf, IM_ARRAYSIZE(buf), "DrawCmd:%5d tris, Tex 0x%p, ClipRect (%4.0f,%4.0f)-(%4.0f,%4.0f)", - pcmd->ElemCount / 3, (void*)(intptr_t)pcmd->TextureId, - pcmd->ClipRect.x, pcmd->ClipRect.y, pcmd->ClipRect.z, pcmd->ClipRect.w); + ImFormatString(buf, IM_ARRAYSIZE(buf), "DrawCmd:%5d tris, Tex %s, ClipRect (%4.0f,%4.0f)-(%4.0f,%4.0f)", + pcmd->ElemCount / 3, texid_desc, pcmd->ClipRect.x, pcmd->ClipRect.y, pcmd->ClipRect.z, pcmd->ClipRect.w); bool pcmd_node_open = TreeNode((void*)(pcmd - draw_list->CmdBuffer.begin()), "%s", buf); if (IsItemHovered() && (cfg->ShowDrawCmdMesh || cfg->ShowDrawCmdBoundingBoxes) && fg_draw_list) DebugNodeDrawCmdShowMeshAndBoundingBox(fg_draw_list, draw_list, pcmd, cfg->ShowDrawCmdMesh, cfg->ShowDrawCmdBoundingBoxes); @@ -14147,8 +20793,8 @@ void ImGui::DebugNodeDrawCmdShowMeshAndBoundingBox(ImDrawList* out_draw_list, co // Draw bounding boxes if (show_aabb) { - out_draw_list->AddRect(ImFloor(clip_rect.Min), ImFloor(clip_rect.Max), IM_COL32(255, 0, 255, 255)); // In pink: clipping rectangle submitted to GPU - out_draw_list->AddRect(ImFloor(vtxs_rect.Min), ImFloor(vtxs_rect.Max), IM_COL32(0, 255, 255, 255)); // In cyan: bounding box of triangles + out_draw_list->AddRect(ImTrunc(clip_rect.Min), ImTrunc(clip_rect.Max), IM_COL32(255, 0, 255, 255)); // In pink: clipping rectangle submitted to GPU + out_draw_list->AddRect(ImTrunc(vtxs_rect.Min), ImTrunc(vtxs_rect.Max), IM_COL32(0, 255, 255, 255)); // In cyan: bounding box of triangles } out_draw_list->Flags = backup_flags; } @@ -14173,7 +20819,7 @@ void ImGui::DebugNodeFont(ImFont* font) SetNextItemWidth(GetFontSize() * 8); DragFloat("Font scale", &font->Scale, 0.005f, 0.3f, 2.0f, "%.1f"); SameLine(); MetricsHelpMarker( - "Note than the default embedded font is NOT meant to be scaled.\n\n" + "Note that the default embedded font is NOT meant to be scaled.\n\n" "Font are currently rendered into bitmaps at a given size at the time of building the atlas. " "You may oversample them to get some flexibility with scaling. " "You can also render at multiple sizes and select which one to use at runtime.\n\n" @@ -14259,11 +20905,8 @@ void ImGui::DebugNodeStorage(ImGuiStorage* storage, const char* label) { if (!TreeNode(label, "%s: %d entries, %d bytes", label, storage->Data.Size, storage->Data.size_in_bytes())) return; - for (int n = 0; n < storage->Data.Size; n++) - { - const ImGuiStorage::ImGuiStoragePair& p = storage->Data[n]; + for (const ImGuiStorage::ImGuiStoragePair& p : storage->Data) BulletText("Key 0x%08X Value { i: %d }", p.key, p.val_i); // Important: we currently don't store a type, real value may not be integer. - } TreePop(); } @@ -14310,20 +20953,36 @@ void ImGui::DebugNodeTabBar(ImGuiTabBar* tab_bar, const char* label) void ImGui::DebugNodeViewport(ImGuiViewportP* viewport) { + ImGuiContext& g = *GImGui; SetNextItemOpen(true, ImGuiCond_Once); - if (TreeNode("viewport0", "Viewport #%d", 0)) + bool open = TreeNode((void*)(intptr_t)viewport->ID, "Viewport #%d, ID: 0x%08X, Parent: 0x%08X, Window: \"%s\"", viewport->Idx, viewport->ID, viewport->ParentViewportId, viewport->Window ? viewport->Window->Name : "N/A"); + if (IsItemHovered()) + g.DebugMetricsConfig.HighlightViewportID = viewport->ID; + if (open) { ImGuiWindowFlags flags = viewport->Flags; - BulletText("Main Pos: (%.0f,%.0f), Size: (%.0f,%.0f)\nWorkArea Offset Left: %.0f Top: %.0f, Right: %.0f, Bottom: %.0f", + BulletText("Main Pos: (%.0f,%.0f), Size: (%.0f,%.0f)\nWorkArea Offset Left: %.0f Top: %.0f, Right: %.0f, Bottom: %.0f\nMonitor: %d, DpiScale: %.0f%%", viewport->Pos.x, viewport->Pos.y, viewport->Size.x, viewport->Size.y, - viewport->WorkOffsetMin.x, viewport->WorkOffsetMin.y, viewport->WorkOffsetMax.x, viewport->WorkOffsetMax.y); - BulletText("Flags: 0x%04X =%s%s%s", viewport->Flags, - (flags & ImGuiViewportFlags_IsPlatformWindow) ? " IsPlatformWindow" : "", + viewport->WorkOffsetMin.x, viewport->WorkOffsetMin.y, viewport->WorkOffsetMax.x, viewport->WorkOffsetMax.y, + viewport->PlatformMonitor, viewport->DpiScale * 100.0f); + if (viewport->Idx > 0) { SameLine(); if (SmallButton("Reset Pos")) { viewport->Pos = ImVec2(200, 200); viewport->UpdateWorkRect(); if (viewport->Window) viewport->Window->Pos = viewport->Pos; } } + BulletText("Flags: 0x%04X =%s%s%s%s%s%s%s%s%s%s%s%s%s", viewport->Flags, + //(flags & ImGuiViewportFlags_IsPlatformWindow) ? " IsPlatformWindow" : "", // Omitting because it is the standard (flags & ImGuiViewportFlags_IsPlatformMonitor) ? " IsPlatformMonitor" : "", - (flags & ImGuiViewportFlags_OwnedByApp) ? " OwnedByApp" : ""); - for (int layer_i = 0; layer_i < IM_ARRAYSIZE(viewport->DrawDataBuilder.Layers); layer_i++) - for (int draw_list_i = 0; draw_list_i < viewport->DrawDataBuilder.Layers[layer_i].Size; draw_list_i++) - DebugNodeDrawList(NULL, viewport->DrawDataBuilder.Layers[layer_i][draw_list_i], "DrawList"); + (flags & ImGuiViewportFlags_IsMinimized) ? " IsMinimized" : "", + (flags & ImGuiViewportFlags_IsFocused) ? " IsFocused" : "", + (flags & ImGuiViewportFlags_OwnedByApp) ? " OwnedByApp" : "", + (flags & ImGuiViewportFlags_NoDecoration) ? " NoDecoration" : "", + (flags & ImGuiViewportFlags_NoTaskBarIcon) ? " NoTaskBarIcon" : "", + (flags & ImGuiViewportFlags_NoFocusOnAppearing) ? " NoFocusOnAppearing" : "", + (flags & ImGuiViewportFlags_NoFocusOnClick) ? " NoFocusOnClick" : "", + (flags & ImGuiViewportFlags_NoInputs) ? " NoInputs" : "", + (flags & ImGuiViewportFlags_NoRendererClear) ? " NoRendererClear" : "", + (flags & ImGuiViewportFlags_NoAutoMerge) ? " NoAutoMerge" : "", + (flags & ImGuiViewportFlags_TopMost) ? " TopMost" : "", + (flags & ImGuiViewportFlags_CanHostOtherWindows) ? " CanHostOtherWindows" : ""); + for (ImDrawList* draw_list : viewport->DrawDataP.CmdLists) + DebugNodeDrawList(NULL, viewport, draw_list, "DrawList"); TreePop(); } } @@ -14350,13 +21009,17 @@ void ImGui::DebugNodeWindow(ImGuiWindow* window, const char* label) if (window->MemoryCompacted) TextDisabled("Note: some memory buffers have been compacted/freed."); + if (g.IO.ConfigDebugIsDebuggerPresent && DebugBreakButton("**DebugBreak**", "in Begin()")) + g.DebugBreakInWindow = window->ID; + ImGuiWindowFlags flags = window->Flags; - DebugNodeDrawList(window, window->DrawList, "DrawList"); + DebugNodeDrawList(window, window->Viewport, window->DrawList, "DrawList"); BulletText("Pos: (%.1f,%.1f), Size: (%.1f,%.1f), ContentSize (%.1f,%.1f) Ideal (%.1f,%.1f)", window->Pos.x, window->Pos.y, window->Size.x, window->Size.y, window->ContentSize.x, window->ContentSize.y, window->ContentSizeIdeal.x, window->ContentSizeIdeal.y); BulletText("Flags: 0x%08X (%s%s%s%s%s%s%s%s%s..)", flags, (flags & ImGuiWindowFlags_ChildWindow) ? "Child " : "", (flags & ImGuiWindowFlags_Tooltip) ? "Tooltip " : "", (flags & ImGuiWindowFlags_Popup) ? "Popup " : "", (flags & ImGuiWindowFlags_Modal) ? "Modal " : "", (flags & ImGuiWindowFlags_ChildMenu) ? "ChildMenu " : "", (flags & ImGuiWindowFlags_NoSavedSettings) ? "NoSavedSettings " : "", (flags & ImGuiWindowFlags_NoMouseInputs)? "NoMouseInputs":"", (flags & ImGuiWindowFlags_NoNavInputs) ? "NoNavInputs" : "", (flags & ImGuiWindowFlags_AlwaysAutoResize) ? "AlwaysAutoResize" : ""); + BulletText("WindowClassId: 0x%08X", window->WindowClass.ClassId); BulletText("Scroll: (%.2f/%.2f,%.2f/%.2f) Scrollbar:%s%s", window->Scroll.x, window->ScrollMax.x, window->Scroll.y, window->ScrollMax.y, window->ScrollbarX ? "X" : "", window->ScrollbarY ? "Y" : ""); BulletText("Active: %d/%d, WriteAccessed: %d, BeginOrderWithinContext: %d", window->Active, window->WasActive, window->WriteAccessed, (window->Active || window->WasActive) ? window->BeginOrderWithinContext : -1); BulletText("Appearing: %d, Hidden: %d (CanSkip %d Cannot %d), SkipItems: %d", window->Appearing, window->Hidden, window->HiddenFramesCanSkipItems, window->HiddenFramesCannotSkipItems, window->SkipItems); @@ -14373,13 +21036,22 @@ void ImGui::DebugNodeWindow(ImGuiWindow* window, const char* label) for (int layer = 0; layer < ImGuiNavLayer_COUNT; layer++) BulletText("NavPreferredScoringPosRel[%d] = {%.1f,%.1f)", layer, (pr[layer].x == FLT_MAX ? -99999.0f : pr[layer].x), (pr[layer].y == FLT_MAX ? -99999.0f : pr[layer].y)); // Display as 99999.0f so it looks neater. BulletText("NavLayersActiveMask: %X, NavLastChildNavWindow: %s", window->DC.NavLayersActiveMask, window->NavLastChildNavWindow ? window->NavLastChildNavWindow->Name : "NULL"); - if (window->RootWindow != window) { DebugNodeWindow(window->RootWindow, "RootWindow"); } - if (window->ParentWindow != NULL) { DebugNodeWindow(window->ParentWindow, "ParentWindow"); } - if (window->DC.ChildWindows.Size > 0) { DebugNodeWindowsList(&window->DC.ChildWindows, "ChildWindows"); } + + BulletText("Viewport: %d%s, ViewportId: 0x%08X, ViewportPos: (%.1f,%.1f)", window->Viewport ? window->Viewport->Idx : -1, window->ViewportOwned ? " (Owned)" : "", window->ViewportId, window->ViewportPos.x, window->ViewportPos.y); + BulletText("ViewportMonitor: %d", window->Viewport ? window->Viewport->PlatformMonitor : -1); + BulletText("DockId: 0x%04X, DockOrder: %d, Act: %d, Vis: %d", window->DockId, window->DockOrder, window->DockIsActive, window->DockTabIsVisible); + if (window->DockNode || window->DockNodeAsHost) + DebugNodeDockNode(window->DockNodeAsHost ? window->DockNodeAsHost : window->DockNode, window->DockNodeAsHost ? "DockNodeAsHost" : "DockNode"); + + if (window->RootWindow != window) { DebugNodeWindow(window->RootWindow, "RootWindow"); } + if (window->RootWindowDockTree != window->RootWindow) { DebugNodeWindow(window->RootWindowDockTree, "RootWindowDockTree"); } + if (window->ParentWindow != NULL) { DebugNodeWindow(window->ParentWindow, "ParentWindow"); } + if (window->ParentWindowForFocusRoute != NULL) { DebugNodeWindow(window->ParentWindowForFocusRoute, "ParentWindowForFocusRoute"); } + if (window->DC.ChildWindows.Size > 0) { DebugNodeWindowsList(&window->DC.ChildWindows, "ChildWindows"); } if (window->ColumnsStorage.Size > 0 && TreeNode("Columns", "Columns sets (%d)", window->ColumnsStorage.Size)) { - for (int n = 0; n < window->ColumnsStorage.Size; n++) - DebugNodeColumns(&window->ColumnsStorage[n]); + for (ImGuiOldColumns& columns : window->ColumnsStorage) + DebugNodeColumns(&columns); TreePop(); } DebugNodeStorage(&window->StateStorage, "Storage"); @@ -14445,9 +21117,36 @@ void ImGui::DebugLogV(const char* fmt, va_list args) const int old_size = g.DebugLogBuf.size(); g.DebugLogBuf.appendf("[%05d] ", g.FrameCount); g.DebugLogBuf.appendfv(fmt, args); + g.DebugLogIndex.append(g.DebugLogBuf.c_str(), old_size, g.DebugLogBuf.size()); if (g.DebugLogFlags & ImGuiDebugLogFlags_OutputToTTY) IMGUI_DEBUG_PRINTF("%s", g.DebugLogBuf.begin() + old_size); - g.DebugLogIndex.append(g.DebugLogBuf.c_str(), old_size, g.DebugLogBuf.size()); +#ifdef IMGUI_ENABLE_TEST_ENGINE + if (g.DebugLogFlags & ImGuiDebugLogFlags_OutputToTestEngine) + IMGUI_TEST_ENGINE_LOG("%s", g.DebugLogBuf.begin() + old_size); +#endif +} + +// FIXME-LAYOUT: To be done automatically via layout mode once we rework ItemSize/ItemAdd into ItemLayout. +static void SameLineOrWrap(const ImVec2& size) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImVec2 pos(window->DC.CursorPosPrevLine.x + g.Style.ItemSpacing.x, window->DC.CursorPosPrevLine.y); + if (window->ClipRect.Contains(ImRect(pos, pos + size))) + ImGui::SameLine(); +} + +static void ShowDebugLogFlag(const char* name, ImGuiDebugLogFlags flags) +{ + ImGuiContext& g = *GImGui; + ImVec2 size(ImGui::GetFrameHeight() + g.Style.ItemInnerSpacing.x + ImGui::CalcTextSize(name).x, ImGui::GetFrameHeight()); + SameLineOrWrap(size); // FIXME-LAYOUT: To be done automatically once we rework ItemSize/ItemAdd into ItemLayout. + if (ImGui::CheckboxFlags(name, &g.DebugLogFlags, flags) && g.IO.KeyShift && (g.DebugLogFlags & flags) != 0) + { + g.DebugLogAutoDisableFrames = 2; + g.DebugLogAutoDisableFlags |= flags; + } + ImGui::SetItemTooltip("Hold SHIFT when clicking to enable for 2 frames only (useful for spammy log entries)"); } void ImGui::ShowDebugLogWindow(bool* p_open) @@ -14461,14 +21160,20 @@ void ImGui::ShowDebugLogWindow(bool* p_open) return; } - CheckboxFlags("All", &g.DebugLogFlags, ImGuiDebugLogFlags_EventMask_); - SameLine(); CheckboxFlags("ActiveId", &g.DebugLogFlags, ImGuiDebugLogFlags_EventActiveId); - SameLine(); CheckboxFlags("Focus", &g.DebugLogFlags, ImGuiDebugLogFlags_EventFocus); - SameLine(); CheckboxFlags("Popup", &g.DebugLogFlags, ImGuiDebugLogFlags_EventPopup); - SameLine(); CheckboxFlags("Nav", &g.DebugLogFlags, ImGuiDebugLogFlags_EventNav); - SameLine(); if (CheckboxFlags("Clipper", &g.DebugLogFlags, ImGuiDebugLogFlags_EventClipper)) { g.DebugLogClipperAutoDisableFrames = 2; } if (IsItemHovered()) SetTooltip("Clipper log auto-disabled after 2 frames"); - //SameLine(); CheckboxFlags("Selection", &g.DebugLogFlags, ImGuiDebugLogFlags_EventSelection); - SameLine(); CheckboxFlags("IO", &g.DebugLogFlags, ImGuiDebugLogFlags_EventIO); + ImGuiDebugLogFlags all_enable_flags = ImGuiDebugLogFlags_EventMask_ & ~ImGuiDebugLogFlags_EventInputRouting; + CheckboxFlags("All", &g.DebugLogFlags, all_enable_flags); + SetItemTooltip("(except InputRouting which is spammy)"); + + ShowDebugLogFlag("ActiveId", ImGuiDebugLogFlags_EventActiveId); + ShowDebugLogFlag("Clipper", ImGuiDebugLogFlags_EventClipper); + ShowDebugLogFlag("Docking", ImGuiDebugLogFlags_EventDocking); + ShowDebugLogFlag("Focus", ImGuiDebugLogFlags_EventFocus); + ShowDebugLogFlag("IO", ImGuiDebugLogFlags_EventIO); + ShowDebugLogFlag("Nav", ImGuiDebugLogFlags_EventNav); + ShowDebugLogFlag("Popup", ImGuiDebugLogFlags_EventPopup); + //ShowDebugLogFlag("Selection", ImGuiDebugLogFlags_EventSelection); + ShowDebugLogFlag("Viewport", ImGuiDebugLogFlags_EventViewport); + ShowDebugLogFlag("InputRouting", ImGuiDebugLogFlags_EventInputRouting); if (SmallButton("Clear")) { @@ -14478,7 +21183,10 @@ void ImGui::ShowDebugLogWindow(bool* p_open) SameLine(); if (SmallButton("Copy")) SetClipboardText(g.DebugLogBuf.c_str()); - BeginChild("##log", ImVec2(0.0f, 0.0f), true, ImGuiWindowFlags_AlwaysVerticalScrollbar | ImGuiWindowFlags_AlwaysHorizontalScrollbar); + BeginChild("##log", ImVec2(0.0f, 0.0f), ImGuiChildFlags_Border, ImGuiWindowFlags_AlwaysVerticalScrollbar | ImGuiWindowFlags_AlwaysHorizontalScrollbar); + + const ImGuiDebugLogFlags backup_log_flags = g.DebugLogFlags; + g.DebugLogFlags &= ~ImGuiDebugLogFlags_EventClipper; ImGuiListClipper clipper; clipper.Begin(g.DebugLogIndex.size()); @@ -14487,10 +21195,10 @@ void ImGui::ShowDebugLogWindow(bool* p_open) { const char* line_begin = g.DebugLogIndex.get_line_begin(g.DebugLogBuf.c_str(), line_no); const char* line_end = g.DebugLogIndex.get_line_end(g.DebugLogBuf.c_str(), line_no); - TextUnformatted(line_begin, line_end); + TextUnformatted(line_begin, line_end); // Display line ImRect text_rect = g.LastItemData.Rect; if (IsItemHovered()) - for (const char* p = line_begin; p <= line_end - 10; p++) + for (const char* p = line_begin; p <= line_end - 10; p++) // Search for 0x???????? identifiers { ImGuiID id = 0; if (p[0] != '0' || (p[1] != 'x' && p[1] != 'X') || sscanf(p + 2, "%X", &id) != 1) @@ -14503,6 +21211,7 @@ void ImGui::ShowDebugLogWindow(bool* p_open) p += 10; } } + g.DebugLogFlags = backup_log_flags; if (GetScrollY() >= GetScrollMaxY()) SetScrollHereY(1.0f); EndChild(); @@ -14511,9 +21220,41 @@ void ImGui::ShowDebugLogWindow(bool* p_open) } //----------------------------------------------------------------------------- -// [SECTION] OTHER DEBUG TOOLS (ITEM PICKER, STACK TOOL) +// [SECTION] OTHER DEBUG TOOLS (ITEM PICKER, ID STACK TOOL) //----------------------------------------------------------------------------- +// Draw a small cross at current CursorPos in current window's DrawList +void ImGui::DebugDrawCursorPos(ImU32 col) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImVec2 pos = window->DC.CursorPos; + window->DrawList->AddLine(ImVec2(pos.x, pos.y - 3.0f), ImVec2(pos.x, pos.y + 4.0f), col, 1.0f); + window->DrawList->AddLine(ImVec2(pos.x - 3.0f, pos.y), ImVec2(pos.x + 4.0f, pos.y), col, 1.0f); +} + +// Draw a 10px wide rectangle around CurposPos.x using Line Y1/Y2 in current window's DrawList +void ImGui::DebugDrawLineExtents(ImU32 col) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + float curr_x = window->DC.CursorPos.x; + float line_y1 = (window->DC.IsSameLine ? window->DC.CursorPosPrevLine.y : window->DC.CursorPos.y); + float line_y2 = line_y1 + (window->DC.IsSameLine ? window->DC.PrevLineSize.y : window->DC.CurrLineSize.y); + window->DrawList->AddLine(ImVec2(curr_x - 5.0f, line_y1), ImVec2(curr_x + 5.0f, line_y1), col, 1.0f); + window->DrawList->AddLine(ImVec2(curr_x - 0.5f, line_y1), ImVec2(curr_x - 0.5f, line_y2), col, 1.0f); + window->DrawList->AddLine(ImVec2(curr_x - 5.0f, line_y2), ImVec2(curr_x + 5.0f, line_y2), col, 1.0f); +} + +// Draw last item rect in ForegroundDrawList (so it is always visible) +void ImGui::DebugDrawItemRect(ImU32 col) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + GetForegroundDrawList(window)->AddRect(g.LastItemData.Rect.Min, g.LastItemData.Rect.Max, col); +} + +// [DEBUG] Locate item position/rectangle given an ID. static const ImU32 DEBUG_LOCATE_ITEM_COLOR = IM_COL32(0, 255, 0, 255); // Green void ImGui::DebugLocateItem(ImGuiID target_id) @@ -14521,8 +21262,10 @@ void ImGui::DebugLocateItem(ImGuiID target_id) ImGuiContext& g = *GImGui; g.DebugLocateId = target_id; g.DebugLocateFrames = 2; + g.DebugBreakInLocateId = false; } +// FIXME: Doesn't work over through a modal window, because they clear HoveredWindow. void ImGui::DebugLocateItemOnHover(ImGuiID target_id) { if (target_id == 0 || !IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem | ImGuiHoveredFlags_AllowWhenBlockedByPopup)) @@ -14530,11 +21273,24 @@ void ImGui::DebugLocateItemOnHover(ImGuiID target_id) ImGuiContext& g = *GImGui; DebugLocateItem(target_id); GetForegroundDrawList(g.CurrentWindow)->AddRect(g.LastItemData.Rect.Min - ImVec2(3.0f, 3.0f), g.LastItemData.Rect.Max + ImVec2(3.0f, 3.0f), DEBUG_LOCATE_ITEM_COLOR); + + // Can't easily use a context menu here because it will mess with focus, active id etc. + if (g.IO.ConfigDebugIsDebuggerPresent && g.MouseStationaryTimer > 1.0f) + { + DebugBreakButtonTooltip(false, "in ItemAdd()"); + if (IsKeyChordPressed(g.DebugBreakKeyChord)) + g.DebugBreakInLocateId = true; + } } void ImGui::DebugLocateItemResolveWithLastItem() { ImGuiContext& g = *GImGui; + + // [DEBUG] Debug break requested by user + if (g.DebugBreakInLocateId) + IM_DEBUG_BREAK(); + ImGuiLastItemData item_data = g.LastItemData; g.DebugLocateId = 0; ImDrawList* draw_list = GetForegroundDrawList(g.CurrentWindow); @@ -14546,6 +21302,12 @@ void ImGui::DebugLocateItemResolveWithLastItem() draw_list->AddLine(p1, p2, DEBUG_LOCATE_ITEM_COLOR); } +void ImGui::DebugStartItemPicker() +{ + ImGuiContext& g = *GImGui; + g.DebugItemPickerActive = true; +} + // [DEBUG] Item picker tool - start with DebugStartItemPicker() - useful to visually select an item and break into its call-stack. void ImGui::UpdateDebugToolItemPicker() { @@ -14580,13 +21342,13 @@ void ImGui::UpdateDebugToolItemPicker() EndTooltip(); } -// [DEBUG] Stack Tool: update queries. Called by NewFrame() +// [DEBUG] ID Stack Tool: update queries. Called by NewFrame() void ImGui::UpdateDebugToolStackQueries() { ImGuiContext& g = *GImGui; - ImGuiStackTool* tool = &g.DebugStackTool; + ImGuiIDStackTool* tool = &g.DebugIDStackTool; - // Clear hook when stack tool is not visible + // Clear hook when id stack tool is not visible g.DebugHookIdInfo = 0; if (g.FrameCount != tool->LastActiveFrame + 1) return; @@ -14620,12 +21382,12 @@ void ImGui::UpdateDebugToolStackQueries() } } -// [DEBUG] Stack tool: hooks called by GetID() family functions +// [DEBUG] ID Stack tool: hooks called by GetID() family functions void ImGui::DebugHookIdInfo(ImGuiID id, ImGuiDataType data_type, const void* data_id, const void* data_id_end) { ImGuiContext& g = *GImGui; ImGuiWindow* window = g.CurrentWindow; - ImGuiStackTool* tool = &g.DebugStackTool; + ImGuiIDStackTool* tool = &g.DebugIDStackTool; // Step 0: stack query // This assumes that the ID was computed with the current ID stack, which tends to be the case for our widget. @@ -14668,7 +21430,7 @@ void ImGui::DebugHookIdInfo(ImGuiID id, ImGuiDataType data_type, const void* dat info->DataType = data_type; } -static int StackToolFormatLevelInfo(ImGuiStackTool* tool, int n, bool format_for_ui, char* buf, size_t buf_size) +static int StackToolFormatLevelInfo(ImGuiIDStackTool* tool, int n, bool format_for_ui, char* buf, size_t buf_size) { ImGuiStackLevelInfo* info = &tool->Results[n]; ImGuiWindow* window = (info->Desc[0] == 0 && n == 0) ? ImGui::FindWindowByID(info->ID) : NULL; @@ -14685,20 +21447,20 @@ static int StackToolFormatLevelInfo(ImGuiStackTool* tool, int n, bool format_for return ImFormatString(buf, buf_size, "???"); } -// Stack Tool: Display UI -void ImGui::ShowStackToolWindow(bool* p_open) +// ID Stack Tool: Display UI +void ImGui::ShowIDStackToolWindow(bool* p_open) { ImGuiContext& g = *GImGui; if (!(g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasSize)) SetNextWindowSize(ImVec2(0.0f, GetFontSize() * 8.0f), ImGuiCond_FirstUseEver); - if (!Begin("Dear ImGui Stack Tool", p_open) || GetCurrentWindow()->BeginCount > 1) + if (!Begin("Dear ImGui ID Stack Tool", p_open) || GetCurrentWindow()->BeginCount > 1) { End(); return; } // Display hovered/active status - ImGuiStackTool* tool = &g.DebugStackTool; + ImGuiIDStackTool* tool = &g.DebugIDStackTool; const ImGuiID hovered_id = g.HoveredIdPreviousFrame; const ImGuiID active_id = g.ActiveId; #ifdef IMGUI_ENABLE_TEST_ENGINE @@ -14714,7 +21476,7 @@ void ImGui::ShowStackToolWindow(bool* p_open) Checkbox("Ctrl+C: copy path to clipboard", &tool->CopyToClipboardOnCtrlC); SameLine(); TextColored((time_since_copy >= 0.0f && time_since_copy < 0.75f && ImFmod(time_since_copy, 0.25f) < 0.25f * 0.5f) ? ImVec4(1.f, 1.f, 0.3f, 1.f) : ImVec4(), "*COPIED*"); - if (tool->CopyToClipboardOnCtrlC && IsKeyDown(ImGuiMod_Ctrl) && IsKeyPressed(ImGuiKey_C)) + if (tool->CopyToClipboardOnCtrlC && Shortcut(ImGuiMod_Ctrl | ImGuiKey_C, 0, ImGuiInputFlags_RouteGlobal)) { tool->CopyToClipboardLastTime = (float)g.Time; char* p = g.TempBuffer.Data; @@ -14767,7 +21529,7 @@ void ImGui::ShowStackToolWindow(bool* p_open) void ImGui::ShowMetricsWindow(bool*) {} void ImGui::ShowFontAtlas(ImFontAtlas*) {} void ImGui::DebugNodeColumns(ImGuiOldColumns*) {} -void ImGui::DebugNodeDrawList(ImGuiWindow*, const ImDrawList*, const char*) {} +void ImGui::DebugNodeDrawList(ImGuiWindow*, ImGuiViewportP*, const ImDrawList*, const char*) {} void ImGui::DebugNodeDrawCmdShowMeshAndBoundingBox(ImDrawList*, const ImDrawList*, const ImDrawCmd*, bool, bool) {} void ImGui::DebugNodeFont(ImFont*) {} void ImGui::DebugNodeStorage(ImGuiStorage*, const char*) {} @@ -14780,10 +21542,9 @@ void ImGui::DebugNodeViewport(ImGuiViewportP*) {} void ImGui::DebugLog(const char*, ...) {} void ImGui::DebugLogV(const char*, va_list) {} void ImGui::ShowDebugLogWindow(bool*) {} -void ImGui::ShowStackToolWindow(bool*) {} +void ImGui::ShowIDStackToolWindow(bool*) {} +void ImGui::DebugStartItemPicker() {} void ImGui::DebugHookIdInfo(ImGuiID, ImGuiDataType, const void*, const void*) {} -void ImGui::UpdateDebugToolItemPicker() {} -void ImGui::UpdateDebugToolStackQueries() {} #endif // #ifndef IMGUI_DISABLE_DEBUG_TOOLS diff --git a/libs/imgui/imgui.h b/libs/imgui/imgui.h index 8da4ea1..c2df670 100644 --- a/libs/imgui/imgui.h +++ b/libs/imgui/imgui.h @@ -1,30 +1,33 @@ -// dear imgui, v1.89.6 +// dear imgui, v1.90.4 // (headers) // Help: -// - Read FAQ at http://dearimgui.com/faq -// - Newcomers, read 'Programmer guide' in imgui.cpp for notes on how to setup Dear ImGui in your codebase. +// - See links below. // - Call and read ImGui::ShowDemoWindow() in imgui_demo.cpp. All applications in examples/ are doing that. -// Read imgui.cpp for details, links and comments. +// - Read top of imgui.cpp for more details, links and comments. // Resources: -// - FAQ http://dearimgui.com/faq -// - Homepage & latest https://github.com/ocornut/imgui +// - FAQ https://dearimgui.com/faq +// - Getting Started https://dearimgui.com/getting-started +// - Homepage https://github.com/ocornut/imgui // - Releases & changelog https://github.com/ocornut/imgui/releases -// - Gallery https://github.com/ocornut/imgui/issues/6478 (please post your screenshots/video there!) +// - Gallery https://github.com/ocornut/imgui/issues/6897 (please post your screenshots/video there!) // - Wiki https://github.com/ocornut/imgui/wiki (lots of good stuff there) // - Glossary https://github.com/ocornut/imgui/wiki/Glossary // - Issues & support https://github.com/ocornut/imgui/issues +// - Tests & Automation https://github.com/ocornut/imgui_test_engine -// Getting Started? -// - For first-time users having issues compiling/linking/running or issues loading fonts: -// please post in https://github.com/ocornut/imgui/discussions if you cannot find a solution in resources above. +// For first-time users having issues compiling/linking/running/loading fonts: +// please post in https://github.com/ocornut/imgui/discussions if you cannot find a solution in resources above. +// Everything else should be asked in 'Issues'! We are building a database of cross-linked knowledge there. // Library Version // (Integer encoded as XYYZZ for use in #if preprocessor conditionals, e.g. '#if IMGUI_VERSION_NUM >= 12345') -#define IMGUI_VERSION "1.89.6" -#define IMGUI_VERSION_NUM 18960 +#define IMGUI_VERSION "1.90.4" +#define IMGUI_VERSION_NUM 19040 #define IMGUI_HAS_TABLE +#define IMGUI_HAS_VIEWPORT // Viewport WIP branch +#define IMGUI_HAS_DOCK // Docking WIP branch /* @@ -33,15 +36,16 @@ Index of this file: // [SECTION] Forward declarations and basic types // [SECTION] Dear ImGui end-user API functions // [SECTION] Flags & Enumerations +// [SECTION] Tables API flags and structures (ImGuiTableFlags, ImGuiTableColumnFlags, ImGuiTableRowFlags, ImGuiTableBgTarget, ImGuiTableSortSpecs, ImGuiTableColumnSortSpecs) // [SECTION] Helpers: Memory allocations macros, ImVector<> // [SECTION] ImGuiStyle // [SECTION] ImGuiIO -// [SECTION] Misc data structures (ImGuiInputTextCallbackData, ImGuiSizeCallbackData, ImGuiPayload, ImGuiTableSortSpecs, ImGuiTableColumnSortSpecs) +// [SECTION] Misc data structures (ImGuiInputTextCallbackData, ImGuiSizeCallbackData, ImGuiWindowClass, ImGuiPayload) // [SECTION] Helpers (ImGuiOnceUponAFrame, ImGuiTextFilter, ImGuiTextBuffer, ImGuiStorage, ImGuiListClipper, Math Operators, ImColor) // [SECTION] Drawing API (ImDrawCallback, ImDrawCmd, ImDrawIdx, ImDrawVert, ImDrawChannel, ImDrawListSplitter, ImDrawFlags, ImDrawListFlags, ImDrawList, ImDrawData) // [SECTION] Font API (ImFontConfig, ImFontGlyph, ImFontGlyphRangesBuilder, ImFontAtlasFlags, ImFontAtlas, ImFont) // [SECTION] Viewports (ImGuiViewportFlags, ImGuiViewport) -// [SECTION] Platform Dependent Interfaces (ImGuiPlatformImeData) +// [SECTION] Platform Dependent Interfaces (ImGuiPlatformIO, ImGuiPlatformMonitor, ImGuiPlatformImeData) // [SECTION] Obsolete functions and types */ @@ -49,7 +53,7 @@ Index of this file: #pragma once // Configuration file with compile-time options -// (edit imconfig.h or '#define IMGUI_USER_CONFIG "myfilename.h" from your build system') +// (edit imconfig.h or '#define IMGUI_USER_CONFIG "myfilename.h" from your build system) #ifdef IMGUI_USER_CONFIG #include IMGUI_USER_CONFIG #endif @@ -69,7 +73,7 @@ Index of this file: // Define attributes of all API symbols declarations (e.g. for DLL under Windows) // IMGUI_API is used for core imgui functions, IMGUI_IMPL_API is used for the default backends files (imgui_impl_xxx.h) -// Using dear imgui via a shared library is not recommended, because we don't guarantee backward nor forward ABI compatibility (also function call overhead, as dear imgui is a call-heavy API) +// Using dear imgui via a shared library is not recommended: we don't guarantee backward nor forward ABI compatibility + this is a call-heavy library and function call overhead adds up. #ifndef IMGUI_API #define IMGUI_API #endif @@ -84,10 +88,11 @@ Index of this file: #endif #define IM_ARRAYSIZE(_ARR) ((int)(sizeof(_ARR) / sizeof(*(_ARR)))) // Size of a static C-style array. Don't use on pointers! #define IM_UNUSED(_VAR) ((void)(_VAR)) // Used to silence "unused variable warnings". Often useful as asserts may be stripped out from final builds. -#define IM_OFFSETOF(_TYPE,_MEMBER) offsetof(_TYPE, _MEMBER) // Offset of _MEMBER within _TYPE. Standardized as offsetof() in C++11 #define IMGUI_CHECKVERSION() ImGui::DebugCheckVersionAndDataLayout(IMGUI_VERSION, sizeof(ImGuiIO), sizeof(ImGuiStyle), sizeof(ImVec2), sizeof(ImVec4), sizeof(ImDrawVert), sizeof(ImDrawIdx)) // Helper Macros - IM_FMTARGS, IM_FMTLIST: Apply printf-style warnings to our formatting functions. +// (MSVC provides an equivalent mechanism via SAL Annotations but it would require the macros in a different +// location. e.g. #include + void myprintf(_Printf_format_string_ const char* format, ...)) #if !defined(IMGUI_USE_STB_SPRINTF) && defined(__MINGW32__) && !defined(__clang__) #define IM_FMTARGS(FMT) __attribute__((format(gnu_printf, FMT, FMT+1))) #define IM_FMTLIST(FMT) __attribute__((format(gnu_printf, FMT, 0))) @@ -115,10 +120,14 @@ Index of this file: #endif #if defined(__clang__) #pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wold-style-cast" -#if __has_warning("-Wzero-as-null-pointer-constant") -#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" +#if __has_warning("-Wunknown-warning-option") +#pragma clang diagnostic ignored "-Wunknown-warning-option" // warning: unknown warning group 'xxx' #endif +#pragma clang diagnostic ignored "-Wunknown-pragmas" // warning: unknown warning group 'xxx' +#pragma clang diagnostic ignored "-Wold-style-cast" +#pragma clang diagnostic ignored "-Wfloat-equal" // warning: comparing floating point with == or != is unsafe +#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" +#pragma clang diagnostic ignored "-Wreserved-identifier" // warning: identifier '_Xxx' is reserved because it starts with '_' followed by a capital letter #elif defined(__GNUC__) #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind @@ -151,6 +160,8 @@ struct ImGuiKeyData; // Storage for ImGuiIO and IsKeyDown(), IsKe struct ImGuiListClipper; // Helper to manually clip large list of items struct ImGuiOnceUponAFrame; // Helper for running a block of code not more than once a frame struct ImGuiPayload; // User data payload for drag and drop operations +struct ImGuiPlatformIO; // Multi-viewport support: interface for Platform/Renderer backends + viewports to render +struct ImGuiPlatformMonitor; // Multi-viewport support: user-provided bounds for each connected monitor/display. Used when positioning popups and tooltips to avoid them straddling monitors struct ImGuiPlatformImeData; // Platform IME data for io.SetPlatformImeDataFn() function. struct ImGuiSizeCallbackData; // Callback data when using SetNextWindowSizeConstraints() (rare/advanced use) struct ImGuiStorage; // Helper for key->value storage @@ -159,7 +170,8 @@ struct ImGuiTableSortSpecs; // Sorting specifications for a table (often struct ImGuiTableColumnSortSpecs; // Sorting specification for one column of a table struct ImGuiTextBuffer; // Helper to hold and append into a text buffer (~string builder) struct ImGuiTextFilter; // Helper to parse and apply text filters (e.g. "aaaaa[,bbbbb][,ccccc]") -struct ImGuiViewport; // A Platform Window (always only one in 'master' branch), in the future may represent Platform Monitor +struct ImGuiViewport; // A Platform Window (always 1 unless multi-viewport are enabled. One per platform window to output to). In the future may represent Platform Monitor +struct ImGuiWindowClass; // Window class (rare/advanced uses: provide hints to the platform backend via altered viewport flags and parent/child info) // Enumerations // - We don't use strongly typed enums much because they add constraints (can't extend in private code, can't store typed in bit fields, extra casting on iteration) @@ -178,7 +190,7 @@ typedef int ImGuiSortDirection; // -> enum ImGuiSortDirection_ // Enum: A typedef int ImGuiStyleVar; // -> enum ImGuiStyleVar_ // Enum: A variable identifier for styling typedef int ImGuiTableBgTarget; // -> enum ImGuiTableBgTarget_ // Enum: A color target for TableSetBgColor() -// Flags (declared as int for compatibility with old C++, to allow using as flags without overhead, and to not pollute the top of this file) +// Flags (declared as int to allow using as flags without overhead, and to not pollute the top of this file) // - Tip: Use your programming IDE navigation facilities on the names in the _central column_ below to find the actual flags/enum lists! // In Visual Studio IDE: CTRL+comma ("Edit.GoToAll") can follow symbols in comments, whereas CTRL+F12 ("Edit.GoToImplementation") cannot. // With Visual Assist installed: ALT+G ("VAssistX.GoToImplementation") can also follow symbols in comments. @@ -187,14 +199,16 @@ typedef int ImDrawListFlags; // -> enum ImDrawListFlags_ // Flags: f typedef int ImFontAtlasFlags; // -> enum ImFontAtlasFlags_ // Flags: for ImFontAtlas build typedef int ImGuiBackendFlags; // -> enum ImGuiBackendFlags_ // Flags: for io.BackendFlags typedef int ImGuiButtonFlags; // -> enum ImGuiButtonFlags_ // Flags: for InvisibleButton() +typedef int ImGuiChildFlags; // -> enum ImGuiChildFlags_ // Flags: for BeginChild() typedef int ImGuiColorEditFlags; // -> enum ImGuiColorEditFlags_ // Flags: for ColorEdit4(), ColorPicker4() etc. typedef int ImGuiConfigFlags; // -> enum ImGuiConfigFlags_ // Flags: for io.ConfigFlags typedef int ImGuiComboFlags; // -> enum ImGuiComboFlags_ // Flags: for BeginCombo() +typedef int ImGuiDockNodeFlags; // -> enum ImGuiDockNodeFlags_ // Flags: for DockSpace() typedef int ImGuiDragDropFlags; // -> enum ImGuiDragDropFlags_ // Flags: for BeginDragDropSource(), AcceptDragDropPayload() typedef int ImGuiFocusedFlags; // -> enum ImGuiFocusedFlags_ // Flags: for IsWindowFocused() typedef int ImGuiHoveredFlags; // -> enum ImGuiHoveredFlags_ // Flags: for IsItemHovered(), IsWindowHovered() etc. typedef int ImGuiInputTextFlags; // -> enum ImGuiInputTextFlags_ // Flags: for InputText(), InputTextMultiline() -typedef int ImGuiKeyChord; // -> ImGuiKey | ImGuiMod_XXX // Flags: for storage only for now: an ImGuiKey optionally OR-ed with one or more ImGuiMod_XXX values. +typedef int ImGuiKeyChord; // -> ImGuiKey | ImGuiMod_XXX // Flags: for IsKeyChordPressed(), Shortcut() etc. an ImGuiKey optionally OR-ed with one or more ImGuiMod_XXX values. typedef int ImGuiPopupFlags; // -> enum ImGuiPopupFlags_ // Flags: for OpenPopup*(), BeginPopupContext*(), IsPopupOpen() typedef int ImGuiSelectableFlags; // -> enum ImGuiSelectableFlags_ // Flags: for Selectable() typedef int ImGuiSliderFlags; // -> enum ImGuiSliderFlags_ // Flags: for DragFloat(), DragInt(), SliderFloat(), SliderInt() etc. @@ -234,8 +248,8 @@ typedef unsigned long long ImU64; // 64-bit unsigned integer // Character types // (we generally use UTF-8 encoded string in the API. This is storage specifically for a decoded character used for keyboard input and display) -typedef unsigned short ImWchar16; // A single decoded U16 character/code point. We encode them as multi bytes UTF-8 when used in strings. typedef unsigned int ImWchar32; // A single decoded U32 character/code point. We encode them as multi bytes UTF-8 when used in strings. +typedef unsigned short ImWchar16; // A single decoded U16 character/code point. We encode them as multi bytes UTF-8 when used in strings. #ifdef IMGUI_USE_WCHAR32 // ImWchar [configurable type: override in imconfig.h with '#define IMGUI_USE_WCHAR32' to support Unicode planes 1-16] typedef ImWchar32 ImWchar; #else @@ -250,6 +264,7 @@ typedef void (*ImGuiMemFreeFunc)(void* ptr, void* user_data); // ImVec2: 2D vector used to store positions, sizes etc. [Compile-time configurable type] // This is a frequently used type in the API. Consider using IM_VEC2_CLASS_EXTRA to create implicit cast from/to our preferred type. +// Add '#define IMGUI_DEFINE_MATH_OPERATORS' in your imconfig.h file to benefit from courtesy maths operators for those types. IM_MSVC_RUNTIME_CHECKS_OFF struct ImVec2 { @@ -293,7 +308,7 @@ namespace ImGui // Main IMGUI_API ImGuiIO& GetIO(); // access the IO structure (mouse/keyboard/gamepad inputs, time, various configuration options/flags) - IMGUI_API ImGuiStyle& GetStyle(); // access the Style structure (colors, sizes). Always use PushStyleCol(), PushStyleVar() to modify style mid-frame! + IMGUI_API ImGuiStyle& GetStyle(); // access the Style structure (colors, sizes). Always use PushStyleColor(), PushStyleVar() to modify style mid-frame! IMGUI_API void NewFrame(); // start a new Dear ImGui frame, you can submit any command from this point until Render()/EndFrame(). IMGUI_API void EndFrame(); // ends the Dear ImGui frame. automatically called by Render(). If you don't need to render data (skipping rendering) you may call EndFrame() without Render()... but you'll have wasted CPU already! If you don't need to render, better to not create any windows and not call NewFrame() at all! IMGUI_API void Render(); // ends the Dear ImGui frame, finalize the draw data. You can then get call GetDrawData(). @@ -303,7 +318,7 @@ namespace ImGui IMGUI_API void ShowDemoWindow(bool* p_open = NULL); // create Demo window. demonstrate most ImGui features. call this to learn about the library! try to make it always available in your application! IMGUI_API void ShowMetricsWindow(bool* p_open = NULL); // create Metrics/Debugger window. display Dear ImGui internals: windows, draw commands, various internal state, etc. IMGUI_API void ShowDebugLogWindow(bool* p_open = NULL); // create Debug Log window. display a simplified log of important dear imgui events. - IMGUI_API void ShowStackToolWindow(bool* p_open = NULL); // create Stack Tool window. hover items with mouse to query information about the source of their unique ID. + IMGUI_API void ShowIDStackToolWindow(bool* p_open = NULL); // create Stack Tool window. hover items with mouse to query information about the source of their unique ID. IMGUI_API void ShowAboutWindow(bool* p_open = NULL); // create About window. display Dear ImGui version, credits and build/system information. IMGUI_API void ShowStyleEditor(ImGuiStyle* ref = NULL); // add style editor block (not a window). you can pass in a reference ImGuiStyle structure to compare to, revert to and save to (else it uses the default style) IMGUI_API bool ShowStyleSelector(const char* label); // add style selector block (not a window), essentially a combo listing the default styles. @@ -324,23 +339,33 @@ namespace ImGui // Some information such as 'flags' or 'p_open' will only be considered by the first call to Begin(). // - Begin() return false to indicate the window is collapsed or fully clipped, so you may early out and omit submitting // anything to the window. Always call a matching End() for each Begin() call, regardless of its return value! - // [Important: due to legacy reason, this is inconsistent with most other functions such as BeginMenu/EndMenu, - // BeginPopup/EndPopup, etc. where the EndXXX call should only be called if the corresponding BeginXXX function - // returned true. Begin and BeginChild are the only odd ones out. Will be fixed in a future update.] + // [Important: due to legacy reason, Begin/End and BeginChild/EndChild are inconsistent with all other functions + // such as BeginMenu/EndMenu, BeginPopup/EndPopup, etc. where the EndXXX call should only be called if the corresponding + // BeginXXX function returned true. Begin and BeginChild are the only odd ones out. Will be fixed in a future update.] // - Note that the bottom of window stack always contains a window called "Debug". IMGUI_API bool Begin(const char* name, bool* p_open = NULL, ImGuiWindowFlags flags = 0); IMGUI_API void End(); // Child Windows // - Use child windows to begin into a self-contained independent scrolling/clipping regions within a host window. Child windows can embed their own child. - // - For each independent axis of 'size': ==0.0f: use remaining host window size / >0.0f: fixed size / <0.0f: use remaining window size minus abs(size) / Each axis can use a different mode, e.g. ImVec2(0,400). - // - BeginChild() returns false to indicate the window is collapsed or fully clipped, so you may early out and omit submitting anything to the window. - // Always call a matching EndChild() for each BeginChild() call, regardless of its return value. - // [Important: due to legacy reason, this is inconsistent with most other functions such as BeginMenu/EndMenu, - // BeginPopup/EndPopup, etc. where the EndXXX call should only be called if the corresponding BeginXXX function - // returned true. Begin and BeginChild are the only odd ones out. Will be fixed in a future update.] - IMGUI_API bool BeginChild(const char* str_id, const ImVec2& size = ImVec2(0, 0), bool border = false, ImGuiWindowFlags flags = 0); - IMGUI_API bool BeginChild(ImGuiID id, const ImVec2& size = ImVec2(0, 0), bool border = false, ImGuiWindowFlags flags = 0); + // - Before 1.90 (November 2023), the "ImGuiChildFlags child_flags = 0" parameter was "bool border = false". + // This API is backward compatible with old code, as we guarantee that ImGuiChildFlags_Border == true. + // Consider updating your old code: + // BeginChild("Name", size, false) -> Begin("Name", size, 0); or Begin("Name", size, ImGuiChildFlags_None); + // BeginChild("Name", size, true) -> Begin("Name", size, ImGuiChildFlags_Border); + // - Manual sizing (each axis can use a different setting e.g. ImVec2(0.0f, 400.0f)): + // == 0.0f: use remaining parent window size for this axis. + // > 0.0f: use specified size for this axis. + // < 0.0f: right/bottom-align to specified distance from available content boundaries. + // - Specifying ImGuiChildFlags_AutoResizeX or ImGuiChildFlags_AutoResizeY makes the sizing automatic based on child contents. + // Combining both ImGuiChildFlags_AutoResizeX _and_ ImGuiChildFlags_AutoResizeY defeats purpose of a scrolling region and is NOT recommended. + // - BeginChild() returns false to indicate the window is collapsed or fully clipped, so you may early out and omit submitting + // anything to the window. Always call a matching EndChild() for each BeginChild() call, regardless of its return value. + // [Important: due to legacy reason, Begin/End and BeginChild/EndChild are inconsistent with all other functions + // such as BeginMenu/EndMenu, BeginPopup/EndPopup, etc. where the EndXXX call should only be called if the corresponding + // BeginXXX function returned true. Begin and BeginChild are the only odd ones out. Will be fixed in a future update.] + IMGUI_API bool BeginChild(const char* str_id, const ImVec2& size = ImVec2(0, 0), ImGuiChildFlags child_flags = 0, ImGuiWindowFlags window_flags = 0); + IMGUI_API bool BeginChild(ImGuiID id, const ImVec2& size = ImVec2(0, 0), ImGuiChildFlags child_flags = 0, ImGuiWindowFlags window_flags = 0); IMGUI_API void EndChild(); // Windows Utilities @@ -348,23 +373,26 @@ namespace ImGui IMGUI_API bool IsWindowAppearing(); IMGUI_API bool IsWindowCollapsed(); IMGUI_API bool IsWindowFocused(ImGuiFocusedFlags flags=0); // is current window focused? or its root/child, depending on flags. see flags for options. - IMGUI_API bool IsWindowHovered(ImGuiHoveredFlags flags=0); // is current window hovered (and typically: not blocked by a popup/modal)? see flags for options. NB: If you are trying to check whether your mouse should be dispatched to imgui or to your app, you should use the 'io.WantCaptureMouse' boolean for that! Please read the FAQ! + IMGUI_API bool IsWindowHovered(ImGuiHoveredFlags flags=0); // is current window hovered and hoverable (e.g. not blocked by a popup/modal)? See ImGuiHoveredFlags_ for options. IMPORTANT: If you are trying to check whether your mouse should be dispatched to Dear ImGui or to your underlying app, you should not use this function! Use the 'io.WantCaptureMouse' boolean for that! Refer to FAQ entry "How can I tell whether to dispatch mouse/keyboard to Dear ImGui or my application?" for details. IMGUI_API ImDrawList* GetWindowDrawList(); // get draw list associated to the current window, to append your own drawing primitives - IMGUI_API ImVec2 GetWindowPos(); // get current window position in screen space (useful if you want to do your own drawing via the DrawList API) - IMGUI_API ImVec2 GetWindowSize(); // get current window size + IMGUI_API float GetWindowDpiScale(); // get DPI scale currently associated to the current window's viewport. + IMGUI_API ImVec2 GetWindowPos(); // get current window position in screen space (note: it is unlikely you need to use this. Consider using current layout pos instead, GetCursorScreenPos()) + IMGUI_API ImVec2 GetWindowSize(); // get current window size (note: it is unlikely you need to use this. Consider using GetCursorScreenPos() and e.g. GetContentRegionAvail() instead) IMGUI_API float GetWindowWidth(); // get current window width (shortcut for GetWindowSize().x) IMGUI_API float GetWindowHeight(); // get current window height (shortcut for GetWindowSize().y) + IMGUI_API ImGuiViewport*GetWindowViewport(); // get viewport currently associated to the current window. // Window manipulation // - Prefer using SetNextXXX functions (before Begin) rather that SetXXX functions (after Begin). IMGUI_API void SetNextWindowPos(const ImVec2& pos, ImGuiCond cond = 0, const ImVec2& pivot = ImVec2(0, 0)); // set next window position. call before Begin(). use pivot=(0.5f,0.5f) to center on given point, etc. IMGUI_API void SetNextWindowSize(const ImVec2& size, ImGuiCond cond = 0); // set next window size. set axis to 0.0f to force an auto-fit on this axis. call before Begin() - IMGUI_API void SetNextWindowSizeConstraints(const ImVec2& size_min, const ImVec2& size_max, ImGuiSizeCallback custom_callback = NULL, void* custom_callback_data = NULL); // set next window size limits. use -1,-1 on either X/Y axis to preserve the current size. Sizes will be rounded down. Use callback to apply non-trivial programmatic constraints. + IMGUI_API void SetNextWindowSizeConstraints(const ImVec2& size_min, const ImVec2& size_max, ImGuiSizeCallback custom_callback = NULL, void* custom_callback_data = NULL); // set next window size limits. use 0.0f or FLT_MAX if you don't want limits. Use -1 for both min and max of same axis to preserve current size (which itself is a constraint). Use callback to apply non-trivial programmatic constraints. IMGUI_API void SetNextWindowContentSize(const ImVec2& size); // set next window content size (~ scrollable client area, which enforce the range of scrollbars). Not including window decorations (title bar, menu bar, etc.) nor WindowPadding. set an axis to 0.0f to leave it automatic. call before Begin() IMGUI_API void SetNextWindowCollapsed(bool collapsed, ImGuiCond cond = 0); // set next window collapsed state. call before Begin() IMGUI_API void SetNextWindowFocus(); // set next window to be focused / top-most. call before Begin() IMGUI_API void SetNextWindowScroll(const ImVec2& scroll); // set next window scrolling value (use < 0.0f to not affect a given axis). IMGUI_API void SetNextWindowBgAlpha(float alpha); // set next window background color alpha. helper to easily override the Alpha component of ImGuiCol_WindowBg/ChildBg/PopupBg. you may also use ImGuiWindowFlags_NoBackground. + IMGUI_API void SetNextWindowViewport(ImGuiID viewport_id); // set next window viewport IMGUI_API void SetWindowPos(const ImVec2& pos, ImGuiCond cond = 0); // (not recommended) set current window position - call within Begin()/End(). prefer using SetNextWindowPos(), as this may incur tearing and side-effects. IMGUI_API void SetWindowSize(const ImVec2& size, ImGuiCond cond = 0); // (not recommended) set current window size - call within Begin()/End(). set to ImVec2(0, 0) to force an auto-fit. prefer using SetNextWindowSize(), as this may incur tearing and minor side-effects. IMGUI_API void SetWindowCollapsed(bool collapsed, ImGuiCond cond = 0); // (not recommended) set current window collapsed state. prefer using SetNextWindowCollapsed(). @@ -426,16 +454,28 @@ namespace ImGui IMGUI_API ImVec2 GetFontTexUvWhitePixel(); // get UV coordinate for a while pixel, useful to draw custom shapes via the ImDrawList API IMGUI_API ImU32 GetColorU32(ImGuiCol idx, float alpha_mul = 1.0f); // retrieve given style color with style alpha applied and optional extra alpha multiplier, packed as a 32-bit value suitable for ImDrawList IMGUI_API ImU32 GetColorU32(const ImVec4& col); // retrieve given color with style alpha applied, packed as a 32-bit value suitable for ImDrawList - IMGUI_API ImU32 GetColorU32(ImU32 col); // retrieve given color with style alpha applied, packed as a 32-bit value suitable for ImDrawList + IMGUI_API ImU32 GetColorU32(ImU32 col, float alpha_mul = 1.0f); // retrieve given color with style alpha applied, packed as a 32-bit value suitable for ImDrawList IMGUI_API const ImVec4& GetStyleColorVec4(ImGuiCol idx); // retrieve style color as stored in ImGuiStyle structure. use to feed back into PushStyleColor(), otherwise use GetColorU32() to get style color with style alpha baked in. - // Cursor / Layout + // Layout cursor positioning // - By "cursor" we mean the current output position. // - The typical widget behavior is to output themselves at the current cursor position, then move the cursor one line down. // - You can call SameLine() between widgets to undo the last carriage return and output at the right of the preceding widget. // - Attention! We currently have inconsistencies between window-local and absolute positions we will aim to fix with future API: - // Window-local coordinates: SameLine(), GetCursorPos(), SetCursorPos(), GetCursorStartPos(), GetContentRegionMax(), GetWindowContentRegion*(), PushTextWrapPos() - // Absolute coordinate: GetCursorScreenPos(), SetCursorScreenPos(), all ImDrawList:: functions. + // - Absolute coordinate: GetCursorScreenPos(), SetCursorScreenPos(), all ImDrawList:: functions. -> this is the preferred way forward. + // - Window-local coordinates: SameLine(), GetCursorPos(), SetCursorPos(), GetCursorStartPos(), GetContentRegionMax(), GetWindowContentRegion*(), PushTextWrapPos() + // - GetCursorScreenPos() = GetCursorPos() + GetWindowPos(). GetWindowPos() is almost only ever useful to convert from window-local to absolute coordinates. + IMGUI_API ImVec2 GetCursorScreenPos(); // cursor position in absolute coordinates (prefer using this, also more useful to work with ImDrawList API). + IMGUI_API void SetCursorScreenPos(const ImVec2& pos); // cursor position in absolute coordinates + IMGUI_API ImVec2 GetCursorPos(); // [window-local] cursor position in window coordinates (relative to window position) + IMGUI_API float GetCursorPosX(); // [window-local] " + IMGUI_API float GetCursorPosY(); // [window-local] " + IMGUI_API void SetCursorPos(const ImVec2& local_pos); // [window-local] " + IMGUI_API void SetCursorPosX(float local_x); // [window-local] " + IMGUI_API void SetCursorPosY(float local_y); // [window-local] " + IMGUI_API ImVec2 GetCursorStartPos(); // [window-local] initial cursor position, in window coordinates + + // Other layout functions IMGUI_API void Separator(); // separator, generally horizontal. inside a menu bar or in horizontal layout mode, this becomes a vertical separator. IMGUI_API void SameLine(float offset_from_start_x=0.0f, float spacing=-1.0f); // call between widgets or groups to layout them horizontally. X position given in window coordinates. IMGUI_API void NewLine(); // undo a SameLine() or force a new line when in a horizontal-layout context. @@ -445,15 +485,6 @@ namespace ImGui IMGUI_API void Unindent(float indent_w = 0.0f); // move content position back to the left, by indent_w, or style.IndentSpacing if indent_w <= 0 IMGUI_API void BeginGroup(); // lock horizontal starting position IMGUI_API void EndGroup(); // unlock horizontal starting position + capture the whole group bounding box into one "item" (so you can use IsItemHovered() or layout primitives such as SameLine() on whole group, etc.) - IMGUI_API ImVec2 GetCursorPos(); // cursor position in window coordinates (relative to window position) - IMGUI_API float GetCursorPosX(); // (some functions are using window-relative coordinates, such as: GetCursorPos, GetCursorStartPos, GetContentRegionMax, GetWindowContentRegion* etc. - IMGUI_API float GetCursorPosY(); // other functions such as GetCursorScreenPos or everything in ImDrawList:: - IMGUI_API void SetCursorPos(const ImVec2& local_pos); // are using the main, absolute coordinate system. - IMGUI_API void SetCursorPosX(float local_x); // GetWindowPos() + GetCursorPos() == GetCursorScreenPos() etc.) - IMGUI_API void SetCursorPosY(float local_y); // - IMGUI_API ImVec2 GetCursorStartPos(); // initial cursor position in window coordinates - IMGUI_API ImVec2 GetCursorScreenPos(); // cursor position in absolute coordinates (useful to work with ImDrawList API). generally top-left == GetMainViewport()->Pos == (0,0) in single viewport mode, and bottom-right == GetMainViewport()->Pos+Size == io.DisplaySize in single-viewport mode. - IMGUI_API void SetCursorScreenPos(const ImVec2& pos); // cursor position in absolute coordinates IMGUI_API void AlignTextToFramePadding(); // vertically align upcoming text baseline to FramePadding.y so that it will align properly to regularly framed items (call if you have text on a line before a framed item) IMGUI_API float GetTextLineHeight(); // ~ FontSize IMGUI_API float GetTextLineHeightWithSpacing(); // ~ FontSize + style.ItemSpacing.y (distance in pixels between 2 consecutive lines of text) @@ -500,7 +531,7 @@ namespace ImGui // - Most widgets return true when the value has been changed or when pressed/selected // - You may also use one of the many IsItemXXX functions (e.g. IsItemActive, IsItemHovered, etc.) to query widget state. IMGUI_API bool Button(const char* label, const ImVec2& size = ImVec2(0, 0)); // button - IMGUI_API bool SmallButton(const char* label); // button with FramePadding=(0,0) to easily embed within text + IMGUI_API bool SmallButton(const char* label); // button with (FramePadding.y == 0) to easily embed within text IMGUI_API bool InvisibleButton(const char* str_id, const ImVec2& size, ImGuiButtonFlags flags = 0); // flexible button behavior without the visuals, frequently useful to build custom behaviors using the public api (along with IsItemActive, IsItemHovered, etc.) IMGUI_API bool ArrowButton(const char* str_id, ImGuiDir dir); // square button with an arrow shape IMGUI_API bool Checkbox(const char* label, bool* v); @@ -513,8 +544,10 @@ namespace ImGui // Widgets: Images // - Read about ImTextureID here: https://github.com/ocornut/imgui/wiki/Image-Loading-and-Displaying-Examples - IMGUI_API void Image(ImTextureID user_texture_id, const ImVec2& size, const ImVec2& uv0 = ImVec2(0, 0), const ImVec2& uv1 = ImVec2(1, 1), const ImVec4& tint_col = ImVec4(1, 1, 1, 1), const ImVec4& border_col = ImVec4(0, 0, 0, 0)); - IMGUI_API bool ImageButton(const char* str_id, ImTextureID user_texture_id, const ImVec2& size, const ImVec2& uv0 = ImVec2(0, 0), const ImVec2& uv1 = ImVec2(1, 1), const ImVec4& bg_col = ImVec4(0, 0, 0, 0), const ImVec4& tint_col = ImVec4(1, 1, 1, 1)); + // - 'uv0' and 'uv1' are texture coordinates. Read about them from the same link above. + // - Note that Image() may add +2.0f to provided size if a border is visible, ImageButton() adds style.FramePadding*2.0f to provided size. + IMGUI_API void Image(ImTextureID user_texture_id, const ImVec2& image_size, const ImVec2& uv0 = ImVec2(0, 0), const ImVec2& uv1 = ImVec2(1, 1), const ImVec4& tint_col = ImVec4(1, 1, 1, 1), const ImVec4& border_col = ImVec4(0, 0, 0, 0)); + IMGUI_API bool ImageButton(const char* str_id, ImTextureID user_texture_id, const ImVec2& image_size, const ImVec2& uv0 = ImVec2(0, 0), const ImVec2& uv1 = ImVec2(1, 1), const ImVec4& bg_col = ImVec4(0, 0, 0, 0), const ImVec4& tint_col = ImVec4(1, 1, 1, 1)); // Widgets: Combo Box (Dropdown) // - The BeginCombo()/EndCombo() api allows you to manage your contents and selection state however you want it, by creating e.g. Selectable() items. @@ -523,7 +556,7 @@ namespace ImGui IMGUI_API void EndCombo(); // only call EndCombo() if BeginCombo() returns true! IMGUI_API bool Combo(const char* label, int* current_item, const char* const items[], int items_count, int popup_max_height_in_items = -1); IMGUI_API bool Combo(const char* label, int* current_item, const char* items_separated_by_zeros, int popup_max_height_in_items = -1); // Separate items with \0 within a string, end item-list with \0\0. e.g. "One\0Two\0Three\0" - IMGUI_API bool Combo(const char* label, int* current_item, bool(*items_getter)(void* data, int idx, const char** out_text), void* data, int items_count, int popup_max_height_in_items = -1); + IMGUI_API bool Combo(const char* label, int* current_item, const char* (*getter)(void* user_data, int idx), void* user_data, int items_count, int popup_max_height_in_items = -1); // Widgets: Drag Sliders // - CTRL+Click on any drag box to turn them into an input box. Manually input values aren't clamped by default and can go off-bounds. Use ImGuiSliderFlags_AlwaysClamp to always clamp. @@ -611,9 +644,9 @@ namespace ImGui IMGUI_API bool TreeNodeEx(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, ...) IM_FMTARGS(3); IMGUI_API bool TreeNodeExV(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args) IM_FMTLIST(3); IMGUI_API bool TreeNodeExV(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args) IM_FMTLIST(3); - IMGUI_API void TreePush(const char* str_id); // ~ Indent()+PushId(). Already called by TreeNode() when returning true, but you can call TreePush/TreePop yourself if desired. + IMGUI_API void TreePush(const char* str_id); // ~ Indent()+PushID(). Already called by TreeNode() when returning true, but you can call TreePush/TreePop yourself if desired. IMGUI_API void TreePush(const void* ptr_id); // " - IMGUI_API void TreePop(); // ~ Unindent()+PopId() + IMGUI_API void TreePop(); // ~ Unindent()+PopID() IMGUI_API float GetTreeNodeToLabelSpacing(); // horizontal distance preceding label when using TreeNode*() or Bullet() == (g.FontSize + style.FramePadding.x*2) for a regular unframed TreeNode IMGUI_API bool CollapsingHeader(const char* label, ImGuiTreeNodeFlags flags = 0); // if returning 'true' the header is open. doesn't indent nor push on ID stack. user doesn't have to call TreePop(). IMGUI_API bool CollapsingHeader(const char* label, bool* p_visible, ImGuiTreeNodeFlags flags = 0); // when 'p_visible != NULL': if '*p_visible==true' display an additional small close button on upper right of the header which will set the bool to false when clicked, if '*p_visible==false' don't display the header. @@ -626,22 +659,22 @@ namespace ImGui IMGUI_API bool Selectable(const char* label, bool* p_selected, ImGuiSelectableFlags flags = 0, const ImVec2& size = ImVec2(0, 0)); // "bool* p_selected" point to the selection state (read-write), as a convenient helper. // Widgets: List Boxes - // - This is essentially a thin wrapper to using BeginChild/EndChild with some stylistic changes. - // - The BeginListBox()/EndListBox() api allows you to manage your contents and selection state however you want it, by creating e.g. Selectable() or any items. + // - This is essentially a thin wrapper to using BeginChild/EndChild with the ImGuiChildFlags_FrameStyle flag for stylistic changes + displaying a label. + // - You can submit contents and manage your selection state however you want it, by creating e.g. Selectable() or any other items. // - The simplified/old ListBox() api are helpers over BeginListBox()/EndListBox() which are kept available for convenience purpose. This is analoguous to how Combos are created. // - Choose frame width: size.x > 0.0f: custom / size.x < 0.0f or -FLT_MIN: right-align / size.x = 0.0f (default): use current ItemWidth // - Choose frame height: size.y > 0.0f: custom / size.y < 0.0f or -FLT_MIN: bottom-align / size.y = 0.0f (default): arbitrary default height which can fit ~7 items IMGUI_API bool BeginListBox(const char* label, const ImVec2& size = ImVec2(0, 0)); // open a framed scrolling region IMGUI_API void EndListBox(); // only call EndListBox() if BeginListBox() returned true! IMGUI_API bool ListBox(const char* label, int* current_item, const char* const items[], int items_count, int height_in_items = -1); - IMGUI_API bool ListBox(const char* label, int* current_item, bool (*items_getter)(void* data, int idx, const char** out_text), void* data, int items_count, int height_in_items = -1); + IMGUI_API bool ListBox(const char* label, int* current_item, const char* (*getter)(void* user_data, int idx), void* user_data, int items_count, int height_in_items = -1); // Widgets: Data Plotting // - Consider using ImPlot (https://github.com/epezent/implot) which is much better! IMGUI_API void PlotLines(const char* label, const float* values, int values_count, int values_offset = 0, const char* overlay_text = NULL, float scale_min = FLT_MAX, float scale_max = FLT_MAX, ImVec2 graph_size = ImVec2(0, 0), int stride = sizeof(float)); IMGUI_API void PlotLines(const char* label, float(*values_getter)(void* data, int idx), void* data, int values_count, int values_offset = 0, const char* overlay_text = NULL, float scale_min = FLT_MAX, float scale_max = FLT_MAX, ImVec2 graph_size = ImVec2(0, 0)); IMGUI_API void PlotHistogram(const char* label, const float* values, int values_count, int values_offset = 0, const char* overlay_text = NULL, float scale_min = FLT_MAX, float scale_max = FLT_MAX, ImVec2 graph_size = ImVec2(0, 0), int stride = sizeof(float)); - IMGUI_API void PlotHistogram(const char* label, float(*values_getter)(void* data, int idx), void* data, int values_count, int values_offset = 0, const char* overlay_text = NULL, float scale_min = FLT_MAX, float scale_max = FLT_MAX, ImVec2 graph_size = ImVec2(0, 0)); + IMGUI_API void PlotHistogram(const char* label, float (*values_getter)(void* data, int idx), void* data, int values_count, int values_offset = 0, const char* overlay_text = NULL, float scale_min = FLT_MAX, float scale_max = FLT_MAX, ImVec2 graph_size = ImVec2(0, 0)); // Widgets: Value() Helpers. // - Those are merely shortcut to calling Text() with a format string. Output single value in "name: value" format (tip: freely declare more in your code to handle your types. you can add functions to the ImGui namespace) @@ -665,12 +698,21 @@ namespace ImGui IMGUI_API bool MenuItem(const char* label, const char* shortcut, bool* p_selected, bool enabled = true); // return true when activated + toggle (*p_selected) if p_selected != NULL // Tooltips - // - Tooltip are windows following the mouse. They do not take focus away. - IMGUI_API bool BeginTooltip(); // begin/append a tooltip window. to create full-featured tooltip (with any kind of items). - IMGUI_API void EndTooltip(); // only call EndTooltip() if BeginTooltip() returns true! - IMGUI_API void SetTooltip(const char* fmt, ...) IM_FMTARGS(1); // set a text-only tooltip, typically use with ImGui::IsItemHovered(). override any previous call to SetTooltip(). + // - Tooltips are windows following the mouse. They do not take focus away. + // - A tooltip window can contain items of any types. SetTooltip() is a shortcut for the 'if (BeginTooltip()) { Text(...); EndTooltip(); }' idiom. + IMGUI_API bool BeginTooltip(); // begin/append a tooltip window. + IMGUI_API void EndTooltip(); // only call EndTooltip() if BeginTooltip()/BeginItemTooltip() returns true! + IMGUI_API void SetTooltip(const char* fmt, ...) IM_FMTARGS(1); // set a text-only tooltip. Often used after a ImGui::IsItemHovered() check. Override any previous call to SetTooltip(). IMGUI_API void SetTooltipV(const char* fmt, va_list args) IM_FMTLIST(1); + // Tooltips: helpers for showing a tooltip when hovering an item + // - BeginItemTooltip() is a shortcut for the 'if (IsItemHovered(ImGuiHoveredFlags_ForTooltip) && BeginTooltip())' idiom. + // - SetItemTooltip() is a shortcut for the 'if (IsItemHovered(ImGuiHoveredFlags_ForTooltip)) { SetTooltip(...); }' idiom. + // - Where 'ImGuiHoveredFlags_ForTooltip' itself is a shortcut to use 'style.HoverFlagsForTooltipMouse' or 'style.HoverFlagsForTooltipNav' depending on active input type. For mouse it defaults to 'ImGuiHoveredFlags_Stationary | ImGuiHoveredFlags_DelayShort'. + IMGUI_API bool BeginItemTooltip(); // begin/append a tooltip window if preceding item was hovered. + IMGUI_API void SetItemTooltip(const char* fmt, ...) IM_FMTARGS(1); // set a text-only tooltip if preceeding item was hovered. override any previous call to SetTooltip(). + IMGUI_API void SetItemTooltipV(const char* fmt, va_list args) IM_FMTLIST(1); + // Popups, Modals // - They block normal mouse hovering detection (and therefore most mouse interactions) behind them. // - If not modal: they can be closed by clicking anywhere outside them, or by pressing ESCAPE. @@ -679,9 +721,7 @@ namespace ImGui // - You can bypass the hovering restriction by using ImGuiHoveredFlags_AllowWhenBlockedByPopup when calling IsItemHovered() or IsWindowHovered(). // - IMPORTANT: Popup identifiers are relative to the current ID stack, so OpenPopup and BeginPopup generally needs to be at the same level of the stack. // This is sometimes leading to confusing mistakes. May rework this in the future. - - // Popups: begin/end functions - // - BeginPopup(): query popup state, if open start appending into the window. Call EndPopup() afterwards. ImGuiWindowFlags are forwarded to the window. + // - BeginPopup(): query popup state, if open start appending into the window. Call EndPopup() afterwards if returned true. ImGuiWindowFlags are forwarded to the window. // - BeginPopupModal(): block every interaction behind the window, cannot be closed by user, add a dimming background, has a title bar. IMGUI_API bool BeginPopup(const char* str_id, ImGuiWindowFlags flags = 0); // return true if the popup is open, and you can start outputting to it. IMGUI_API bool BeginPopupModal(const char* name, bool* p_open = NULL, ImGuiWindowFlags flags = 0); // return true if the modal is open, and you can start outputting to it. @@ -731,12 +771,10 @@ namespace ImGui // TableNextColumn() will automatically wrap-around into the next row if needed. // - IMPORTANT: Comparatively to the old Columns() API, we need to call TableNextColumn() for the first column! // - Summary of possible call flow: - // -------------------------------------------------------------------------------------------------------- - // TableNextRow() -> TableSetColumnIndex(0) -> Text("Hello 0") -> TableSetColumnIndex(1) -> Text("Hello 1") // OK - // TableNextRow() -> TableNextColumn() -> Text("Hello 0") -> TableNextColumn() -> Text("Hello 1") // OK - // TableNextColumn() -> Text("Hello 0") -> TableNextColumn() -> Text("Hello 1") // OK: TableNextColumn() automatically gets to next row! - // TableNextRow() -> Text("Hello 0") // Not OK! Missing TableSetColumnIndex() or TableNextColumn()! Text will not appear! - // -------------------------------------------------------------------------------------------------------- + // - TableNextRow() -> TableSetColumnIndex(0) -> Text("Hello 0") -> TableSetColumnIndex(1) -> Text("Hello 1") // OK + // - TableNextRow() -> TableNextColumn() -> Text("Hello 0") -> TableNextColumn() -> Text("Hello 1") // OK + // - TableNextColumn() -> Text("Hello 0") -> TableNextColumn() -> Text("Hello 1") // OK: TableNextColumn() automatically gets to next row! + // - TableNextRow() -> Text("Hello 0") // Not OK! Missing TableSetColumnIndex() or TableNextColumn()! Text will not appear! // - 5. Call EndTable() IMGUI_API bool BeginTable(const char* str_id, int column, ImGuiTableFlags flags = 0, const ImVec2& outer_size = ImVec2(0.0f, 0.0f), float inner_width = 0.0f); IMGUI_API void EndTable(); // only call EndTable() if BeginTable() returns true! @@ -754,8 +792,9 @@ namespace ImGui // - Use TableSetupScrollFreeze() to lock columns/rows so they stay visible when scrolled. IMGUI_API void TableSetupColumn(const char* label, ImGuiTableColumnFlags flags = 0, float init_width_or_weight = 0.0f, ImGuiID user_id = 0); IMGUI_API void TableSetupScrollFreeze(int cols, int rows); // lock columns/rows so they stay visible when scrolled. - IMGUI_API void TableHeadersRow(); // submit all headers cells based on data provided to TableSetupColumn() + submit context menu IMGUI_API void TableHeader(const char* label); // submit one header cell manually (rarely used) + IMGUI_API void TableHeadersRow(); // submit a row with headers cells based on data provided to TableSetupColumn() + submit context menu + IMGUI_API void TableAngledHeadersRow(); // submit a row with angled headers for every column with the ImGuiTableColumnFlags_AngledHeader flag. MUST BE FIRST ROW. // Tables: Sorting & Miscellaneous functions // - Sorting: call TableGetSortSpecs() to retrieve latest sort specs for the table. NULL when not sorting. @@ -792,6 +831,26 @@ namespace ImGui IMGUI_API bool TabItemButton(const char* label, ImGuiTabItemFlags flags = 0); // create a Tab behaving like a button. return true when clicked. cannot be selected in the tab bar. IMGUI_API void SetTabItemClosed(const char* tab_or_docked_window_label); // notify TabBar or Docking system of a closed tab/window ahead (useful to reduce visual flicker on reorderable tab bars). For tab-bar: call after BeginTabBar() and before Tab submissions. Otherwise call with a window name. + // Docking + // [BETA API] Enable with io.ConfigFlags |= ImGuiConfigFlags_DockingEnable. + // Note: You can use most Docking facilities without calling any API. You DO NOT need to call DockSpace() to use Docking! + // - Drag from window title bar or their tab to dock/undock. Hold SHIFT to disable docking. + // - Drag from window menu button (upper-left button) to undock an entire node (all windows). + // - When io.ConfigDockingWithShift == true, you instead need to hold SHIFT to enable docking. + // About dockspaces: + // - Use DockSpaceOverViewport() to create an explicit dock node covering the screen or a specific viewport. + // This is often used with ImGuiDockNodeFlags_PassthruCentralNode to make it transparent. + // - Use DockSpace() to create an explicit dock node _within_ an existing window. See Docking demo for details. + // - Important: Dockspaces need to be submitted _before_ any window they can host. Submit it early in your frame! + // - Important: Dockspaces need to be kept alive if hidden, otherwise windows docked into it will be undocked. + // e.g. if you have multiple tabs with a dockspace inside each tab: submit the non-visible dockspaces with ImGuiDockNodeFlags_KeepAliveOnly. + IMGUI_API ImGuiID DockSpace(ImGuiID id, const ImVec2& size = ImVec2(0, 0), ImGuiDockNodeFlags flags = 0, const ImGuiWindowClass* window_class = NULL); + IMGUI_API ImGuiID DockSpaceOverViewport(const ImGuiViewport* viewport = NULL, ImGuiDockNodeFlags flags = 0, const ImGuiWindowClass* window_class = NULL); + IMGUI_API void SetNextWindowDockID(ImGuiID dock_id, ImGuiCond cond = 0); // set next window dock id + IMGUI_API void SetNextWindowClass(const ImGuiWindowClass* window_class); // set next window class (control docking compatibility + provide hints to platform backend via custom viewport flags and platform parent/child relationship) + IMGUI_API ImGuiID GetWindowDockID(); + IMGUI_API bool IsWindowDocked(); // is current window docked into another window? + // Logging/Capture // - All text output from the interface can be captured into tty/file/clipboard. By default, tree nodes are automatically opened during logging. IMGUI_API void LogToTTY(int auto_open_depth = -1); // start logging to tty (stdout) @@ -813,7 +872,7 @@ namespace ImGui IMGUI_API bool BeginDragDropTarget(); // call after submitting an item that may receive a payload. If this returns true, you can call AcceptDragDropPayload() + EndDragDropTarget() IMGUI_API const ImGuiPayload* AcceptDragDropPayload(const char* type, ImGuiDragDropFlags flags = 0); // accept contents of a given type. If ImGuiDragDropFlags_AcceptBeforeDelivery is set you can peek into the payload before the mouse button is released. IMGUI_API void EndDragDropTarget(); // only call EndDragDropTarget() if BeginDragDropTarget() returns true! - IMGUI_API const ImGuiPayload* GetDragDropPayload(); // peek directly into the current payload from anywhere. may return NULL. use ImGuiPayload::IsDataType() to test for the payload type. + IMGUI_API const ImGuiPayload* GetDragDropPayload(); // peek directly into the current payload from anywhere. returns NULL when drag and drop is finished or inactive. use ImGuiPayload::IsDataType() to test for the payload type. // Disabling [BETA API] // - Disable all user interactions and dim items visuals (applying style.DisabledAlpha over current colors) @@ -832,6 +891,9 @@ namespace ImGui IMGUI_API void SetItemDefaultFocus(); // make last item the default focused item of a window. IMGUI_API void SetKeyboardFocusHere(int offset = 0); // focus keyboard on the next widget. Use positive 'offset' to access sub components of a multiple component widget. Use -1 to access previous widget. + // Overlapping mode + IMGUI_API void SetNextItemAllowOverlap(); // allow next item to be overlapped by a subsequent item. Useful with invisible buttons, selectable, treenode covering an area where subsequent items may need to be added. Note that both Selectable() and TreeNode() have dedicated flags doing this. + // Item/Widgets Utilities and Query Functions // - Most of the functions are referring to the previous Item that has been submitted. // - See Demo Window under "Widgets->Querying Status" for an interactive visualization of most of those functions. @@ -852,7 +914,6 @@ namespace ImGui IMGUI_API ImVec2 GetItemRectMin(); // get upper-left bounding rectangle of the last item (screen space) IMGUI_API ImVec2 GetItemRectMax(); // get lower-right bounding rectangle of the last item (screen space) IMGUI_API ImVec2 GetItemRectSize(); // get size of last item - IMGUI_API void SetItemAllowOverlap(); // allow last item to be overlapped by a subsequent item. sometimes useful with invisible buttons, selectables, etc. to catch unused area. // Viewports // - Currently represents the Platform Window created by the application which is hosting our Dear ImGui windows. @@ -861,8 +922,10 @@ namespace ImGui IMGUI_API ImGuiViewport* GetMainViewport(); // return primary/default viewport. This can never be NULL. // Background/Foreground Draw Lists - IMGUI_API ImDrawList* GetBackgroundDrawList(); // this draw list will be the first rendered one. Useful to quickly draw shapes/text behind dear imgui contents. - IMGUI_API ImDrawList* GetForegroundDrawList(); // this draw list will be the last rendered one. Useful to quickly draw shapes/text over dear imgui contents. + IMGUI_API ImDrawList* GetBackgroundDrawList(); // get background draw list for the viewport associated to the current window. this draw list will be the first rendering one. Useful to quickly draw shapes/text behind dear imgui contents. + IMGUI_API ImDrawList* GetForegroundDrawList(); // get foreground draw list for the viewport associated to the current window. this draw list will be the last rendered one. Useful to quickly draw shapes/text over dear imgui contents. + IMGUI_API ImDrawList* GetBackgroundDrawList(ImGuiViewport* viewport); // get background draw list for the given viewport. this draw list will be the first rendering one. Useful to quickly draw shapes/text behind dear imgui contents. + IMGUI_API ImDrawList* GetForegroundDrawList(ImGuiViewport* viewport); // get foreground draw list for the given viewport. this draw list will be the last rendered one. Useful to quickly draw shapes/text over dear imgui contents. // Miscellaneous Utilities IMGUI_API bool IsRectVisible(const ImVec2& size); // test if rectangle (of given size, starting from cursor position) is visible / not clipped. @@ -873,8 +936,6 @@ namespace ImGui IMGUI_API const char* GetStyleColorName(ImGuiCol idx); // get a string corresponding to the enum value (for display, saving, etc.). IMGUI_API void SetStateStorage(ImGuiStorage* storage); // replace current window storage with our own (if you want to manipulate it yourself, typically clear subsection of it) IMGUI_API ImGuiStorage* GetStateStorage(); - IMGUI_API bool BeginChildFrame(ImGuiID id, const ImVec2& size, ImGuiWindowFlags flags = 0); // helper to create a child window / scrolling region that looks like a normal widget frame - IMGUI_API void EndChildFrame(); // always call EndChildFrame() regardless of BeginChildFrame() return values (which indicates a collapsed/clipped window) // Text Utilities IMGUI_API ImVec2 CalcTextSize(const char* text, const char* text_end = NULL, bool hide_text_after_double_hash = false, float wrap_width = -1.0f); @@ -893,6 +954,7 @@ namespace ImGui IMGUI_API bool IsKeyDown(ImGuiKey key); // is key being held. IMGUI_API bool IsKeyPressed(ImGuiKey key, bool repeat = true); // was key pressed (went from !Down to Down)? if repeat=true, uses io.KeyRepeatDelay / KeyRepeatRate IMGUI_API bool IsKeyReleased(ImGuiKey key); // was key released (went from Down to !Down)? + IMGUI_API bool IsKeyChordPressed(ImGuiKeyChord key_chord); // was key chord (mods + key) pressed, e.g. you can pass 'ImGuiMod_Ctrl | ImGuiKey_S' as a key-chord. This doesn't do any routing or focus check, please consider using Shortcut() function instead. IMGUI_API int GetKeyPressedAmount(ImGuiKey key, float repeat_delay, float rate); // uses provided repeat rate/delay. return a count, most often 0 or 1 but might be >1 if RepeatRate is small enough that DeltaTime > RepeatRate IMGUI_API const char* GetKeyName(ImGuiKey key); // [DEBUG] returns English name of the key. Those names a provided for debugging purpose and are not meant to be saved persistently not compared. IMGUI_API void SetNextFrameWantCaptureKeyboard(bool want_capture_keyboard); // Override io.WantCaptureKeyboard flag next frame (said flag is left for your application to handle, typically when true it instructs your app to ignore inputs). e.g. force capture keyboard when your widget is being hovered. This is equivalent to setting "io.WantCaptureKeyboard = want_capture_keyboard"; after the next NewFrame() call. @@ -933,7 +995,10 @@ namespace ImGui IMGUI_API const char* SaveIniSettingsToMemory(size_t* out_ini_size = NULL); // return a zero-terminated string with the .ini data which you can save by your own mean. call when io.WantSaveIniSettings is set, then save data by your own mean and clear io.WantSaveIniSettings. // Debug Utilities + // - Your main debugging friend is the ShowMetricsWindow() function, which is also accessible from Demo->Tools->Metrics Debugger IMGUI_API void DebugTextEncoding(const char* text); + IMGUI_API void DebugFlashStyleColor(ImGuiCol idx); + IMGUI_API void DebugStartItemPicker(); IMGUI_API bool DebugCheckVersionAndDataLayout(const char* version_str, size_t sz_io, size_t sz_style, size_t sz_vec2, size_t sz_vec4, size_t sz_drawvert, size_t sz_drawidx); // This is called by IMGUI_CHECKVERSION() macro. // Memory Allocators @@ -945,6 +1010,16 @@ namespace ImGui IMGUI_API void* MemAlloc(size_t size); IMGUI_API void MemFree(void* ptr); + // (Optional) Platform/OS interface for multi-viewport support + // Read comments around the ImGuiPlatformIO structure for more details. + // Note: You may use GetWindowViewport() to get the current viewport of the current window. + IMGUI_API ImGuiPlatformIO& GetPlatformIO(); // platform/renderer functions, for backend to setup + viewports list. + IMGUI_API void UpdatePlatformWindows(); // call in main loop. will call CreateWindow/ResizeWindow/etc. platform functions for each secondary viewport, and DestroyWindow for each inactive viewport. + IMGUI_API void RenderPlatformWindowsDefault(void* platform_render_arg = NULL, void* renderer_render_arg = NULL); // call in main loop. will call RenderWindow/SwapBuffers platform functions for each secondary viewport which doesn't have the ImGuiViewportFlags_Minimized flag set. May be reimplemented by user for custom rendering needs. + IMGUI_API void DestroyPlatformWindows(); // call DestroyWindow platform functions for all viewports. call from backend Shutdown() if you need to close platform windows before imgui shutdown. otherwise will be called by DestroyContext(). + IMGUI_API ImGuiViewport* FindViewportByID(ImGuiID id); // this is a helper for backends. + IMGUI_API ImGuiViewport* FindViewportByPlatformHandle(void* platform_handle); // this is a helper for backends. the type platform_handle is decided by the backend (e.g. HWND, MyWindow*, GLFWwindow* etc.) + } // namespace ImGui //----------------------------------------------------------------------------- @@ -972,21 +1047,49 @@ enum ImGuiWindowFlags_ ImGuiWindowFlags_NoBringToFrontOnFocus = 1 << 13, // Disable bringing window to front when taking focus (e.g. clicking on it or programmatically giving it focus) ImGuiWindowFlags_AlwaysVerticalScrollbar= 1 << 14, // Always show vertical scrollbar (even if ContentSize.y < Size.y) ImGuiWindowFlags_AlwaysHorizontalScrollbar=1<< 15, // Always show horizontal scrollbar (even if ContentSize.x < Size.x) - ImGuiWindowFlags_AlwaysUseWindowPadding = 1 << 16, // Ensure child windows without border uses style.WindowPadding (ignored by default for non-bordered child windows, because more convenient) - ImGuiWindowFlags_NoNavInputs = 1 << 18, // No gamepad/keyboard navigation within the window - ImGuiWindowFlags_NoNavFocus = 1 << 19, // No focusing toward this window with gamepad/keyboard navigation (e.g. skipped by CTRL+TAB) - ImGuiWindowFlags_UnsavedDocument = 1 << 20, // Display a dot next to the title. When used in a tab/docking context, tab is selected when clicking the X + closure is not assumed (will wait for user to stop submitting the tab). Otherwise closure is assumed when pressing the X, so if you keep submitting the tab may reappear at end of tab bar. + ImGuiWindowFlags_NoNavInputs = 1 << 16, // No gamepad/keyboard navigation within the window + ImGuiWindowFlags_NoNavFocus = 1 << 17, // No focusing toward this window with gamepad/keyboard navigation (e.g. skipped by CTRL+TAB) + ImGuiWindowFlags_UnsavedDocument = 1 << 18, // Display a dot next to the title. When used in a tab/docking context, tab is selected when clicking the X + closure is not assumed (will wait for user to stop submitting the tab). Otherwise closure is assumed when pressing the X, so if you keep submitting the tab may reappear at end of tab bar. + ImGuiWindowFlags_NoDocking = 1 << 19, // Disable docking of this window ImGuiWindowFlags_NoNav = ImGuiWindowFlags_NoNavInputs | ImGuiWindowFlags_NoNavFocus, ImGuiWindowFlags_NoDecoration = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoCollapse, ImGuiWindowFlags_NoInputs = ImGuiWindowFlags_NoMouseInputs | ImGuiWindowFlags_NoNavInputs | ImGuiWindowFlags_NoNavFocus, // [Internal] - ImGuiWindowFlags_NavFlattened = 1 << 23, // [BETA] On child window: allow gamepad/keyboard navigation to cross over parent border to this child or between sibling child windows. + ImGuiWindowFlags_NavFlattened = 1 << 23, // [BETA] On child window: share focus scope, allow gamepad/keyboard navigation to cross over parent border to this child or between sibling child windows. ImGuiWindowFlags_ChildWindow = 1 << 24, // Don't use! For internal use by BeginChild() ImGuiWindowFlags_Tooltip = 1 << 25, // Don't use! For internal use by BeginTooltip() ImGuiWindowFlags_Popup = 1 << 26, // Don't use! For internal use by BeginPopup() ImGuiWindowFlags_Modal = 1 << 27, // Don't use! For internal use by BeginPopupModal() ImGuiWindowFlags_ChildMenu = 1 << 28, // Don't use! For internal use by BeginMenu() + ImGuiWindowFlags_DockNodeHost = 1 << 29, // Don't use! For internal use by Begin()/NewFrame() + + // Obsolete names +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + ImGuiWindowFlags_AlwaysUseWindowPadding = 1 << 30, // Obsoleted in 1.90: Use ImGuiChildFlags_AlwaysUseWindowPadding in BeginChild() call. +#endif +}; + +// Flags for ImGui::BeginChild() +// (Legacy: bit 0 must always correspond to ImGuiChildFlags_Border to be backward compatible with old API using 'bool border = false'. +// About using AutoResizeX/AutoResizeY flags: +// - May be combined with SetNextWindowSizeConstraints() to set a min/max size for each axis (see "Demo->Child->Auto-resize with Constraints"). +// - Size measurement for a given axis is only performed when the child window is within visible boundaries, or is just appearing. +// - This allows BeginChild() to return false when not within boundaries (e.g. when scrolling), which is more optimal. BUT it won't update its auto-size while clipped. +// While not perfect, it is a better default behavior as the always-on performance gain is more valuable than the occasional "resizing after becoming visible again" glitch. +// - You may also use ImGuiChildFlags_AlwaysAutoResize to force an update even when child window is not in view. +// HOWEVER PLEASE UNDERSTAND THAT DOING SO WILL PREVENT BeginChild() FROM EVER RETURNING FALSE, disabling benefits of coarse clipping. +enum ImGuiChildFlags_ +{ + ImGuiChildFlags_None = 0, + ImGuiChildFlags_Border = 1 << 0, // Show an outer border and enable WindowPadding. (IMPORTANT: this is always == 1 == true for legacy reason) + ImGuiChildFlags_AlwaysUseWindowPadding = 1 << 1, // Pad with style.WindowPadding even if no border are drawn (no padding by default for non-bordered child windows because it makes more sense) + ImGuiChildFlags_ResizeX = 1 << 2, // Allow resize from right border (layout direction). Enable .ini saving (unless ImGuiWindowFlags_NoSavedSettings passed to window flags) + ImGuiChildFlags_ResizeY = 1 << 3, // Allow resize from bottom border (layout direction). " + ImGuiChildFlags_AutoResizeX = 1 << 4, // Enable auto-resizing width. Read "IMPORTANT: Size measurement" details above. + ImGuiChildFlags_AutoResizeY = 1 << 5, // Enable auto-resizing height. Read "IMPORTANT: Size measurement" details above. + ImGuiChildFlags_AlwaysAutoResize = 1 << 6, // Combined with AutoResizeX/AutoResizeY. Always measure size even when child is hidden, always return true, always disable clipping optimization! NOT RECOMMENDED. + ImGuiChildFlags_FrameStyle = 1 << 7, // Style the child window like a framed item: use FrameBg, FrameRounding, FrameBorderSize, FramePadding instead of ChildBg, ChildRounding, ChildBorderSize, WindowPadding. }; // Flags for ImGui::InputText() @@ -1026,25 +1129,30 @@ enum ImGuiTreeNodeFlags_ ImGuiTreeNodeFlags_None = 0, ImGuiTreeNodeFlags_Selected = 1 << 0, // Draw as selected ImGuiTreeNodeFlags_Framed = 1 << 1, // Draw frame with background (e.g. for CollapsingHeader) - ImGuiTreeNodeFlags_AllowItemOverlap = 1 << 2, // Hit testing to allow subsequent widgets to overlap this one + ImGuiTreeNodeFlags_AllowOverlap = 1 << 2, // Hit testing to allow subsequent widgets to overlap this one ImGuiTreeNodeFlags_NoTreePushOnOpen = 1 << 3, // Don't do a TreePush() when open (e.g. for CollapsingHeader) = no extra indent nor pushing on ID stack ImGuiTreeNodeFlags_NoAutoOpenOnLog = 1 << 4, // Don't automatically and temporarily open node when Logging is active (by default logging will automatically open tree nodes) ImGuiTreeNodeFlags_DefaultOpen = 1 << 5, // Default node to be open ImGuiTreeNodeFlags_OpenOnDoubleClick = 1 << 6, // Need double-click to open node ImGuiTreeNodeFlags_OpenOnArrow = 1 << 7, // Only open when clicking on the arrow part. If ImGuiTreeNodeFlags_OpenOnDoubleClick is also set, single-click arrow or double-click all box to open. ImGuiTreeNodeFlags_Leaf = 1 << 8, // No collapsing, no arrow (use as a convenience for leaf nodes). - ImGuiTreeNodeFlags_Bullet = 1 << 9, // Display a bullet instead of arrow + ImGuiTreeNodeFlags_Bullet = 1 << 9, // Display a bullet instead of arrow. IMPORTANT: node can still be marked open/close if you don't set the _Leaf flag! ImGuiTreeNodeFlags_FramePadding = 1 << 10, // Use FramePadding (even for an unframed text node) to vertically align text baseline to regular widget height. Equivalent to calling AlignTextToFramePadding(). ImGuiTreeNodeFlags_SpanAvailWidth = 1 << 11, // Extend hit box to the right-most edge, even if not framed. This is not the default in order to allow adding other items on the same line. In the future we may refactor the hit system to be front-to-back, allowing natural overlaps and then this can become the default. ImGuiTreeNodeFlags_SpanFullWidth = 1 << 12, // Extend hit box to the left-most and right-most edges (bypass the indented area). - ImGuiTreeNodeFlags_NavLeftJumpsBackHere = 1 << 13, // (WIP) Nav: left direction may move to this TreeNode() from any of its child (items submitted between TreeNode and TreePop) - //ImGuiTreeNodeFlags_NoScrollOnOpen = 1 << 14, // FIXME: TODO: Disable automatic scroll on TreePop() if node got just open and contents is not visible + ImGuiTreeNodeFlags_SpanAllColumns = 1 << 13, // Frame will span all columns of its container table (text will still fit in current column) + ImGuiTreeNodeFlags_NavLeftJumpsBackHere = 1 << 14, // (WIP) Nav: left direction may move to this TreeNode() from any of its child (items submitted between TreeNode and TreePop) + //ImGuiTreeNodeFlags_NoScrollOnOpen = 1 << 15, // FIXME: TODO: Disable automatic scroll on TreePop() if node got just open and contents is not visible ImGuiTreeNodeFlags_CollapsingHeader = ImGuiTreeNodeFlags_Framed | ImGuiTreeNodeFlags_NoTreePushOnOpen | ImGuiTreeNodeFlags_NoAutoOpenOnLog, + +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + ImGuiTreeNodeFlags_AllowItemOverlap = ImGuiTreeNodeFlags_AllowOverlap, // Renamed in 1.89.7 +#endif }; // Flags for OpenPopup*(), BeginPopupContext*(), IsPopupOpen() functions. -// - To be backward compatible with older API which took an 'int mouse_button = 1' argument, we need to treat -// small flags values as a mouse button index, so we encode the mouse button in the first few bits of the flags. +// - To be backward compatible with older API which took an 'int mouse_button = 1' argument instead of 'ImGuiPopupFlags flags', +// we need to treat small flags values as a mouse button index, so we encode the mouse button in the first few bits of the flags. // It is therefore guaranteed to be legal to pass a mouse button index in ImGuiPopupFlags. // - For the same reason, we exceptionally default the ImGuiPopupFlags argument of BeginPopupContextXXX functions to 1 instead of 0. // IMPORTANT: because the default parameter is 1 (==ImGuiPopupFlags_MouseButtonRight), if you rely on the default parameter @@ -1058,10 +1166,12 @@ enum ImGuiPopupFlags_ ImGuiPopupFlags_MouseButtonMiddle = 2, // For BeginPopupContext*(): open on Middle Mouse release. Guaranteed to always be == 2 (same as ImGuiMouseButton_Middle) ImGuiPopupFlags_MouseButtonMask_ = 0x1F, ImGuiPopupFlags_MouseButtonDefault_ = 1, - ImGuiPopupFlags_NoOpenOverExistingPopup = 1 << 5, // For OpenPopup*(), BeginPopupContext*(): don't open if there's already a popup at the same level of the popup stack - ImGuiPopupFlags_NoOpenOverItems = 1 << 6, // For BeginPopupContextWindow(): don't return true when hovering items, only when hovering empty space - ImGuiPopupFlags_AnyPopupId = 1 << 7, // For IsPopupOpen(): ignore the ImGuiID parameter and test for any popup. - ImGuiPopupFlags_AnyPopupLevel = 1 << 8, // For IsPopupOpen(): search/test at any level of the popup stack (default test in the current level) + ImGuiPopupFlags_NoReopen = 1 << 5, // For OpenPopup*(), BeginPopupContext*(): don't reopen same popup if already open (won't reposition, won't reinitialize navigation) + //ImGuiPopupFlags_NoReopenAlwaysNavInit = 1 << 6, // For OpenPopup*(), BeginPopupContext*(): focus and initialize navigation even when not reopening. + ImGuiPopupFlags_NoOpenOverExistingPopup = 1 << 7, // For OpenPopup*(), BeginPopupContext*(): don't open if there's already a popup at the same level of the popup stack + ImGuiPopupFlags_NoOpenOverItems = 1 << 8, // For BeginPopupContextWindow(): don't return true when hovering items, only when hovering empty space + ImGuiPopupFlags_AnyPopupId = 1 << 10, // For IsPopupOpen(): ignore the ImGuiID parameter and test for any popup. + ImGuiPopupFlags_AnyPopupLevel = 1 << 11, // For IsPopupOpen(): search/test at any level of the popup stack (default test in the current level) ImGuiPopupFlags_AnyPopup = ImGuiPopupFlags_AnyPopupId | ImGuiPopupFlags_AnyPopupLevel, }; @@ -1070,10 +1180,14 @@ enum ImGuiSelectableFlags_ { ImGuiSelectableFlags_None = 0, ImGuiSelectableFlags_DontClosePopups = 1 << 0, // Clicking this doesn't close parent popup window - ImGuiSelectableFlags_SpanAllColumns = 1 << 1, // Selectable frame can span all columns (text will still fit in current column) + ImGuiSelectableFlags_SpanAllColumns = 1 << 1, // Frame will span all columns of its container table (text will still fit in current column) ImGuiSelectableFlags_AllowDoubleClick = 1 << 2, // Generate press events on double clicks too ImGuiSelectableFlags_Disabled = 1 << 3, // Cannot be selected, display grayed out text - ImGuiSelectableFlags_AllowItemOverlap = 1 << 4, // (WIP) Hit testing to allow subsequent widgets to overlap this one + ImGuiSelectableFlags_AllowOverlap = 1 << 4, // (WIP) Hit testing to allow subsequent widgets to overlap this one + +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + ImGuiSelectableFlags_AllowItemOverlap = ImGuiSelectableFlags_AllowOverlap, // Renamed in 1.89.7 +#endif }; // Flags for ImGui::BeginCombo() @@ -1087,6 +1201,7 @@ enum ImGuiComboFlags_ ImGuiComboFlags_HeightLargest = 1 << 4, // As many fitting items as possible ImGuiComboFlags_NoArrowButton = 1 << 5, // Display on the preview box without the square arrow button ImGuiComboFlags_NoPreview = 1 << 6, // Display only a square arrow button + ImGuiComboFlags_WidthFitPreview = 1 << 7, // Width dynamically calculated from preview contents ImGuiComboFlags_HeightMask_ = ImGuiComboFlags_HeightSmall | ImGuiComboFlags_HeightRegular | ImGuiComboFlags_HeightLarge | ImGuiComboFlags_HeightLargest, }; @@ -1097,7 +1212,7 @@ enum ImGuiTabBarFlags_ ImGuiTabBarFlags_Reorderable = 1 << 0, // Allow manually dragging tabs to re-order them + New tabs are appended at the end of list ImGuiTabBarFlags_AutoSelectNewTabs = 1 << 1, // Automatically select new tabs when they appear ImGuiTabBarFlags_TabListPopupButton = 1 << 2, // Disable buttons to open the tab list popup - ImGuiTabBarFlags_NoCloseWithMiddleMouseButton = 1 << 3, // Disable behavior of closing tabs (that are submitted with p_open != NULL) with middle mouse button. You can still repro this behavior on user's side with if (IsItemHovered() && IsMouseClicked(2)) *p_open = false. + ImGuiTabBarFlags_NoCloseWithMiddleMouseButton = 1 << 3, // Disable behavior of closing tabs (that are submitted with p_open != NULL) with middle mouse button. You may handle this behavior manually on user's side with if (IsItemHovered() && IsMouseClicked(2)) *p_open = false. ImGuiTabBarFlags_NoTabListScrollingButtons = 1 << 4, // Disable scrolling buttons (apply when fitting policy is ImGuiTabBarFlags_FittingPolicyScroll) ImGuiTabBarFlags_NoTooltip = 1 << 5, // Disable tooltips when hovering a tab ImGuiTabBarFlags_FittingPolicyResizeDown = 1 << 6, // Resize tabs when they don't fit @@ -1110,147 +1225,15 @@ enum ImGuiTabBarFlags_ enum ImGuiTabItemFlags_ { ImGuiTabItemFlags_None = 0, - ImGuiTabItemFlags_UnsavedDocument = 1 << 0, // Display a dot next to the title + tab is selected when clicking the X + closure is not assumed (will wait for user to stop submitting the tab). Otherwise closure is assumed when pressing the X, so if you keep submitting the tab may reappear at end of tab bar. + ImGuiTabItemFlags_UnsavedDocument = 1 << 0, // Display a dot next to the title + set ImGuiTabItemFlags_NoAssumedClosure. ImGuiTabItemFlags_SetSelected = 1 << 1, // Trigger flag to programmatically make the tab selected when calling BeginTabItem() - ImGuiTabItemFlags_NoCloseWithMiddleMouseButton = 1 << 2, // Disable behavior of closing tabs (that are submitted with p_open != NULL) with middle mouse button. You can still repro this behavior on user's side with if (IsItemHovered() && IsMouseClicked(2)) *p_open = false. - ImGuiTabItemFlags_NoPushId = 1 << 3, // Don't call PushID(tab->ID)/PopID() on BeginTabItem()/EndTabItem() + ImGuiTabItemFlags_NoCloseWithMiddleMouseButton = 1 << 2, // Disable behavior of closing tabs (that are submitted with p_open != NULL) with middle mouse button. You may handle this behavior manually on user's side with if (IsItemHovered() && IsMouseClicked(2)) *p_open = false. + ImGuiTabItemFlags_NoPushId = 1 << 3, // Don't call PushID()/PopID() on BeginTabItem()/EndTabItem() ImGuiTabItemFlags_NoTooltip = 1 << 4, // Disable tooltip for the given tab ImGuiTabItemFlags_NoReorder = 1 << 5, // Disable reordering this tab or having another tab cross over this tab ImGuiTabItemFlags_Leading = 1 << 6, // Enforce the tab position to the left of the tab bar (after the tab list popup button) ImGuiTabItemFlags_Trailing = 1 << 7, // Enforce the tab position to the right of the tab bar (before the scrolling buttons) -}; - -// Flags for ImGui::BeginTable() -// - Important! Sizing policies have complex and subtle side effects, much more so than you would expect. -// Read comments/demos carefully + experiment with live demos to get acquainted with them. -// - The DEFAULT sizing policies are: -// - Default to ImGuiTableFlags_SizingFixedFit if ScrollX is on, or if host window has ImGuiWindowFlags_AlwaysAutoResize. -// - Default to ImGuiTableFlags_SizingStretchSame if ScrollX is off. -// - When ScrollX is off: -// - Table defaults to ImGuiTableFlags_SizingStretchSame -> all Columns defaults to ImGuiTableColumnFlags_WidthStretch with same weight. -// - Columns sizing policy allowed: Stretch (default), Fixed/Auto. -// - Fixed Columns (if any) will generally obtain their requested width (unless the table cannot fit them all). -// - Stretch Columns will share the remaining width according to their respective weight. -// - Mixed Fixed/Stretch columns is possible but has various side-effects on resizing behaviors. -// The typical use of mixing sizing policies is: any number of LEADING Fixed columns, followed by one or two TRAILING Stretch columns. -// (this is because the visible order of columns have subtle but necessary effects on how they react to manual resizing). -// - When ScrollX is on: -// - Table defaults to ImGuiTableFlags_SizingFixedFit -> all Columns defaults to ImGuiTableColumnFlags_WidthFixed -// - Columns sizing policy allowed: Fixed/Auto mostly. -// - Fixed Columns can be enlarged as needed. Table will show a horizontal scrollbar if needed. -// - When using auto-resizing (non-resizable) fixed columns, querying the content width to use item right-alignment e.g. SetNextItemWidth(-FLT_MIN) doesn't make sense, would create a feedback loop. -// - Using Stretch columns OFTEN DOES NOT MAKE SENSE if ScrollX is on, UNLESS you have specified a value for 'inner_width' in BeginTable(). -// If you specify a value for 'inner_width' then effectively the scrolling space is known and Stretch or mixed Fixed/Stretch columns become meaningful again. -// - Read on documentation at the top of imgui_tables.cpp for details. -enum ImGuiTableFlags_ -{ - // Features - ImGuiTableFlags_None = 0, - ImGuiTableFlags_Resizable = 1 << 0, // Enable resizing columns. - ImGuiTableFlags_Reorderable = 1 << 1, // Enable reordering columns in header row (need calling TableSetupColumn() + TableHeadersRow() to display headers) - ImGuiTableFlags_Hideable = 1 << 2, // Enable hiding/disabling columns in context menu. - ImGuiTableFlags_Sortable = 1 << 3, // Enable sorting. Call TableGetSortSpecs() to obtain sort specs. Also see ImGuiTableFlags_SortMulti and ImGuiTableFlags_SortTristate. - ImGuiTableFlags_NoSavedSettings = 1 << 4, // Disable persisting columns order, width and sort settings in the .ini file. - ImGuiTableFlags_ContextMenuInBody = 1 << 5, // Right-click on columns body/contents will display table context menu. By default it is available in TableHeadersRow(). - // Decorations - ImGuiTableFlags_RowBg = 1 << 6, // Set each RowBg color with ImGuiCol_TableRowBg or ImGuiCol_TableRowBgAlt (equivalent of calling TableSetBgColor with ImGuiTableBgFlags_RowBg0 on each row manually) - ImGuiTableFlags_BordersInnerH = 1 << 7, // Draw horizontal borders between rows. - ImGuiTableFlags_BordersOuterH = 1 << 8, // Draw horizontal borders at the top and bottom. - ImGuiTableFlags_BordersInnerV = 1 << 9, // Draw vertical borders between columns. - ImGuiTableFlags_BordersOuterV = 1 << 10, // Draw vertical borders on the left and right sides. - ImGuiTableFlags_BordersH = ImGuiTableFlags_BordersInnerH | ImGuiTableFlags_BordersOuterH, // Draw horizontal borders. - ImGuiTableFlags_BordersV = ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_BordersOuterV, // Draw vertical borders. - ImGuiTableFlags_BordersInner = ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_BordersInnerH, // Draw inner borders. - ImGuiTableFlags_BordersOuter = ImGuiTableFlags_BordersOuterV | ImGuiTableFlags_BordersOuterH, // Draw outer borders. - ImGuiTableFlags_Borders = ImGuiTableFlags_BordersInner | ImGuiTableFlags_BordersOuter, // Draw all borders. - ImGuiTableFlags_NoBordersInBody = 1 << 11, // [ALPHA] Disable vertical borders in columns Body (borders will always appear in Headers). -> May move to style - ImGuiTableFlags_NoBordersInBodyUntilResize = 1 << 12, // [ALPHA] Disable vertical borders in columns Body until hovered for resize (borders will always appear in Headers). -> May move to style - // Sizing Policy (read above for defaults) - ImGuiTableFlags_SizingFixedFit = 1 << 13, // Columns default to _WidthFixed or _WidthAuto (if resizable or not resizable), matching contents width. - ImGuiTableFlags_SizingFixedSame = 2 << 13, // Columns default to _WidthFixed or _WidthAuto (if resizable or not resizable), matching the maximum contents width of all columns. Implicitly enable ImGuiTableFlags_NoKeepColumnsVisible. - ImGuiTableFlags_SizingStretchProp = 3 << 13, // Columns default to _WidthStretch with default weights proportional to each columns contents widths. - ImGuiTableFlags_SizingStretchSame = 4 << 13, // Columns default to _WidthStretch with default weights all equal, unless overridden by TableSetupColumn(). - // Sizing Extra Options - ImGuiTableFlags_NoHostExtendX = 1 << 16, // Make outer width auto-fit to columns, overriding outer_size.x value. Only available when ScrollX/ScrollY are disabled and Stretch columns are not used. - ImGuiTableFlags_NoHostExtendY = 1 << 17, // Make outer height stop exactly at outer_size.y (prevent auto-extending table past the limit). Only available when ScrollX/ScrollY are disabled. Data below the limit will be clipped and not visible. - ImGuiTableFlags_NoKeepColumnsVisible = 1 << 18, // Disable keeping column always minimally visible when ScrollX is off and table gets too small. Not recommended if columns are resizable. - ImGuiTableFlags_PreciseWidths = 1 << 19, // Disable distributing remainder width to stretched columns (width allocation on a 100-wide table with 3 columns: Without this flag: 33,33,34. With this flag: 33,33,33). With larger number of columns, resizing will appear to be less smooth. - // Clipping - ImGuiTableFlags_NoClip = 1 << 20, // Disable clipping rectangle for every individual columns (reduce draw command count, items will be able to overflow into other columns). Generally incompatible with TableSetupScrollFreeze(). - // Padding - ImGuiTableFlags_PadOuterX = 1 << 21, // Default if BordersOuterV is on. Enable outermost padding. Generally desirable if you have headers. - ImGuiTableFlags_NoPadOuterX = 1 << 22, // Default if BordersOuterV is off. Disable outermost padding. - ImGuiTableFlags_NoPadInnerX = 1 << 23, // Disable inner padding between columns (double inner padding if BordersOuterV is on, single inner padding if BordersOuterV is off). - // Scrolling - ImGuiTableFlags_ScrollX = 1 << 24, // Enable horizontal scrolling. Require 'outer_size' parameter of BeginTable() to specify the container size. Changes default sizing policy. Because this creates a child window, ScrollY is currently generally recommended when using ScrollX. - ImGuiTableFlags_ScrollY = 1 << 25, // Enable vertical scrolling. Require 'outer_size' parameter of BeginTable() to specify the container size. - // Sorting - ImGuiTableFlags_SortMulti = 1 << 26, // Hold shift when clicking headers to sort on multiple column. TableGetSortSpecs() may return specs where (SpecsCount > 1). - ImGuiTableFlags_SortTristate = 1 << 27, // Allow no sorting, disable default sorting. TableGetSortSpecs() may return specs where (SpecsCount == 0). - - // [Internal] Combinations and masks - ImGuiTableFlags_SizingMask_ = ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_SizingFixedSame | ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_SizingStretchSame, -}; - -// Flags for ImGui::TableSetupColumn() -enum ImGuiTableColumnFlags_ -{ - // Input configuration flags - ImGuiTableColumnFlags_None = 0, - ImGuiTableColumnFlags_Disabled = 1 << 0, // Overriding/master disable flag: hide column, won't show in context menu (unlike calling TableSetColumnEnabled() which manipulates the user accessible state) - ImGuiTableColumnFlags_DefaultHide = 1 << 1, // Default as a hidden/disabled column. - ImGuiTableColumnFlags_DefaultSort = 1 << 2, // Default as a sorting column. - ImGuiTableColumnFlags_WidthStretch = 1 << 3, // Column will stretch. Preferable with horizontal scrolling disabled (default if table sizing policy is _SizingStretchSame or _SizingStretchProp). - ImGuiTableColumnFlags_WidthFixed = 1 << 4, // Column will not stretch. Preferable with horizontal scrolling enabled (default if table sizing policy is _SizingFixedFit and table is resizable). - ImGuiTableColumnFlags_NoResize = 1 << 5, // Disable manual resizing. - ImGuiTableColumnFlags_NoReorder = 1 << 6, // Disable manual reordering this column, this will also prevent other columns from crossing over this column. - ImGuiTableColumnFlags_NoHide = 1 << 7, // Disable ability to hide/disable this column. - ImGuiTableColumnFlags_NoClip = 1 << 8, // Disable clipping for this column (all NoClip columns will render in a same draw command). - ImGuiTableColumnFlags_NoSort = 1 << 9, // Disable ability to sort on this field (even if ImGuiTableFlags_Sortable is set on the table). - ImGuiTableColumnFlags_NoSortAscending = 1 << 10, // Disable ability to sort in the ascending direction. - ImGuiTableColumnFlags_NoSortDescending = 1 << 11, // Disable ability to sort in the descending direction. - ImGuiTableColumnFlags_NoHeaderLabel = 1 << 12, // TableHeadersRow() will not submit label for this column. Convenient for some small columns. Name will still appear in context menu. - ImGuiTableColumnFlags_NoHeaderWidth = 1 << 13, // Disable header text width contribution to automatic column width. - ImGuiTableColumnFlags_PreferSortAscending = 1 << 14, // Make the initial sort direction Ascending when first sorting on this column (default). - ImGuiTableColumnFlags_PreferSortDescending = 1 << 15, // Make the initial sort direction Descending when first sorting on this column. - ImGuiTableColumnFlags_IndentEnable = 1 << 16, // Use current Indent value when entering cell (default for column 0). - ImGuiTableColumnFlags_IndentDisable = 1 << 17, // Ignore current Indent value when entering cell (default for columns > 0). Indentation changes _within_ the cell will still be honored. - - // Output status flags, read-only via TableGetColumnFlags() - ImGuiTableColumnFlags_IsEnabled = 1 << 24, // Status: is enabled == not hidden by user/api (referred to as "Hide" in _DefaultHide and _NoHide) flags. - ImGuiTableColumnFlags_IsVisible = 1 << 25, // Status: is visible == is enabled AND not clipped by scrolling. - ImGuiTableColumnFlags_IsSorted = 1 << 26, // Status: is currently part of the sort specs - ImGuiTableColumnFlags_IsHovered = 1 << 27, // Status: is hovered by mouse - - // [Internal] Combinations and masks - ImGuiTableColumnFlags_WidthMask_ = ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_WidthFixed, - ImGuiTableColumnFlags_IndentMask_ = ImGuiTableColumnFlags_IndentEnable | ImGuiTableColumnFlags_IndentDisable, - ImGuiTableColumnFlags_StatusMask_ = ImGuiTableColumnFlags_IsEnabled | ImGuiTableColumnFlags_IsVisible | ImGuiTableColumnFlags_IsSorted | ImGuiTableColumnFlags_IsHovered, - ImGuiTableColumnFlags_NoDirectResize_ = 1 << 30, // [Internal] Disable user resizing this column directly (it may however we resized indirectly from its left edge) -}; - -// Flags for ImGui::TableNextRow() -enum ImGuiTableRowFlags_ -{ - ImGuiTableRowFlags_None = 0, - ImGuiTableRowFlags_Headers = 1 << 0, // Identify header row (set default background color + width of its contents accounted differently for auto column width) -}; - -// Enum for ImGui::TableSetBgColor() -// Background colors are rendering in 3 layers: -// - Layer 0: draw with RowBg0 color if set, otherwise draw with ColumnBg0 if set. -// - Layer 1: draw with RowBg1 color if set, otherwise draw with ColumnBg1 if set. -// - Layer 2: draw with CellBg color if set. -// The purpose of the two row/columns layers is to let you decide if a background color change should override or blend with the existing color. -// When using ImGuiTableFlags_RowBg on the table, each row has the RowBg0 color automatically set for odd/even rows. -// If you set the color of RowBg0 target, your color will override the existing RowBg0 color. -// If you set the color of RowBg1 or ColumnBg1 target, your color will blend over the RowBg0 color. -enum ImGuiTableBgTarget_ -{ - ImGuiTableBgTarget_None = 0, - ImGuiTableBgTarget_RowBg0 = 1, // Set row background color 0 (generally used for background, automatically set when ImGuiTableFlags_RowBg is used) - ImGuiTableBgTarget_RowBg1 = 2, // Set row background color 1 (generally used for selection marking) - ImGuiTableBgTarget_CellBg = 3, // Set cell background color (top-most color) + ImGuiTabItemFlags_NoAssumedClosure = 1 << 8, // Tab is selected when trying to close + closure is not immediately assumed (will wait for user to stop submitting the tab). Otherwise closure is assumed when pressing the X, so if you keep submitting the tab may reappear at end of tab bar. }; // Flags for ImGui::IsWindowFocused() @@ -1261,7 +1244,7 @@ enum ImGuiFocusedFlags_ ImGuiFocusedFlags_RootWindow = 1 << 1, // Test from root window (top most parent of the current hierarchy) ImGuiFocusedFlags_AnyWindow = 1 << 2, // Return true if any window is focused. Important: If you are trying to tell how to dispatch your low-level inputs, do NOT use this. Use 'io.WantCaptureMouse' instead! Please read the FAQ! ImGuiFocusedFlags_NoPopupHierarchy = 1 << 3, // Do not consider popup hierarchy (do not treat popup emitter as parent of popup) (when used with _ChildWindows or _RootWindow) - //ImGuiFocusedFlags_DockHierarchy = 1 << 4, // Consider docking hierarchy (treat dockspace host as parent of docked window) (when used with _ChildWindows or _RootWindow) + ImGuiFocusedFlags_DockHierarchy = 1 << 4, // Consider docking hierarchy (treat dockspace host as parent of docked window) (when used with _ChildWindows or _RootWindow) ImGuiFocusedFlags_RootAndChildWindows = ImGuiFocusedFlags_RootWindow | ImGuiFocusedFlags_ChildWindows, }; @@ -1275,20 +1258,55 @@ enum ImGuiHoveredFlags_ ImGuiHoveredFlags_RootWindow = 1 << 1, // IsWindowHovered() only: Test from root window (top most parent of the current hierarchy) ImGuiHoveredFlags_AnyWindow = 1 << 2, // IsWindowHovered() only: Return true if any window is hovered ImGuiHoveredFlags_NoPopupHierarchy = 1 << 3, // IsWindowHovered() only: Do not consider popup hierarchy (do not treat popup emitter as parent of popup) (when used with _ChildWindows or _RootWindow) - //ImGuiHoveredFlags_DockHierarchy = 1 << 4, // IsWindowHovered() only: Consider docking hierarchy (treat dockspace host as parent of docked window) (when used with _ChildWindows or _RootWindow) + ImGuiHoveredFlags_DockHierarchy = 1 << 4, // IsWindowHovered() only: Consider docking hierarchy (treat dockspace host as parent of docked window) (when used with _ChildWindows or _RootWindow) ImGuiHoveredFlags_AllowWhenBlockedByPopup = 1 << 5, // Return true even if a popup window is normally blocking access to this item/window //ImGuiHoveredFlags_AllowWhenBlockedByModal = 1 << 6, // Return true even if a modal popup window is normally blocking access to this item/window. FIXME-TODO: Unavailable yet. ImGuiHoveredFlags_AllowWhenBlockedByActiveItem = 1 << 7, // Return true even if an active item is blocking access to this item/window. Useful for Drag and Drop patterns. - ImGuiHoveredFlags_AllowWhenOverlapped = 1 << 8, // IsItemHovered() only: Return true even if the position is obstructed or overlapped by another window - ImGuiHoveredFlags_AllowWhenDisabled = 1 << 9, // IsItemHovered() only: Return true even if the item is disabled - ImGuiHoveredFlags_NoNavOverride = 1 << 10, // Disable using gamepad/keyboard navigation state when active, always query mouse. + ImGuiHoveredFlags_AllowWhenOverlappedByItem = 1 << 8, // IsItemHovered() only: Return true even if the item uses AllowOverlap mode and is overlapped by another hoverable item. + ImGuiHoveredFlags_AllowWhenOverlappedByWindow = 1 << 9, // IsItemHovered() only: Return true even if the position is obstructed or overlapped by another window. + ImGuiHoveredFlags_AllowWhenDisabled = 1 << 10, // IsItemHovered() only: Return true even if the item is disabled + ImGuiHoveredFlags_NoNavOverride = 1 << 11, // IsItemHovered() only: Disable using gamepad/keyboard navigation state when active, always query mouse + ImGuiHoveredFlags_AllowWhenOverlapped = ImGuiHoveredFlags_AllowWhenOverlappedByItem | ImGuiHoveredFlags_AllowWhenOverlappedByWindow, ImGuiHoveredFlags_RectOnly = ImGuiHoveredFlags_AllowWhenBlockedByPopup | ImGuiHoveredFlags_AllowWhenBlockedByActiveItem | ImGuiHoveredFlags_AllowWhenOverlapped, ImGuiHoveredFlags_RootAndChildWindows = ImGuiHoveredFlags_RootWindow | ImGuiHoveredFlags_ChildWindows, - // Hovering delays (for tooltips) - ImGuiHoveredFlags_DelayNormal = 1 << 11, // Return true after io.HoverDelayNormal elapsed (~0.30 sec) - ImGuiHoveredFlags_DelayShort = 1 << 12, // Return true after io.HoverDelayShort elapsed (~0.10 sec) - ImGuiHoveredFlags_NoSharedDelay = 1 << 13, // Disable shared delay system where moving from one item to the next keeps the previous timer for a short time (standard for tooltips with long delays) + // Tooltips mode + // - typically used in IsItemHovered() + SetTooltip() sequence. + // - this is a shortcut to pull flags from 'style.HoverFlagsForTooltipMouse' or 'style.HoverFlagsForTooltipNav' where you can reconfigure desired behavior. + // e.g. 'TooltipHoveredFlagsForMouse' defaults to 'ImGuiHoveredFlags_Stationary | ImGuiHoveredFlags_DelayShort'. + // - for frequently actioned or hovered items providing a tooltip, you want may to use ImGuiHoveredFlags_ForTooltip (stationary + delay) so the tooltip doesn't show too often. + // - for items which main purpose is to be hovered, or items with low affordance, or in less consistent apps, prefer no delay or shorter delay. + ImGuiHoveredFlags_ForTooltip = 1 << 12, // Shortcut for standard flags when using IsItemHovered() + SetTooltip() sequence. + + // (Advanced) Mouse Hovering delays. + // - generally you can use ImGuiHoveredFlags_ForTooltip to use application-standardized flags. + // - use those if you need specific overrides. + ImGuiHoveredFlags_Stationary = 1 << 13, // Require mouse to be stationary for style.HoverStationaryDelay (~0.15 sec) _at least one time_. After this, can move on same item/window. Using the stationary test tends to reduces the need for a long delay. + ImGuiHoveredFlags_DelayNone = 1 << 14, // IsItemHovered() only: Return true immediately (default). As this is the default you generally ignore this. + ImGuiHoveredFlags_DelayShort = 1 << 15, // IsItemHovered() only: Return true after style.HoverDelayShort elapsed (~0.15 sec) (shared between items) + requires mouse to be stationary for style.HoverStationaryDelay (once per item). + ImGuiHoveredFlags_DelayNormal = 1 << 16, // IsItemHovered() only: Return true after style.HoverDelayNormal elapsed (~0.40 sec) (shared between items) + requires mouse to be stationary for style.HoverStationaryDelay (once per item). + ImGuiHoveredFlags_NoSharedDelay = 1 << 17, // IsItemHovered() only: Disable shared delay system where moving from one item to the next keeps the previous timer for a short time (standard for tooltips with long delays) +}; + +// Flags for ImGui::DockSpace(), shared/inherited by child nodes. +// (Some flags can be applied to individual nodes directly) +// FIXME-DOCK: Also see ImGuiDockNodeFlagsPrivate_ which may involve using the WIP and internal DockBuilder api. +enum ImGuiDockNodeFlags_ +{ + ImGuiDockNodeFlags_None = 0, + ImGuiDockNodeFlags_KeepAliveOnly = 1 << 0, // // Don't display the dockspace node but keep it alive. Windows docked into this dockspace node won't be undocked. + //ImGuiDockNodeFlags_NoCentralNode = 1 << 1, // // Disable Central Node (the node which can stay empty) + ImGuiDockNodeFlags_NoDockingOverCentralNode = 1 << 2, // // Disable docking over the Central Node, which will be always kept empty. + ImGuiDockNodeFlags_PassthruCentralNode = 1 << 3, // // Enable passthru dockspace: 1) DockSpace() will render a ImGuiCol_WindowBg background covering everything excepted the Central Node when empty. Meaning the host window should probably use SetNextWindowBgAlpha(0.0f) prior to Begin() when using this. 2) When Central Node is empty: let inputs pass-through + won't display a DockingEmptyBg background. See demo for details. + ImGuiDockNodeFlags_NoDockingSplit = 1 << 4, // // Disable other windows/nodes from splitting this node. + ImGuiDockNodeFlags_NoResize = 1 << 5, // Saved // Disable resizing node using the splitter/separators. Useful with programmatically setup dockspaces. + ImGuiDockNodeFlags_AutoHideTabBar = 1 << 6, // // Tab bar will automatically hide when there is a single window in the dock node. + ImGuiDockNodeFlags_NoUndocking = 1 << 7, // // Disable undocking this node. + +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + ImGuiDockNodeFlags_NoSplit = ImGuiDockNodeFlags_NoDockingSplit, // Renamed in 1.90 + ImGuiDockNodeFlags_NoDockingInCentralNode = ImGuiDockNodeFlags_NoDockingOverCentralNode, // Renamed in 1.90 +#endif }; // Flags for ImGui::BeginDragDropSource(), ImGui::AcceptDragDropPayload() @@ -1348,11 +1366,17 @@ enum ImGuiSortDirection_ ImGuiSortDirection_Descending = 2 // Descending = 9->0, Z->A etc. }; +// Since 1.90, defining IMGUI_DISABLE_OBSOLETE_FUNCTIONS automatically defines IMGUI_DISABLE_OBSOLETE_KEYIO as well. +#if defined(IMGUI_DISABLE_OBSOLETE_FUNCTIONS) && !defined(IMGUI_DISABLE_OBSOLETE_KEYIO) +#define IMGUI_DISABLE_OBSOLETE_KEYIO +#endif + // A key identifier (ImGuiKey_XXX or ImGuiMod_XXX value): can represent Keyboard, Mouse and Gamepad values. // All our named keys are >= 512. Keys value 0 to 511 are left unused as legacy native/opaque key values (< 1.87). // Since >= 1.89 we increased typing (went from int to enum), some legacy code may need a cast to ImGuiKey. // Read details about the 1.87 and 1.89 transition : https://github.com/ocornut/imgui/issues/4921 // Note that "Keys" related to physical keys and are not the same concept as input "Characters", the later are submitted via io.AddInputCharacter(). +// The keyboard key enum values are named after the keys on a standard US keyboard, and on other keyboard types the keys reported may not match the keycaps. enum ImGuiKey : int { // Keyboard @@ -1381,6 +1405,8 @@ enum ImGuiKey : int ImGuiKey_U, ImGuiKey_V, ImGuiKey_W, ImGuiKey_X, ImGuiKey_Y, ImGuiKey_Z, ImGuiKey_F1, ImGuiKey_F2, ImGuiKey_F3, ImGuiKey_F4, ImGuiKey_F5, ImGuiKey_F6, ImGuiKey_F7, ImGuiKey_F8, ImGuiKey_F9, ImGuiKey_F10, ImGuiKey_F11, ImGuiKey_F12, + ImGuiKey_F13, ImGuiKey_F14, ImGuiKey_F15, ImGuiKey_F16, ImGuiKey_F17, ImGuiKey_F18, + ImGuiKey_F19, ImGuiKey_F20, ImGuiKey_F21, ImGuiKey_F22, ImGuiKey_F23, ImGuiKey_F24, ImGuiKey_Apostrophe, // ' ImGuiKey_Comma, // , ImGuiKey_Minus, // - @@ -1406,6 +1432,8 @@ enum ImGuiKey : int ImGuiKey_KeypadAdd, ImGuiKey_KeypadEnter, ImGuiKey_KeypadEqual, + ImGuiKey_AppBack, // Available on some keyboard/mouses. Often referred as "Browser Back" + ImGuiKey_AppForward, // Gamepad (some of those are analog values, 0.0f to 1.0f) // NAVIGATION ACTION // (download controller mapping PNG/PSD at http://dearimgui.com/controls_sheets) @@ -1474,7 +1502,7 @@ enum ImGuiKey : int #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS ImGuiKey_ModCtrl = ImGuiMod_Ctrl, ImGuiKey_ModShift = ImGuiMod_Shift, ImGuiKey_ModAlt = ImGuiMod_Alt, ImGuiKey_ModSuper = ImGuiMod_Super, // Renamed in 1.89 - ImGuiKey_KeyPadEnter = ImGuiKey_KeypadEnter, // Renamed in 1.87 + //ImGuiKey_KeyPadEnter = ImGuiKey_KeypadEnter, // Renamed in 1.87 #endif }; @@ -1501,6 +1529,15 @@ enum ImGuiConfigFlags_ ImGuiConfigFlags_NoMouse = 1 << 4, // Instruct imgui to clear mouse position/buttons in NewFrame(). This allows ignoring the mouse information set by the backend. ImGuiConfigFlags_NoMouseCursorChange = 1 << 5, // Instruct backend to not alter mouse cursor shape and visibility. Use if the backend cursor changes are interfering with yours and you don't want to use SetMouseCursor() to change mouse cursor. You may want to honor requests from imgui by reading GetMouseCursor() yourself instead. + // [BETA] Docking + ImGuiConfigFlags_DockingEnable = 1 << 6, // Docking enable flags. + + // [BETA] Viewports + // When using viewports it is recommended that your default value for ImGuiCol_WindowBg is opaque (Alpha=1.0) so transition to a viewport won't be noticeable. + ImGuiConfigFlags_ViewportsEnable = 1 << 10, // Viewport enable flags (require both ImGuiBackendFlags_PlatformHasViewports + ImGuiBackendFlags_RendererHasViewports set by the respective backends) + ImGuiConfigFlags_DpiEnableScaleViewports= 1 << 14, // [BETA: Don't use] FIXME-DPI: Reposition and resize imgui windows when the DpiScale of a viewport changed (mostly useful for the main viewport hosting other window). Note that resizing the main window itself is up to your application. + ImGuiConfigFlags_DpiEnableScaleFonts = 1 << 15, // [BETA: Don't use] FIXME-DPI: Request bitmap-scaled fonts to match DpiScale. This is a very low-quality workaround. The correct way to handle DPI is _currently_ to replace the atlas and/or fonts in the Platform_OnChangedViewport callback, but this is all early work in progress. + // User storage (to allow your backend/engine to communicate to code that may be shared between multiple projects. Those flags are NOT used by core Dear ImGui) ImGuiConfigFlags_IsSRGB = 1 << 20, // Application is SRGB-aware. ImGuiConfigFlags_IsTouchScreen = 1 << 21, // Application is using a touch screen instead of a mouse. @@ -1514,6 +1551,11 @@ enum ImGuiBackendFlags_ ImGuiBackendFlags_HasMouseCursors = 1 << 1, // Backend Platform supports honoring GetMouseCursor() value to change the OS cursor shape. ImGuiBackendFlags_HasSetMousePos = 1 << 2, // Backend Platform supports io.WantSetMousePos requests to reposition the OS mouse position (only used if ImGuiConfigFlags_NavEnableSetMousePos is set). ImGuiBackendFlags_RendererHasVtxOffset = 1 << 3, // Backend Renderer supports ImDrawCmd::VtxOffset. This enables output of large meshes (64K+ vertices) while still using 16-bit indices. + + // [BETA] Viewports + ImGuiBackendFlags_PlatformHasViewports = 1 << 10, // Backend Platform supports multiple viewports. + ImGuiBackendFlags_HasMouseHoveredViewport=1 << 11, // Backend Platform supports calling io.AddMouseViewportEvent() with the viewport under the mouse. IF POSSIBLE, ignore viewports with the ImGuiViewportFlags_NoInputs flag (Win32 backend, GLFW 3.30+ backend can do this, SDL backend cannot). If this cannot be done, Dear ImGui needs to use a flawed heuristic to find the viewport under. + ImGuiBackendFlags_RendererHasViewports = 1 << 12, // Backend Renderer supports multiple viewports. }; // Enumeration for PushStyleColor() / PopStyleColor() @@ -1529,15 +1571,15 @@ enum ImGuiCol_ ImGuiCol_FrameBg, // Background of checkbox, radio button, plot, slider, text input ImGuiCol_FrameBgHovered, ImGuiCol_FrameBgActive, - ImGuiCol_TitleBg, - ImGuiCol_TitleBgActive, - ImGuiCol_TitleBgCollapsed, + ImGuiCol_TitleBg, // Title bar + ImGuiCol_TitleBgActive, // Title bar when focused + ImGuiCol_TitleBgCollapsed, // Title bar when collapsed ImGuiCol_MenuBarBg, ImGuiCol_ScrollbarBg, ImGuiCol_ScrollbarGrab, ImGuiCol_ScrollbarGrabHovered, ImGuiCol_ScrollbarGrabActive, - ImGuiCol_CheckMark, + ImGuiCol_CheckMark, // Checkbox tick and RadioButton circle ImGuiCol_SliderGrab, ImGuiCol_SliderGrabActive, ImGuiCol_Button, @@ -1557,6 +1599,8 @@ enum ImGuiCol_ ImGuiCol_TabActive, ImGuiCol_TabUnfocused, ImGuiCol_TabUnfocusedActive, + ImGuiCol_DockingPreview, // Preview overlay color when about to docking something + ImGuiCol_DockingEmptyBg, // Background color for empty node (e.g. CentralNode with no window docked into it) ImGuiCol_PlotLines, ImGuiCol_PlotLinesHovered, ImGuiCol_PlotHistogram, @@ -1608,11 +1652,13 @@ enum ImGuiStyleVar_ ImGuiStyleVar_GrabMinSize, // float GrabMinSize ImGuiStyleVar_GrabRounding, // float GrabRounding ImGuiStyleVar_TabRounding, // float TabRounding + ImGuiStyleVar_TabBarBorderSize, // float TabBarBorderSize ImGuiStyleVar_ButtonTextAlign, // ImVec2 ButtonTextAlign ImGuiStyleVar_SelectableTextAlign, // ImVec2 SelectableTextAlign ImGuiStyleVar_SeparatorTextBorderSize,// float SeparatorTextBorderSize ImGuiStyleVar_SeparatorTextAlign, // ImVec2 SeparatorTextAlign ImGuiStyleVar_SeparatorTextPadding,// ImVec2 SeparatorTextPadding + ImGuiStyleVar_DockingSeparatorSize,// float DockingSeparatorSize ImGuiStyleVar_COUNT }; @@ -1728,7 +1774,7 @@ enum ImGuiMouseSource : int ImGuiMouseSource_COUNT }; -// Enumeration for ImGui::SetWindow***(), SetNextWindow***(), SetNextItem***() functions +// Enumeration for ImGui::SetNextWindow***(), SetWindow***(), SetNextItem***() functions // Represent a condition. // Important: Treat as a regular enum! Do NOT combine multiple values using binary operators! All the functions above treat 0 as a shortcut to ImGuiCond_Always. enum ImGuiCond_ @@ -1740,6 +1786,170 @@ enum ImGuiCond_ ImGuiCond_Appearing = 1 << 3, // Set the variable if the object/window is appearing after being hidden/inactive (or the first time) }; +//----------------------------------------------------------------------------- +// [SECTION] Tables API flags and structures (ImGuiTableFlags, ImGuiTableColumnFlags, ImGuiTableRowFlags, ImGuiTableBgTarget, ImGuiTableSortSpecs, ImGuiTableColumnSortSpecs) +//----------------------------------------------------------------------------- + +// Flags for ImGui::BeginTable() +// - Important! Sizing policies have complex and subtle side effects, much more so than you would expect. +// Read comments/demos carefully + experiment with live demos to get acquainted with them. +// - The DEFAULT sizing policies are: +// - Default to ImGuiTableFlags_SizingFixedFit if ScrollX is on, or if host window has ImGuiWindowFlags_AlwaysAutoResize. +// - Default to ImGuiTableFlags_SizingStretchSame if ScrollX is off. +// - When ScrollX is off: +// - Table defaults to ImGuiTableFlags_SizingStretchSame -> all Columns defaults to ImGuiTableColumnFlags_WidthStretch with same weight. +// - Columns sizing policy allowed: Stretch (default), Fixed/Auto. +// - Fixed Columns (if any) will generally obtain their requested width (unless the table cannot fit them all). +// - Stretch Columns will share the remaining width according to their respective weight. +// - Mixed Fixed/Stretch columns is possible but has various side-effects on resizing behaviors. +// The typical use of mixing sizing policies is: any number of LEADING Fixed columns, followed by one or two TRAILING Stretch columns. +// (this is because the visible order of columns have subtle but necessary effects on how they react to manual resizing). +// - When ScrollX is on: +// - Table defaults to ImGuiTableFlags_SizingFixedFit -> all Columns defaults to ImGuiTableColumnFlags_WidthFixed +// - Columns sizing policy allowed: Fixed/Auto mostly. +// - Fixed Columns can be enlarged as needed. Table will show a horizontal scrollbar if needed. +// - When using auto-resizing (non-resizable) fixed columns, querying the content width to use item right-alignment e.g. SetNextItemWidth(-FLT_MIN) doesn't make sense, would create a feedback loop. +// - Using Stretch columns OFTEN DOES NOT MAKE SENSE if ScrollX is on, UNLESS you have specified a value for 'inner_width' in BeginTable(). +// If you specify a value for 'inner_width' then effectively the scrolling space is known and Stretch or mixed Fixed/Stretch columns become meaningful again. +// - Read on documentation at the top of imgui_tables.cpp for details. +enum ImGuiTableFlags_ +{ + // Features + ImGuiTableFlags_None = 0, + ImGuiTableFlags_Resizable = 1 << 0, // Enable resizing columns. + ImGuiTableFlags_Reorderable = 1 << 1, // Enable reordering columns in header row (need calling TableSetupColumn() + TableHeadersRow() to display headers) + ImGuiTableFlags_Hideable = 1 << 2, // Enable hiding/disabling columns in context menu. + ImGuiTableFlags_Sortable = 1 << 3, // Enable sorting. Call TableGetSortSpecs() to obtain sort specs. Also see ImGuiTableFlags_SortMulti and ImGuiTableFlags_SortTristate. + ImGuiTableFlags_NoSavedSettings = 1 << 4, // Disable persisting columns order, width and sort settings in the .ini file. + ImGuiTableFlags_ContextMenuInBody = 1 << 5, // Right-click on columns body/contents will display table context menu. By default it is available in TableHeadersRow(). + // Decorations + ImGuiTableFlags_RowBg = 1 << 6, // Set each RowBg color with ImGuiCol_TableRowBg or ImGuiCol_TableRowBgAlt (equivalent of calling TableSetBgColor with ImGuiTableBgFlags_RowBg0 on each row manually) + ImGuiTableFlags_BordersInnerH = 1 << 7, // Draw horizontal borders between rows. + ImGuiTableFlags_BordersOuterH = 1 << 8, // Draw horizontal borders at the top and bottom. + ImGuiTableFlags_BordersInnerV = 1 << 9, // Draw vertical borders between columns. + ImGuiTableFlags_BordersOuterV = 1 << 10, // Draw vertical borders on the left and right sides. + ImGuiTableFlags_BordersH = ImGuiTableFlags_BordersInnerH | ImGuiTableFlags_BordersOuterH, // Draw horizontal borders. + ImGuiTableFlags_BordersV = ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_BordersOuterV, // Draw vertical borders. + ImGuiTableFlags_BordersInner = ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_BordersInnerH, // Draw inner borders. + ImGuiTableFlags_BordersOuter = ImGuiTableFlags_BordersOuterV | ImGuiTableFlags_BordersOuterH, // Draw outer borders. + ImGuiTableFlags_Borders = ImGuiTableFlags_BordersInner | ImGuiTableFlags_BordersOuter, // Draw all borders. + ImGuiTableFlags_NoBordersInBody = 1 << 11, // [ALPHA] Disable vertical borders in columns Body (borders will always appear in Headers). -> May move to style + ImGuiTableFlags_NoBordersInBodyUntilResize = 1 << 12, // [ALPHA] Disable vertical borders in columns Body until hovered for resize (borders will always appear in Headers). -> May move to style + // Sizing Policy (read above for defaults) + ImGuiTableFlags_SizingFixedFit = 1 << 13, // Columns default to _WidthFixed or _WidthAuto (if resizable or not resizable), matching contents width. + ImGuiTableFlags_SizingFixedSame = 2 << 13, // Columns default to _WidthFixed or _WidthAuto (if resizable or not resizable), matching the maximum contents width of all columns. Implicitly enable ImGuiTableFlags_NoKeepColumnsVisible. + ImGuiTableFlags_SizingStretchProp = 3 << 13, // Columns default to _WidthStretch with default weights proportional to each columns contents widths. + ImGuiTableFlags_SizingStretchSame = 4 << 13, // Columns default to _WidthStretch with default weights all equal, unless overridden by TableSetupColumn(). + // Sizing Extra Options + ImGuiTableFlags_NoHostExtendX = 1 << 16, // Make outer width auto-fit to columns, overriding outer_size.x value. Only available when ScrollX/ScrollY are disabled and Stretch columns are not used. + ImGuiTableFlags_NoHostExtendY = 1 << 17, // Make outer height stop exactly at outer_size.y (prevent auto-extending table past the limit). Only available when ScrollX/ScrollY are disabled. Data below the limit will be clipped and not visible. + ImGuiTableFlags_NoKeepColumnsVisible = 1 << 18, // Disable keeping column always minimally visible when ScrollX is off and table gets too small. Not recommended if columns are resizable. + ImGuiTableFlags_PreciseWidths = 1 << 19, // Disable distributing remainder width to stretched columns (width allocation on a 100-wide table with 3 columns: Without this flag: 33,33,34. With this flag: 33,33,33). With larger number of columns, resizing will appear to be less smooth. + // Clipping + ImGuiTableFlags_NoClip = 1 << 20, // Disable clipping rectangle for every individual columns (reduce draw command count, items will be able to overflow into other columns). Generally incompatible with TableSetupScrollFreeze(). + // Padding + ImGuiTableFlags_PadOuterX = 1 << 21, // Default if BordersOuterV is on. Enable outermost padding. Generally desirable if you have headers. + ImGuiTableFlags_NoPadOuterX = 1 << 22, // Default if BordersOuterV is off. Disable outermost padding. + ImGuiTableFlags_NoPadInnerX = 1 << 23, // Disable inner padding between columns (double inner padding if BordersOuterV is on, single inner padding if BordersOuterV is off). + // Scrolling + ImGuiTableFlags_ScrollX = 1 << 24, // Enable horizontal scrolling. Require 'outer_size' parameter of BeginTable() to specify the container size. Changes default sizing policy. Because this creates a child window, ScrollY is currently generally recommended when using ScrollX. + ImGuiTableFlags_ScrollY = 1 << 25, // Enable vertical scrolling. Require 'outer_size' parameter of BeginTable() to specify the container size. + // Sorting + ImGuiTableFlags_SortMulti = 1 << 26, // Hold shift when clicking headers to sort on multiple column. TableGetSortSpecs() may return specs where (SpecsCount > 1). + ImGuiTableFlags_SortTristate = 1 << 27, // Allow no sorting, disable default sorting. TableGetSortSpecs() may return specs where (SpecsCount == 0). + // Miscellaneous + ImGuiTableFlags_HighlightHoveredColumn = 1 << 28, // Highlight column headers when hovered (may evolve into a fuller highlight) + + // [Internal] Combinations and masks + ImGuiTableFlags_SizingMask_ = ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_SizingFixedSame | ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_SizingStretchSame, +}; + +// Flags for ImGui::TableSetupColumn() +enum ImGuiTableColumnFlags_ +{ + // Input configuration flags + ImGuiTableColumnFlags_None = 0, + ImGuiTableColumnFlags_Disabled = 1 << 0, // Overriding/master disable flag: hide column, won't show in context menu (unlike calling TableSetColumnEnabled() which manipulates the user accessible state) + ImGuiTableColumnFlags_DefaultHide = 1 << 1, // Default as a hidden/disabled column. + ImGuiTableColumnFlags_DefaultSort = 1 << 2, // Default as a sorting column. + ImGuiTableColumnFlags_WidthStretch = 1 << 3, // Column will stretch. Preferable with horizontal scrolling disabled (default if table sizing policy is _SizingStretchSame or _SizingStretchProp). + ImGuiTableColumnFlags_WidthFixed = 1 << 4, // Column will not stretch. Preferable with horizontal scrolling enabled (default if table sizing policy is _SizingFixedFit and table is resizable). + ImGuiTableColumnFlags_NoResize = 1 << 5, // Disable manual resizing. + ImGuiTableColumnFlags_NoReorder = 1 << 6, // Disable manual reordering this column, this will also prevent other columns from crossing over this column. + ImGuiTableColumnFlags_NoHide = 1 << 7, // Disable ability to hide/disable this column. + ImGuiTableColumnFlags_NoClip = 1 << 8, // Disable clipping for this column (all NoClip columns will render in a same draw command). + ImGuiTableColumnFlags_NoSort = 1 << 9, // Disable ability to sort on this field (even if ImGuiTableFlags_Sortable is set on the table). + ImGuiTableColumnFlags_NoSortAscending = 1 << 10, // Disable ability to sort in the ascending direction. + ImGuiTableColumnFlags_NoSortDescending = 1 << 11, // Disable ability to sort in the descending direction. + ImGuiTableColumnFlags_NoHeaderLabel = 1 << 12, // TableHeadersRow() will not submit horizontal label for this column. Convenient for some small columns. Name will still appear in context menu or in angled headers. + ImGuiTableColumnFlags_NoHeaderWidth = 1 << 13, // Disable header text width contribution to automatic column width. + ImGuiTableColumnFlags_PreferSortAscending = 1 << 14, // Make the initial sort direction Ascending when first sorting on this column (default). + ImGuiTableColumnFlags_PreferSortDescending = 1 << 15, // Make the initial sort direction Descending when first sorting on this column. + ImGuiTableColumnFlags_IndentEnable = 1 << 16, // Use current Indent value when entering cell (default for column 0). + ImGuiTableColumnFlags_IndentDisable = 1 << 17, // Ignore current Indent value when entering cell (default for columns > 0). Indentation changes _within_ the cell will still be honored. + ImGuiTableColumnFlags_AngledHeader = 1 << 18, // TableHeadersRow() will submit an angled header row for this column. Note this will add an extra row. + + // Output status flags, read-only via TableGetColumnFlags() + ImGuiTableColumnFlags_IsEnabled = 1 << 24, // Status: is enabled == not hidden by user/api (referred to as "Hide" in _DefaultHide and _NoHide) flags. + ImGuiTableColumnFlags_IsVisible = 1 << 25, // Status: is visible == is enabled AND not clipped by scrolling. + ImGuiTableColumnFlags_IsSorted = 1 << 26, // Status: is currently part of the sort specs + ImGuiTableColumnFlags_IsHovered = 1 << 27, // Status: is hovered by mouse + + // [Internal] Combinations and masks + ImGuiTableColumnFlags_WidthMask_ = ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_WidthFixed, + ImGuiTableColumnFlags_IndentMask_ = ImGuiTableColumnFlags_IndentEnable | ImGuiTableColumnFlags_IndentDisable, + ImGuiTableColumnFlags_StatusMask_ = ImGuiTableColumnFlags_IsEnabled | ImGuiTableColumnFlags_IsVisible | ImGuiTableColumnFlags_IsSorted | ImGuiTableColumnFlags_IsHovered, + ImGuiTableColumnFlags_NoDirectResize_ = 1 << 30, // [Internal] Disable user resizing this column directly (it may however we resized indirectly from its left edge) +}; + +// Flags for ImGui::TableNextRow() +enum ImGuiTableRowFlags_ +{ + ImGuiTableRowFlags_None = 0, + ImGuiTableRowFlags_Headers = 1 << 0, // Identify header row (set default background color + width of its contents accounted differently for auto column width) +}; + +// Enum for ImGui::TableSetBgColor() +// Background colors are rendering in 3 layers: +// - Layer 0: draw with RowBg0 color if set, otherwise draw with ColumnBg0 if set. +// - Layer 1: draw with RowBg1 color if set, otherwise draw with ColumnBg1 if set. +// - Layer 2: draw with CellBg color if set. +// The purpose of the two row/columns layers is to let you decide if a background color change should override or blend with the existing color. +// When using ImGuiTableFlags_RowBg on the table, each row has the RowBg0 color automatically set for odd/even rows. +// If you set the color of RowBg0 target, your color will override the existing RowBg0 color. +// If you set the color of RowBg1 or ColumnBg1 target, your color will blend over the RowBg0 color. +enum ImGuiTableBgTarget_ +{ + ImGuiTableBgTarget_None = 0, + ImGuiTableBgTarget_RowBg0 = 1, // Set row background color 0 (generally used for background, automatically set when ImGuiTableFlags_RowBg is used) + ImGuiTableBgTarget_RowBg1 = 2, // Set row background color 1 (generally used for selection marking) + ImGuiTableBgTarget_CellBg = 3, // Set cell background color (top-most color) +}; + +// Sorting specifications for a table (often handling sort specs for a single column, occasionally more) +// Obtained by calling TableGetSortSpecs(). +// When 'SpecsDirty == true' you can sort your data. It will be true with sorting specs have changed since last call, or the first time. +// Make sure to set 'SpecsDirty = false' after sorting, else you may wastefully sort your data every frame! +struct ImGuiTableSortSpecs +{ + const ImGuiTableColumnSortSpecs* Specs; // Pointer to sort spec array. + int SpecsCount; // Sort spec count. Most often 1. May be > 1 when ImGuiTableFlags_SortMulti is enabled. May be == 0 when ImGuiTableFlags_SortTristate is enabled. + bool SpecsDirty; // Set to true when specs have changed since last time! Use this to sort again, then clear the flag. + + ImGuiTableSortSpecs() { memset(this, 0, sizeof(*this)); } +}; + +// Sorting specification for one column of a table (sizeof == 12 bytes) +struct ImGuiTableColumnSortSpecs +{ + ImGuiID ColumnUserID; // User id of the column (if specified by a TableSetupColumn() call) + ImS16 ColumnIndex; // Index of the column + ImS16 SortOrder; // Index within parent ImGuiTableSortSpecs (always stored in order starting from 0, tables sorted on a single criteria will always have a 0 here) + ImGuiSortDirection SortDirection : 8; // ImGuiSortDirection_Ascending or ImGuiSortDirection_Descending + + ImGuiTableColumnSortSpecs() { memset(this, 0, sizeof(*this)); } +}; + //----------------------------------------------------------------------------- // [SECTION] Helpers: Memory allocations macros, ImVector<> //----------------------------------------------------------------------------- @@ -1829,6 +2039,7 @@ struct ImVector inline bool contains(const T& v) const { const T* data = Data; const T* data_end = Data + Size; while (data < data_end) if (*data++ == v) return true; return false; } inline T* find(const T& v) { T* data = Data; const T* data_end = Data + Size; while (data < data_end) if (*data == v) break; else ++data; return data; } inline const T* find(const T& v) const { const T* data = Data; const T* data_end = Data + Size; while (data < data_end) if (*data == v) break; else ++data; return data; } + inline int find_index(const T& v) const { const T* data_end = Data + Size; const T* it = find(v); if (it == data_end) return -1; const ptrdiff_t off = it - Data; return (int)off; } inline bool find_erase(const T& v) { const T* it = find(v); if (it < Data + Size) { erase(it); return true; } return false; } inline bool find_erase_unsorted(const T& v) { const T* it = find(v); if (it < Data + Size) { erase_unsorted(it); return true; } return false; } inline int index_from_ptr(const T* it) const { IM_ASSERT(it >= Data && it < Data + Size); const ptrdiff_t off = it - Data; return (int)off; } @@ -1862,7 +2073,7 @@ struct ImGuiStyle float FrameBorderSize; // Thickness of border around frames. Generally set to 0.0f or 1.0f. (Other values are not well tested and more CPU/GPU costly). ImVec2 ItemSpacing; // Horizontal and vertical spacing between widgets/lines. ImVec2 ItemInnerSpacing; // Horizontal and vertical spacing between within elements of a composed widget (e.g. a slider and its label). - ImVec2 CellPadding; // Padding within a table cell + ImVec2 CellPadding; // Padding within a table cell. CellPadding.y may be altered between different rows. ImVec2 TouchExtraPadding; // Expand reactive bounding box for touch-based system where touch position is not accurate enough. Unfortunately we don't sort widgets so priority on overlap will always be given to the first widget. So don't grow this too much! float IndentSpacing; // Horizontal indentation when e.g. entering a tree node. Generally == (FontSize + FramePadding.x*2). float ColumnsMinSpacing; // Minimum horizontal spacing between two columns. Preferably > (FramePadding.x + 1). @@ -1874,6 +2085,8 @@ struct ImGuiStyle float TabRounding; // Radius of upper corners of a tab. Set to 0.0f to have rectangular tabs. float TabBorderSize; // Thickness of border around tabs. float TabMinWidthForCloseButton; // Minimum width for close button to appear on an unselected tab when hovered. Set to 0.0f to always show when hovering, set to FLT_MAX to never show close button unless selected. + float TabBarBorderSize; // Thickness of tab-bar separator, which takes on the tab active color to denote focus. + float TableAngledHeadersAngle; // Angle of angled headers (supported values range from -50.0f degrees to +50.0f degrees). ImGuiDir ColorButtonPosition; // Side of the color button in the ColorEdit4 widget (left/right). Defaults to ImGuiDir_Right. ImVec2 ButtonTextAlign; // Alignment of button text when button is larger than text. Defaults to (0.5f, 0.5f) (centered). ImVec2 SelectableTextAlign; // Alignment of selectable text. Defaults to (0.0f, 0.0f) (top-left aligned). It's generally important to keep this left-aligned if you want to lay multiple items on a same line. @@ -1882,7 +2095,8 @@ struct ImGuiStyle ImVec2 SeparatorTextPadding; // Horizontal offset of text from each edge of the separator + spacing on other axis. Generally small values. .y is recommended to be == FramePadding.y. ImVec2 DisplayWindowPadding; // Window position are clamped to be visible within the display area or monitors by at least this amount. Only applies to regular windows. ImVec2 DisplaySafeAreaPadding; // If you cannot see the edges of your screen (e.g. on a TV) increase the safe area padding. Apply to popups/tooltips as well regular windows. NB: Prefer configuring your TV sets correctly! - float MouseCursorScale; // Scale software rendered mouse cursor (when io.MouseDrawCursor is enabled). May be removed later. + float DockingSeparatorSize; // Thickness of resizing border between docked windows + float MouseCursorScale; // Scale software rendered mouse cursor (when io.MouseDrawCursor is enabled). We apply per-monitor DPI scaling over this scale. May be removed later. bool AntiAliasedLines; // Enable anti-aliased lines/borders. Disable if you are really tight on CPU/GPU. Latched at the beginning of the frame (copied to ImDrawList). bool AntiAliasedLinesUseTex; // Enable anti-aliased lines/borders using textures where possible. Require backend to render with bilinear filtering (NOT point/nearest filtering). Latched at the beginning of the frame (copied to ImDrawList). bool AntiAliasedFill; // Enable anti-aliased edges around filled shapes (rounded rectangles, circles, etc.). Disable if you are really tight on CPU/GPU. Latched at the beginning of the frame (copied to ImDrawList). @@ -1890,6 +2104,14 @@ struct ImGuiStyle float CircleTessellationMaxError; // Maximum error (in pixels) allowed when using AddCircle()/AddCircleFilled() or drawing rounded corner rectangles with no explicit segment count specified. Decrease for higher quality but more geometry. ImVec4 Colors[ImGuiCol_COUNT]; + // Behaviors + // (It is possible to modify those fields mid-frame if specific behavior need it, unlike e.g. configuration fields in ImGuiIO) + float HoverStationaryDelay; // Delay for IsItemHovered(ImGuiHoveredFlags_Stationary). Time required to consider mouse stationary. + float HoverDelayShort; // Delay for IsItemHovered(ImGuiHoveredFlags_DelayShort). Usually used along with HoverStationaryDelay. + float HoverDelayNormal; // Delay for IsItemHovered(ImGuiHoveredFlags_DelayNormal). " + ImGuiHoveredFlags HoverFlagsForTooltipMouse;// Default flags when using IsItemHovered(ImGuiHoveredFlags_ForTooltip) or BeginItemTooltip()/SetItemTooltip() while using mouse. + ImGuiHoveredFlags HoverFlagsForTooltipNav; // Default flags when using IsItemHovered(ImGuiHoveredFlags_ForTooltip) or BeginItemTooltip()/SetItemTooltip() while using keyboard/gamepad. + IMGUI_API ImGuiStyle(); IMGUI_API void ScaleAllSizes(float scale_factor); }; @@ -1924,13 +2146,6 @@ struct ImGuiIO float IniSavingRate; // = 5.0f // Minimum time between saving positions/sizes to .ini file, in seconds. const char* IniFilename; // = "imgui.ini" // Path to .ini file (important: default "imgui.ini" is relative to current working dir!). Set NULL to disable automatic .ini loading/saving or if you want to manually call LoadIniSettingsXXX() / SaveIniSettingsXXX() functions. const char* LogFilename; // = "imgui_log.txt"// Path to .log file (default parameter to ImGui::LogToFile when no file is specified). - float MouseDoubleClickTime; // = 0.30f // Time for a double-click, in seconds. - float MouseDoubleClickMaxDist; // = 6.0f // Distance threshold to stay in to validate a double-click, in pixels. - float MouseDragThreshold; // = 6.0f // Distance threshold before considering we are dragging. - float KeyRepeatDelay; // = 0.275f // When holding a key/button, time before it starts repeating, in seconds (for buttons in Repeat mode, etc.). - float KeyRepeatRate; // = 0.050f // When holding a key/button, rate at which it repeats, in seconds. - float HoverDelayNormal; // = 0.30 sec // Delay on hovering before IsItemHovered(ImGuiHoveredFlags_DelayNormal) returns true. - float HoverDelayShort; // = 0.10 sec // Delay on hovering before IsItemHovered(ImGuiHoveredFlags_DelayShort) returns true. void* UserData; // = NULL // Store your own data. ImFontAtlas*Fonts; // // Font atlas: load, rasterize and pack one or more fonts into a single texture. @@ -1939,6 +2154,18 @@ struct ImGuiIO ImFont* FontDefault; // = NULL // Font to use on NewFrame(). Use NULL to uses Fonts->Fonts[0]. ImVec2 DisplayFramebufferScale; // = (1, 1) // For retina display or other situations where window coordinates are different from framebuffer coordinates. This generally ends up in ImDrawData::FramebufferScale. + // Docking options (when ImGuiConfigFlags_DockingEnable is set) + bool ConfigDockingNoSplit; // = false // Simplified docking mode: disable window splitting, so docking is limited to merging multiple windows together into tab-bars. + bool ConfigDockingWithShift; // = false // Enable docking with holding Shift key (reduce visual noise, allows dropping in wider space) + bool ConfigDockingAlwaysTabBar; // = false // [BETA] [FIXME: This currently creates regression with auto-sizing and general overhead] Make every single floating window display within a docking node. + bool ConfigDockingTransparentPayload;// = false // [BETA] Make window or viewport transparent when docking and only display docking boxes on the target viewport. Useful if rendering of multiple viewport cannot be synced. Best used with ConfigViewportsNoAutoMerge. + + // Viewport options (when ImGuiConfigFlags_ViewportsEnable is set) + bool ConfigViewportsNoAutoMerge; // = false; // Set to make all floating imgui windows always create their own viewport. Otherwise, they are merged into the main host viewports when overlapping it. May also set ImGuiViewportFlags_NoAutoMerge on individual viewport. + bool ConfigViewportsNoTaskBarIcon; // = false // Disable default OS task bar icon flag for secondary viewports. When a viewport doesn't want a task bar icon, ImGuiViewportFlags_NoTaskBarIcon will be set on it. + bool ConfigViewportsNoDecoration; // = true // Disable default OS window decoration flag for secondary viewports. When a viewport doesn't want window decorations, ImGuiViewportFlags_NoDecoration will be set on it. Enabling decoration can create subsequent issues at OS levels (e.g. minimum window size). + bool ConfigViewportsNoDefaultParent; // = false // Disable default OS parenting to main viewport for secondary viewports. By default, viewports are marked with ParentViewportId = , expecting the platform backend to setup a parent/child relationship between the OS windows (some backend may ignore this). Set to true if you want the default to be 0, then all viewports will be top-level OS windows. + // Miscellaneous options bool MouseDrawCursor; // = false // Request ImGui to draw a mouse cursor for you (if you are on a platform without a mouse cursor). Cannot be easily renamed to 'io.ConfigXXX' because this is frequently used by backend implementations. bool ConfigMacOSXBehaviors; // = defined(__APPLE__) // OS X style: Text editing cursor movement using Alt instead of Ctrl, Shortcuts using Cmd/Super instead of Ctrl, Line/Text Start and End using Cmd+Arrows instead of Home/End, Double click selects by word instead of selecting whole text, Multi-selection in lists uses Cmd/Super instead of Ctrl. @@ -1950,18 +2177,39 @@ struct ImGuiIO bool ConfigWindowsMoveFromTitleBarOnly; // = false // Enable allowing to move windows only when clicking on their title bar. Does not apply to windows without a title bar. float ConfigMemoryCompactTimer; // = 60.0f // Timer (in seconds) to free transient windows/tables memory buffers when unused. Set to -1.0f to disable. + // Inputs Behaviors + // (other variables, ones which are expected to be tweaked within UI code, are exposed in ImGuiStyle) + float MouseDoubleClickTime; // = 0.30f // Time for a double-click, in seconds. + float MouseDoubleClickMaxDist; // = 6.0f // Distance threshold to stay in to validate a double-click, in pixels. + float MouseDragThreshold; // = 6.0f // Distance threshold before considering we are dragging. + float KeyRepeatDelay; // = 0.275f // When holding a key/button, time before it starts repeating, in seconds (for buttons in Repeat mode, etc.). + float KeyRepeatRate; // = 0.050f // When holding a key/button, rate at which it repeats, in seconds. + + //------------------------------------------------------------------ // Debug options - // - tools to test correct Begin/End and BeginChild/EndChild behaviors. - // - presently Begin()/End() and BeginChild()/EndChild() needs to ALWAYS be called in tandem, regardless of return value of BeginXXX() - // this is inconsistent with other BeginXXX functions and create confusion for many users. - // - we expect to update the API eventually. In the meanwhile we provide tools to facilitate checking user-code behavior. + //------------------------------------------------------------------ + + // Option to enable various debug tools showing buttons that will call the IM_DEBUG_BREAK() macro. + // - The Item Picker tool will be available regardless of this being enabled, in order to maximize its discoverability. + // - Requires a debugger being attached, otherwise IM_DEBUG_BREAK() options will appear to crash your application. + // e.g. io.ConfigDebugIsDebuggerPresent = ::IsDebuggerPresent() on Win32, or refer to ImOsIsDebuggerPresent() imgui_test_engine/imgui_te_utils.cpp for a Unix compatible version). + bool ConfigDebugIsDebuggerPresent; // = false // Enable various tools calling IM_DEBUG_BREAK(). + + // Tools to test correct Begin/End and BeginChild/EndChild behaviors. + // - Presently Begin()/End() and BeginChild()/EndChild() needs to ALWAYS be called in tandem, regardless of return value of BeginXXX() + // - This is inconsistent with other BeginXXX functions and create confusion for many users. + // - We expect to update the API eventually. In the meanwhile we provide tools to facilitate checking user-code behavior. bool ConfigDebugBeginReturnValueOnce;// = false // First-time calls to Begin()/BeginChild() will return false. NEEDS TO BE SET AT APPLICATION BOOT TIME if you don't want to miss windows. bool ConfigDebugBeginReturnValueLoop;// = false // Some calls to Begin()/BeginChild() will return false. Will cycle through window depths then repeat. Suggested use: add "io.ConfigDebugBeginReturnValue = io.KeyShift" in your main loop then occasionally press SHIFT. Windows should be flickering while running. - // - option to deactivate io.AddFocusEvent(false) handling. May facilitate interactions with a debugger when focus loss leads to clearing inputs data. - // - backends may have other side-effects on focus loss, so this will reduce side-effects but not necessary remove all of them. - // - consider using e.g. Win32's IsDebuggerPresent() as an additional filter (or see ImOsIsDebuggerPresent() in imgui_test_engine/imgui_te_utils.cpp for a Unix compatible version). + + // Option to deactivate io.AddFocusEvent(false) handling. + // - May facilitate interactions with a debugger when focus loss leads to clearing inputs data. + // - Backends may have other side-effects on focus loss, so this will reduce side-effects but not necessary remove all of them. bool ConfigDebugIgnoreFocusLoss; // = false // Ignore io.AddFocusEvent(false), consequently not calling io.ClearInputKeys() in input processing. + // Option to audit .ini data + bool ConfigDebugIniSettings; // = false // Save .ini data with extra comments (particularly helpful for Docking, but makes saving slower) + //------------------------------------------------------------------ // Platform Functions // (the imgui_impl_xxxx backend files are setting those up for you) @@ -1983,11 +2231,9 @@ struct ImGuiIO // Optional: Notify OS Input Method Editor of the screen position of your cursor for text input position (e.g. when using Japanese/Chinese IME on Windows) // (default to use native imm32 api on Windows) void (*SetPlatformImeDataFn)(ImGuiViewport* viewport, ImGuiPlatformImeData* data); -#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS - void* ImeWindowHandle; // = NULL // [Obsolete] Set ImGuiViewport::PlatformHandleRaw instead. Set this to your HWND to get automatic IME cursor positioning. -#else - void* _UnusedPadding; // Unused field to keep data structure the same size. -#endif + + // Optional: Platform locale + ImWchar PlatformLocaleDecimalPoint; // '.' // [Experimental] Configure decimal point e.g. '.' or ',' useful for some languages (e.g. German), generally pulled from *localeconv()->decimal_point //------------------------------------------------------------------ // Input - Call before calling NewFrame() @@ -2000,6 +2246,7 @@ struct ImGuiIO IMGUI_API void AddMouseButtonEvent(int button, bool down); // Queue a mouse button change IMGUI_API void AddMouseWheelEvent(float wheel_x, float wheel_y); // Queue a mouse wheel update. wheel_y<0: scroll down, wheel_y>0: scroll up, wheel_x<0: scroll right, wheel_x>0: scroll left. IMGUI_API void AddMouseSourceEvent(ImGuiMouseSource source); // Queue a mouse source change (Mouse/TouchScreen/Pen) + IMGUI_API void AddMouseViewportEvent(ImGuiID id); // Queue a mouse hovered viewport. Requires backend to set ImGuiBackendFlags_HasMouseHoveredViewport to call this (for multi-viewport support). IMGUI_API void AddFocusEvent(bool focused); // Queue a gain/loss of focus for the application (generally based on OS/platform focus of your window) IMGUI_API void AddInputCharacter(unsigned int c); // Queue a new character input IMGUI_API void AddInputCharacterUTF16(ImWchar16 c); // Queue a new character input from a UTF-16 character, it can be a surrogate @@ -2007,8 +2254,11 @@ struct ImGuiIO IMGUI_API void SetKeyEventNativeData(ImGuiKey key, int native_keycode, int native_scancode, int native_legacy_index = -1); // [Optional] Specify index for legacy <1.87 IsKeyXXX() functions with native indices + specify native keycode, scancode. IMGUI_API void SetAppAcceptingEvents(bool accepting_events); // Set master flag for accepting key/mouse/text events (default to true). Useful if you have native dialog boxes that are interrupting your application loop/refresh, and you want to disable events being queued while your app is frozen. - IMGUI_API void ClearInputCharacters(); // [Internal] Clear the text input buffer manually - IMGUI_API void ClearInputKeys(); // [Internal] Release all keys + IMGUI_API void ClearEventsQueue(); // Clear all incoming events. + IMGUI_API void ClearInputKeys(); // Clear current keyboard/mouse/gamepad state + current frame text input buffer. Equivalent to releasing all keys/buttons. +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + IMGUI_API void ClearInputCharacters(); // [Obsoleted in 1.89.8] Clear the current frame text input buffer. Now included within ClearInputKeys(). +#endif //------------------------------------------------------------------ // Output - Updated by NewFrame() or EndFrame()/Render() @@ -2028,7 +2278,6 @@ struct ImGuiIO int MetricsRenderIndices; // Indices output during last call to Render() = number of triangles * 3 int MetricsRenderWindows; // Number of visible windows int MetricsActiveWindows; // Number of active windows - int MetricsActiveAllocations; // Number of active allocations, updated by MemAlloc/MemFree based on current context. May be off if you have multiple imgui contexts. ImVec2 MouseDelta; // Mouse delta. Note that this is zero if either current or previous position are invalid (-FLT_MAX,-FLT_MAX), so a disappearing/reappearing mouse won't have a huge delta. // Legacy: before 1.87, we required backend to fill io.KeyMap[] (imgui->native map) during initialization and io.KeysDown[] (native indices) every frame. @@ -2038,6 +2287,7 @@ struct ImGuiIO int KeyMap[ImGuiKey_COUNT]; // [LEGACY] Input: map of indices into the KeysDown[512] entries array which represent your "native" keyboard state. The first 512 are now unused and should be kept zero. Legacy backend will write into KeyMap[] using ImGuiKey_ indices which are always >512. bool KeysDown[ImGuiKey_COUNT]; // [LEGACY] Input: Keyboard keys that are pressed (ideally left in the "native" order your engine has access to keyboard keys, so you can use your own defines/enums for keys). This used to be [512] sized. It is now ImGuiKey_COUNT to allow legacy io.KeysDown[GetKeyIndex(...)] to work without an overflow. float NavInputs[ImGuiNavInput_COUNT]; // [LEGACY] Since 1.88, NavInputs[] was removed. Backends from 1.60 to 1.86 won't build. Feed gamepad inputs via io.AddKeyEvent() and ImGuiKey_GamepadXXX enums. + //void* ImeWindowHandle; // [Obsoleted in 1.87] Set ImGuiViewport::PlatformHandleRaw instead. Set this to your HWND to get automatic IME cursor positioning. #endif //------------------------------------------------------------------ @@ -2054,6 +2304,7 @@ struct ImGuiIO float MouseWheel; // Mouse wheel Vertical: 1 unit scrolls about 5 lines text. >0 scrolls Up, <0 scrolls Down. Hold SHIFT to turn vertical scroll into horizontal scroll. float MouseWheelH; // Mouse wheel Horizontal. >0 scrolls Left, <0 scrolls Right. Most users don't have a mouse with a horizontal wheel, may not be filled by all backends. ImGuiMouseSource MouseSource; // Mouse actual input peripheral (Mouse/TouchScreen/Pen). + ImGuiID MouseHoveredViewport; // (Optional) Modify using io.AddMouseViewportEvent(). With multi-viewports: viewport the OS mouse is hovering. If possible _IGNORING_ viewports with the ImGuiViewportFlags_NoInputs flag is much better (few backends can handle that). Set io.BackendFlags |= ImGuiBackendFlags_HasMouseHoveredViewport if you can provide this info. If you don't imgui will infer the value using the rectangles and last focused time of the viewports it knows about (ignoring other OS windows). bool KeyCtrl; // Keyboard modifier down: Control bool KeyShift; // Keyboard modifier down: Shift bool KeyAlt; // Keyboard modifier down: Alt @@ -2076,6 +2327,7 @@ struct ImGuiIO bool MouseWheelRequestAxisSwap; // On a non-Mac system, holding SHIFT requests WheelY to perform the equivalent of a WheelX event. On a Mac system this is already enforced by the system. float MouseDownDuration[5]; // Duration the mouse button has been down (0.0f == just clicked) float MouseDownDurationPrev[5]; // Previous time the mouse button has been down + ImVec2 MouseDragMaxDistanceAbs[5]; // Maximum distance, absolute, on each axis, of how much mouse has traveled from the clicking point float MouseDragMaxDistanceSqr[5]; // Squared maximum distance of how much mouse has traveled from the clicking point (used for moving thresholds) float PenPressure; // Touch/Pen pressure (0.0f to 1.0f, should be >0.0f only when MouseDown[0] == true). Helper storage currently unused by Dear ImGui. bool AppFocusLost; // Only modify via AddFocusEvent() @@ -2089,7 +2341,7 @@ struct ImGuiIO }; //----------------------------------------------------------------------------- -// [SECTION] Misc data structures +// [SECTION] Misc data structures (ImGuiInputTextCallbackData, ImGuiSizeCallbackData, ImGuiPayload) //----------------------------------------------------------------------------- // Shared state of InputText(), passed as an argument to your callback when a ImGuiInputTextFlags_Callback* flag is used. @@ -2141,6 +2393,28 @@ struct ImGuiSizeCallbackData ImVec2 DesiredSize; // Read-write. Desired size, based on user's mouse position. Write to this field to restrain resizing. }; +// [ALPHA] Rarely used / very advanced uses only. Use with SetNextWindowClass() and DockSpace() functions. +// Important: the content of this class is still highly WIP and likely to change and be refactored +// before we stabilize Docking features. Please be mindful if using this. +// Provide hints: +// - To the platform backend via altered viewport flags (enable/disable OS decoration, OS task bar icons, etc.) +// - To the platform backend for OS level parent/child relationships of viewport. +// - To the docking system for various options and filtering. +struct ImGuiWindowClass +{ + ImGuiID ClassId; // User data. 0 = Default class (unclassed). Windows of different classes cannot be docked with each others. + ImGuiID ParentViewportId; // Hint for the platform backend. -1: use default. 0: request platform backend to not parent the platform. != 0: request platform backend to create a parent<>child relationship between the platform windows. Not conforming backends are free to e.g. parent every viewport to the main viewport or not. + ImGuiID FocusRouteParentWindowId; // ID of parent window for shortcut focus route evaluation, e.g. Shortcut() call from Parent Window will succeed when this window is focused. + ImGuiViewportFlags ViewportFlagsOverrideSet; // Viewport flags to set when a window of this class owns a viewport. This allows you to enforce OS decoration or task bar icon, override the defaults on a per-window basis. + ImGuiViewportFlags ViewportFlagsOverrideClear; // Viewport flags to clear when a window of this class owns a viewport. This allows you to enforce OS decoration or task bar icon, override the defaults on a per-window basis. + ImGuiTabItemFlags TabItemFlagsOverrideSet; // [EXPERIMENTAL] TabItem flags to set when a window of this class gets submitted into a dock node tab bar. May use with ImGuiTabItemFlags_Leading or ImGuiTabItemFlags_Trailing. + ImGuiDockNodeFlags DockNodeFlagsOverrideSet; // [EXPERIMENTAL] Dock node flags to set when a window of this class is hosted by a dock node (it doesn't have to be selected!) + bool DockingAlwaysTabBar; // Set to true to enforce single floating windows of this class always having their own docking node (equivalent of setting the global io.ConfigDockingAlwaysTabBar) + bool DockingAllowUnclassed; // Set to true to allow windows of this class to be docked/merged with an unclassed window. // FIXME-DOCK: Move to DockNodeFlags override? + + ImGuiWindowClass() { memset(this, 0, sizeof(*this)); ParentViewportId = (ImGuiID)-1; DockingAllowUnclassed = true; } +}; + // Data payload for Drag and Drop operations: AcceptDragDropPayload(), GetDragDropPayload() struct ImGuiPayload { @@ -2163,30 +2437,6 @@ struct ImGuiPayload bool IsDelivery() const { return Delivery; } }; -// Sorting specification for one column of a table (sizeof == 12 bytes) -struct ImGuiTableColumnSortSpecs -{ - ImGuiID ColumnUserID; // User id of the column (if specified by a TableSetupColumn() call) - ImS16 ColumnIndex; // Index of the column - ImS16 SortOrder; // Index within parent ImGuiTableSortSpecs (always stored in order starting from 0, tables sorted on a single criteria will always have a 0 here) - ImGuiSortDirection SortDirection : 8; // ImGuiSortDirection_Ascending or ImGuiSortDirection_Descending (you can use this or SortSign, whichever is more convenient for your sort function) - - ImGuiTableColumnSortSpecs() { memset(this, 0, sizeof(*this)); } -}; - -// Sorting specifications for a table (often handling sort specs for a single column, occasionally more) -// Obtained by calling TableGetSortSpecs(). -// When 'SpecsDirty == true' you can sort your data. It will be true with sorting specs have changed since last call, or the first time. -// Make sure to set 'SpecsDirty = false' after sorting, else you may wastefully sort your data every frame! -struct ImGuiTableSortSpecs -{ - const ImGuiTableColumnSortSpecs* Specs; // Pointer to sort spec array. - int SpecsCount; // Sort spec count. Most often 1. May be > 1 when ImGuiTableFlags_SortMulti is enabled. May be == 0 when ImGuiTableFlags_SortTristate is enabled. - bool SpecsDirty; // Set to true when specs have changed since last time! Use this to sort again, then clear the flag. - - ImGuiTableSortSpecs() { memset(this, 0, sizeof(*this)); } -}; - //----------------------------------------------------------------------------- // [SECTION] Helpers (ImGuiOnceUponAFrame, ImGuiTextFilter, ImGuiTextBuffer, ImGuiStorage, ImGuiListClipper, Math Operators, ImColor) //----------------------------------------------------------------------------- @@ -2270,9 +2520,9 @@ struct ImGuiStorage { ImGuiID key; union { int val_i; float val_f; void* val_p; }; - ImGuiStoragePair(ImGuiID _key, int _val_i) { key = _key; val_i = _val_i; } - ImGuiStoragePair(ImGuiID _key, float _val_f) { key = _key; val_f = _val_f; } - ImGuiStoragePair(ImGuiID _key, void* _val_p) { key = _key; val_p = _val_p; } + ImGuiStoragePair(ImGuiID _key, int _val) { key = _key; val_i = _val; } + ImGuiStoragePair(ImGuiID _key, float _val) { key = _key; val_f = _val; } + ImGuiStoragePair(ImGuiID _key, void* _val) { key = _key; val_p = _val; } }; ImVector Data; @@ -2299,11 +2549,10 @@ struct ImGuiStorage IMGUI_API float* GetFloatRef(ImGuiID key, float default_val = 0.0f); IMGUI_API void** GetVoidPtrRef(ImGuiID key, void* default_val = NULL); - // Use on your own storage if you know only integer are being stored (open/close all tree nodes) - IMGUI_API void SetAllInt(int val); - - // For quicker full rebuild of a storage (instead of an incremental one), you may add all your contents and then sort once. + // Advanced: for quicker full rebuild of a storage (instead of an incremental one), you may add all your contents and then sort once. IMGUI_API void BuildSortByKey(); + // Obsolete: use on your own storage if you know only integer are being stored (open/close all tree nodes) + IMGUI_API void SetAllInt(int val); }; // Helper: Manually clip large list of items. @@ -2344,12 +2593,14 @@ struct ImGuiListClipper IMGUI_API void End(); // Automatically called on the last call of Step() that returns false. IMGUI_API bool Step(); // Call until it returns false. The DisplayStart/DisplayEnd fields will be set and you can process/draw those items. - // Call IncludeRangeByIndices() *BEFORE* first call to Step() if you need a range of items to not be clipped, regardless of their visibility. + // Call IncludeItemByIndex() or IncludeItemsByIndex() *BEFORE* first call to Step() if you need a range of items to not be clipped, regardless of their visibility. // (Due to alignment / padding of certain items it is possible that an extra item may be included on either end of the display range). - IMGUI_API void IncludeRangeByIndices(int item_begin, int item_end); // item_end is exclusive e.g. use (42, 42+1) to make item 42 never clipped. + inline void IncludeItemByIndex(int item_index) { IncludeItemsByIndex(item_index, item_index + 1); } + IMGUI_API void IncludeItemsByIndex(int item_begin, int item_end); // item_end is exclusive e.g. use (42, 42+1) to make item 42 never clipped. #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS - inline void ForceDisplayRangeByIndices(int item_begin, int item_end) { IncludeRangeByIndices(item_begin, item_end); } // [renamed in 1.89.6] + inline void IncludeRangeByIndices(int item_begin, int item_end) { IncludeItemsByIndex(item_begin, item_end); } // [renamed in 1.89.9] + inline void ForceDisplayRangeByIndices(int item_begin, int item_end) { IncludeItemsByIndex(item_begin, item_end); } // [renamed in 1.89.6] //inline ImGuiListClipper(int items_count, float items_height = -1.0f) { memset(this, 0, sizeof(*this)); ItemsCount = -1; Begin(items_count, items_height); } // [removed in 1.79] #endif }; @@ -2374,9 +2625,13 @@ static inline ImVec2& operator+=(ImVec2& lhs, const ImVec2& rhs) { lhs.x static inline ImVec2& operator-=(ImVec2& lhs, const ImVec2& rhs) { lhs.x -= rhs.x; lhs.y -= rhs.y; return lhs; } static inline ImVec2& operator*=(ImVec2& lhs, const ImVec2& rhs) { lhs.x *= rhs.x; lhs.y *= rhs.y; return lhs; } static inline ImVec2& operator/=(ImVec2& lhs, const ImVec2& rhs) { lhs.x /= rhs.x; lhs.y /= rhs.y; return lhs; } +static inline bool operator==(const ImVec2& lhs, const ImVec2& rhs) { return lhs.x == rhs.x && lhs.y == rhs.y; } +static inline bool operator!=(const ImVec2& lhs, const ImVec2& rhs) { return lhs.x != rhs.x || lhs.y != rhs.y; } static inline ImVec4 operator+(const ImVec4& lhs, const ImVec4& rhs) { return ImVec4(lhs.x + rhs.x, lhs.y + rhs.y, lhs.z + rhs.z, lhs.w + rhs.w); } static inline ImVec4 operator-(const ImVec4& lhs, const ImVec4& rhs) { return ImVec4(lhs.x - rhs.x, lhs.y - rhs.y, lhs.z - rhs.z, lhs.w - rhs.w); } static inline ImVec4 operator*(const ImVec4& lhs, const ImVec4& rhs) { return ImVec4(lhs.x * rhs.x, lhs.y * rhs.y, lhs.z * rhs.z, lhs.w * rhs.w); } +static inline bool operator==(const ImVec4& lhs, const ImVec4& rhs) { return lhs.x == rhs.x && lhs.y == rhs.y && lhs.z == rhs.z && lhs.w == rhs.w; } +static inline bool operator!=(const ImVec4& lhs, const ImVec4& rhs) { return lhs.x != rhs.x || lhs.y != rhs.y || lhs.z != rhs.z || lhs.w != rhs.w; } IM_MSVC_RUNTIME_CHECKS_RESTORE #endif @@ -2413,8 +2668,8 @@ struct ImColor constexpr ImColor() { } constexpr ImColor(float r, float g, float b, float a = 1.0f) : Value(r, g, b, a) { } constexpr ImColor(const ImVec4& col) : Value(col) {} - ImColor(int r, int g, int b, int a = 255) { float sc = 1.0f / 255.0f; Value.x = (float)r * sc; Value.y = (float)g * sc; Value.z = (float)b * sc; Value.w = (float)a * sc; } - ImColor(ImU32 rgba) { float sc = 1.0f / 255.0f; Value.x = (float)((rgba >> IM_COL32_R_SHIFT) & 0xFF) * sc; Value.y = (float)((rgba >> IM_COL32_G_SHIFT) & 0xFF) * sc; Value.z = (float)((rgba >> IM_COL32_B_SHIFT) & 0xFF) * sc; Value.w = (float)((rgba >> IM_COL32_A_SHIFT) & 0xFF) * sc; } + constexpr ImColor(int r, int g, int b, int a = 255) : Value((float)r * (1.0f / 255.0f), (float)g * (1.0f / 255.0f), (float)b * (1.0f / 255.0f), (float)a* (1.0f / 255.0f)) {} + constexpr ImColor(ImU32 rgba) : Value((float)((rgba >> IM_COL32_R_SHIFT) & 0xFF) * (1.0f / 255.0f), (float)((rgba >> IM_COL32_G_SHIFT) & 0xFF) * (1.0f / 255.0f), (float)((rgba >> IM_COL32_B_SHIFT) & 0xFF) * (1.0f / 255.0f), (float)((rgba >> IM_COL32_A_SHIFT) & 0xFF) * (1.0f / 255.0f)) {} inline operator ImU32() const { return ImGui::ColorConvertFloat4ToU32(Value); } inline operator ImVec4() const { return Value; } @@ -2446,9 +2701,9 @@ typedef void (*ImDrawCallback)(const ImDrawList* parent_list, const ImDrawCmd* c // Special Draw callback value to request renderer backend to reset the graphics/render state. // The renderer backend needs to handle this special value, otherwise it will crash trying to call a function at this address. -// This is useful for example if you submitted callbacks which you know have altered the render state and you want it to be restored. -// It is not done by default because they are many perfectly useful way of altering render state for imgui contents (e.g. changing shader/blending settings before an Image call). -#define ImDrawCallback_ResetRenderState (ImDrawCallback)(-1) +// This is useful, for example, if you submitted callbacks which you know have altered the render state and you want it to be restored. +// Render state is not reset by default because they are many perfectly useful way of altering render state (e.g. changing shader/blending settings before an Image call). +#define ImDrawCallback_ResetRenderState (ImDrawCallback)(-8) // Typically, 1 command = 1 GPU draw call (unless command is a callback) // - VtxOffset: When 'io.BackendFlags & ImGuiBackendFlags_RendererHasVtxOffset' is enabled, @@ -2612,6 +2867,8 @@ struct ImDrawList IMGUI_API void AddCircleFilled(const ImVec2& center, float radius, ImU32 col, int num_segments = 0); IMGUI_API void AddNgon(const ImVec2& center, float radius, ImU32 col, int num_segments, float thickness = 1.0f); IMGUI_API void AddNgonFilled(const ImVec2& center, float radius, ImU32 col, int num_segments); + IMGUI_API void AddEllipse(const ImVec2& center, float radius_x, float radius_y, ImU32 col, float rot = 0.0f, int num_segments = 0, float thickness = 1.0f); + IMGUI_API void AddEllipseFilled(const ImVec2& center, float radius_x, float radius_y, ImU32 col, float rot = 0.0f, int num_segments = 0); IMGUI_API void AddText(const ImVec2& pos, ImU32 col, const char* text_begin, const char* text_end = NULL); IMGUI_API void AddText(const ImFont* font, float font_size, const ImVec2& pos, ImU32 col, const char* text_begin, const char* text_end = NULL, float wrap_width = 0.0f, const ImVec4* cpu_fine_clip_rect = NULL); IMGUI_API void AddPolyline(const ImVec2* points, int num_points, ImU32 col, ImDrawFlags flags, float thickness); @@ -2628,7 +2885,8 @@ struct ImDrawList IMGUI_API void AddImageRounded(ImTextureID user_texture_id, const ImVec2& p_min, const ImVec2& p_max, const ImVec2& uv_min, const ImVec2& uv_max, ImU32 col, float rounding, ImDrawFlags flags = 0); // Stateful path API, add points then finish with PathFillConvex() or PathStroke() - // - Filled shapes must always use clockwise winding order. The anti-aliasing fringe depends on it. Counter-clockwise shapes will have "inward" anti-aliasing. + // - Important: filled shapes must always use clockwise winding order! The anti-aliasing fringe depends on it. Counter-clockwise shapes will have "inward" anti-aliasing. + // so e.g. 'PathArcTo(center, radius, PI * -0.5f, PI)' is ok, whereas 'PathArcTo(center, radius, PI, PI * -0.5f)' won't have correct anti-aliasing when followed by PathFillConvex(). inline void PathClear() { _Path.Size = 0; } inline void PathLineTo(const ImVec2& pos) { _Path.push_back(pos); } inline void PathLineToMergeDuplicate(const ImVec2& pos) { if (_Path.Size == 0 || memcmp(&_Path.Data[_Path.Size - 1], &pos, 8) != 0) _Path.push_back(pos); } @@ -2636,6 +2894,7 @@ struct ImDrawList inline void PathStroke(ImU32 col, ImDrawFlags flags = 0, float thickness = 1.0f) { AddPolyline(_Path.Data, _Path.Size, col, flags, thickness); _Path.Size = 0; } IMGUI_API void PathArcTo(const ImVec2& center, float radius, float a_min, float a_max, int num_segments = 0); IMGUI_API void PathArcToFast(const ImVec2& center, float radius, int a_min_of_12, int a_max_of_12); // Use precomputed angles for a 12 steps circle + IMGUI_API void PathEllipticalArcTo(const ImVec2& center, float radius_x, float radius_y, float rot, float a_min, float a_max, int num_segments = 0); // Ellipse IMGUI_API void PathBezierCubicCurveTo(const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, int num_segments = 0); // Cubic Bezier (4 control points) IMGUI_API void PathBezierQuadraticCurveTo(const ImVec2& p2, const ImVec2& p3, int num_segments = 0); // Quadratic Bezier (3 control points) IMGUI_API void PathRect(const ImVec2& rect_min, const ImVec2& rect_max, float rounding = 0.0f, ImDrawFlags flags = 0); @@ -2648,7 +2907,7 @@ struct ImDrawList // Advanced: Channels // - Use to split render into layers. By switching channels to can render out-of-order (e.g. submit FG primitives before BG primitives) // - Use to minimize draw calls (e.g. if going back-and-forth between multiple clipping rectangles, prefer to append into separate channels then merge at the end) - // - FIXME-OBSOLETE: This API shouldn't have been in ImDrawList in the first place! + // - This API shouldn't have been in ImDrawList in the first place! // Prefer using your own persistent instance of ImDrawListSplitter as you can stack them. // Using the ImDrawList::ChannelsXXXX you cannot stack a split over another. inline void ChannelsSplit(int count) { _Splitter.Split(this, count); } @@ -2689,18 +2948,20 @@ struct ImDrawList // as this is one of the oldest structure exposed by the library! Basically, ImDrawList == CmdList) struct ImDrawData { - bool Valid; // Only valid after Render() is called and before the next NewFrame() is called. - int CmdListsCount; // Number of ImDrawList* to render - int TotalIdxCount; // For convenience, sum of all ImDrawList's IdxBuffer.Size - int TotalVtxCount; // For convenience, sum of all ImDrawList's VtxBuffer.Size - ImDrawList** CmdLists; // Array of ImDrawList* to render. The ImDrawList are owned by ImGuiContext and only pointed to from here. - ImVec2 DisplayPos; // Top-left position of the viewport to render (== top-left of the orthogonal projection matrix to use) (== GetMainViewport()->Pos for the main viewport, == (0.0) in most single-viewport applications) - ImVec2 DisplaySize; // Size of the viewport to render (== GetMainViewport()->Size for the main viewport, == io.DisplaySize in most single-viewport applications) - ImVec2 FramebufferScale; // Amount of pixels for each unit of DisplaySize. Based on io.DisplayFramebufferScale. Generally (1,1) on normal display, (2,2) on OSX with Retina display. + bool Valid; // Only valid after Render() is called and before the next NewFrame() is called. + int CmdListsCount; // Number of ImDrawList* to render + int TotalIdxCount; // For convenience, sum of all ImDrawList's IdxBuffer.Size + int TotalVtxCount; // For convenience, sum of all ImDrawList's VtxBuffer.Size + ImVector CmdLists; // Array of ImDrawList* to render. The ImDrawLists are owned by ImGuiContext and only pointed to from here. + ImVec2 DisplayPos; // Top-left position of the viewport to render (== top-left of the orthogonal projection matrix to use) (== GetMainViewport()->Pos for the main viewport, == (0.0) in most single-viewport applications) + ImVec2 DisplaySize; // Size of the viewport to render (== GetMainViewport()->Size for the main viewport, == io.DisplaySize in most single-viewport applications) + ImVec2 FramebufferScale; // Amount of pixels for each unit of DisplaySize. Based on io.DisplayFramebufferScale. Generally (1,1) on normal display, (2,2) on OSX with Retina display. + ImGuiViewport* OwnerViewport; // Viewport carrying the ImDrawData instance, might be of use to the renderer (generally not). // Functions ImDrawData() { Clear(); } - void Clear() { memset(this, 0, sizeof(*this)); } // The ImDrawList are owned by ImGuiContext! + IMGUI_API void Clear(); + IMGUI_API void AddDrawList(ImDrawList* draw_list); // Helper to add an external draw list into an existing ImDrawData. IMGUI_API void DeIndexAllBuffers(); // Helper to convert all buffers from indexed to non-indexed, in case you cannot render indexed. Note: this is slow and most likely a waste of resources. Always prefer indexed rendering! IMGUI_API void ScaleClipRects(const ImVec2& fb_scale); // Helper to scale the ClipRect field of each ImDrawCmd. Use if your final output buffer is at a different scale than Dear ImGui expects, or if there is a difference between your window resolution and framebuffer resolution. }; @@ -2716,7 +2977,7 @@ struct ImFontConfig bool FontDataOwnedByAtlas; // true // TTF/OTF data ownership taken by the container ImFontAtlas (will delete memory itself). int FontNo; // 0 // Index of font within TTF/OTF file float SizePixels; // // Size in pixels for rasterizer (more or less maps to the resulting font height). - int OversampleH; // 3 // Rasterize at higher quality for sub-pixel positioning. Note the difference between 2 and 3 is minimal so you can reduce this to 2 to save memory. Read https://github.com/nothings/stb/blob/master/tests/oversample/README.md for details. + int OversampleH; // 2 // Rasterize at higher quality for sub-pixel positioning. Note the difference between 2 and 3 is minimal. You can reduce this to 1 for large glyphs save memory. Read https://github.com/nothings/stb/blob/master/tests/oversample/README.md for details. int OversampleV; // 1 // Rasterize at higher quality for sub-pixel positioning. This is not really useful as we don't use sub-pixel positions on the Y axis. bool PixelSnapH; // false // Align every glyph to pixel boundary. Useful e.g. if you are merging a non-pixel aligned font with the default font. If enabled, you can set OversampleH/V to 1. ImVec2 GlyphExtraSpacing; // 0, 0 // Extra spacing (in pixels) between glyphs. Only X axis is supported for now. @@ -2726,7 +2987,8 @@ struct ImFontConfig float GlyphMaxAdvanceX; // FLT_MAX // Maximum AdvanceX for glyphs bool MergeMode; // false // Merge into previous ImFont, so you can combine multiple inputs font into one ImFont (e.g. ASCII font + icons + Japanese glyphs). You may want to use GlyphOffset.y when merge font of different heights. unsigned int FontBuilderFlags; // 0 // Settings for custom font builder. THIS IS BUILDER IMPLEMENTATION DEPENDENT. Leave as zero if unsure. - float RasterizerMultiply; // 1.0f // Brighten (>1.0f) or darken (<1.0f) font output. Brightening small fonts may be a good workaround to make them more readable. + float RasterizerMultiply; // 1.0f // Linearly brighten (>1.0f) or darken (<1.0f) font output. Brightening small fonts may be a good workaround to make them more readable. This is a silly thing we may remove in the future. + float RasterizerDensity; // 1.0f // DPI scale for rasterization, not altering other font metrics: make it easy to swap between e.g. a 100% and a 400% fonts for a zooming display. IMPORTANT: If you increase this it is expected that you increase font scale accordingly, otherwise quality may look lowered. ImWchar EllipsisChar; // -1 // Explicitly specify unicode codepoint of ellipsis character. When fonts are being merged first specified ellipsis will be used. // [Internal] @@ -2810,8 +3072,8 @@ struct ImFontAtlas IMGUI_API ImFont* AddFont(const ImFontConfig* font_cfg); IMGUI_API ImFont* AddFontDefault(const ImFontConfig* font_cfg = NULL); IMGUI_API ImFont* AddFontFromFileTTF(const char* filename, float size_pixels, const ImFontConfig* font_cfg = NULL, const ImWchar* glyph_ranges = NULL); - IMGUI_API ImFont* AddFontFromMemoryTTF(void* font_data, int font_size, float size_pixels, const ImFontConfig* font_cfg = NULL, const ImWchar* glyph_ranges = NULL); // Note: Transfer ownership of 'ttf_data' to ImFontAtlas! Will be deleted after destruction of the atlas. Set font_cfg->FontDataOwnedByAtlas=false to keep ownership of your data and it won't be freed. - IMGUI_API ImFont* AddFontFromMemoryCompressedTTF(const void* compressed_font_data, int compressed_font_size, float size_pixels, const ImFontConfig* font_cfg = NULL, const ImWchar* glyph_ranges = NULL); // 'compressed_font_data' still owned by caller. Compress with binary_to_compressed_c.cpp. + IMGUI_API ImFont* AddFontFromMemoryTTF(void* font_data, int font_data_size, float size_pixels, const ImFontConfig* font_cfg = NULL, const ImWchar* glyph_ranges = NULL); // Note: Transfer ownership of 'ttf_data' to ImFontAtlas! Will be deleted after destruction of the atlas. Set font_cfg->FontDataOwnedByAtlas=false to keep ownership of your data and it won't be freed. + IMGUI_API ImFont* AddFontFromMemoryCompressedTTF(const void* compressed_font_data, int compressed_font_data_size, float size_pixels, const ImFontConfig* font_cfg = NULL, const ImWchar* glyph_ranges = NULL); // 'compressed_font_data' still owned by caller. Compress with binary_to_compressed_c.cpp. IMGUI_API ImFont* AddFontFromMemoryCompressedBase85TTF(const char* compressed_font_data_base85, float size_pixels, const ImFontConfig* font_cfg = NULL, const ImWchar* glyph_ranges = NULL); // 'compressed_font_data_base85' still owned by caller. Compress with binary_to_compressed_c.cpp with -base85 parameter. IMGUI_API void ClearInputData(); // Clear input data (all ImFontConfig structures including sizes, TTF data, glyph ranges, etc.) = all the data used to build the texture and fonts. IMGUI_API void ClearTexData(); // Clear output texture data (CPU side). Saves RAM once the texture has been copied to graphics memory. @@ -2970,11 +3232,24 @@ enum ImGuiViewportFlags_ ImGuiViewportFlags_None = 0, ImGuiViewportFlags_IsPlatformWindow = 1 << 0, // Represent a Platform Window ImGuiViewportFlags_IsPlatformMonitor = 1 << 1, // Represent a Platform Monitor (unused yet) - ImGuiViewportFlags_OwnedByApp = 1 << 2, // Platform Window: is created/managed by the application (rather than a dear imgui backend) + ImGuiViewportFlags_OwnedByApp = 1 << 2, // Platform Window: Was created/managed by the user application? (rather than our backend) + ImGuiViewportFlags_NoDecoration = 1 << 3, // Platform Window: Disable platform decorations: title bar, borders, etc. (generally set all windows, but if ImGuiConfigFlags_ViewportsDecoration is set we only set this on popups/tooltips) + ImGuiViewportFlags_NoTaskBarIcon = 1 << 4, // Platform Window: Disable platform task bar icon (generally set on popups/tooltips, or all windows if ImGuiConfigFlags_ViewportsNoTaskBarIcon is set) + ImGuiViewportFlags_NoFocusOnAppearing = 1 << 5, // Platform Window: Don't take focus when created. + ImGuiViewportFlags_NoFocusOnClick = 1 << 6, // Platform Window: Don't take focus when clicked on. + ImGuiViewportFlags_NoInputs = 1 << 7, // Platform Window: Make mouse pass through so we can drag this window while peaking behind it. + ImGuiViewportFlags_NoRendererClear = 1 << 8, // Platform Window: Renderer doesn't need to clear the framebuffer ahead (because we will fill it entirely). + ImGuiViewportFlags_NoAutoMerge = 1 << 9, // Platform Window: Avoid merging this window into another host window. This can only be set via ImGuiWindowClass viewport flags override (because we need to now ahead if we are going to create a viewport in the first place!). + ImGuiViewportFlags_TopMost = 1 << 10, // Platform Window: Display on top (for tooltips only). + ImGuiViewportFlags_CanHostOtherWindows = 1 << 11, // Viewport can host multiple imgui windows (secondary viewports are associated to a single window). // FIXME: In practice there's still probably code making the assumption that this is always and only on the MainViewport. Will fix once we add support for "no main viewport". + + // Output status flags (from Platform) + ImGuiViewportFlags_IsMinimized = 1 << 12, // Platform Window: Window is minimized, can skip render. When minimized we tend to avoid using the viewport pos/size for clipping window or testing if they are contained in the viewport. + ImGuiViewportFlags_IsFocused = 1 << 13, // Platform Window: Window is focused (last call to Platform_GetWindowFocus() returned true) }; // - Currently represents the Platform Window created by the application which is hosting our Dear ImGui windows. -// - In 'docking' branch with multi-viewport enabled, we extend this concept to have multiple active viewports. +// - With multi-viewport enabled, we extend this concept to have multiple active viewports. // - In the future we will extend this concept further to also represent Platform Monitor and support a "no main platform window" operation mode. // - About Main Area vs Work Area: // - Main Area = entire viewport. @@ -2982,16 +3257,32 @@ enum ImGuiViewportFlags_ // - Windows are generally trying to stay within the Work Area of their host viewport. struct ImGuiViewport { + ImGuiID ID; // Unique identifier for the viewport ImGuiViewportFlags Flags; // See ImGuiViewportFlags_ ImVec2 Pos; // Main Area: Position of the viewport (Dear ImGui coordinates are the same as OS desktop/native coordinates) ImVec2 Size; // Main Area: Size of the viewport. ImVec2 WorkPos; // Work Area: Position of the viewport minus task bars, menus bars, status bars (>= Pos) ImVec2 WorkSize; // Work Area: Size of the viewport minus task bars, menu bars, status bars (<= Size) + float DpiScale; // 1.0f = 96 DPI = No extra scale. + ImGuiID ParentViewportId; // (Advanced) 0: no parent. Instruct the platform backend to setup a parent/child relationship between platform windows. + ImDrawData* DrawData; // The ImDrawData corresponding to this viewport. Valid after Render() and until the next call to NewFrame(). // Platform/Backend Dependent Data - void* PlatformHandleRaw; // void* to hold lower-level, platform-native window handle (under Win32 this is expected to be a HWND, unused for other platforms) + // Our design separate the Renderer and Platform backends to facilitate combining default backends with each others. + // When our create your own backend for a custom engine, it is possible that both Renderer and Platform will be handled + // by the same system and you may not need to use all the UserData/Handle fields. + // The library never uses those fields, they are merely storage to facilitate backend implementation. + void* RendererUserData; // void* to hold custom data structure for the renderer (e.g. swap chain, framebuffers etc.). generally set by your Renderer_CreateWindow function. + void* PlatformUserData; // void* to hold custom data structure for the OS / platform (e.g. windowing info, render context). generally set by your Platform_CreateWindow function. + void* PlatformHandle; // void* for FindViewportByPlatformHandle(). (e.g. suggested to use natural platform handle such as HWND, GLFWWindow*, SDL_Window*) + void* PlatformHandleRaw; // void* to hold lower-level, platform-native window handle (under Win32 this is expected to be a HWND, unused for other platforms), when using an abstraction layer like GLFW or SDL (where PlatformHandle would be a SDL_Window*) + bool PlatformWindowCreated; // Platform window has been created (Platform_CreateWindow() has been called). This is false during the first frame where a viewport is being created. + bool PlatformRequestMove; // Platform window requested move (e.g. window was moved by the OS / host window manager, authoritative position will be OS window position) + bool PlatformRequestResize; // Platform window requested resize (e.g. window was resized by the OS / host window manager, authoritative size will be OS window size) + bool PlatformRequestClose; // Platform window requested closure (e.g. window was moved by the OS / host window manager, e.g. pressing ALT-F4) ImGuiViewport() { memset(this, 0, sizeof(*this)); } + ~ImGuiViewport() { IM_ASSERT(PlatformUserData == NULL && RendererUserData == NULL); } // Helpers ImVec2 GetCenter() const { return ImVec2(Pos.x + Size.x * 0.5f, Pos.y + Size.y * 0.5f); } @@ -2999,8 +3290,126 @@ struct ImGuiViewport }; //----------------------------------------------------------------------------- -// [SECTION] Platform Dependent Interfaces +// [SECTION] Platform Dependent Interfaces (for e.g. multi-viewport support) //----------------------------------------------------------------------------- +// [BETA] (Optional) This is completely optional, for advanced users! +// If you are new to Dear ImGui and trying to integrate it into your engine, you can probably ignore this for now. +// +// This feature allows you to seamlessly drag Dear ImGui windows outside of your application viewport. +// This is achieved by creating new Platform/OS windows on the fly, and rendering into them. +// Dear ImGui manages the viewport structures, and the backend create and maintain one Platform/OS window for each of those viewports. +// +// See Glossary https://github.com/ocornut/imgui/wiki/Glossary for details about some of the terminology. +// See Thread https://github.com/ocornut/imgui/issues/1542 for gifs, news and questions about this evolving feature. +// +// About the coordinates system: +// - When multi-viewports are enabled, all Dear ImGui coordinates become absolute coordinates (same as OS coordinates!) +// - So e.g. ImGui::SetNextWindowPos(ImVec2(0,0)) will position a window relative to your primary monitor! +// - If you want to position windows relative to your main application viewport, use ImGui::GetMainViewport()->Pos as a base position. +// +// Steps to use multi-viewports in your application, when using a default backend from the examples/ folder: +// - Application: Enable feature with 'io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable'. +// - Backend: The backend initialization will setup all necessary ImGuiPlatformIO's functions and update monitors info every frame. +// - Application: In your main loop, call ImGui::UpdatePlatformWindows(), ImGui::RenderPlatformWindowsDefault() after EndFrame() or Render(). +// - Application: Fix absolute coordinates used in ImGui::SetWindowPos() or ImGui::SetNextWindowPos() calls. +// +// Steps to use multi-viewports in your application, when using a custom backend: +// - Important: THIS IS NOT EASY TO DO and comes with many subtleties not described here! +// It's also an experimental feature, so some of the requirements may evolve. +// Consider using default backends if you can. Either way, carefully follow and refer to examples/ backends for details. +// - Application: Enable feature with 'io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable'. +// - Backend: Hook ImGuiPlatformIO's Platform_* and Renderer_* callbacks (see below). +// Set 'io.BackendFlags |= ImGuiBackendFlags_PlatformHasViewports' and 'io.BackendFlags |= ImGuiBackendFlags_PlatformHasViewports'. +// Update ImGuiPlatformIO's Monitors list every frame. +// Update MousePos every frame, in absolute coordinates. +// - Application: In your main loop, call ImGui::UpdatePlatformWindows(), ImGui::RenderPlatformWindowsDefault() after EndFrame() or Render(). +// You may skip calling RenderPlatformWindowsDefault() if its API is not convenient for your needs. Read comments below. +// - Application: Fix absolute coordinates used in ImGui::SetWindowPos() or ImGui::SetNextWindowPos() calls. +// +// About ImGui::RenderPlatformWindowsDefault(): +// - This function is a mostly a _helper_ for the common-most cases, and to facilitate using default backends. +// - You can check its simple source code to understand what it does. +// It basically iterates secondary viewports and call 4 functions that are setup in ImGuiPlatformIO, if available: +// Platform_RenderWindow(), Renderer_RenderWindow(), Platform_SwapBuffers(), Renderer_SwapBuffers() +// Those functions pointers exists only for the benefit of RenderPlatformWindowsDefault(). +// - If you have very specific rendering needs (e.g. flipping multiple swap-chain simultaneously, unusual sync/threading issues, etc.), +// you may be tempted to ignore RenderPlatformWindowsDefault() and write customized code to perform your renderingg. +// You may decide to setup the platform_io's *RenderWindow and *SwapBuffers pointers and call your functions through those pointers, +// or you may decide to never setup those pointers and call your code directly. They are a convenience, not an obligatory interface. +//----------------------------------------------------------------------------- + +// (Optional) Access via ImGui::GetPlatformIO() +struct ImGuiPlatformIO +{ + //------------------------------------------------------------------ + // Input - Backend interface/functions + Monitor List + //------------------------------------------------------------------ + + // (Optional) Platform functions (e.g. Win32, GLFW, SDL2) + // For reference, the second column shows which function are generally calling the Platform Functions: + // N = ImGui::NewFrame() ~ beginning of the dear imgui frame: read info from platform/OS windows (latest size/position) + // F = ImGui::Begin(), ImGui::EndFrame() ~ during the dear imgui frame + // U = ImGui::UpdatePlatformWindows() ~ after the dear imgui frame: create and update all platform/OS windows + // R = ImGui::RenderPlatformWindowsDefault() ~ render + // D = ImGui::DestroyPlatformWindows() ~ shutdown + // The general idea is that NewFrame() we will read the current Platform/OS state, and UpdatePlatformWindows() will write to it. + // + // The functions are designed so we can mix and match 2 imgui_impl_xxxx files, one for the Platform (~window/input handling), one for Renderer. + // Custom engine backends will often provide both Platform and Renderer interfaces and so may not need to use all functions. + // Platform functions are typically called before their Renderer counterpart, apart from Destroy which are called the other way. + + // Platform function --------------------------------------------------- Called by ----- + void (*Platform_CreateWindow)(ImGuiViewport* vp); // . . U . . // Create a new platform window for the given viewport + void (*Platform_DestroyWindow)(ImGuiViewport* vp); // N . U . D // + void (*Platform_ShowWindow)(ImGuiViewport* vp); // . . U . . // Newly created windows are initially hidden so SetWindowPos/Size/Title can be called on them before showing the window + void (*Platform_SetWindowPos)(ImGuiViewport* vp, ImVec2 pos); // . . U . . // Set platform window position (given the upper-left corner of client area) + ImVec2 (*Platform_GetWindowPos)(ImGuiViewport* vp); // N . . . . // + void (*Platform_SetWindowSize)(ImGuiViewport* vp, ImVec2 size); // . . U . . // Set platform window client area size (ignoring OS decorations such as OS title bar etc.) + ImVec2 (*Platform_GetWindowSize)(ImGuiViewport* vp); // N . . . . // Get platform window client area size + void (*Platform_SetWindowFocus)(ImGuiViewport* vp); // N . . . . // Move window to front and set input focus + bool (*Platform_GetWindowFocus)(ImGuiViewport* vp); // . . U . . // + bool (*Platform_GetWindowMinimized)(ImGuiViewport* vp); // N . . . . // Get platform window minimized state. When minimized, we generally won't attempt to get/set size and contents will be culled more easily + void (*Platform_SetWindowTitle)(ImGuiViewport* vp, const char* str); // . . U . . // Set platform window title (given an UTF-8 string) + void (*Platform_SetWindowAlpha)(ImGuiViewport* vp, float alpha); // . . U . . // (Optional) Setup global transparency (not per-pixel transparency) + void (*Platform_UpdateWindow)(ImGuiViewport* vp); // . . U . . // (Optional) Called by UpdatePlatformWindows(). Optional hook to allow the platform backend from doing general book-keeping every frame. + void (*Platform_RenderWindow)(ImGuiViewport* vp, void* render_arg); // . . . R . // (Optional) Main rendering (platform side! This is often unused, or just setting a "current" context for OpenGL bindings). 'render_arg' is the value passed to RenderPlatformWindowsDefault(). + void (*Platform_SwapBuffers)(ImGuiViewport* vp, void* render_arg); // . . . R . // (Optional) Call Present/SwapBuffers (platform side! This is often unused!). 'render_arg' is the value passed to RenderPlatformWindowsDefault(). + float (*Platform_GetWindowDpiScale)(ImGuiViewport* vp); // N . . . . // (Optional) [BETA] FIXME-DPI: DPI handling: Return DPI scale for this viewport. 1.0f = 96 DPI. + void (*Platform_OnChangedViewport)(ImGuiViewport* vp); // . F . . . // (Optional) [BETA] FIXME-DPI: DPI handling: Called during Begin() every time the viewport we are outputting into changes, so backend has a chance to swap fonts to adjust style. + int (*Platform_CreateVkSurface)(ImGuiViewport* vp, ImU64 vk_inst, const void* vk_allocators, ImU64* out_vk_surface); // (Optional) For a Vulkan Renderer to call into Platform code (since the surface creation needs to tie them both). + + // (Optional) Renderer functions (e.g. DirectX, OpenGL, Vulkan) + void (*Renderer_CreateWindow)(ImGuiViewport* vp); // . . U . . // Create swap chain, frame buffers etc. (called after Platform_CreateWindow) + void (*Renderer_DestroyWindow)(ImGuiViewport* vp); // N . U . D // Destroy swap chain, frame buffers etc. (called before Platform_DestroyWindow) + void (*Renderer_SetWindowSize)(ImGuiViewport* vp, ImVec2 size); // . . U . . // Resize swap chain, frame buffers etc. (called after Platform_SetWindowSize) + void (*Renderer_RenderWindow)(ImGuiViewport* vp, void* render_arg); // . . . R . // (Optional) Clear framebuffer, setup render target, then render the viewport->DrawData. 'render_arg' is the value passed to RenderPlatformWindowsDefault(). + void (*Renderer_SwapBuffers)(ImGuiViewport* vp, void* render_arg); // . . . R . // (Optional) Call Present/SwapBuffers. 'render_arg' is the value passed to RenderPlatformWindowsDefault(). + + // (Optional) Monitor list + // - Updated by: app/backend. Update every frame to dynamically support changing monitor or DPI configuration. + // - Used by: dear imgui to query DPI info, clamp popups/tooltips within same monitor and not have them straddle monitors. + ImVector Monitors; + + //------------------------------------------------------------------ + // Output - List of viewports to render into platform windows + //------------------------------------------------------------------ + + // Viewports list (the list is updated by calling ImGui::EndFrame or ImGui::Render) + // (in the future we will attempt to organize this feature to remove the need for a "main viewport") + ImVector Viewports; // Main viewports, followed by all secondary viewports. + ImGuiPlatformIO() { memset(this, 0, sizeof(*this)); } // Zero clear +}; + +// (Optional) This is required when enabling multi-viewport. Represent the bounds of each connected monitor/display and their DPI. +// We use this information for multiple DPI support + clamping the position of popups and tooltips so they don't straddle multiple monitors. +struct ImGuiPlatformMonitor +{ + ImVec2 MainPos, MainSize; // Coordinates of the area displayed on this monitor (Min = upper left, Max = bottom right) + ImVec2 WorkPos, WorkSize; // Coordinates without task bars / side bars / menu bars. Used to avoid positioning popups/tooltips inside this region. If you don't have this info, please copy the value for MainPos/MainSize. + float DpiScale; // 1.0f = 96 DPI + void* PlatformHandle; // Backend dependant data (e.g. HMONITOR, GLFWmonitor*, SDL Display Index, NSScreen*) + ImGuiPlatformMonitor() { MainPos = MainSize = WorkPos = WorkSize = ImVec2(0, 0); DpiScale = 1.0f; PlatformHandle = NULL; } +}; // (Optional) Support for IME (Input Method Editor) via the io.SetPlatformImeDataFn() function. struct ImGuiPlatformImeData @@ -3030,6 +3439,16 @@ namespace ImGui #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS namespace ImGui { + // OBSOLETED in 1.90.0 (from September 2023) + static inline bool BeginChildFrame(ImGuiID id, const ImVec2& size, ImGuiWindowFlags window_flags = 0) { return BeginChild(id, size, ImGuiChildFlags_FrameStyle, window_flags); } + static inline void EndChildFrame() { EndChild(); } + //static inline bool BeginChild(const char* str_id, const ImVec2& size_arg, bool border, ImGuiWindowFlags window_flags){ return BeginChild(str_id, size_arg, border ? ImGuiChildFlags_Border : ImGuiChildFlags_None, window_flags); } // Unnecessary as true == ImGuiChildFlags_Border + //static inline bool BeginChild(ImGuiID id, const ImVec2& size_arg, bool border, ImGuiWindowFlags window_flags) { return BeginChild(id, size_arg, border ? ImGuiChildFlags_Border : ImGuiChildFlags_None, window_flags); } // Unnecessary as true == ImGuiChildFlags_Border + static inline void ShowStackToolWindow(bool* p_open = NULL) { ShowIDStackToolWindow(p_open); } + IMGUI_API bool ListBox(const char* label, int* current_item, bool (*old_callback)(void* user_data, int idx, const char** out_text), void* user_data, int items_count, int height_in_items = -1); + IMGUI_API bool Combo(const char* label, int* current_item, bool (*old_callback)(void* user_data, int idx, const char** out_text), void* user_data, int items_count, int popup_max_height_in_items = -1); + // OBSOLETED in 1.89.7 (from June 2023) + IMGUI_API void SetItemAllowOverlap(); // Use SetNextItemAllowOverlap() before item. // OBSOLETED in 1.89.4 (from March 2023) static inline void PushAllowKeyboardFocus(bool tab_stop) { PushTabStop(tab_stop); } static inline void PopAllowKeyboardFocus() { PopTabStop(); } @@ -3038,12 +3457,12 @@ namespace ImGui // OBSOLETED in 1.88 (from May 2022) static inline void CaptureKeyboardFromApp(bool want_capture_keyboard = true) { SetNextFrameWantCaptureKeyboard(want_capture_keyboard); } // Renamed as name was misleading + removed default value. static inline void CaptureMouseFromApp(bool want_capture_mouse = true) { SetNextFrameWantCaptureMouse(want_capture_mouse); } // Renamed as name was misleading + removed default value. - // OBSOLETED in 1.86 (from November 2021) - IMGUI_API void CalcListClipping(int items_count, float items_height, int* out_items_display_start, int* out_items_display_end); // Calculate coarse clipping for large list of evenly sized items. Prefer using ImGuiListClipper. - // OBSOLETED in 1.85 (from August 2021) - static inline float GetWindowContentRegionWidth() { return GetWindowContentRegionMax().x - GetWindowContentRegionMin().x; } // Some of the older obsolete names along with their replacement (commented out so they are not reported in IDE) + //-- OBSOLETED in 1.86 (from November 2021) + //IMGUI_API void CalcListClipping(int items_count, float items_height, int* out_items_display_start, int* out_items_display_end); // Code removed, see 1.90 for last version of the code. Calculate range of visible items for large list of evenly sized items. Prefer using ImGuiListClipper. + //-- OBSOLETED in 1.85 (from August 2021) + //static inline float GetWindowContentRegionWidth() { return GetWindowContentRegionMax().x - GetWindowContentRegionMin().x; } //-- OBSOLETED in 1.81 (from February 2021) //static inline bool ListBoxHeader(const char* label, const ImVec2& size = ImVec2(0, 0)) { return BeginListBox(label, size); } //static inline bool ListBoxHeader(const char* label, int items_count, int height_in_items = -1) { float height = GetTextLineHeightWithSpacing() * ((height_in_items < 0 ? ImMin(items_count, 7) : height_in_items) + 0.25f) + GetStyle().FramePadding.y * 2.0f; return BeginListBox(label, ImVec2(0.0f, height)); } // Helper to calculate size from items_count and height_in_items @@ -3094,21 +3513,21 @@ namespace ImGui //static inline void SetScrollPosHere() { SetScrollHere(); } // OBSOLETED in 1.42 } -// OBSOLETED in 1.82 (from Mars 2021): flags for AddRect(), AddRectFilled(), AddImageRounded(), PathRect() -typedef ImDrawFlags ImDrawCornerFlags; -enum ImDrawCornerFlags_ -{ - ImDrawCornerFlags_None = ImDrawFlags_RoundCornersNone, // Was == 0 prior to 1.82, this is now == ImDrawFlags_RoundCornersNone which is != 0 and not implicit - ImDrawCornerFlags_TopLeft = ImDrawFlags_RoundCornersTopLeft, // Was == 0x01 (1 << 0) prior to 1.82. Order matches ImDrawFlags_NoRoundCorner* flag (we exploit this internally). - ImDrawCornerFlags_TopRight = ImDrawFlags_RoundCornersTopRight, // Was == 0x02 (1 << 1) prior to 1.82. - ImDrawCornerFlags_BotLeft = ImDrawFlags_RoundCornersBottomLeft, // Was == 0x04 (1 << 2) prior to 1.82. - ImDrawCornerFlags_BotRight = ImDrawFlags_RoundCornersBottomRight, // Was == 0x08 (1 << 3) prior to 1.82. - ImDrawCornerFlags_All = ImDrawFlags_RoundCornersAll, // Was == 0x0F prior to 1.82 - ImDrawCornerFlags_Top = ImDrawCornerFlags_TopLeft | ImDrawCornerFlags_TopRight, - ImDrawCornerFlags_Bot = ImDrawCornerFlags_BotLeft | ImDrawCornerFlags_BotRight, - ImDrawCornerFlags_Left = ImDrawCornerFlags_TopLeft | ImDrawCornerFlags_BotLeft, - ImDrawCornerFlags_Right = ImDrawCornerFlags_TopRight | ImDrawCornerFlags_BotRight, -}; +//-- OBSOLETED in 1.82 (from Mars 2021): flags for AddRect(), AddRectFilled(), AddImageRounded(), PathRect() +//typedef ImDrawFlags ImDrawCornerFlags; +//enum ImDrawCornerFlags_ +//{ +// ImDrawCornerFlags_None = ImDrawFlags_RoundCornersNone, // Was == 0 prior to 1.82, this is now == ImDrawFlags_RoundCornersNone which is != 0 and not implicit +// ImDrawCornerFlags_TopLeft = ImDrawFlags_RoundCornersTopLeft, // Was == 0x01 (1 << 0) prior to 1.82. Order matches ImDrawFlags_NoRoundCorner* flag (we exploit this internally). +// ImDrawCornerFlags_TopRight = ImDrawFlags_RoundCornersTopRight, // Was == 0x02 (1 << 1) prior to 1.82. +// ImDrawCornerFlags_BotLeft = ImDrawFlags_RoundCornersBottomLeft, // Was == 0x04 (1 << 2) prior to 1.82. +// ImDrawCornerFlags_BotRight = ImDrawFlags_RoundCornersBottomRight, // Was == 0x08 (1 << 3) prior to 1.82. +// ImDrawCornerFlags_All = ImDrawFlags_RoundCornersAll, // Was == 0x0F prior to 1.82 +// ImDrawCornerFlags_Top = ImDrawCornerFlags_TopLeft | ImDrawCornerFlags_TopRight, +// ImDrawCornerFlags_Bot = ImDrawCornerFlags_BotLeft | ImDrawCornerFlags_BotRight, +// ImDrawCornerFlags_Left = ImDrawCornerFlags_TopLeft | ImDrawCornerFlags_BotLeft, +// ImDrawCornerFlags_Right = ImDrawCornerFlags_TopRight | ImDrawCornerFlags_BotRight, +//}; // RENAMED and MERGED both ImGuiKey_ModXXX and ImGuiModFlags_XXX into ImGuiMod_XXX (from September 2022) // RENAMED ImGuiKeyModFlags -> ImGuiModFlags in 1.88 (from April 2022). Exceptionally commented out ahead of obscolescence schedule to reduce confusion and because they were not meant to be used in the first place. @@ -3117,6 +3536,8 @@ enum ImGuiModFlags_ { ImGuiModFlags_None = 0, ImGuiModFlags_Ctrl = ImGuiMod_Ctrl //typedef ImGuiKeyChord ImGuiKeyModFlags; // == int //enum ImGuiKeyModFlags_ { ImGuiKeyModFlags_None = 0, ImGuiKeyModFlags_Ctrl = ImGuiMod_Ctrl, ImGuiKeyModFlags_Shift = ImGuiMod_Shift, ImGuiKeyModFlags_Alt = ImGuiMod_Alt, ImGuiKeyModFlags_Super = ImGuiMod_Super }; +#define IM_OFFSETOF(_TYPE,_MEMBER) offsetof(_TYPE, _MEMBER) // OBSOLETED IN 1.90 (now using C++11 standard version) + #endif // #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS // RENAMED IMGUI_DISABLE_METRICS_WINDOW > IMGUI_DISABLE_DEBUG_TOOLS in 1.88 (from June 2022) @@ -3139,9 +3560,14 @@ enum ImGuiModFlags_ { ImGuiModFlags_None = 0, ImGuiModFlags_Ctrl = ImGuiMod_Ctrl #pragma warning (pop) #endif -// Include imgui_user.h at the end of imgui.h (convenient for user to only explicitly include vanilla imgui.h) +// Include imgui_user.h at the end of imgui.h +// May be convenient for some users to only explicitly include vanilla imgui.h and have extra stuff included. #ifdef IMGUI_INCLUDE_IMGUI_USER_H +#ifdef IMGUI_USER_H_FILENAME +#include IMGUI_USER_H_FILENAME +#else #include "imgui_user.h" #endif +#endif #endif // #ifndef IMGUI_DISABLE diff --git a/libs/imgui/imgui_demo.cpp b/libs/imgui/imgui_demo.cpp index fd0adc9..9b4ba87 100644 --- a/libs/imgui/imgui_demo.cpp +++ b/libs/imgui/imgui_demo.cpp @@ -1,16 +1,18 @@ -// dear imgui, v1.89.6 +// dear imgui, v1.90.4 // (demo code) // Help: // - Read FAQ at http://dearimgui.com/faq -// - Newcomers, read 'Programmer guide' in imgui.cpp for notes on how to setup Dear ImGui in your codebase. // - Call and read ImGui::ShowDemoWindow() in imgui_demo.cpp. All applications in examples/ are doing that. +// - Need help integrating Dear ImGui in your codebase? +// - Read Getting Started https://github.com/ocornut/imgui/wiki/Getting-Started +// - Read 'Programmer guide' in imgui.cpp for notes on how to setup Dear ImGui in your codebase. // Read imgui.cpp for more details, documentation and comments. // Get the latest version at https://github.com/ocornut/imgui -// ------------------------------------------------- +//--------------------------------------------------- // PLEASE DO NOT REMOVE THIS FILE FROM YOUR PROJECT! -// ------------------------------------------------- +//--------------------------------------------------- // Message to the person tempted to delete this file when integrating Dear ImGui into their codebase: // Think again! It is the most useful reference code that you and other coders will want to refer to and call. // Have the ImGui::ShowDemoWindow() function wired in an always-available debug menu of your game/app! @@ -24,14 +26,23 @@ // Thank you, // -Your beloved friend, imgui_demo.cpp (which you won't delete) -// Message to beginner C/C++ programmers about the meaning of the 'static' keyword: -// In this demo code, we frequently use 'static' variables inside functions. A static variable persists across calls, -// so it is essentially like a global variable but declared inside the scope of the function. We do this as a way to -// gather code and data in the same place, to make the demo source code faster to read, faster to write, and smaller -// in size. It also happens to be a convenient way of storing simple UI related information as long as your function -// doesn't need to be reentrant or used in multiple threads. This might be a pattern you will want to use in your code, -// but most of the real data you would be editing is likely going to be stored outside your functions. +//-------------------------------------------- +// ABOUT THE MEANING OF THE 'static' KEYWORD: +//-------------------------------------------- +// In this demo code, we frequently use 'static' variables inside functions. +// A static variable persists across calls. It is essentially a global variable but declared inside the scope of the function. +// Think of "static int n = 0;" as "global int n = 0;" ! +// We do this IN THE DEMO because we want: +// - to gather code and data in the same place. +// - to make the demo source code faster to read, faster to change, smaller in size. +// - it is also a convenient way of storing simple UI related information as long as your function +// doesn't need to be reentrant or used in multiple threads. +// This might be a pattern you will want to use in your code, but most of the data you would be working +// with in a complex codebase is likely going to be stored outside your functions. +//----------------------------------------- +// ABOUT THE CODING STYLE OF OUR DEMO CODE +//----------------------------------------- // The Demo code in this file is designed to be easy to copy-and-paste into your application! // Because of this: // - We never omit the ImGui:: prefix when calling functions, even though most code here is in the same namespace. @@ -43,7 +54,7 @@ // Because we can't assume anything about your support of maths operators, we cannot use them in imgui_demo.cpp. // Navigating this file: -// - In Visual Studio IDE: CTRL+comma ("Edit.GoToAll") can follow symbols in comments, whereas CTRL+F12 ("Edit.GoToImplementation") cannot. +// - In Visual Studio: CTRL+comma ("Edit.GoToAll") can follow symbols in comments, whereas CTRL+F12 ("Edit.GoToImplementation") cannot. // - With Visual Assist installed: ALT+G ("VAssistX.GoToImplementation") can also follow symbols in comments. /* @@ -74,6 +85,7 @@ Index of this file: // [SECTION] Example App: Fullscreen window / ShowExampleAppFullscreen() // [SECTION] Example App: Manipulating window titles / ShowExampleAppWindowTitles() // [SECTION] Example App: Custom Rendering using ImDrawList API / ShowExampleAppCustomRendering() +// [SECTION] Example App: Docking, DockSpace / ShowExampleAppDockSpace() // [SECTION] Example App: Documents Handling / ShowExampleAppDocuments() */ @@ -91,10 +103,9 @@ Index of this file: #include // sqrtf, powf, cosf, sinf, floorf, ceilf #include // vsnprintf, sscanf, printf #include // NULL, malloc, free, atoi -#if defined(_MSC_VER) && _MSC_VER <= 1500 // MSVC 2008 or earlier -#include // intptr_t -#else #include // intptr_t +#if !defined(_MSC_VER) || _MSC_VER >= 1800 +#include // PRId64/PRIu64, not avail in some MinGW headers. #endif // Visual Studio warnings @@ -144,14 +155,13 @@ Index of this file: #define vsnprintf _vsnprintf #endif -// Format specifiers, printing 64-bit hasn't been decently standardized... -// In a real application you should be using PRId64 and PRIu64 from (non-windows) and on Windows define them yourself. -#ifdef _MSC_VER -#define IM_PRId64 "I64d" -#define IM_PRIu64 "I64u" -#else -#define IM_PRId64 "lld" -#define IM_PRIu64 "llu" +// Format specifiers for 64-bit values (hasn't been decently standardized before VS2013) +#if !defined(PRId64) && defined(_MSC_VER) +#define PRId64 "I64d" +#define PRIu64 "I64u" +#elif !defined(PRId64) +#define PRId64 "lld" +#define PRIu64 "llu" #endif // Helpers macros @@ -162,7 +172,8 @@ Index of this file: #define IM_MAX(A, B) (((A) >= (B)) ? (A) : (B)) #define IM_CLAMP(V, MN, MX) ((V) < (MN) ? (MN) : (V) > (MX) ? (MX) : (V)) -// Enforce cdecl calling convention for functions called by the standard library, in case compilation settings changed the default to e.g. __vectorcall +// Enforce cdecl calling convention for functions called by the standard library, +// in case compilation settings changed the default to e.g. __vectorcall #ifndef IMGUI_CDECL #ifdef _MSC_VER #define IMGUI_CDECL __cdecl @@ -178,19 +189,20 @@ Index of this file: #if !defined(IMGUI_DISABLE_DEMO_WINDOWS) // Forward Declarations -static void ShowExampleAppDocuments(bool* p_open); static void ShowExampleAppMainMenuBar(); static void ShowExampleAppConsole(bool* p_open); +static void ShowExampleAppCustomRendering(bool* p_open); +static void ShowExampleAppDockSpace(bool* p_open); +static void ShowExampleAppDocuments(bool* p_open); static void ShowExampleAppLog(bool* p_open); static void ShowExampleAppLayout(bool* p_open); static void ShowExampleAppPropertyEditor(bool* p_open); -static void ShowExampleAppLongText(bool* p_open); +static void ShowExampleAppSimpleOverlay(bool* p_open); static void ShowExampleAppAutoResize(bool* p_open); static void ShowExampleAppConstrainedResize(bool* p_open); -static void ShowExampleAppSimpleOverlay(bool* p_open); static void ShowExampleAppFullscreen(bool* p_open); +static void ShowExampleAppLongText(bool* p_open); static void ShowExampleAppWindowTitles(bool* p_open); -static void ShowExampleAppCustomRendering(bool* p_open); static void ShowExampleMenuFile(); // We split the contents of the big ShowDemoWindow() function into smaller functions @@ -211,7 +223,7 @@ static void ShowDemoWindowInputs(); static void HelpMarker(const char* desc) { ImGui::TextDisabled("(?)"); - if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort) && ImGui::BeginTooltip()) + if (ImGui::BeginItemTooltip()) { ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f); ImGui::TextUnformatted(desc); @@ -220,6 +232,16 @@ static void HelpMarker(const char* desc) } } +static void ShowDockingDisabledMessage() +{ + ImGuiIO& io = ImGui::GetIO(); + ImGui::Text("ERROR: Docking is not enabled! See Demo > Configuration."); + ImGui::Text("Set io.ConfigFlags |= ImGuiConfigFlags_DockingEnable in your code, or "); + ImGui::SameLine(0.0f, 0.0f); + if (ImGui::SmallButton("click here")) + io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; +} + // Helper to wire demo markers located in code to an interactive browser typedef void (*ImGuiDemoMarkerCallback)(const char* file, int line, const char* section, void* user_data); extern ImGuiDemoMarkerCallback GImGuiDemoMarkerCallback; @@ -246,59 +268,61 @@ void* GImGuiDemoMarkerCallbackUserData = NULL; void ImGui::ShowDemoWindow(bool* p_open) { // Exceptionally add an extra assert here for people confused about initial Dear ImGui setup - // Most functions would normally just crash if the context is missing. - IM_ASSERT(ImGui::GetCurrentContext() != NULL && "Missing dear imgui context. Refer to examples app!"); + // Most functions would normally just assert/crash if the context is missing. + IM_ASSERT(ImGui::GetCurrentContext() != NULL && "Missing Dear ImGui context. Refer to examples app!"); // Examples Apps (accessible from the "Examples" menu) static bool show_app_main_menu_bar = false; - static bool show_app_documents = false; static bool show_app_console = false; + static bool show_app_custom_rendering = false; + static bool show_app_dockspace = false; + static bool show_app_documents = false; static bool show_app_log = false; static bool show_app_layout = false; static bool show_app_property_editor = false; - static bool show_app_long_text = false; + static bool show_app_simple_overlay = false; static bool show_app_auto_resize = false; static bool show_app_constrained_resize = false; - static bool show_app_simple_overlay = false; static bool show_app_fullscreen = false; + static bool show_app_long_text = false; static bool show_app_window_titles = false; - static bool show_app_custom_rendering = false; if (show_app_main_menu_bar) ShowExampleAppMainMenuBar(); - if (show_app_documents) ShowExampleAppDocuments(&show_app_documents); + if (show_app_dockspace) ShowExampleAppDockSpace(&show_app_dockspace); // Process the Docking app first, as explicit DockSpace() nodes needs to be submitted early (read comments near the DockSpace function) + if (show_app_documents) ShowExampleAppDocuments(&show_app_documents); // Process the Document app next, as it may also use a DockSpace() if (show_app_console) ShowExampleAppConsole(&show_app_console); + if (show_app_custom_rendering) ShowExampleAppCustomRendering(&show_app_custom_rendering); if (show_app_log) ShowExampleAppLog(&show_app_log); if (show_app_layout) ShowExampleAppLayout(&show_app_layout); if (show_app_property_editor) ShowExampleAppPropertyEditor(&show_app_property_editor); - if (show_app_long_text) ShowExampleAppLongText(&show_app_long_text); + if (show_app_simple_overlay) ShowExampleAppSimpleOverlay(&show_app_simple_overlay); if (show_app_auto_resize) ShowExampleAppAutoResize(&show_app_auto_resize); if (show_app_constrained_resize) ShowExampleAppConstrainedResize(&show_app_constrained_resize); - if (show_app_simple_overlay) ShowExampleAppSimpleOverlay(&show_app_simple_overlay); if (show_app_fullscreen) ShowExampleAppFullscreen(&show_app_fullscreen); + if (show_app_long_text) ShowExampleAppLongText(&show_app_long_text); if (show_app_window_titles) ShowExampleAppWindowTitles(&show_app_window_titles); - if (show_app_custom_rendering) ShowExampleAppCustomRendering(&show_app_custom_rendering); - // Dear ImGui Tools/Apps (accessible from the "Tools" menu) - static bool show_app_metrics = false; - static bool show_app_debug_log = false; - static bool show_app_stack_tool = false; - static bool show_app_about = false; - static bool show_app_style_editor = false; + // Dear ImGui Tools (accessible from the "Tools" menu) + static bool show_tool_metrics = false; + static bool show_tool_debug_log = false; + static bool show_tool_id_stack_tool = false; + static bool show_tool_style_editor = false; + static bool show_tool_about = false; - if (show_app_metrics) - ImGui::ShowMetricsWindow(&show_app_metrics); - if (show_app_debug_log) - ImGui::ShowDebugLogWindow(&show_app_debug_log); - if (show_app_stack_tool) - ImGui::ShowStackToolWindow(&show_app_stack_tool); - if (show_app_about) - ImGui::ShowAboutWindow(&show_app_about); - if (show_app_style_editor) + if (show_tool_metrics) + ImGui::ShowMetricsWindow(&show_tool_metrics); + if (show_tool_debug_log) + ImGui::ShowDebugLogWindow(&show_tool_debug_log); + if (show_tool_id_stack_tool) + ImGui::ShowIDStackToolWindow(&show_tool_id_stack_tool); + if (show_tool_style_editor) { - ImGui::Begin("Dear ImGui Style Editor", &show_app_style_editor); + ImGui::Begin("Dear ImGui Style Editor", &show_tool_style_editor); ImGui::ShowStyleEditor(); ImGui::End(); } + if (show_tool_about) + ImGui::ShowAboutWindow(&show_tool_about); // Demonstrate the various window flags. Typically you would just use the default! static bool no_titlebar = false; @@ -311,6 +335,7 @@ void ImGui::ShowDemoWindow(bool* p_open) static bool no_nav = false; static bool no_background = false; static bool no_bring_to_front = false; + static bool no_docking = false; static bool unsaved_document = false; ImGuiWindowFlags window_flags = 0; @@ -323,6 +348,7 @@ void ImGui::ShowDemoWindow(bool* p_open) if (no_nav) window_flags |= ImGuiWindowFlags_NoNav; if (no_background) window_flags |= ImGuiWindowFlags_NoBackground; if (no_bring_to_front) window_flags |= ImGuiWindowFlags_NoBringToFrontOnFocus; + if (no_docking) window_flags |= ImGuiWindowFlags_NoDocking; if (unsaved_document) window_flags |= ImGuiWindowFlags_UnsavedDocument; if (no_close) p_open = NULL; // Don't pass our bool* to Begin @@ -359,18 +385,24 @@ void ImGui::ShowDemoWindow(bool* p_open) { IMGUI_DEMO_MARKER("Menu/Examples"); ImGui::MenuItem("Main menu bar", NULL, &show_app_main_menu_bar); + + ImGui::SeparatorText("Mini apps"); ImGui::MenuItem("Console", NULL, &show_app_console); + ImGui::MenuItem("Custom rendering", NULL, &show_app_custom_rendering); + ImGui::MenuItem("Dockspace", NULL, &show_app_dockspace); + ImGui::MenuItem("Documents", NULL, &show_app_documents); ImGui::MenuItem("Log", NULL, &show_app_log); - ImGui::MenuItem("Simple layout", NULL, &show_app_layout); ImGui::MenuItem("Property editor", NULL, &show_app_property_editor); - ImGui::MenuItem("Long text display", NULL, &show_app_long_text); + ImGui::MenuItem("Simple layout", NULL, &show_app_layout); + ImGui::MenuItem("Simple overlay", NULL, &show_app_simple_overlay); + + ImGui::SeparatorText("Concepts"); ImGui::MenuItem("Auto-resizing window", NULL, &show_app_auto_resize); ImGui::MenuItem("Constrained-resizing window", NULL, &show_app_constrained_resize); - ImGui::MenuItem("Simple overlay", NULL, &show_app_simple_overlay); ImGui::MenuItem("Fullscreen window", NULL, &show_app_fullscreen); + ImGui::MenuItem("Long text display", NULL, &show_app_long_text); ImGui::MenuItem("Manipulating window titles", NULL, &show_app_window_titles); - ImGui::MenuItem("Custom rendering", NULL, &show_app_custom_rendering); - ImGui::MenuItem("Documents", NULL, &show_app_documents); + ImGui::EndMenu(); } //if (ImGui::MenuItem("MenuItem")) {} // You can also use MenuItem() inside a menu bar! @@ -382,11 +414,17 @@ void ImGui::ShowDemoWindow(bool* p_open) #else const bool has_debug_tools = false; #endif - ImGui::MenuItem("Metrics/Debugger", NULL, &show_app_metrics, has_debug_tools); - ImGui::MenuItem("Debug Log", NULL, &show_app_debug_log, has_debug_tools); - ImGui::MenuItem("Stack Tool", NULL, &show_app_stack_tool, has_debug_tools); - ImGui::MenuItem("Style Editor", NULL, &show_app_style_editor); - ImGui::MenuItem("About Dear ImGui", NULL, &show_app_about); + ImGui::MenuItem("Metrics/Debugger", NULL, &show_tool_metrics, has_debug_tools); + ImGui::MenuItem("Debug Log", NULL, &show_tool_debug_log, has_debug_tools); + ImGui::MenuItem("ID Stack Tool", NULL, &show_tool_id_stack_tool, has_debug_tools); + ImGui::MenuItem("Style Editor", NULL, &show_tool_style_editor); + bool is_debugger_present = ImGui::GetIO().ConfigDebugIsDebuggerPresent; + if (ImGui::MenuItem("Item Picker", NULL, false, has_debug_tools && is_debugger_present)) + ImGui::DebugStartItemPicker(); + if (!is_debugger_present) + ImGui::SetItemTooltip("Requires io.ConfigDebugIsDebuggerPresent=true to be set.\n\nWe otherwise disable the menu option to avoid casual users crashing the application.\n\nYou can however always access the Item Picker in Metrics->Tools."); + ImGui::Separator(); + ImGui::MenuItem("About Dear ImGui", NULL, &show_tool_about); ImGui::EndMenu(); } ImGui::EndMenuBar(); @@ -408,7 +446,7 @@ void ImGui::ShowDemoWindow(bool* p_open) ImGui::BulletText("See the ShowDemoWindow() code in imgui_demo.cpp. <- you are here!"); ImGui::BulletText("See comments in imgui.cpp."); ImGui::BulletText("See example applications in the examples/ folder."); - ImGui::BulletText("Read the FAQ at http://www.dearimgui.com/faq/"); + ImGui::BulletText("Read the FAQ at https://www.dearimgui.com/faq/"); ImGui::BulletText("Set 'io.ConfigFlags |= NavEnableKeyboard' for keyboard controls."); ImGui::BulletText("Set 'io.ConfigFlags |= NavEnableGamepad' for gamepad controls."); @@ -444,11 +482,50 @@ void ImGui::ShowDemoWindow(bool* p_open) } ImGui::CheckboxFlags("io.ConfigFlags: NoMouseCursorChange", &io.ConfigFlags, ImGuiConfigFlags_NoMouseCursorChange); ImGui::SameLine(); HelpMarker("Instruct backend to not alter mouse cursor shape and visibility."); + ImGui::Checkbox("io.ConfigInputTrickleEventQueue", &io.ConfigInputTrickleEventQueue); ImGui::SameLine(); HelpMarker("Enable input queue trickling: some types of events submitted during the same frame (e.g. button down + up) will be spread over multiple frames, improving interactions with low framerates."); ImGui::Checkbox("io.MouseDrawCursor", &io.MouseDrawCursor); ImGui::SameLine(); HelpMarker("Instruct Dear ImGui to render a mouse cursor itself. Note that a mouse cursor rendered via your application GPU rendering path will feel more laggy than hardware cursor, but will be more in sync with your other visuals.\n\nSome desktop applications may use both kinds of cursors (e.g. enable software cursor only when resizing/dragging something)."); + ImGui::SeparatorText("Docking"); + ImGui::CheckboxFlags("io.ConfigFlags: DockingEnable", &io.ConfigFlags, ImGuiConfigFlags_DockingEnable); + ImGui::SameLine(); + if (io.ConfigDockingWithShift) + HelpMarker("Drag from window title bar or their tab to dock/undock. Hold SHIFT to enable docking.\n\nDrag from window menu button (upper-left button) to undock an entire node (all windows)."); + else + HelpMarker("Drag from window title bar or their tab to dock/undock. Hold SHIFT to disable docking.\n\nDrag from window menu button (upper-left button) to undock an entire node (all windows)."); + if (io.ConfigFlags & ImGuiConfigFlags_DockingEnable) + { + ImGui::Indent(); + ImGui::Checkbox("io.ConfigDockingNoSplit", &io.ConfigDockingNoSplit); + ImGui::SameLine(); HelpMarker("Simplified docking mode: disable window splitting, so docking is limited to merging multiple windows together into tab-bars."); + ImGui::Checkbox("io.ConfigDockingWithShift", &io.ConfigDockingWithShift); + ImGui::SameLine(); HelpMarker("Enable docking when holding Shift only (allow to drop in wider space, reduce visual noise)"); + ImGui::Checkbox("io.ConfigDockingAlwaysTabBar", &io.ConfigDockingAlwaysTabBar); + ImGui::SameLine(); HelpMarker("Create a docking node and tab-bar on single floating windows."); + ImGui::Checkbox("io.ConfigDockingTransparentPayload", &io.ConfigDockingTransparentPayload); + ImGui::SameLine(); HelpMarker("Make window or viewport transparent when docking and only display docking boxes on the target viewport. Useful if rendering of multiple viewport cannot be synced. Best used with ConfigViewportsNoAutoMerge."); + ImGui::Unindent(); + } + + ImGui::SeparatorText("Multi-viewports"); + ImGui::CheckboxFlags("io.ConfigFlags: ViewportsEnable", &io.ConfigFlags, ImGuiConfigFlags_ViewportsEnable); + ImGui::SameLine(); HelpMarker("[beta] Enable beta multi-viewports support. See ImGuiPlatformIO for details."); + if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) + { + ImGui::Indent(); + ImGui::Checkbox("io.ConfigViewportsNoAutoMerge", &io.ConfigViewportsNoAutoMerge); + ImGui::SameLine(); HelpMarker("Set to make all floating imgui windows always create their own viewport. Otherwise, they are merged into the main host viewports when overlapping it."); + ImGui::Checkbox("io.ConfigViewportsNoTaskBarIcon", &io.ConfigViewportsNoTaskBarIcon); + ImGui::SameLine(); HelpMarker("Toggling this at runtime is normally unsupported (most platform backends won't refresh the task bar icon state right away)."); + ImGui::Checkbox("io.ConfigViewportsNoDecoration", &io.ConfigViewportsNoDecoration); + ImGui::SameLine(); HelpMarker("Toggling this at runtime is normally unsupported (most platform backends won't refresh the decoration right away)."); + ImGui::Checkbox("io.ConfigViewportsNoDefaultParent", &io.ConfigViewportsNoDefaultParent); + ImGui::SameLine(); HelpMarker("Toggling this at runtime is normally unsupported (most platform backends won't refresh the parenting right away)."); + ImGui::Unindent(); + } + ImGui::SeparatorText("Widgets"); ImGui::Checkbox("io.ConfigInputTextCursorBlink", &io.ConfigInputTextCursorBlink); ImGui::SameLine(); HelpMarker("Enable blinking cursor (optional as some users consider it to be distracting)."); @@ -463,14 +540,18 @@ void ImGui::ShowDemoWindow(bool* p_open) ImGui::Text("Also see Style->Rendering for rendering options."); ImGui::SeparatorText("Debug"); + ImGui::Checkbox("io.ConfigDebugIsDebuggerPresent", &io.ConfigDebugIsDebuggerPresent); + ImGui::SameLine(); HelpMarker("Enable various tools calling IM_DEBUG_BREAK().\n\nRequires a debugger being attached, otherwise IM_DEBUG_BREAK() options will appear to crash your application."); ImGui::BeginDisabled(); ImGui::Checkbox("io.ConfigDebugBeginReturnValueOnce", &io.ConfigDebugBeginReturnValueOnce); // . ImGui::EndDisabled(); - ImGui::SameLine(); HelpMarker("First calls to Begin()/BeginChild() will return false.\n\nTHIS OPTION IS DISABLED because it needs to be set at application boot-time to make sense. Showing the disabled option is a way to make this feature easier to discover"); + ImGui::SameLine(); HelpMarker("First calls to Begin()/BeginChild() will return false.\n\nTHIS OPTION IS DISABLED because it needs to be set at application boot-time to make sense. Showing the disabled option is a way to make this feature easier to discover."); ImGui::Checkbox("io.ConfigDebugBeginReturnValueLoop", &io.ConfigDebugBeginReturnValueLoop); ImGui::SameLine(); HelpMarker("Some calls to Begin()/BeginChild() will return false.\n\nWill cycle through window depths then repeat. Windows should be flickering while running."); ImGui::Checkbox("io.ConfigDebugIgnoreFocusLoss", &io.ConfigDebugIgnoreFocusLoss); ImGui::SameLine(); HelpMarker("Option to deactivate io.AddFocusEvent(false) handling. May facilitate interactions with a debugger when focus loss leads to clearing inputs data."); + ImGui::Checkbox("io.ConfigDebugIniSettings", &io.ConfigDebugIniSettings); + ImGui::SameLine(); HelpMarker("Option to save .ini data with extra comments (particularly helpful for Docking, but makes saving slower)."); ImGui::TreePop(); ImGui::Spacing(); @@ -483,12 +564,16 @@ void ImGui::ShowDemoWindow(bool* p_open) "Those flags are set by the backends (imgui_impl_xxx files) to specify their capabilities.\n" "Here we expose them as read-only fields to avoid breaking interactions with your backend."); + // Make a local copy to avoid modifying actual backend flags. // FIXME: Maybe we need a BeginReadonly() equivalent to keep label bright? ImGui::BeginDisabled(); - ImGui::CheckboxFlags("io.BackendFlags: HasGamepad", &io.BackendFlags, ImGuiBackendFlags_HasGamepad); - ImGui::CheckboxFlags("io.BackendFlags: HasMouseCursors", &io.BackendFlags, ImGuiBackendFlags_HasMouseCursors); - ImGui::CheckboxFlags("io.BackendFlags: HasSetMousePos", &io.BackendFlags, ImGuiBackendFlags_HasSetMousePos); - ImGui::CheckboxFlags("io.BackendFlags: RendererHasVtxOffset", &io.BackendFlags, ImGuiBackendFlags_RendererHasVtxOffset); + ImGui::CheckboxFlags("io.BackendFlags: HasGamepad", &io.BackendFlags, ImGuiBackendFlags_HasGamepad); + ImGui::CheckboxFlags("io.BackendFlags: HasMouseCursors", &io.BackendFlags, ImGuiBackendFlags_HasMouseCursors); + ImGui::CheckboxFlags("io.BackendFlags: HasSetMousePos", &io.BackendFlags, ImGuiBackendFlags_HasSetMousePos); + ImGui::CheckboxFlags("io.BackendFlags: PlatformHasViewports", &io.BackendFlags, ImGuiBackendFlags_PlatformHasViewports); + ImGui::CheckboxFlags("io.BackendFlags: HasMouseHoveredViewport",&io.BackendFlags, ImGuiBackendFlags_HasMouseHoveredViewport); + ImGui::CheckboxFlags("io.BackendFlags: RendererHasVtxOffset", &io.BackendFlags, ImGuiBackendFlags_RendererHasVtxOffset); + ImGui::CheckboxFlags("io.BackendFlags: RendererHasViewports", &io.BackendFlags, ImGuiBackendFlags_RendererHasViewports); ImGui::EndDisabled(); ImGui::TreePop(); ImGui::Spacing(); @@ -538,6 +623,7 @@ void ImGui::ShowDemoWindow(bool* p_open) ImGui::TableNextColumn(); ImGui::Checkbox("No nav", &no_nav); ImGui::TableNextColumn(); ImGui::Checkbox("No background", &no_background); ImGui::TableNextColumn(); ImGui::Checkbox("No bring to front", &no_bring_to_front); + ImGui::TableNextColumn(); ImGui::Checkbox("No docking", &no_docking); ImGui::TableNextColumn(); ImGui::Checkbox("Unsaved document", &unsaved_document); ImGui::EndTable(); } @@ -624,37 +710,8 @@ static void ShowDemoWindowWidgets() ImGui::SameLine(); ImGui::Text("%d", counter); - { - // Tooltips - IMGUI_DEMO_MARKER("Widgets/Basic/Tooltips"); - //ImGui::AlignTextToFramePadding(); - ImGui::Text("Tooltips:"); - - ImGui::SameLine(); - ImGui::SmallButton("Basic"); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("I am a tooltip"); - - ImGui::SameLine(); - ImGui::SmallButton("Fancy"); - if (ImGui::IsItemHovered() && ImGui::BeginTooltip()) - { - ImGui::Text("I am a fancy tooltip"); - static float arr[] = { 0.6f, 0.1f, 1.0f, 0.5f, 0.92f, 0.1f, 0.2f }; - ImGui::PlotLines("Curve", arr, IM_ARRAYSIZE(arr)); - ImGui::Text("Sin(time) = %f", sinf((float)ImGui::GetTime())); - ImGui::EndTooltip(); - } - - ImGui::SameLine(); - ImGui::SmallButton("Delayed"); - if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayNormal)) // With a delay - ImGui::SetTooltip("I am a tooltip with a delay."); - - ImGui::SameLine(); - HelpMarker( - "Tooltip are created by using the IsItemHovered() function over any kind of item."); - } + ImGui::Button("Tooltip"); + ImGui::SetItemTooltip("I am a tooltip"); ImGui::LabelText("label", "Value"); @@ -772,7 +829,8 @@ static void ShowDemoWindowWidgets() static int item_current = 0; ImGui::Combo("combo", &item_current, items, IM_ARRAYSIZE(items)); ImGui::SameLine(); HelpMarker( - "Using the simplified one-liner Combo API here.\nRefer to the \"Combo\" section below for an explanation of how to use the more flexible and general BeginCombo/EndCombo API."); + "Using the simplified one-liner Combo API here.\n" + "Refer to the \"Combo\" section below for an explanation of how to use the more flexible and general BeginCombo/EndCombo API."); } { @@ -783,22 +841,116 @@ static void ShowDemoWindowWidgets() static int item_current = 1; ImGui::ListBox("listbox", &item_current, items, IM_ARRAYSIZE(items), 4); ImGui::SameLine(); HelpMarker( - "Using the simplified one-liner ListBox API here.\nRefer to the \"List boxes\" section below for an explanation of how to use the more flexible and general BeginListBox/EndListBox API."); + "Using the simplified one-liner ListBox API here.\n" + "Refer to the \"List boxes\" section below for an explanation of how to use the more flexible and general BeginListBox/EndListBox API."); } ImGui::TreePop(); } + IMGUI_DEMO_MARKER("Widgets/Tooltips"); + if (ImGui::TreeNode("Tooltips")) + { + // Tooltips are windows following the mouse. They do not take focus away. + ImGui::SeparatorText("General"); + + // Typical use cases: + // - Short-form (text only): SetItemTooltip("Hello"); + // - Short-form (any contents): if (BeginItemTooltip()) { Text("Hello"); EndTooltip(); } + + // - Full-form (text only): if (IsItemHovered(...)) { SetTooltip("Hello"); } + // - Full-form (any contents): if (IsItemHovered(...) && BeginTooltip()) { Text("Hello"); EndTooltip(); } + + HelpMarker( + "Tooltip are typically created by using a IsItemHovered() + SetTooltip() sequence.\n\n" + "We provide a helper SetItemTooltip() function to perform the two with standards flags."); + + ImVec2 sz = ImVec2(-FLT_MIN, 0.0f); + + ImGui::Button("Basic", sz); + ImGui::SetItemTooltip("I am a tooltip"); + + ImGui::Button("Fancy", sz); + if (ImGui::BeginItemTooltip()) + { + ImGui::Text("I am a fancy tooltip"); + static float arr[] = { 0.6f, 0.1f, 1.0f, 0.5f, 0.92f, 0.1f, 0.2f }; + ImGui::PlotLines("Curve", arr, IM_ARRAYSIZE(arr)); + ImGui::Text("Sin(time) = %f", sinf((float)ImGui::GetTime())); + ImGui::EndTooltip(); + } + + ImGui::SeparatorText("Always On"); + + // Showcase NOT relying on a IsItemHovered() to emit a tooltip. + // Here the tooltip is always emitted when 'always_on == true'. + static int always_on = 0; + ImGui::RadioButton("Off", &always_on, 0); + ImGui::SameLine(); + ImGui::RadioButton("Always On (Simple)", &always_on, 1); + ImGui::SameLine(); + ImGui::RadioButton("Always On (Advanced)", &always_on, 2); + if (always_on == 1) + ImGui::SetTooltip("I am following you around."); + else if (always_on == 2 && ImGui::BeginTooltip()) + { + ImGui::ProgressBar(sinf((float)ImGui::GetTime()) * 0.5f + 0.5f, ImVec2(ImGui::GetFontSize() * 25, 0.0f)); + ImGui::EndTooltip(); + } + + ImGui::SeparatorText("Custom"); + + HelpMarker( + "Passing ImGuiHoveredFlags_ForTooltip to IsItemHovered() is the preferred way to standardize" + "tooltip activation details across your application. You may however decide to use custom" + "flags for a specific tooltip instance."); + + // The following examples are passed for documentation purpose but may not be useful to most users. + // Passing ImGuiHoveredFlags_ForTooltip to IsItemHovered() will pull ImGuiHoveredFlags flags values from + // 'style.HoverFlagsForTooltipMouse' or 'style.HoverFlagsForTooltipNav' depending on whether mouse or gamepad/keyboard is being used. + // With default settings, ImGuiHoveredFlags_ForTooltip is equivalent to ImGuiHoveredFlags_DelayShort + ImGuiHoveredFlags_Stationary. + ImGui::Button("Manual", sz); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_ForTooltip)) + ImGui::SetTooltip("I am a manually emitted tooltip."); + + ImGui::Button("DelayNone", sz); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayNone)) + ImGui::SetTooltip("I am a tooltip with no delay."); + + ImGui::Button("DelayShort", sz); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort | ImGuiHoveredFlags_NoSharedDelay)) + ImGui::SetTooltip("I am a tooltip with a short delay (%0.2f sec).", ImGui::GetStyle().HoverDelayShort); + + ImGui::Button("DelayLong", sz); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayNormal | ImGuiHoveredFlags_NoSharedDelay)) + ImGui::SetTooltip("I am a tooltip with a long delay (%0.2f sec).", ImGui::GetStyle().HoverDelayNormal); + + ImGui::Button("Stationary", sz); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_Stationary)) + ImGui::SetTooltip("I am a tooltip requiring mouse to be stationary before activating."); + + // Using ImGuiHoveredFlags_ForTooltip will pull flags from 'style.HoverFlagsForTooltipMouse' or 'style.HoverFlagsForTooltipNav', + // which default value include the ImGuiHoveredFlags_AllowWhenDisabled flag. + // As a result, Set + ImGui::BeginDisabled(); + ImGui::Button("Disabled item", sz); + ImGui::EndDisabled(); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_ForTooltip)) + ImGui::SetTooltip("I am a a tooltip for a disabled item."); + + ImGui::TreePop(); + } + // Testing ImGuiOnceUponAFrame helper. //static ImGuiOnceUponAFrame once; //for (int i = 0; i < 5; i++) // if (once) // ImGui::Text("This will be displayed only once."); - IMGUI_DEMO_MARKER("Widgets/Trees"); - if (ImGui::TreeNode("Trees")) + IMGUI_DEMO_MARKER("Widgets/Tree Nodes"); + if (ImGui::TreeNode("Tree Nodes")) { - IMGUI_DEMO_MARKER("Widgets/Trees/Basic trees"); + IMGUI_DEMO_MARKER("Widgets/Tree Nodes/Basic trees"); if (ImGui::TreeNode("Basic trees")) { for (int i = 0; i < 5; i++) @@ -819,7 +971,7 @@ static void ShowDemoWindowWidgets() ImGui::TreePop(); } - IMGUI_DEMO_MARKER("Widgets/Trees/Advanced, with Selectable nodes"); + IMGUI_DEMO_MARKER("Widgets/Tree Nodes/Advanced, with Selectable nodes"); if (ImGui::TreeNode("Advanced, with Selectable nodes")) { HelpMarker( @@ -832,6 +984,7 @@ static void ShowDemoWindowWidgets() ImGui::CheckboxFlags("ImGuiTreeNodeFlags_OpenOnDoubleClick", &base_flags, ImGuiTreeNodeFlags_OpenOnDoubleClick); ImGui::CheckboxFlags("ImGuiTreeNodeFlags_SpanAvailWidth", &base_flags, ImGuiTreeNodeFlags_SpanAvailWidth); ImGui::SameLine(); HelpMarker("Extend hit area to all available width instead of allowing more items to be laid out after the node."); ImGui::CheckboxFlags("ImGuiTreeNodeFlags_SpanFullWidth", &base_flags, ImGuiTreeNodeFlags_SpanFullWidth); + ImGui::CheckboxFlags("ImGuiTreeNodeFlags_SpanAllColumns", &base_flags, ImGuiTreeNodeFlags_SpanAllColumns); ImGui::SameLine(); HelpMarker("For use in Tables only."); ImGui::Checkbox("Align label with current X position", &align_label_with_current_x_position); ImGui::Checkbox("Test tree node as drag source", &test_drag_and_drop); ImGui::Text("Hello!"); @@ -1007,7 +1160,7 @@ static void ShowDemoWindowWidgets() "CJK text will only appear if the font was loaded with the appropriate CJK character ranges. " "Call io.Fonts->AddFontFromFileTTF() manually to load extra character ranges. " "Read docs/FONTS.md for details."); - ImGui::Text("Hiragana: \xe3\x81\x8b\xe3\x81\x8d\xe3\x81\x8f\xe3\x81\x91\xe3\x81\x93 (kakikukeko)"); // Normally we would use u8"blah blah" with the proper characters directly in the string. + ImGui::Text("Hiragana: \xe3\x81\x8b\xe3\x81\x8d\xe3\x81\x8f\xe3\x81\x91\xe3\x81\x93 (kakikukeko)"); ImGui::Text("Kanjis: \xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e (nihongo)"); static char buf[32] = "\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e"; //static char buf[32] = u8"NIHONGO"; // <- this is how you would write it with C++11, using real kanjis @@ -1051,10 +1204,10 @@ static void ShowDemoWindowWidgets() ImVec2 pos = ImGui::GetCursorScreenPos(); ImVec2 uv_min = ImVec2(0.0f, 0.0f); // Top-left ImVec2 uv_max = ImVec2(1.0f, 1.0f); // Lower-right - ImVec4 tint_col = use_text_color_for_tint ? ImGui::GetStyleColorVec4(ImGuiCol_Text) : ImVec4(1.0f, 1.0f, 1.0f, 1.0f); // No tint + ImVec4 tint_col = use_text_color_for_tint ? ImGui::GetStyleColorVec4(ImGuiCol_Text) : ImVec4(1.0f, 1.0f, 1.0f, 1.0f); // No tint ImVec4 border_col = ImGui::GetStyleColorVec4(ImGuiCol_Border); ImGui::Image(my_tex_id, ImVec2(my_tex_w, my_tex_h), uv_min, uv_max, tint_col, border_col); - if (ImGui::IsItemHovered() && ImGui::BeginTooltip()) + if (ImGui::BeginItemTooltip()) { float region_sz = 32.0f; float region_x = io.MousePos.x - pos.x - region_sz * 0.5f; @@ -1110,16 +1263,29 @@ static void ShowDemoWindowWidgets() ImGui::CheckboxFlags("ImGuiComboFlags_PopupAlignLeft", &flags, ImGuiComboFlags_PopupAlignLeft); ImGui::SameLine(); HelpMarker("Only makes a difference if the popup is larger than the combo"); if (ImGui::CheckboxFlags("ImGuiComboFlags_NoArrowButton", &flags, ImGuiComboFlags_NoArrowButton)) - flags &= ~ImGuiComboFlags_NoPreview; // Clear the other flag, as we cannot combine both + flags &= ~ImGuiComboFlags_NoPreview; // Clear incompatible flags if (ImGui::CheckboxFlags("ImGuiComboFlags_NoPreview", &flags, ImGuiComboFlags_NoPreview)) - flags &= ~ImGuiComboFlags_NoArrowButton; // Clear the other flag, as we cannot combine both + flags &= ~(ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_WidthFitPreview); // Clear incompatible flags + if (ImGui::CheckboxFlags("ImGuiComboFlags_WidthFitPreview", &flags, ImGuiComboFlags_WidthFitPreview)) + flags &= ~ImGuiComboFlags_NoPreview; + + // Override default popup height + if (ImGui::CheckboxFlags("ImGuiComboFlags_HeightSmall", &flags, ImGuiComboFlags_HeightSmall)) + flags &= ~(ImGuiComboFlags_HeightMask_ & ~ImGuiComboFlags_HeightSmall); + if (ImGui::CheckboxFlags("ImGuiComboFlags_HeightRegular", &flags, ImGuiComboFlags_HeightRegular)) + flags &= ~(ImGuiComboFlags_HeightMask_ & ~ImGuiComboFlags_HeightRegular); + if (ImGui::CheckboxFlags("ImGuiComboFlags_HeightLargest", &flags, ImGuiComboFlags_HeightLargest)) + flags &= ~(ImGuiComboFlags_HeightMask_ & ~ImGuiComboFlags_HeightLargest); // Using the generic BeginCombo() API, you have full control over how to display the combo contents. // (your selection data could be an index, a pointer to the object, an id for the object, a flag intrusively // stored in the object itself, etc.) const char* items[] = { "AAAA", "BBBB", "CCCC", "DDDD", "EEEE", "FFFF", "GGGG", "HHHH", "IIII", "JJJJ", "KKKK", "LLLLLLL", "MMMM", "OOOOOOO" }; static int item_current_idx = 0; // Here we store our selection data as an index. - const char* combo_preview_value = items[item_current_idx]; // Pass in the preview value visible before opening the combo (it could be anything) + + // Pass in the preview value visible before opening the combo (it could technically be different contents or not pulled from items[]) + const char* combo_preview_value = items[item_current_idx]; + if (ImGui::BeginCombo("combo 1", combo_preview_value, flags)) { for (int n = 0; n < IM_ARRAYSIZE(items); n++) @@ -1135,6 +1301,10 @@ static void ShowDemoWindowWidgets() ImGui::EndCombo(); } + ImGui::Spacing(); + ImGui::SeparatorText("One-liner variants"); + HelpMarker("Flags above don't apply to this section."); + // Simplified one-liner Combo() API, using values packed in a single constant string // This is a convenience for when the selection set is small and known at compile-time. static int item_current_2 = 0; @@ -1146,9 +1316,8 @@ static void ShowDemoWindowWidgets() ImGui::Combo("combo 3 (array)", &item_current_3, items, IM_ARRAYSIZE(items)); // Simplified one-liner Combo() using an accessor function - struct Funcs { static bool ItemGetter(void* data, int n, const char** out_str) { *out_str = ((const char**)data)[n]; return true; } }; static int item_current_4 = 0; - ImGui::Combo("combo 4 (function)", &item_current_4, &Funcs::ItemGetter, items, IM_ARRAYSIZE(items)); + ImGui::Combo("combo 4 (function)", &item_current_4, [](void* data, int n) { return ((const char**)data)[n]; }, items, IM_ARRAYSIZE(items)); ImGui::TreePop(); } @@ -1156,6 +1325,11 @@ static void ShowDemoWindowWidgets() IMGUI_DEMO_MARKER("Widgets/List Boxes"); if (ImGui::TreeNode("List boxes")) { + // BeginListBox() is essentially a thin wrapper to using BeginChild()/EndChild() + // using the ImGuiChildFlags_FrameStyle flag for stylistic changes + displaying a label. + // You may be tempted to simply use BeginChild() directly. However note that BeginChild() requires EndChild() + // to always be called (inconsistent with BeginListBox()/EndListBox()). + // Using the generic BeginListBox() API, you have full control over how to display the combo contents. // (your selection data could be an index, a pointer to the object, an id for the object, a flag intrusively // stored in the object itself, etc.) @@ -1208,16 +1382,16 @@ static void ShowDemoWindowWidgets() IMGUI_DEMO_MARKER("Widgets/Selectables/Basic"); if (ImGui::TreeNode("Basic")) { - static bool selection[5] = { false, true, false, false, false }; + static bool selection[5] = { false, true, false, false }; ImGui::Selectable("1. I am selectable", &selection[0]); ImGui::Selectable("2. I am selectable", &selection[1]); - ImGui::Text("(I am not selectable)"); - ImGui::Selectable("4. I am selectable", &selection[3]); - if (ImGui::Selectable("5. I am double clickable", selection[4], ImGuiSelectableFlags_AllowDoubleClick)) + ImGui::Selectable("3. I am selectable", &selection[2]); + if (ImGui::Selectable("4. I am double clickable", selection[3], ImGuiSelectableFlags_AllowDoubleClick)) if (ImGui::IsMouseDoubleClicked(0)) - selection[4] = !selection[4]; + selection[3] = !selection[3]; ImGui::TreePop(); } + IMGUI_DEMO_MARKER("Widgets/Selectables/Single Selection"); if (ImGui::TreeNode("Selection State: Single Selection")) { @@ -1249,17 +1423,18 @@ static void ShowDemoWindowWidgets() } ImGui::TreePop(); } - IMGUI_DEMO_MARKER("Widgets/Selectables/Rendering more text into the same line"); - if (ImGui::TreeNode("Rendering more text into the same line")) + IMGUI_DEMO_MARKER("Widgets/Selectables/Rendering more items on the same line"); + if (ImGui::TreeNode("Rendering more items on the same line")) { - // Using the Selectable() override that takes "bool* p_selected" parameter, - // this function toggle your bool value automatically. + // (1) Using SetNextItemAllowOverlap() + // (2) Using the Selectable() override that takes "bool* p_selected" parameter, the bool value is toggled automatically. static bool selected[3] = { false, false, false }; - ImGui::Selectable("main.c", &selected[0]); ImGui::SameLine(300); ImGui::Text(" 2,345 bytes"); - ImGui::Selectable("Hello.cpp", &selected[1]); ImGui::SameLine(300); ImGui::Text("12,345 bytes"); - ImGui::Selectable("Hello.h", &selected[2]); ImGui::SameLine(300); ImGui::Text(" 2,345 bytes"); + ImGui::SetNextItemAllowOverlap(); ImGui::Selectable("main.c", &selected[0]); ImGui::SameLine(); ImGui::SmallButton("Link 1"); + ImGui::SetNextItemAllowOverlap(); ImGui::Selectable("Hello.cpp", &selected[1]); ImGui::SameLine(); ImGui::SmallButton("Link 2"); + ImGui::SetNextItemAllowOverlap(); ImGui::Selectable("Hello.h", &selected[2]); ImGui::SameLine(); ImGui::SmallButton("Link 3"); ImGui::TreePop(); } + IMGUI_DEMO_MARKER("Widgets/Selectables/In columns"); if (ImGui::TreeNode("In columns")) { @@ -1295,6 +1470,7 @@ static void ShowDemoWindowWidgets() } ImGui::TreePop(); } + IMGUI_DEMO_MARKER("Widgets/Selectables/Grid"); if (ImGui::TreeNode("Grid")) { @@ -1380,6 +1556,7 @@ static void ShowDemoWindowWidgets() HelpMarker("You can use the ImGuiInputTextFlags_CallbackResize facility if you need to wire InputTextMultiline() to a dynamic string type. See misc/cpp/imgui_stdlib.h for an example. (This is not demonstrated in imgui_demo.cpp because we don't want to include in here)"); ImGui::CheckboxFlags("ImGuiInputTextFlags_ReadOnly", &flags, ImGuiInputTextFlags_ReadOnly); ImGui::CheckboxFlags("ImGuiInputTextFlags_AllowTabInput", &flags, ImGuiInputTextFlags_AllowTabInput); + ImGui::SameLine(); HelpMarker("When _AllowTabInput is set, passing through the widget with Tabbing doesn't automatically activate it, in order to also cycling through subsequent widgets."); ImGui::CheckboxFlags("ImGuiInputTextFlags_CtrlEnterForNewLine", &flags, ImGuiInputTextFlags_CtrlEnterForNewLine); ImGui::InputTextMultiline("##source", text, IM_ARRAYSIZE(text), ImVec2(-FLT_MIN, ImGui::GetTextLineHeight() * 16), flags); ImGui::TreePop(); @@ -1393,8 +1570,8 @@ static void ShowDemoWindowWidgets() // Modify character input by altering 'data->Eventchar' (ImGuiInputTextFlags_CallbackCharFilter callback) static int FilterCasingSwap(ImGuiInputTextCallbackData* data) { - if (data->EventChar >= 'a' && data->EventChar <= 'z') { data->EventChar = data->EventChar - 'A' - 'a'; } // Lowercase becomes uppercase - else if (data->EventChar >= 'A' && data->EventChar <= 'Z') { data->EventChar = data->EventChar + 'a' - 'A'; } // Uppercase becomes lowercase + if (data->EventChar >= 'a' && data->EventChar <= 'z') { data->EventChar -= 'a' - 'A'; } // Lowercase becomes uppercase + else if (data->EventChar >= 'A' && data->EventChar <= 'Z') { data->EventChar += 'a' - 'A'; } // Uppercase becomes lowercase return 0; } @@ -1428,6 +1605,7 @@ static void ShowDemoWindowWidgets() ImGui::TreePop(); } + IMGUI_DEMO_MARKER("Widgets/Text Input/Completion, History, Edit Callbacks"); if (ImGui::TreeNode("Completion, History, Edit Callbacks")) { struct Funcs @@ -1469,16 +1647,21 @@ static void ShowDemoWindowWidgets() }; static char buf1[64]; ImGui::InputText("Completion", buf1, 64, ImGuiInputTextFlags_CallbackCompletion, Funcs::MyCallback); - ImGui::SameLine(); HelpMarker("Here we append \"..\" each time Tab is pressed. See 'Examples>Console' for a more meaningful demonstration of using this callback."); + ImGui::SameLine(); HelpMarker( + "Here we append \"..\" each time Tab is pressed. " + "See 'Examples>Console' for a more meaningful demonstration of using this callback."); static char buf2[64]; ImGui::InputText("History", buf2, 64, ImGuiInputTextFlags_CallbackHistory, Funcs::MyCallback); - ImGui::SameLine(); HelpMarker("Here we replace and select text each time Up/Down are pressed. See 'Examples>Console' for a more meaningful demonstration of using this callback."); + ImGui::SameLine(); HelpMarker( + "Here we replace and select text each time Up/Down are pressed. " + "See 'Examples>Console' for a more meaningful demonstration of using this callback."); static char buf3[64]; static int edit_count = 0; ImGui::InputText("Edit", buf3, 64, ImGuiInputTextFlags_CallbackEdit, Funcs::MyCallback, (void*)&edit_count); - ImGui::SameLine(); HelpMarker("Here we toggle the casing of the first character on every edit + count edits."); + ImGui::SameLine(); HelpMarker( + "Here we toggle the casing of the first character on every edit + count edits."); ImGui::SameLine(); ImGui::Text("(%d)", edit_count); ImGui::TreePop(); @@ -1527,6 +1710,18 @@ static void ShowDemoWindowWidgets() ImGui::TreePop(); } + IMGUI_DEMO_MARKER("Widgets/Text Input/Miscellaneous"); + if (ImGui::TreeNode("Miscellaneous")) + { + static char buf1[16]; + static ImGuiInputTextFlags flags = ImGuiInputTextFlags_EscapeClearsAll; + ImGui::CheckboxFlags("ImGuiInputTextFlags_EscapeClearsAll", &flags, ImGuiInputTextFlags_EscapeClearsAll); + ImGui::CheckboxFlags("ImGuiInputTextFlags_ReadOnly", &flags, ImGuiInputTextFlags_ReadOnly); + ImGui::CheckboxFlags("ImGuiInputTextFlags_NoUndoRedo", &flags, ImGuiInputTextFlags_NoUndoRedo); + ImGui::InputText("Hello", buf1, IM_ARRAYSIZE(buf1), flags); + ImGui::TreePop(); + } + ImGui::TreePop(); } @@ -1641,8 +1836,9 @@ static void ShowDemoWindowWidgets() ImGui::EndPopup(); } - // Demo Trailing Tabs: click the "+" button to add a new tab (in your app you may want to use a font icon instead of the "+") - // Note that we submit it before the regular tabs, but because of the ImGuiTabItemFlags_Trailing flag it will always appear at the end. + // Demo Trailing Tabs: click the "+" button to add a new tab. + // (In your app you may want to use a font icon instead of the "+") + // We submit it before the regular tabs, but thanks to the ImGuiTabItemFlags_Trailing flag it will always appear at the end. if (show_trailing_button) if (ImGui::TabItemButton("+", ImGuiTabItemFlags_Trailing | ImGuiTabItemFlags_NoTooltip)) active_tabs.push_back(next_tab_id++); // Add new tab @@ -1926,7 +2122,8 @@ static void ShowDemoWindowWidgets() if (ImGui::Button("Default: Float + HDR + Hue Wheel")) ImGui::SetColorEditOptions(ImGuiColorEditFlags_Float | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_PickerHueWheel); - // Always both a small version of both types of pickers (to make it more visible in the demo to people who are skimming quickly through it) + // Always display a small version of both types of pickers + // (that's in order to make it more visible in the demo to people who are skimming quickly through it) ImGui::Text("Both types:"); float w = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.y) * 0.40f; ImGui::SetNextItemWidth(w); @@ -2079,12 +2276,12 @@ static void ShowDemoWindowWidgets() ImGui::SliderScalar("slider u32 low", ImGuiDataType_U32, &u32_v, &u32_zero, &u32_fifty,"%u"); ImGui::SliderScalar("slider u32 high", ImGuiDataType_U32, &u32_v, &u32_hi_a, &u32_hi_b, "%u"); ImGui::SliderScalar("slider u32 full", ImGuiDataType_U32, &u32_v, &u32_min, &u32_max, "%u"); - ImGui::SliderScalar("slider s64 low", ImGuiDataType_S64, &s64_v, &s64_zero, &s64_fifty,"%" IM_PRId64); - ImGui::SliderScalar("slider s64 high", ImGuiDataType_S64, &s64_v, &s64_hi_a, &s64_hi_b, "%" IM_PRId64); - ImGui::SliderScalar("slider s64 full", ImGuiDataType_S64, &s64_v, &s64_min, &s64_max, "%" IM_PRId64); - ImGui::SliderScalar("slider u64 low", ImGuiDataType_U64, &u64_v, &u64_zero, &u64_fifty,"%" IM_PRIu64 " ms"); - ImGui::SliderScalar("slider u64 high", ImGuiDataType_U64, &u64_v, &u64_hi_a, &u64_hi_b, "%" IM_PRIu64 " ms"); - ImGui::SliderScalar("slider u64 full", ImGuiDataType_U64, &u64_v, &u64_min, &u64_max, "%" IM_PRIu64 " ms"); + ImGui::SliderScalar("slider s64 low", ImGuiDataType_S64, &s64_v, &s64_zero, &s64_fifty,"%" PRId64); + ImGui::SliderScalar("slider s64 high", ImGuiDataType_S64, &s64_v, &s64_hi_a, &s64_hi_b, "%" PRId64); + ImGui::SliderScalar("slider s64 full", ImGuiDataType_S64, &s64_v, &s64_min, &s64_max, "%" PRId64); + ImGui::SliderScalar("slider u64 low", ImGuiDataType_U64, &u64_v, &u64_zero, &u64_fifty,"%" PRIu64 " ms"); + ImGui::SliderScalar("slider u64 high", ImGuiDataType_U64, &u64_v, &u64_hi_a, &u64_hi_b, "%" PRIu64 " ms"); + ImGui::SliderScalar("slider u64 full", ImGuiDataType_U64, &u64_v, &u64_min, &u64_max, "%" PRIu64 " ms"); ImGui::SliderScalar("slider float low", ImGuiDataType_Float, &f32_v, &f32_zero, &f32_one); ImGui::SliderScalar("slider float low log", ImGuiDataType_Float, &f32_v, &f32_zero, &f32_one, "%.10f", ImGuiSliderFlags_Logarithmic); ImGui::SliderScalar("slider float high", ImGuiDataType_Float, &f32_v, &f32_lo_a, &f32_hi_a, "%e"); @@ -2097,8 +2294,8 @@ static void ShowDemoWindowWidgets() ImGui::SliderScalar("slider u8 reverse", ImGuiDataType_U8, &u8_v, &u8_max, &u8_min, "%u"); ImGui::SliderScalar("slider s32 reverse", ImGuiDataType_S32, &s32_v, &s32_fifty, &s32_zero, "%d"); ImGui::SliderScalar("slider u32 reverse", ImGuiDataType_U32, &u32_v, &u32_fifty, &u32_zero, "%u"); - ImGui::SliderScalar("slider s64 reverse", ImGuiDataType_S64, &s64_v, &s64_fifty, &s64_zero, "%" IM_PRId64); - ImGui::SliderScalar("slider u64 reverse", ImGuiDataType_U64, &u64_v, &u64_fifty, &u64_zero, "%" IM_PRIu64 " ms"); + ImGui::SliderScalar("slider s64 reverse", ImGuiDataType_S64, &s64_v, &s64_fifty, &s64_zero, "%" PRId64); + ImGui::SliderScalar("slider u64 reverse", ImGuiDataType_U64, &u64_v, &u64_fifty, &u64_zero, "%" PRIu64 " ms"); IMGUI_DEMO_MARKER("Widgets/Data Types/Inputs"); static bool inputs_step = true; @@ -2331,6 +2528,36 @@ static void ShowDemoWindowWidgets() ImGui::TreePop(); } + IMGUI_DEMO_MARKER("Widgets/Drag and Drop/Tooltip at target location"); + if (ImGui::TreeNode("Tooltip at target location")) + { + for (int n = 0; n < 2; n++) + { + // Drop targets + ImGui::Button(n ? "drop here##1" : "drop here##0"); + if (ImGui::BeginDragDropTarget()) + { + ImGuiDragDropFlags drop_target_flags = ImGuiDragDropFlags_AcceptBeforeDelivery | ImGuiDragDropFlags_AcceptNoPreviewTooltip; + if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_4F, drop_target_flags)) + { + IM_UNUSED(payload); + ImGui::SetMouseCursor(ImGuiMouseCursor_NotAllowed); + ImGui::BeginTooltip(); + ImGui::Text("Cannot drop here!"); + ImGui::EndTooltip(); + } + ImGui::EndDragDropTarget(); + } + + // Drop source + static ImVec4 col4 = { 1.0f, 0.0f, 0.2f, 1.0f }; + if (n == 0) + ImGui::ColorButton("drag me", col4); + + } + ImGui::TreePop(); + } + ImGui::TreePop(); } @@ -2375,8 +2602,10 @@ static void ShowDemoWindowWidgets() if (item_type == 15){ const char* items[] = { "Apple", "Banana", "Cherry", "Kiwi" }; static int current = 1; ret = ImGui::ListBox("ITEM: ListBox", ¤t, items, IM_ARRAYSIZE(items), IM_ARRAYSIZE(items)); } bool hovered_delay_none = ImGui::IsItemHovered(); + bool hovered_delay_stationary = ImGui::IsItemHovered(ImGuiHoveredFlags_Stationary); bool hovered_delay_short = ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort); bool hovered_delay_normal = ImGui::IsItemHovered(ImGuiHoveredFlags_DelayNormal); + bool hovered_delay_tooltip = ImGui::IsItemHovered(ImGuiHoveredFlags_ForTooltip); // = Normal + Stationary // Display the values of IsItemHovered() and other common item state functions. // Note that the ImGuiHoveredFlags_XXX flags can be combined. @@ -2388,7 +2617,8 @@ static void ShowDemoWindowWidgets() "IsItemHovered() = %d\n" "IsItemHovered(_AllowWhenBlockedByPopup) = %d\n" "IsItemHovered(_AllowWhenBlockedByActiveItem) = %d\n" - "IsItemHovered(_AllowWhenOverlapped) = %d\n" + "IsItemHovered(_AllowWhenOverlappedByItem) = %d\n" + "IsItemHovered(_AllowWhenOverlappedByWindow) = %d\n" "IsItemHovered(_AllowWhenDisabled) = %d\n" "IsItemHovered(_RectOnly) = %d\n" "IsItemActive() = %d\n" @@ -2407,7 +2637,8 @@ static void ShowDemoWindowWidgets() ImGui::IsItemHovered(), ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup), ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem), - ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenOverlapped), + ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenOverlappedByItem), + ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenOverlappedByWindow), ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled), ImGui::IsItemHovered(ImGuiHoveredFlags_RectOnly), ImGui::IsItemActive(), @@ -2423,7 +2654,13 @@ static void ShowDemoWindowWidgets() ImGui::GetItemRectSize().x, ImGui::GetItemRectSize().y ); ImGui::BulletText( - "w/ Hovering Delay: None = %d, Fast %d, Normal = %d", hovered_delay_none, hovered_delay_short, hovered_delay_normal); + "with Hovering Delay or Stationary test:\n" + "IsItemHovered() = = %d\n" + "IsItemHovered(_Stationary) = %d\n" + "IsItemHovered(_DelayShort) = %d\n" + "IsItemHovered(_DelayNormal) = %d\n" + "IsItemHovered(_Tooltip) = %d", + hovered_delay_none, hovered_delay_stationary, hovered_delay_short, hovered_delay_normal, hovered_delay_tooltip); if (item_disabled) ImGui::EndDisabled(); @@ -2442,25 +2679,31 @@ static void ShowDemoWindowWidgets() static bool embed_all_inside_a_child_window = false; ImGui::Checkbox("Embed everything inside a child window for testing _RootWindow flag.", &embed_all_inside_a_child_window); if (embed_all_inside_a_child_window) - ImGui::BeginChild("outer_child", ImVec2(0, ImGui::GetFontSize() * 20.0f), true); + ImGui::BeginChild("outer_child", ImVec2(0, ImGui::GetFontSize() * 20.0f), ImGuiChildFlags_Border); // Testing IsWindowFocused() function with its various flags. ImGui::BulletText( "IsWindowFocused() = %d\n" "IsWindowFocused(_ChildWindows) = %d\n" "IsWindowFocused(_ChildWindows|_NoPopupHierarchy) = %d\n" + "IsWindowFocused(_ChildWindows|_DockHierarchy) = %d\n" "IsWindowFocused(_ChildWindows|_RootWindow) = %d\n" "IsWindowFocused(_ChildWindows|_RootWindow|_NoPopupHierarchy) = %d\n" + "IsWindowFocused(_ChildWindows|_RootWindow|_DockHierarchy) = %d\n" "IsWindowFocused(_RootWindow) = %d\n" "IsWindowFocused(_RootWindow|_NoPopupHierarchy) = %d\n" + "IsWindowFocused(_RootWindow|_DockHierarchy) = %d\n" "IsWindowFocused(_AnyWindow) = %d\n", ImGui::IsWindowFocused(), ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows), ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows | ImGuiFocusedFlags_NoPopupHierarchy), + ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows | ImGuiFocusedFlags_DockHierarchy), ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows | ImGuiFocusedFlags_RootWindow), ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows | ImGuiFocusedFlags_RootWindow | ImGuiFocusedFlags_NoPopupHierarchy), + ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows | ImGuiFocusedFlags_RootWindow | ImGuiFocusedFlags_DockHierarchy), ImGui::IsWindowFocused(ImGuiFocusedFlags_RootWindow), ImGui::IsWindowFocused(ImGuiFocusedFlags_RootWindow | ImGuiFocusedFlags_NoPopupHierarchy), + ImGui::IsWindowFocused(ImGuiFocusedFlags_RootWindow | ImGuiFocusedFlags_DockHierarchy), ImGui::IsWindowFocused(ImGuiFocusedFlags_AnyWindow)); // Testing IsWindowHovered() function with its various flags. @@ -2470,25 +2713,33 @@ static void ShowDemoWindowWidgets() "IsWindowHovered(_AllowWhenBlockedByActiveItem) = %d\n" "IsWindowHovered(_ChildWindows) = %d\n" "IsWindowHovered(_ChildWindows|_NoPopupHierarchy) = %d\n" + "IsWindowHovered(_ChildWindows|_DockHierarchy) = %d\n" "IsWindowHovered(_ChildWindows|_RootWindow) = %d\n" "IsWindowHovered(_ChildWindows|_RootWindow|_NoPopupHierarchy) = %d\n" + "IsWindowHovered(_ChildWindows|_RootWindow|_DockHierarchy) = %d\n" "IsWindowHovered(_RootWindow) = %d\n" "IsWindowHovered(_RootWindow|_NoPopupHierarchy) = %d\n" + "IsWindowHovered(_RootWindow|_DockHierarchy) = %d\n" "IsWindowHovered(_ChildWindows|_AllowWhenBlockedByPopup) = %d\n" - "IsWindowHovered(_AnyWindow) = %d\n", + "IsWindowHovered(_AnyWindow) = %d\n" + "IsWindowHovered(_Stationary) = %d\n", ImGui::IsWindowHovered(), ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup), ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem), ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows), ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows | ImGuiHoveredFlags_NoPopupHierarchy), + ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows | ImGuiHoveredFlags_DockHierarchy), ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows | ImGuiHoveredFlags_RootWindow), ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows | ImGuiHoveredFlags_RootWindow | ImGuiHoveredFlags_NoPopupHierarchy), + ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows | ImGuiHoveredFlags_RootWindow | ImGuiHoveredFlags_DockHierarchy), ImGui::IsWindowHovered(ImGuiHoveredFlags_RootWindow), ImGui::IsWindowHovered(ImGuiHoveredFlags_RootWindow | ImGuiHoveredFlags_NoPopupHierarchy), + ImGui::IsWindowHovered(ImGuiHoveredFlags_RootWindow | ImGuiHoveredFlags_DockHierarchy), ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows | ImGuiHoveredFlags_AllowWhenBlockedByPopup), - ImGui::IsWindowHovered(ImGuiHoveredFlags_AnyWindow)); + ImGui::IsWindowHovered(ImGuiHoveredFlags_AnyWindow), + ImGui::IsWindowHovered(ImGuiHoveredFlags_Stationary)); - ImGui::BeginChild("child", ImVec2(0, 50), true); + ImGui::BeginChild("child", ImVec2(0, 50), ImGuiChildFlags_Border); ImGui::Text("This is another child window for testing the _ChildWindows flag."); ImGui::EndChild(); if (embed_all_inside_a_child_window) @@ -2496,10 +2747,13 @@ static void ShowDemoWindowWidgets() // Calling IsItemHovered() after begin returns the hovered status of the title bar. // This is useful in particular if you want to create a context menu associated to the title bar of a window. + // This will also work when docked into a Tab (the Tab replace the Title Bar and guarantee the same properties). static bool test_window = false; ImGui::Checkbox("Hovered/Active tests after Begin() for title bar testing", &test_window); if (test_window) { + // FIXME-DOCK: This window cannot be docked within the ImGui Demo window, this will cause a feedback loop and get them stuck. + // Could we fix this through an ImGuiWindowClass feature? Or an API call to tag our parent as "don't skip items"? ImGui::Begin("Title bar Hovered/Active tests", &test_window); if (ImGui::BeginPopupContextItem()) // <-- This is using IsItemHovered() { @@ -2572,7 +2826,7 @@ static void ShowDemoWindowLayout() ImGuiWindowFlags window_flags = ImGuiWindowFlags_HorizontalScrollbar; if (disable_mouse_wheel) window_flags |= ImGuiWindowFlags_NoScrollWithMouse; - ImGui::BeginChild("ChildL", ImVec2(ImGui::GetContentRegionAvail().x * 0.5f, 260), false, window_flags); + ImGui::BeginChild("ChildL", ImVec2(ImGui::GetContentRegionAvail().x * 0.5f, 260), ImGuiChildFlags_None, window_flags); for (int i = 0; i < 100; i++) ImGui::Text("%04d: scrollable region", i); ImGui::EndChild(); @@ -2588,7 +2842,7 @@ static void ShowDemoWindowLayout() if (!disable_menu) window_flags |= ImGuiWindowFlags_MenuBar; ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 5.0f); - ImGui::BeginChild("ChildR", ImVec2(0, 260), true, window_flags); + ImGui::BeginChild("ChildR", ImVec2(0, 260), ImGuiChildFlags_Border, window_flags); if (!disable_menu && ImGui::BeginMenuBar()) { if (ImGui::BeginMenu("Menu")) @@ -2613,6 +2867,35 @@ static void ShowDemoWindowLayout() ImGui::PopStyleVar(); } + // Child 3: manual-resize + ImGui::SeparatorText("Manual-resize"); + { + HelpMarker("Drag bottom border to resize. Double-click bottom border to auto-fit to vertical contents."); + ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::GetStyleColorVec4(ImGuiCol_FrameBg)); + if (ImGui::BeginChild("ResizableChild", ImVec2(-FLT_MIN, ImGui::GetTextLineHeightWithSpacing() * 8), ImGuiChildFlags_Border | ImGuiChildFlags_ResizeY)) + for (int n = 0; n < 10; n++) + ImGui::Text("Line %04d", n); + ImGui::PopStyleColor(); + ImGui::EndChild(); + } + + // Child 4: auto-resizing height with a limit + ImGui::SeparatorText("Auto-resize with constraints"); + { + static int draw_lines = 3; + static int max_height_in_lines = 10; + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8); + ImGui::DragInt("Lines Count", &draw_lines, 0.2f); + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8); + ImGui::DragInt("Max Height (in Lines)", &max_height_in_lines, 0.2f); + + ImGui::SetNextWindowSizeConstraints(ImVec2(0.0f, ImGui::GetTextLineHeightWithSpacing() * 1), ImVec2(FLT_MAX, ImGui::GetTextLineHeightWithSpacing() * max_height_in_lines)); + if (ImGui::BeginChild("ConstrainedChild", ImVec2(-FLT_MIN, 0.0f), ImGuiChildFlags_Border | ImGuiChildFlags_AutoResizeY)) + for (int n = 0; n < draw_lines; n++) + ImGui::Text("Line %04d", n); + ImGui::EndChild(); + } + ImGui::SeparatorText("Misc/Advanced"); // Demonstrate a few extra things @@ -2624,19 +2907,33 @@ static void ShowDemoWindowLayout() // the POV of the parent window). See 'Demo->Querying Status (Edited/Active/Hovered etc.)' for details. { static int offset_x = 0; + static bool override_bg_color = true; + static ImGuiChildFlags child_flags = ImGuiChildFlags_Border | ImGuiChildFlags_ResizeX | ImGuiChildFlags_ResizeY; ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8); ImGui::DragInt("Offset X", &offset_x, 1.0f, -1000, 1000); + ImGui::Checkbox("Override ChildBg color", &override_bg_color); + ImGui::CheckboxFlags("ImGuiChildFlags_Border", &child_flags, ImGuiChildFlags_Border); + ImGui::CheckboxFlags("ImGuiChildFlags_AlwaysUseWindowPadding", &child_flags, ImGuiChildFlags_AlwaysUseWindowPadding); + ImGui::CheckboxFlags("ImGuiChildFlags_ResizeX", &child_flags, ImGuiChildFlags_ResizeX); + ImGui::CheckboxFlags("ImGuiChildFlags_ResizeY", &child_flags, ImGuiChildFlags_ResizeY); + ImGui::CheckboxFlags("ImGuiChildFlags_FrameStyle", &child_flags, ImGuiChildFlags_FrameStyle); + ImGui::SameLine(); HelpMarker("Style the child window like a framed item: use FrameBg, FrameRounding, FrameBorderSize, FramePadding instead of ChildBg, ChildRounding, ChildBorderSize, WindowPadding."); + if (child_flags & ImGuiChildFlags_FrameStyle) + override_bg_color = false; ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (float)offset_x); - ImGui::PushStyleColor(ImGuiCol_ChildBg, IM_COL32(255, 0, 0, 100)); - ImGui::BeginChild("Red", ImVec2(200, 100), true, ImGuiWindowFlags_None); + if (override_bg_color) + ImGui::PushStyleColor(ImGuiCol_ChildBg, IM_COL32(255, 0, 0, 100)); + ImGui::BeginChild("Red", ImVec2(200, 100), child_flags, ImGuiWindowFlags_None); + if (override_bg_color) + ImGui::PopStyleColor(); + for (int n = 0; n < 50; n++) ImGui::Text("Some test %d", n); ImGui::EndChild(); bool child_is_hovered = ImGui::IsItemHovered(); ImVec2 child_rect_min = ImGui::GetItemRectMin(); ImVec2 child_rect_max = ImGui::GetItemRectMax(); - ImGui::PopStyleColor(); ImGui::Text("Hovered: %d", child_is_hovered); ImGui::Text("Rect of child window is: (%.0f,%.0f) (%.0f,%.0f)", child_rect_min.x, child_rect_min.y, child_rect_max.x, child_rect_max.y); } @@ -2729,11 +3026,11 @@ static void ShowDemoWindowLayout() // Text IMGUI_DEMO_MARKER("Layout/Basic Horizontal Layout/SameLine"); ImGui::Text("Two items: Hello"); ImGui::SameLine(); - ImGui::TextColored(ImVec4(1,1,0,1), "Sailor"); + ImGui::TextColored(ImVec4(1, 1, 0, 1), "Sailor"); // Adjust spacing ImGui::Text("More spacing: Hello"); ImGui::SameLine(0, 20); - ImGui::TextColored(ImVec4(1,1,0,1), "Sailor"); + ImGui::TextColored(ImVec4(1, 1, 0, 1), "Sailor"); // Button ImGui::AlignTextToFramePadding(); @@ -2784,7 +3081,7 @@ static void ShowDemoWindowLayout() ImGui::PushID(i); ImGui::ListBox("", &selection[i], items, IM_ARRAYSIZE(items)); ImGui::PopID(); - //if (ImGui::IsItemHovered()) ImGui::SetTooltip("ListBox %d hovered", i); + //ImGui::SetItemTooltip("ListBox %d hovered", i); } ImGui::PopItemWidth(); @@ -2837,8 +3134,7 @@ static void ShowDemoWindowLayout() ImGui::SameLine(); ImGui::Button("EEE"); ImGui::EndGroup(); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("First group hovered"); + ImGui::SetItemTooltip("First group hovered"); } // Capture the group size and create widgets using the same size ImVec2 size = ImGui::GetItemRectSize(); @@ -3026,7 +3322,7 @@ static void ShowDemoWindowLayout() const ImGuiWindowFlags child_flags = enable_extra_decorations ? ImGuiWindowFlags_MenuBar : 0; const ImGuiID child_id = ImGui::GetID((void*)(intptr_t)i); - const bool child_is_visible = ImGui::BeginChild(child_id, ImVec2(child_w, 200.0f), true, child_flags); + const bool child_is_visible = ImGui::BeginChild(child_id, ImVec2(child_w, 200.0f), ImGuiChildFlags_Border, child_flags); if (ImGui::BeginMenuBar()) { ImGui::TextUnformatted("abc"); @@ -3073,7 +3369,7 @@ static void ShowDemoWindowLayout() float child_height = ImGui::GetTextLineHeight() + style.ScrollbarSize + style.WindowPadding.y * 2.0f; ImGuiWindowFlags child_flags = ImGuiWindowFlags_HorizontalScrollbar | (enable_extra_decorations ? ImGuiWindowFlags_AlwaysVerticalScrollbar : 0); ImGuiID child_id = ImGui::GetID((void*)(intptr_t)i); - bool child_is_visible = ImGui::BeginChild(child_id, ImVec2(-100, child_height), true, child_flags); + bool child_is_visible = ImGui::BeginChild(child_id, ImVec2(-100, child_height), ImGuiChildFlags_Border, child_flags); if (scroll_to_off) ImGui::SetScrollX(scroll_to_off_px); if (scroll_to_pos) @@ -3115,7 +3411,7 @@ static void ShowDemoWindowLayout() ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2.0f, 1.0f)); ImVec2 scrolling_child_size = ImVec2(0, ImGui::GetFrameHeightWithSpacing() * 7 + 30); - ImGui::BeginChild("scrolling", scrolling_child_size, true, ImGuiWindowFlags_HorizontalScrollbar); + ImGui::BeginChild("scrolling", scrolling_child_size, ImGuiChildFlags_Border, ImGuiWindowFlags_HorizontalScrollbar); for (int line = 0; line < lines; line++) { // Display random stuff. For the sake of this trivial demo we are using basic Button() + SameLine() @@ -3184,7 +3480,9 @@ static void ShowDemoWindowLayout() IMGUI_DEMO_MARKER("Layout/Scrolling/Horizontal contents size demo window"); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(2, 0)); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2, 0)); - HelpMarker("Test of different widgets react and impact the work rectangle growing when horizontal scrolling is enabled.\n\nUse 'Metrics->Tools->Show windows rectangles' to visualize rectangles."); + HelpMarker( + "Test how different widgets react and impact the work rectangle growing when horizontal scrolling is enabled.\n\n" + "Use 'Metrics->Tools->Show windows rectangles' to visualize rectangles."); ImGui::Checkbox("H-scrollbar", &show_h_scrollbar); ImGui::Checkbox("Button", &show_button); // Will grow contents size (unless explicitly overwritten) ImGui::Checkbox("Tree nodes", &show_tree_nodes); // Will grow contents size and display highlight over full width @@ -3259,7 +3557,7 @@ static void ShowDemoWindowLayout() } if (show_child) { - ImGui::BeginChild("child", ImVec2(0, 0), true); + ImGui::BeginChild("child", ImVec2(0, 0), ImGuiChildFlags_Border); ImGui::EndChild(); } ImGui::End(); @@ -3332,6 +3630,37 @@ static void ShowDemoWindowLayout() ImGui::TreePop(); } + + IMGUI_DEMO_MARKER("Layout/Overlap Mode"); + if (ImGui::TreeNode("Overlap Mode")) + { + static bool enable_allow_overlap = true; + + HelpMarker( + "Hit-testing is by default performed in item submission order, which generally is perceived as 'back-to-front'.\n\n" + "By using SetNextItemAllowOverlap() you can notify that an item may be overlapped by another. " + "Doing so alters the hovering logic: items using AllowOverlap mode requires an extra frame to accept hovered state."); + ImGui::Checkbox("Enable AllowOverlap", &enable_allow_overlap); + + ImVec2 button1_pos = ImGui::GetCursorScreenPos(); + ImVec2 button2_pos = ImVec2(button1_pos.x + 50.0f, button1_pos.y + 50.0f); + if (enable_allow_overlap) + ImGui::SetNextItemAllowOverlap(); + ImGui::Button("Button 1", ImVec2(80, 80)); + ImGui::SetCursorScreenPos(button2_pos); + ImGui::Button("Button 2", ImVec2(80, 80)); + + // This is typically used with width-spanning items. + // (note that Selectable() has a dedicated flag ImGuiSelectableFlags_AllowOverlap, which is a shortcut + // for using SetNextItemAllowOverlap(). For demo purpose we use SetNextItemAllowOverlap() here.) + if (enable_allow_overlap) + ImGui::SetNextItemAllowOverlap(); + ImGui::Selectable("Some Selectable", false); + ImGui::SameLine(); + ImGui::SmallButton("++"); + + ImGui::TreePop(); + } } static void ShowDemoWindowPopups() @@ -3399,8 +3728,7 @@ static void ShowDemoWindowPopups() ImGui::Separator(); ImGui::Text("Tooltip here"); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("I am a tooltip over a popup"); + ImGui::SetItemTooltip("I am a tooltip over a popup"); if (ImGui::Button("Stacked Popup")) ImGui::OpenPopup("another popup"); @@ -3484,8 +3812,7 @@ static void ShowDemoWindowPopups() ImGui::CloseCurrentPopup(); ImGui::EndPopup(); } - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("Right-click to open popup"); + ImGui::SetItemTooltip("Right-click to open popup"); } } @@ -3665,6 +3992,14 @@ struct MyItem // very often by the sorting algorithm it would be a little wasteful. static const ImGuiTableSortSpecs* s_current_sort_specs; + static void SortWithSortSpecs(ImGuiTableSortSpecs* sort_specs, MyItem* items, int items_count) + { + s_current_sort_specs = sort_specs; // Store in variable accessible by the sort function. + if (items_count > 1) + qsort(items, (size_t)items_count, sizeof(items[0]), MyItem::CompareWithSortSpecs); + s_current_sort_specs = NULL; + } + // Compare function to be used by qsort() static int IMGUI_CDECL CompareWithSortSpecs(const void* lhs, const void* rhs) { @@ -3691,7 +4026,8 @@ struct MyItem } // qsort() is instable so always return a way to differenciate items. - // Your own compare function may want to avoid fallback on implicit sort specs e.g. a Name compare if it wasn't already part of the sort specs. + // Your own compare function may want to avoid fallback on implicit sort specs. + // e.g. a Name compare if it wasn't already part of the sort specs. return (a->ID - b->ID); } }; @@ -3737,7 +4073,7 @@ static void EditTableSizingFlags(ImGuiTableFlags* p_flags) } ImGui::SameLine(); ImGui::TextDisabled("(?)"); - if (ImGui::IsItemHovered() && ImGui::BeginTooltip()) + if (ImGui::BeginItemTooltip()) { ImGui::PushTextWrapPos(ImGui::GetFontSize() * 50.0f); for (int m = 0; m < IM_ARRAYSIZE(policies); m++) @@ -3775,6 +4111,7 @@ static void EditTableColumnsFlags(ImGuiTableColumnFlags* p_flags) ImGui::CheckboxFlags("_PreferSortDescending", p_flags, ImGuiTableColumnFlags_PreferSortDescending); ImGui::CheckboxFlags("_IndentEnable", p_flags, ImGuiTableColumnFlags_IndentEnable); ImGui::SameLine(); HelpMarker("Default for column 0"); ImGui::CheckboxFlags("_IndentDisable", p_flags, ImGuiTableColumnFlags_IndentDisable); ImGui::SameLine(); HelpMarker("Default for column >0"); + ImGui::CheckboxFlags("_AngledHeader", p_flags, ImGuiTableColumnFlags_AngledHeader); } static void ShowTableColumnsStatusFlags(ImGuiTableColumnFlags flags) @@ -3799,10 +4136,10 @@ static void ShowDemoWindowTables() ImGui::PushID("Tables"); int open_action = -1; - if (ImGui::Button("Open all")) + if (ImGui::Button("Expand all")) open_action = 1; ImGui::SameLine(); - if (ImGui::Button("Close all")) + if (ImGui::Button("Collapse all")) open_action = 0; ImGui::SameLine(); @@ -3873,8 +4210,9 @@ static void ShowDemoWindowTables() // as TableNextColumn() will automatically wrap around and create new rows as needed. // This is generally more convenient when your cells all contains the same type of data. HelpMarker( - "Only using TableNextColumn(), which tends to be convenient for tables where every cell contains the same type of contents.\n" - "This is also more similar to the old NextColumn() function of the Columns API, and provided to facilitate the Columns->Tables API transition."); + "Only using TableNextColumn(), which tends to be convenient for tables where every cell contains " + "the same type of contents.\n This is also more similar to the old NextColumn() function of the " + "Columns API, and provided to facilitate the Columns->Tables API transition."); if (ImGui::BeginTable("table3", 3)) { for (int item = 0; item < 14; item++) @@ -3930,8 +4268,8 @@ static void ShowDemoWindowTables() if (ImGui::BeginTable("table1", 3, flags)) { - // Display headers so we can inspect their interaction with borders. - // (Headers are not the main purpose of this section of the demo, so we are not elaborating on them too much. See other sections for details) + // Display headers so we can inspect their interaction with borders + // (Headers are not the main purpose of this section of the demo, so we are not elaborating on them now. See other sections for details) if (display_headers) { ImGui::TableSetupColumn("One"); @@ -3970,7 +4308,9 @@ static void ShowDemoWindowTables() PushStyleCompact(); ImGui::CheckboxFlags("ImGuiTableFlags_Resizable", &flags, ImGuiTableFlags_Resizable); ImGui::CheckboxFlags("ImGuiTableFlags_BordersV", &flags, ImGuiTableFlags_BordersV); - ImGui::SameLine(); HelpMarker("Using the _Resizable flag automatically enables the _BordersInnerV flag as well, this is why the resize borders are still showing when unchecking this."); + ImGui::SameLine(); HelpMarker( + "Using the _Resizable flag automatically enables the _BordersInnerV flag as well, " + "this is why the resize borders are still showing when unchecking this."); PopStyleCompact(); if (ImGui::BeginTable("table1", 3, flags)) @@ -4088,6 +4428,7 @@ static void ShowDemoWindowTables() ImGui::CheckboxFlags("ImGuiTableFlags_Hideable", &flags, ImGuiTableFlags_Hideable); ImGui::CheckboxFlags("ImGuiTableFlags_NoBordersInBody", &flags, ImGuiTableFlags_NoBordersInBody); ImGui::CheckboxFlags("ImGuiTableFlags_NoBordersInBodyUntilResize", &flags, ImGuiTableFlags_NoBordersInBodyUntilResize); ImGui::SameLine(); HelpMarker("Disable vertical borders in columns Body until hovered for resize (borders will always appear in Headers)"); + ImGui::CheckboxFlags("ImGuiTableFlags_HighlightHoveredColumn", &flags, ImGuiTableFlags_HighlightHoveredColumn); PopStyleCompact(); if (ImGui::BeginTable("table1", 3, flags)) @@ -4110,7 +4451,8 @@ static void ShowDemoWindowTables() ImGui::EndTable(); } - // Use outer_size.x == 0.0f instead of default to make the table as tight as possible (only valid when no scrolling and no stretch column) + // Use outer_size.x == 0.0f instead of default to make the table as tight as possible + // (only valid when no scrolling and no stretch column) if (ImGui::BeginTable("table2", 3, flags | ImGuiTableFlags_SizingFixedFit, ImVec2(0.0f, 0.0f))) { ImGui::TableSetupColumn("One"); @@ -4143,7 +4485,8 @@ static void ShowDemoWindowTables() "e.g.:\n" "- BorderOuterV\n" "- any form of row selection\n" - "Because of this, activating BorderOuterV sets the default to PadOuterX. Using PadOuterX or NoPadOuterX you can override the default.\n\n" + "Because of this, activating BorderOuterV sets the default to PadOuterX. " + "Using PadOuterX or NoPadOuterX you can override the default.\n\n" "Actual padding values are using style.CellPadding.\n\n" "In this demo we don't show horizontal borders to emphasize how they don't affect default horizontal padding."); @@ -4259,7 +4602,8 @@ static void ShowDemoWindowTables() EditTableSizingFlags(&sizing_policy_flags[table_n]); // To make it easier to understand the different sizing policy, - // For each policy: we display one table where the columns have equal contents width, and one where the columns have different contents width. + // For each policy: we display one table where the columns have equal contents width, + // and one where the columns have different contents width. if (ImGui::BeginTable("table1", 3, sizing_policy_flags[table_n] | flags1)) { for (int row = 0; row < 3; row++) @@ -4288,7 +4632,9 @@ static void ShowDemoWindowTables() ImGui::Spacing(); ImGui::TextUnformatted("Advanced"); ImGui::SameLine(); - HelpMarker("This section allows you to interact and see the effect of various sizing policies depending on whether Scroll is enabled and the contents of your columns."); + HelpMarker( + "This section allows you to interact and see the effect of various sizing policies " + "depending on whether Scroll is enabled and the contents of your columns."); enum ContentsType { CT_ShowWidth, CT_ShortText, CT_LongText, CT_Button, CT_FillButton, CT_InputText }; static ImGuiTableFlags flags = ImGuiTableFlags_ScrollY | ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable; @@ -4303,7 +4649,9 @@ static void ShowDemoWindowTables() if (contents_type == CT_FillButton) { ImGui::SameLine(); - HelpMarker("Be mindful that using right-alignment (e.g. size.x = -FLT_MIN) creates a feedback loop where contents width can feed into auto-column width can feed into contents width."); + HelpMarker( + "Be mindful that using right-alignment (e.g. size.x = -FLT_MIN) creates a feedback loop " + "where contents width can feed into auto-column width can feed into contents width."); } ImGui::DragInt("Columns", &column_count, 0.1f, 1, 64, "%d", ImGuiSliderFlags_AlwaysClamp); ImGui::CheckboxFlags("ImGuiTableFlags_Resizable", &flags, ImGuiTableFlags_Resizable); @@ -4349,7 +4697,9 @@ static void ShowDemoWindowTables() IMGUI_DEMO_MARKER("Tables/Vertical scrolling, with clipping"); if (ImGui::TreeNode("Vertical scrolling, with clipping")) { - HelpMarker("Here we activate ScrollY, which will create a child window container to allow hosting scrollable contents.\n\nWe also demonstrate using ImGuiListClipper to virtualize the submission of many items."); + HelpMarker( + "Here we activate ScrollY, which will create a child window container to allow hosting scrollable contents.\n\n" + "We also demonstrate using ImGuiListClipper to virtualize the submission of many items."); static ImGuiTableFlags flags = ImGuiTableFlags_ScrollY | ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersOuter | ImGuiTableFlags_BordersV | ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable | ImGuiTableFlags_Hideable; PushStyleCompact(); @@ -4395,8 +4745,9 @@ static void ShowDemoWindowTables() HelpMarker( "When ScrollX is enabled, the default sizing policy becomes ImGuiTableFlags_SizingFixedFit, " "as automatically stretching columns doesn't make much sense with horizontal scrolling.\n\n" - "Also note that as of the current version, you will almost always want to enable ScrollY along with ScrollX," - "because the container window won't automatically extend vertically to fix contents (this may be improved in future versions)."); + "Also note that as of the current version, you will almost always want to enable ScrollY along with ScrollX, " + "because the container window won't automatically extend vertically to fix contents " + "(this may be improved in future versions)."); static ImGuiTableFlags flags = ImGuiTableFlags_ScrollX | ImGuiTableFlags_ScrollY | ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersOuter | ImGuiTableFlags_BordersV | ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable | ImGuiTableFlags_Hideable; static int freeze_cols = 1; static int freeze_rows = 1; @@ -4453,7 +4804,8 @@ static void ShowDemoWindowTables() HelpMarker( "Showcase using Stretch columns + ScrollX together: " "this is rather unusual and only makes sense when specifying an 'inner_width' for the table!\n" - "Without an explicit value, inner_width is == outer_size.x and therefore using Stretch columns + ScrollX together doesn't make sense."); + "Without an explicit value, inner_width is == outer_size.x and therefore using Stretch columns " + "along with ScrollX doesn't make sense."); static ImGuiTableFlags flags2 = ImGuiTableFlags_SizingStretchSame | ImGuiTableFlags_ScrollX | ImGuiTableFlags_ScrollY | ImGuiTableFlags_BordersOuter | ImGuiTableFlags_RowBg | ImGuiTableFlags_ContextMenuInBody; static float inner_width = 1000.0f; PushStyleCompact(); @@ -4511,8 +4863,9 @@ static void ShowDemoWindowTables() } // Create the real table we care about for the example! - // We use a scrolling table to be able to showcase the difference between the _IsEnabled and _IsVisible flags above, otherwise in - // a non-scrolling table columns are always visible (unless using ImGuiTableFlags_NoKeepColumnsVisible + resizing the parent window down) + // We use a scrolling table to be able to showcase the difference between the _IsEnabled and _IsVisible flags above, + // otherwise in a non-scrolling table columns are always visible (unless using ImGuiTableFlags_NoKeepColumnsVisible + // + resizing the parent window down). const ImGuiTableFlags flags = ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_ScrollX | ImGuiTableFlags_ScrollY | ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersOuter | ImGuiTableFlags_BordersV @@ -4520,15 +4873,22 @@ static void ShowDemoWindowTables() ImVec2 outer_size = ImVec2(0.0f, TEXT_BASE_HEIGHT * 9); if (ImGui::BeginTable("table_columns_flags", column_count, flags, outer_size)) { + bool has_angled_header = false; for (int column = 0; column < column_count; column++) + { + has_angled_header |= (column_flags[column] & ImGuiTableColumnFlags_AngledHeader) != 0; ImGui::TableSetupColumn(column_names[column], column_flags[column]); + } + if (has_angled_header) + ImGui::TableAngledHeadersRow(); ImGui::TableHeadersRow(); for (int column = 0; column < column_count; column++) column_flags_out[column] = ImGui::TableGetColumnFlags(column); float indent_step = (float)((int)TEXT_BASE_WIDTH / 2); for (int row = 0; row < 8; row++) { - ImGui::Indent(indent_step); // Add some indentation to demonstrate usage of per-column IndentEnable/IndentDisable flags. + // Add some indentation to demonstrate usage of per-column IndentEnable/IndentDisable flags. + ImGui::Indent(indent_step); ImGui::TableNextRow(); for (int column = 0; column < column_count; column++) { @@ -4577,7 +4937,9 @@ static void ShowDemoWindowTables() ImGui::EndTable(); } - HelpMarker("Using TableSetupColumn() to setup explicit width.\n\nUnless _NoKeepColumnsVisible is set, fixed columns with set width may still be shrunk down if there's not enough space in the host."); + HelpMarker( + "Using TableSetupColumn() to setup explicit width.\n\nUnless _NoKeepColumnsVisible is set, " + "fixed columns with set width may still be shrunk down if there's not enough space in the host."); static ImGuiTableFlags flags2 = ImGuiTableFlags_None; PushStyleCompact(); @@ -4587,7 +4949,8 @@ static void ShowDemoWindowTables() PopStyleCompact(); if (ImGui::BeginTable("table2", 4, flags2)) { - // We could also set ImGuiTableFlags_SizingFixedFit on the table and all columns will default to ImGuiTableColumnFlags_WidthFixed. + // We could also set ImGuiTableFlags_SizingFixedFit on the table and then all columns + // will default to ImGuiTableColumnFlags_WidthFixed. ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 100.0f); ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, TEXT_BASE_WIDTH * 15.0f); ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, TEXT_BASE_WIDTH * 30.0f); @@ -4659,10 +5022,13 @@ static void ShowDemoWindowTables() IMGUI_DEMO_MARKER("Tables/Row height"); if (ImGui::TreeNode("Row height")) { - HelpMarker("You can pass a 'min_row_height' to TableNextRow().\n\nRows are padded with 'style.CellPadding.y' on top and bottom, so effectively the minimum row height will always be >= 'style.CellPadding.y * 2.0f'.\n\nWe cannot honor a _maximum_ row height as that would require a unique clipping rectangle per row."); - if (ImGui::BeginTable("table_row_height", 1, ImGuiTableFlags_BordersOuter | ImGuiTableFlags_BordersInnerV)) + HelpMarker( + "You can pass a 'min_row_height' to TableNextRow().\n\nRows are padded with 'style.CellPadding.y' on top and bottom, " + "so effectively the minimum row height will always be >= 'style.CellPadding.y * 2.0f'.\n\n" + "We cannot honor a _maximum_ row height as that would require a unique clipping rectangle per row."); + if (ImGui::BeginTable("table_row_height", 1, ImGuiTableFlags_Borders)) { - for (int row = 0; row < 10; row++) + for (int row = 0; row < 8; row++) { float min_row_height = (float)(int)(TEXT_BASE_HEIGHT * 0.30f * row); ImGui::TableNextRow(ImGuiTableRowFlags_None, min_row_height); @@ -4671,6 +5037,48 @@ static void ShowDemoWindowTables() } ImGui::EndTable(); } + + HelpMarker( + "Showcase using SameLine(0,0) to share Current Line Height between cells.\n\n" + "Please note that Tables Row Height is not the same thing as Current Line Height, " + "as a table cell may contains multiple lines."); + if (ImGui::BeginTable("table_share_lineheight", 2, ImGuiTableFlags_Borders)) + { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::ColorButton("##1", ImVec4(0.13f, 0.26f, 0.40f, 1.0f), ImGuiColorEditFlags_None, ImVec2(40, 40)); + ImGui::TableNextColumn(); + ImGui::Text("Line 1"); + ImGui::Text("Line 2"); + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::ColorButton("##2", ImVec4(0.13f, 0.26f, 0.40f, 1.0f), ImGuiColorEditFlags_None, ImVec2(40, 40)); + ImGui::TableNextColumn(); + ImGui::SameLine(0.0f, 0.0f); // Reuse line height from previous column + ImGui::Text("Line 1, with SameLine(0,0)"); + ImGui::Text("Line 2"); + + ImGui::EndTable(); + } + + HelpMarker("Showcase altering CellPadding.y between rows. Note that CellPadding.x is locked for the entire table."); + if (ImGui::BeginTable("table_changing_cellpadding_y", 1, ImGuiTableFlags_Borders)) + { + ImGuiStyle& style = ImGui::GetStyle(); + for (int row = 0; row < 8; row++) + { + if ((row % 3) == 2) + ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(style.CellPadding.x, 20.0f)); + ImGui::TableNextRow(ImGuiTableRowFlags_None); + ImGui::TableNextColumn(); + ImGui::Text("CellPadding.y = %.2f", style.CellPadding.y); + if ((row % 3) == 2) + ImGui::PopStyleVar(); + } + ImGui::EndTable(); + } + ImGui::TreePop(); } @@ -4806,6 +5214,11 @@ static void ShowDemoWindowTables() { static ImGuiTableFlags flags = ImGuiTableFlags_BordersV | ImGuiTableFlags_BordersOuterH | ImGuiTableFlags_Resizable | ImGuiTableFlags_RowBg | ImGuiTableFlags_NoBordersInBody; + static ImGuiTreeNodeFlags tree_node_flags = ImGuiTreeNodeFlags_SpanAllColumns; + ImGui::CheckboxFlags("ImGuiTreeNodeFlags_SpanFullWidth", &tree_node_flags, ImGuiTreeNodeFlags_SpanFullWidth); + ImGui::CheckboxFlags("ImGuiTreeNodeFlags_SpanAllColumns", &tree_node_flags, ImGuiTreeNodeFlags_SpanAllColumns); + + HelpMarker("See \"Columns flags\" section to configure how indentation is applied to individual columns."); if (ImGui::BeginTable("3ways", 3, flags)) { // The first column will use the default _WidthStretch when ScrollX is Off and _WidthFixed when ScrollX is On @@ -4829,7 +5242,7 @@ static void ShowDemoWindowTables() const bool is_folder = (node->ChildCount > 0); if (is_folder) { - bool open = ImGui::TreeNodeEx(node->Name, ImGuiTreeNodeFlags_SpanFullWidth); + bool open = ImGui::TreeNodeEx(node->Name, tree_node_flags); ImGui::TableNextColumn(); ImGui::TextDisabled("--"); ImGui::TableNextColumn(); @@ -4843,7 +5256,7 @@ static void ShowDemoWindowTables() } else { - ImGui::TreeNodeEx(node->Name, ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_Bullet | ImGuiTreeNodeFlags_NoTreePushOnOpen | ImGuiTreeNodeFlags_SpanFullWidth); + ImGui::TreeNodeEx(node->Name, tree_node_flags | ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_Bullet | ImGuiTreeNodeFlags_NoTreePushOnOpen); ImGui::TableNextColumn(); ImGui::Text("%d", node->Size); ImGui::TableNextColumn(); @@ -4878,7 +5291,8 @@ static void ShowDemoWindowTables() { HelpMarker( "Showcase using PushItemWidth() and how it is preserved on a per-column basis.\n\n" - "Note that on auto-resizing non-resizable fixed columns, querying the content width for e.g. right-alignment doesn't make sense."); + "Note that on auto-resizing non-resizable fixed columns, querying the content width for " + "e.g. right-alignment doesn't make sense."); if (ImGui::BeginTable("table_item_width", 3, ImGuiTableFlags_Borders)) { ImGui::TableSetupColumn("small"); @@ -4964,13 +5378,72 @@ static void ShowDemoWindowTables() ImGui::TreePop(); } - // Demonstrate creating custom context menus inside columns, while playing it nice with context menus provided by TableHeadersRow()/TableHeader() + // Demonstrate using ImGuiTableColumnFlags_AngledHeader flag to create angled headers + if (open_action != -1) + ImGui::SetNextItemOpen(open_action != 0); + IMGUI_DEMO_MARKER("Tables/Angled headers"); + if (ImGui::TreeNode("Angled headers")) + { + const char* column_names[] = { "Track", "cabasa", "ride", "smash", "tom-hi", "tom-mid", "tom-low", "hihat-o", "hihat-c", "snare-s", "snare-c", "clap", "rim", "kick" }; + const int columns_count = IM_ARRAYSIZE(column_names); + const int rows_count = 12; + + static ImGuiTableFlags table_flags = ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_ScrollX | ImGuiTableFlags_ScrollY | ImGuiTableFlags_BordersOuter | ImGuiTableFlags_BordersInnerH | ImGuiTableFlags_Hideable | ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable | ImGuiTableFlags_HighlightHoveredColumn; + static ImGuiTableColumnFlags column_flags = ImGuiTableColumnFlags_AngledHeader | ImGuiTableColumnFlags_WidthFixed; + static bool bools[columns_count * rows_count] = {}; // Dummy storage selection storage + static int frozen_cols = 1; + static int frozen_rows = 2; + ImGui::CheckboxFlags("_ScrollX", &table_flags, ImGuiTableFlags_ScrollX); + ImGui::CheckboxFlags("_ScrollY", &table_flags, ImGuiTableFlags_ScrollY); + ImGui::CheckboxFlags("_Resizable", &table_flags, ImGuiTableFlags_Resizable); + ImGui::CheckboxFlags("_NoBordersInBody", &table_flags, ImGuiTableFlags_NoBordersInBody); + ImGui::CheckboxFlags("_HighlightHoveredColumn", &table_flags, ImGuiTableFlags_HighlightHoveredColumn); + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8); + ImGui::SliderInt("Frozen columns", &frozen_cols, 0, 2); + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8); + ImGui::SliderInt("Frozen rows", &frozen_rows, 0, 2); + ImGui::CheckboxFlags("Disable header contributing to column width", &column_flags, ImGuiTableColumnFlags_NoHeaderWidth); + + if (ImGui::BeginTable("table_angled_headers", columns_count, table_flags, ImVec2(0.0f, TEXT_BASE_HEIGHT * 12))) + { + ImGui::TableSetupColumn(column_names[0], ImGuiTableColumnFlags_NoHide | ImGuiTableColumnFlags_NoReorder); + for (int n = 1; n < columns_count; n++) + ImGui::TableSetupColumn(column_names[n], column_flags); + ImGui::TableSetupScrollFreeze(frozen_cols, frozen_rows); + + ImGui::TableAngledHeadersRow(); // Draw angled headers for all columns with the ImGuiTableColumnFlags_AngledHeader flag. + ImGui::TableHeadersRow(); // Draw remaining headers and allow access to context-menu and other functions. + for (int row = 0; row < rows_count; row++) + { + ImGui::PushID(row); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::AlignTextToFramePadding(); + ImGui::Text("Track %d", row); + for (int column = 1; column < columns_count; column++) + if (ImGui::TableSetColumnIndex(column)) + { + ImGui::PushID(column); + ImGui::Checkbox("", &bools[row * columns_count + column]); + ImGui::PopID(); + } + ImGui::PopID(); + } + ImGui::EndTable(); + } + ImGui::TreePop(); + } + + // Demonstrate creating custom context menus inside columns, + // while playing it nice with context menus provided by TableHeadersRow()/TableHeader() if (open_action != -1) ImGui::SetNextItemOpen(open_action != 0); IMGUI_DEMO_MARKER("Tables/Context menus"); if (ImGui::TreeNode("Context menus")) { - HelpMarker("By default, right-clicking over a TableHeadersRow()/TableHeader() line will open the default context-menu.\nUsing ImGuiTableFlags_ContextMenuInBody we also allow right-clicking over columns body."); + HelpMarker( + "By default, right-clicking over a TableHeadersRow()/TableHeader() line will open the default context-menu.\n" + "Using ImGuiTableFlags_ContextMenuInBody we also allow right-clicking over columns body."); static ImGuiTableFlags flags1 = ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable | ImGuiTableFlags_Hideable | ImGuiTableFlags_Borders | ImGuiTableFlags_ContextMenuInBody; PushStyleCompact(); @@ -5007,7 +5480,9 @@ static void ShowDemoWindowTables() // [2.1] Right-click on the TableHeadersRow() line to open the default table context menu. // [2.2] Right-click on the ".." to open a custom popup // [2.3] Right-click in columns to open another custom popup - HelpMarker("Demonstrate mixing table context menu (over header), item context button (over button) and custom per-colum context menu (over column body)."); + HelpMarker( + "Demonstrate mixing table context menu (over header), item context button (over button) " + "and custom per-colunm context menu (over column body)."); ImGuiTableFlags flags2 = ImGuiTableFlags_Resizable | ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_Reorderable | ImGuiTableFlags_Hideable | ImGuiTableFlags_Borders; if (ImGui::BeginTable("table_context_menu_2", COLUMNS_COUNT, flags2)) { @@ -5082,6 +5557,7 @@ static void ShowDemoWindowTables() static ImGuiTableFlags flags = ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable | ImGuiTableFlags_Hideable | ImGuiTableFlags_Borders | ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_NoSavedSettings; ImGui::CheckboxFlags("ImGuiTableFlags_ScrollY", &flags, ImGuiTableFlags_ScrollY); ImGui::CheckboxFlags("ImGuiTableFlags_SizingFixedFit", &flags, ImGuiTableFlags_SizingFixedFit); + ImGui::CheckboxFlags("ImGuiTableFlags_HighlightHoveredColumn", &flags, ImGuiTableFlags_HighlightHoveredColumn); for (int n = 0; n < 3; n++) { char buf[32]; @@ -5162,14 +5638,11 @@ static void ShowDemoWindowTables() ImGui::TableHeadersRow(); // Sort our data if sort specs have been changed! - if (ImGuiTableSortSpecs* sorts_specs = ImGui::TableGetSortSpecs()) - if (sorts_specs->SpecsDirty) + if (ImGuiTableSortSpecs* sort_specs = ImGui::TableGetSortSpecs()) + if (sort_specs->SpecsDirty) { - MyItem::s_current_sort_specs = sorts_specs; // Store in variable accessible by the sort function. - if (items.Size > 1) - qsort(&items[0], (size_t)items.Size, sizeof(items[0]), MyItem::CompareWithSortSpecs); - MyItem::s_current_sort_specs = NULL; - sorts_specs->SpecsDirty = false; + MyItem::SortWithSortSpecs(sort_specs, items.Data, items.Size); + sort_specs->SpecsDirty = false; } // Demonstrate using clipper for large vertical lists @@ -5212,6 +5685,7 @@ static void ShowDemoWindowTables() | ImGuiTableFlags_RowBg | ImGuiTableFlags_Borders | ImGuiTableFlags_NoBordersInBody | ImGuiTableFlags_ScrollX | ImGuiTableFlags_ScrollY | ImGuiTableFlags_SizingFixedFit; + static ImGuiTableColumnFlags columns_base_flags = ImGuiTableColumnFlags_None; enum ContentsType { CT_Text, CT_Button, CT_SmallButton, CT_FillButton, CT_Selectable, CT_SelectableSpanRow }; static int contents_type = CT_SelectableSpanRow; @@ -5305,9 +5779,17 @@ static void ShowDemoWindowTables() ImGui::TreePop(); } - if (ImGui::TreeNodeEx("Other:", ImGuiTreeNodeFlags_DefaultOpen)) + if (ImGui::TreeNodeEx("Headers:", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::Checkbox("show_headers", &show_headers); + ImGui::CheckboxFlags("ImGuiTableFlags_HighlightHoveredColumn", &flags, ImGuiTableFlags_HighlightHoveredColumn); + ImGui::CheckboxFlags("ImGuiTableColumnFlags_AngledHeader", &columns_base_flags, ImGuiTableColumnFlags_AngledHeader); + ImGui::SameLine(); HelpMarker("Enable AngledHeader on all columns. Best enabled on selected narrow columns (see \"Angled headers\" section of the demo)."); + ImGui::TreePop(); + } + + if (ImGui::TreeNodeEx("Other:", ImGuiTreeNodeFlags_DefaultOpen)) + { ImGui::Checkbox("show_wrapped_text", &show_wrapped_text); ImGui::DragFloat2("##OuterSize", &outer_size_value.x); @@ -5368,24 +5850,22 @@ static void ShowDemoWindowTables() // Declare columns // We use the "user_id" parameter of TableSetupColumn() to specify a user id that will be stored in the sort specifications. // This is so our sort function can identify a column given our own identifier. We could also identify them based on their index! - ImGui::TableSetupColumn("ID", ImGuiTableColumnFlags_DefaultSort | ImGuiTableColumnFlags_WidthFixed | ImGuiTableColumnFlags_NoHide, 0.0f, MyItemColumnID_ID); - ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthFixed, 0.0f, MyItemColumnID_Name); - ImGui::TableSetupColumn("Action", ImGuiTableColumnFlags_NoSort | ImGuiTableColumnFlags_WidthFixed, 0.0f, MyItemColumnID_Action); - ImGui::TableSetupColumn("Quantity", ImGuiTableColumnFlags_PreferSortDescending, 0.0f, MyItemColumnID_Quantity); - ImGui::TableSetupColumn("Description", (flags & ImGuiTableFlags_NoHostExtendX) ? 0 : ImGuiTableColumnFlags_WidthStretch, 0.0f, MyItemColumnID_Description); - ImGui::TableSetupColumn("Hidden", ImGuiTableColumnFlags_DefaultHide | ImGuiTableColumnFlags_NoSort); + ImGui::TableSetupColumn("ID", columns_base_flags | ImGuiTableColumnFlags_DefaultSort | ImGuiTableColumnFlags_WidthFixed | ImGuiTableColumnFlags_NoHide, 0.0f, MyItemColumnID_ID); + ImGui::TableSetupColumn("Name", columns_base_flags | ImGuiTableColumnFlags_WidthFixed, 0.0f, MyItemColumnID_Name); + ImGui::TableSetupColumn("Action", columns_base_flags | ImGuiTableColumnFlags_NoSort | ImGuiTableColumnFlags_WidthFixed, 0.0f, MyItemColumnID_Action); + ImGui::TableSetupColumn("Quantity", columns_base_flags | ImGuiTableColumnFlags_PreferSortDescending, 0.0f, MyItemColumnID_Quantity); + ImGui::TableSetupColumn("Description", columns_base_flags | ((flags & ImGuiTableFlags_NoHostExtendX) ? 0 : ImGuiTableColumnFlags_WidthStretch), 0.0f, MyItemColumnID_Description); + ImGui::TableSetupColumn("Hidden", columns_base_flags | ImGuiTableColumnFlags_DefaultHide | ImGuiTableColumnFlags_NoSort); ImGui::TableSetupScrollFreeze(freeze_cols, freeze_rows); // Sort our data if sort specs have been changed! - ImGuiTableSortSpecs* sorts_specs = ImGui::TableGetSortSpecs(); - if (sorts_specs && sorts_specs->SpecsDirty) + ImGuiTableSortSpecs* sort_specs = ImGui::TableGetSortSpecs(); + if (sort_specs && sort_specs->SpecsDirty) items_need_sort = true; - if (sorts_specs && items_need_sort && items.Size > 1) + if (sort_specs && items_need_sort && items.Size > 1) { - MyItem::s_current_sort_specs = sorts_specs; // Store in variable accessible by the sort function. - qsort(&items[0], (size_t)items.Size, sizeof(items[0]), MyItem::CompareWithSortSpecs); - MyItem::s_current_sort_specs = NULL; - sorts_specs->SpecsDirty = false; + MyItem::SortWithSortSpecs(sort_specs, items.Data, items.Size); + sort_specs->SpecsDirty = false; } items_need_sort = false; @@ -5394,6 +5874,8 @@ static void ShowDemoWindowTables() const bool sorts_specs_using_quantity = (ImGui::TableGetColumnFlags(3) & ImGuiTableColumnFlags_IsSorted) != 0; // Show headers + if (show_headers && (columns_base_flags & ImGuiTableColumnFlags_AngledHeader) != 0) + ImGui::TableAngledHeadersRow(); if (show_headers) ImGui::TableHeadersRow(); @@ -5435,7 +5917,7 @@ static void ShowDemoWindowTables() ImGui::Button(label, ImVec2(-FLT_MIN, 0.0f)); else if (contents_type == CT_Selectable || contents_type == CT_SelectableSpanRow) { - ImGuiSelectableFlags selectable_flags = (contents_type == CT_SelectableSpanRow) ? ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowItemOverlap : ImGuiSelectableFlags_None; + ImGuiSelectableFlags selectable_flags = (contents_type == CT_SelectableSpanRow) ? ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowOverlap : ImGuiSelectableFlags_None; if (ImGui::Selectable(label, item_is_selected, selectable_flags, ImVec2(0, row_min_height))) { if (ImGui::GetIO().KeyCtrl) @@ -5459,7 +5941,7 @@ static void ShowDemoWindowTables() // Here we demonstrate marking our data set as needing to be sorted again if we modified a quantity, // and we are currently sorting on the column showing the Quantity. // To avoid triggering a sort while holding the button, we only trigger it when the button has been released. - // You will probably need a more advanced system in your code if you want to automatically sort when a specific entry changes. + // You will probably need some extra logic if you want to automatically sort when a specific entry changes. if (ImGui::TableSetColumnIndex(2)) { if (ImGui::SmallButton("Chop")) { item->Quantity += 1; } @@ -5661,7 +6143,7 @@ static void ShowDemoWindowColumns() { ImGui::SetNextWindowContentSize(ImVec2(1500.0f, 0.0f)); ImVec2 child_size = ImVec2(0, ImGui::GetFontSize() * 20.0f); - ImGui::BeginChild("##ScrollingRegion", child_size, false, ImGuiWindowFlags_HorizontalScrollbar); + ImGui::BeginChild("##ScrollingRegion", child_size, ImGuiChildFlags_None, ImGuiWindowFlags_HorizontalScrollbar); ImGui::Columns(10); // Also demonstrate using clipper for large vertical lists @@ -5747,13 +6229,15 @@ static void ShowDemoWindowInputs() for (int i = 0; i < IM_ARRAYSIZE(io.MouseDown); i++) if (ImGui::IsMouseDown(i)) { ImGui::SameLine(); ImGui::Text("b%d (%.02f secs)", i, io.MouseDownDuration[i]); } ImGui::Text("Mouse wheel: %.1f", io.MouseWheel); - // We iterate both legacy native range and named ImGuiKey ranges, which is a little odd but this allows displaying the data for old/new backends. - // User code should never have to go through such hoops! You can generally iterate between ImGuiKey_NamedKey_BEGIN and ImGuiKey_NamedKey_END. + // We iterate both legacy native range and named ImGuiKey ranges. This is a little unusual/odd but this allows + // displaying the data for old/new backends. + // User code should never have to go through such hoops! + // You can generally iterate between ImGuiKey_NamedKey_BEGIN and ImGuiKey_NamedKey_END. #ifdef IMGUI_DISABLE_OBSOLETE_KEYIO struct funcs { static bool IsLegacyNativeDupe(ImGuiKey) { return false; } }; ImGuiKey start_key = ImGuiKey_NamedKey_BEGIN; #else - struct funcs { static bool IsLegacyNativeDupe(ImGuiKey key) { return key < 512 && ImGui::GetIO().KeyMap[key] != -1; } }; // Hide Native<>ImGuiKey duplicates when both exists in the array + struct funcs { static bool IsLegacyNativeDupe(ImGuiKey key) { return key >= 0 && key < 512 && ImGui::GetIO().KeyMap[key] != -1; } }; // Hide Native<>ImGuiKey duplicates when both exists in the array ImGuiKey start_key = (ImGuiKey)0; #endif ImGui::Text("Keys down:"); for (ImGuiKey key = start_key; key < ImGuiKey_NamedKey_END; key = (ImGuiKey)(key + 1)) { if (funcs::IsLegacyNativeDupe(key) || !ImGui::IsKeyDown(key)) continue; ImGui::SameLine(); ImGui::Text((key < ImGuiKey_NamedKey_BEGIN) ? "\"%s\"" : "\"%s\" %d", ImGui::GetKeyName(key), key); } @@ -5787,7 +6271,8 @@ static void ShowDemoWindowInputs() { HelpMarker( "Hovering the colored canvas will override io.WantCaptureXXX fields.\n" - "Notice how normally (when set to none), the value of io.WantCaptureKeyboard would be false when hovering and true when clicking."); + "Notice how normally (when set to none), the value of io.WantCaptureKeyboard would be false when hovering " + "and true when clicking."); static int capture_override_mouse = -1; static int capture_override_keyboard = -1; const char* capture_override_desc[] = { "None", "Set to false", "Set to true" }; @@ -5938,10 +6423,11 @@ void ImGui::ShowAboutWindow(bool* p_open) return; } IMGUI_DEMO_MARKER("Tools/About Dear ImGui"); - ImGui::Text("Dear ImGui %s", ImGui::GetVersion()); + ImGui::Text("Dear ImGui %s (%d)", IMGUI_VERSION, IMGUI_VERSION_NUM); ImGui::Separator(); ImGui::Text("By Omar Cornut and all Dear ImGui contributors."); ImGui::Text("Dear ImGui is licensed under the MIT License, see LICENSE for more information."); + ImGui::Text("If your company uses this, please consider sponsoring the project!"); static bool show_config_info = false; ImGui::Checkbox("Config/Build Information", &show_config_info); @@ -5952,7 +6438,7 @@ void ImGui::ShowAboutWindow(bool* p_open) bool copy_to_clipboard = ImGui::Button("Copy to clipboard"); ImVec2 child_size = ImVec2(0, ImGui::GetTextLineHeightWithSpacing() * 18); - ImGui::BeginChildFrame(ImGui::GetID("cfg_infos"), child_size, ImGuiWindowFlags_NoMove); + ImGui::BeginChild(ImGui::GetID("cfg_infos"), child_size, ImGuiChildFlags_FrameStyle); if (copy_to_clipboard) { ImGui::LogToClipboard(); @@ -6028,6 +6514,12 @@ void ImGui::ShowAboutWindow(bool* p_open) #endif #ifdef __EMSCRIPTEN__ ImGui::Text("define: __EMSCRIPTEN__"); +#endif +#ifdef IMGUI_HAS_VIEWPORT + ImGui::Text("define: IMGUI_HAS_VIEWPORT"); +#endif +#ifdef IMGUI_HAS_DOCK + ImGui::Text("define: IMGUI_HAS_DOCK"); #endif ImGui::Separator(); ImGui::Text("io.BackendPlatformName: %s", io.BackendPlatformName ? io.BackendPlatformName : "NULL"); @@ -6039,7 +6531,19 @@ void ImGui::ShowAboutWindow(bool* p_open) if (io.ConfigFlags & ImGuiConfigFlags_NavNoCaptureKeyboard) ImGui::Text(" NavNoCaptureKeyboard"); if (io.ConfigFlags & ImGuiConfigFlags_NoMouse) ImGui::Text(" NoMouse"); if (io.ConfigFlags & ImGuiConfigFlags_NoMouseCursorChange) ImGui::Text(" NoMouseCursorChange"); + if (io.ConfigFlags & ImGuiConfigFlags_DockingEnable) ImGui::Text(" DockingEnable"); + if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) ImGui::Text(" ViewportsEnable"); + if (io.ConfigFlags & ImGuiConfigFlags_DpiEnableScaleViewports) ImGui::Text(" DpiEnableScaleViewports"); + if (io.ConfigFlags & ImGuiConfigFlags_DpiEnableScaleFonts) ImGui::Text(" DpiEnableScaleFonts"); if (io.MouseDrawCursor) ImGui::Text("io.MouseDrawCursor"); + if (io.ConfigViewportsNoAutoMerge) ImGui::Text("io.ConfigViewportsNoAutoMerge"); + if (io.ConfigViewportsNoTaskBarIcon) ImGui::Text("io.ConfigViewportsNoTaskBarIcon"); + if (io.ConfigViewportsNoDecoration) ImGui::Text("io.ConfigViewportsNoDecoration"); + if (io.ConfigViewportsNoDefaultParent) ImGui::Text("io.ConfigViewportsNoDefaultParent"); + if (io.ConfigDockingNoSplit) ImGui::Text("io.ConfigDockingNoSplit"); + if (io.ConfigDockingWithShift) ImGui::Text("io.ConfigDockingWithShift"); + if (io.ConfigDockingAlwaysTabBar) ImGui::Text("io.ConfigDockingAlwaysTabBar"); + if (io.ConfigDockingTransparentPayload) ImGui::Text("io.ConfigDockingTransparentPayload"); if (io.ConfigMacOSXBehaviors) ImGui::Text("io.ConfigMacOSXBehaviors"); if (io.ConfigInputTextCursorBlink) ImGui::Text("io.ConfigInputTextCursorBlink"); if (io.ConfigWindowsResizeFromEdges) ImGui::Text("io.ConfigWindowsResizeFromEdges"); @@ -6049,7 +6553,10 @@ void ImGui::ShowAboutWindow(bool* p_open) if (io.BackendFlags & ImGuiBackendFlags_HasGamepad) ImGui::Text(" HasGamepad"); if (io.BackendFlags & ImGuiBackendFlags_HasMouseCursors) ImGui::Text(" HasMouseCursors"); if (io.BackendFlags & ImGuiBackendFlags_HasSetMousePos) ImGui::Text(" HasSetMousePos"); + if (io.BackendFlags & ImGuiBackendFlags_PlatformHasViewports) ImGui::Text(" PlatformHasViewports"); + if (io.BackendFlags & ImGuiBackendFlags_HasMouseHoveredViewport)ImGui::Text(" HasMouseHoveredViewport"); if (io.BackendFlags & ImGuiBackendFlags_RendererHasVtxOffset) ImGui::Text(" RendererHasVtxOffset"); + if (io.BackendFlags & ImGuiBackendFlags_RendererHasViewports) ImGui::Text(" RendererHasViewports"); ImGui::Separator(); ImGui::Text("io.Fonts: %d fonts, Flags: 0x%08X, TexSize: %d,%d", io.Fonts->Fonts.Size, io.Fonts->Flags, io.Fonts->TexWidth, io.Fonts->TexHeight); ImGui::Text("io.DisplaySize: %.2f,%.2f", io.DisplaySize.x, io.DisplaySize.y); @@ -6068,7 +6575,7 @@ void ImGui::ShowAboutWindow(bool* p_open) ImGui::LogText("\n```\n"); ImGui::LogFinish(); } - ImGui::EndChildFrame(); + ImGui::EndChild(); } ImGui::End(); } @@ -6092,9 +6599,8 @@ void ImGui::ShowFontSelector(const char* label) ImFont* font_current = ImGui::GetFont(); if (ImGui::BeginCombo(label, font_current->GetDebugName())) { - for (int n = 0; n < io.Fonts->Fonts.Size; n++) + for (ImFont* font : io.Fonts->Fonts) { - ImFont* font = io.Fonts->Fonts[n]; ImGui::PushID((void*)font); if (ImGui::Selectable(font->GetDebugName(), font == font_current)) io.FontDefault = font; @@ -6180,7 +6686,6 @@ void ImGui::ShowStyleEditor(ImGuiStyle* ref) ImGui::SeparatorText("Main"); ImGui::SliderFloat2("WindowPadding", (float*)&style.WindowPadding, 0.0f, 20.0f, "%.0f"); ImGui::SliderFloat2("FramePadding", (float*)&style.FramePadding, 0.0f, 20.0f, "%.0f"); - ImGui::SliderFloat2("CellPadding", (float*)&style.CellPadding, 0.0f, 20.0f, "%.0f"); ImGui::SliderFloat2("ItemSpacing", (float*)&style.ItemSpacing, 0.0f, 20.0f, "%.0f"); ImGui::SliderFloat2("ItemInnerSpacing", (float*)&style.ItemInnerSpacing, 0.0f, 20.0f, "%.0f"); ImGui::SliderFloat2("TouchExtraPadding", (float*)&style.TouchExtraPadding, 0.0f, 10.0f, "%.0f"); @@ -6194,6 +6699,7 @@ void ImGui::ShowStyleEditor(ImGuiStyle* ref) ImGui::SliderFloat("PopupBorderSize", &style.PopupBorderSize, 0.0f, 1.0f, "%.0f"); ImGui::SliderFloat("FrameBorderSize", &style.FrameBorderSize, 0.0f, 1.0f, "%.0f"); ImGui::SliderFloat("TabBorderSize", &style.TabBorderSize, 0.0f, 1.0f, "%.0f"); + ImGui::SliderFloat("TabBarBorderSize", &style.TabBarBorderSize, 0.0f, 2.0f, "%.0f"); ImGui::SeparatorText("Rounding"); ImGui::SliderFloat("WindowRounding", &style.WindowRounding, 0.0f, 12.0f, "%.0f"); @@ -6204,6 +6710,10 @@ void ImGui::ShowStyleEditor(ImGuiStyle* ref) ImGui::SliderFloat("GrabRounding", &style.GrabRounding, 0.0f, 12.0f, "%.0f"); ImGui::SliderFloat("TabRounding", &style.TabRounding, 0.0f, 12.0f, "%.0f"); + ImGui::SeparatorText("Tables"); + ImGui::SliderFloat2("CellPadding", (float*)&style.CellPadding, 0.0f, 20.0f, "%.0f"); + ImGui::SliderAngle("TableAngledHeadersAngle", &style.TableAngledHeadersAngle, -50.0f, +50.0f); + ImGui::SeparatorText("Widgets"); ImGui::SliderFloat2("WindowTitleAlign", (float*)&style.WindowTitleAlign, 0.0f, 1.0f, "%.2f"); int window_menu_button_position = style.WindowMenuButtonPosition + 1; @@ -6216,11 +6726,28 @@ void ImGui::ShowStyleEditor(ImGuiStyle* ref) ImGui::SameLine(); HelpMarker("Alignment applies when a selectable is larger than its text content."); ImGui::SliderFloat("SeparatorTextBorderSize", &style.SeparatorTextBorderSize, 0.0f, 10.0f, "%.0f"); ImGui::SliderFloat2("SeparatorTextAlign", (float*)&style.SeparatorTextAlign, 0.0f, 1.0f, "%.2f"); - ImGui::SliderFloat2("SeparatorTextPadding", (float*)&style.SeparatorTextPadding, 0.0f, 40.0f, "%0.f"); + ImGui::SliderFloat2("SeparatorTextPadding", (float*)&style.SeparatorTextPadding, 0.0f, 40.0f, "%.0f"); ImGui::SliderFloat("LogSliderDeadzone", &style.LogSliderDeadzone, 0.0f, 12.0f, "%.0f"); + ImGui::SeparatorText("Docking"); + ImGui::SliderFloat("DockingSplitterSize", &style.DockingSeparatorSize, 0.0f, 12.0f, "%.0f"); + + ImGui::SeparatorText("Tooltips"); + for (int n = 0; n < 2; n++) + if (ImGui::TreeNodeEx(n == 0 ? "HoverFlagsForTooltipMouse" : "HoverFlagsForTooltipNav")) + { + ImGuiHoveredFlags* p = (n == 0) ? &style.HoverFlagsForTooltipMouse : &style.HoverFlagsForTooltipNav; + ImGui::CheckboxFlags("ImGuiHoveredFlags_DelayNone", p, ImGuiHoveredFlags_DelayNone); + ImGui::CheckboxFlags("ImGuiHoveredFlags_DelayShort", p, ImGuiHoveredFlags_DelayShort); + ImGui::CheckboxFlags("ImGuiHoveredFlags_DelayNormal", p, ImGuiHoveredFlags_DelayNormal); + ImGui::CheckboxFlags("ImGuiHoveredFlags_Stationary", p, ImGuiHoveredFlags_Stationary); + ImGui::CheckboxFlags("ImGuiHoveredFlags_NoSharedDelay", p, ImGuiHoveredFlags_NoSharedDelay); + ImGui::TreePop(); + } + ImGui::SeparatorText("Misc"); ImGui::SliderFloat2("DisplaySafeAreaPadding", (float*)&style.DisplaySafeAreaPadding, 0.0f, 30.0f, "%.0f"); ImGui::SameLine(); HelpMarker("Adjust if you cannot see the edges of your screen (e.g. on a TV where scaling has not been configured)."); + ImGui::EndTabItem(); } @@ -6260,14 +6787,21 @@ void ImGui::ShowStyleEditor(ImGuiStyle* ref) "Left-click on color square to open color picker,\n" "Right-click to open edit options menu."); - ImGui::BeginChild("##colors", ImVec2(0, 0), true, ImGuiWindowFlags_AlwaysVerticalScrollbar | ImGuiWindowFlags_AlwaysHorizontalScrollbar | ImGuiWindowFlags_NavFlattened); - ImGui::PushItemWidth(-160); + ImGui::SetNextWindowSizeConstraints(ImVec2(0.0f, ImGui::GetTextLineHeightWithSpacing() * 10), ImVec2(FLT_MAX, FLT_MAX)); + ImGui::BeginChild("##colors", ImVec2(0, 0), ImGuiChildFlags_Border, ImGuiWindowFlags_AlwaysVerticalScrollbar | ImGuiWindowFlags_AlwaysHorizontalScrollbar | ImGuiWindowFlags_NavFlattened); + ImGui::PushItemWidth(ImGui::GetFontSize() * -12); for (int i = 0; i < ImGuiCol_COUNT; i++) { const char* name = ImGui::GetStyleColorName(i); if (!filter.PassFilter(name)) continue; ImGui::PushID(i); +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + if (ImGui::Button("?")) + ImGui::DebugFlashStyleColor((ImGuiCol)i); + ImGui::SetItemTooltip("Flash given color to identify places where it is used."); + ImGui::SameLine(); +#endif ImGui::ColorEdit4("##color", (float*)&style.Colors[i], ImGuiColorEditFlags_AlphaBar | alpha_flags); if (memcmp(&style.Colors[i], &ref->Colors[i], sizeof(ImVec4)) != 0) { @@ -6487,7 +7021,7 @@ static void ShowExampleMenuFile() { static bool enabled = true; ImGui::MenuItem("Enabled", "", &enabled); - ImGui::BeginChild("child", ImVec2(0, 60), true); + ImGui::BeginChild("child", ImVec2(0, 60), ImGuiChildFlags_Border); for (int i = 0; i < 10; i++) ImGui::Text("Scrolling Text %d", i); ImGui::EndChild(); @@ -6572,19 +7106,19 @@ struct ExampleAppConsole { ClearLog(); for (int i = 0; i < History.Size; i++) - free(History[i]); + ImGui::MemFree(History[i]); } // Portable helpers static int Stricmp(const char* s1, const char* s2) { int d; while ((d = toupper(*s2) - toupper(*s1)) == 0 && *s1) { s1++; s2++; } return d; } static int Strnicmp(const char* s1, const char* s2, int n) { int d = 0; while (n > 0 && (d = toupper(*s2) - toupper(*s1)) == 0 && *s1) { s1++; s2++; n--; } return d; } - static char* Strdup(const char* s) { IM_ASSERT(s); size_t len = strlen(s) + 1; void* buf = malloc(len); IM_ASSERT(buf); return (char*)memcpy(buf, (const void*)s, len); } + static char* Strdup(const char* s) { IM_ASSERT(s); size_t len = strlen(s) + 1; void* buf = ImGui::MemAlloc(len); IM_ASSERT(buf); return (char*)memcpy(buf, (const void*)s, len); } static void Strtrim(char* s) { char* str_end = s + strlen(s); while (str_end > s && str_end[-1] == ' ') str_end--; *str_end = 0; } void ClearLog() { for (int i = 0; i < Items.Size; i++) - free(Items[i]); + ImGui::MemFree(Items[i]); Items.clear(); } @@ -6653,7 +7187,7 @@ struct ExampleAppConsole // Reserve enough left-over height for 1 separator + 1 input text const float footer_height_to_reserve = ImGui::GetStyle().ItemSpacing.y + ImGui::GetFrameHeightWithSpacing(); - if (ImGui::BeginChild("ScrollingRegion", ImVec2(0, -footer_height_to_reserve), false, ImGuiWindowFlags_HorizontalScrollbar)) + if (ImGui::BeginChild("ScrollingRegion", ImVec2(0, -footer_height_to_reserve), ImGuiChildFlags_None, ImGuiWindowFlags_HorizontalScrollbar)) { if (ImGui::BeginPopupContextWindow()) { @@ -6688,9 +7222,8 @@ struct ExampleAppConsole ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4, 1)); // Tighten spacing if (copy_to_clipboard) ImGui::LogToClipboard(); - for (int i = 0; i < Items.Size; i++) + for (const char* item : Items) { - const char* item = Items[i]; if (!Filter.PassFilter(item)) continue; @@ -6751,7 +7284,7 @@ struct ExampleAppConsole for (int i = History.Size - 1; i >= 0; i--) if (Stricmp(History[i], command_line) == 0) { - free(History[i]); + ImGui::MemFree(History[i]); History.erase(History.begin() + i); break; } @@ -6965,7 +7498,7 @@ struct ExampleAppLog ImGui::Separator(); - if (ImGui::BeginChild("scrolling", ImVec2(0, 0), false, ImGuiWindowFlags_HorizontalScrollbar)) + if (ImGui::BeginChild("scrolling", ImVec2(0, 0), ImGuiChildFlags_None, ImGuiWindowFlags_HorizontalScrollbar)) { if (clear) Clear(); @@ -7084,7 +7617,7 @@ static void ShowExampleAppLayout(bool* p_open) // Left static int selected = 0; { - ImGui::BeginChild("left pane", ImVec2(150, 0), true); + ImGui::BeginChild("left pane", ImVec2(150, 0), ImGuiChildFlags_Border | ImGuiChildFlags_ResizeX); for (int i = 0; i < 100; i++) { // FIXME: Good candidate to use ImGuiSelectableFlags_SelectOnNav @@ -7179,6 +7712,7 @@ static void ShowPlaceholderObject(const char* prefix, int uid) } // Demonstrate create a simple property editor. +// This demo is a bit lackluster nowadays, would be nice to improve. static void ShowExampleAppPropertyEditor(bool* p_open) { ImGui::SetNextWindowSize(ImVec2(430, 450), ImGuiCond_FirstUseEver); @@ -7187,23 +7721,24 @@ static void ShowExampleAppPropertyEditor(bool* p_open) ImGui::End(); return; } - IMGUI_DEMO_MARKER("Examples/Property Editor"); + IMGUI_DEMO_MARKER("Examples/Property Editor"); HelpMarker( "This example shows how you may implement a property editor using two columns.\n" - "All objects/fields data are dummies here.\n" - "Remember that in many simple cases, you can use ImGui::SameLine(xxx) to position\n" - "your cursor horizontally instead of using the Columns() API."); + "All objects/fields data are dummies here.\n"); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2, 2)); - if (ImGui::BeginTable("split", 2, ImGuiTableFlags_BordersOuter | ImGuiTableFlags_Resizable)) + if (ImGui::BeginTable("##split", 2, ImGuiTableFlags_BordersOuter | ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY)) { + ImGui::TableSetupScrollFreeze(0, 1); + ImGui::TableSetupColumn("Object"); + ImGui::TableSetupColumn("Contents"); + ImGui::TableHeadersRow(); + // Iterate placeholder objects (all the same data) for (int obj_i = 0; obj_i < 4; obj_i++) - { ShowPlaceholderObject("Object", obj_i); - //ImGui::Separator(); - } + ImGui::EndTable(); } ImGui::PopStyleVar(); @@ -7310,18 +7845,31 @@ static void ShowExampleAppConstrainedResize(bool* p_open) { // Helper functions to demonstrate programmatic constraints // FIXME: This doesn't take account of decoration size (e.g. title bar), library should make this easier. - static void AspectRatio(ImGuiSizeCallbackData* data) { float aspect_ratio = *(float*)data->UserData; data->DesiredSize.x = IM_MAX(data->CurrentSize.x, data->CurrentSize.y); data->DesiredSize.y = (float)(int)(data->DesiredSize.x / aspect_ratio); } - static void Square(ImGuiSizeCallbackData* data) { data->DesiredSize.x = data->DesiredSize.y = IM_MAX(data->CurrentSize.x, data->CurrentSize.y); } - static void Step(ImGuiSizeCallbackData* data) { float step = *(float*)data->UserData; data->DesiredSize = ImVec2((int)(data->CurrentSize.x / step + 0.5f) * step, (int)(data->CurrentSize.y / step + 0.5f) * step); } + // FIXME: None of the three demos works consistently when resizing from borders. + static void AspectRatio(ImGuiSizeCallbackData* data) + { + float aspect_ratio = *(float*)data->UserData; + data->DesiredSize.y = (float)(int)(data->DesiredSize.x / aspect_ratio); + } + static void Square(ImGuiSizeCallbackData* data) + { + data->DesiredSize.x = data->DesiredSize.y = IM_MAX(data->DesiredSize.x, data->DesiredSize.y); + } + static void Step(ImGuiSizeCallbackData* data) + { + float step = *(float*)data->UserData; + data->DesiredSize = ImVec2((int)(data->DesiredSize.x / step + 0.5f) * step, (int)(data->DesiredSize.y / step + 0.5f) * step); + } }; const char* test_desc[] = { "Between 100x100 and 500x500", "At least 100x100", - "Resize vertical only", - "Resize horizontal only", + "Resize vertical + lock current width", + "Resize horizontal + lock current height", "Width Between 400 and 500", + "Height at least 400", "Custom: Aspect Ratio 16:9", "Custom: Always Square", "Custom: Fixed Steps (100)", @@ -7330,7 +7878,7 @@ static void ShowExampleAppConstrainedResize(bool* p_open) // Options static bool auto_resize = false; static bool window_padding = true; - static int type = 5; // Aspect Ratio + static int type = 6; // Aspect Ratio static int display_lines = 10; // Submit constraint @@ -7338,12 +7886,13 @@ static void ShowExampleAppConstrainedResize(bool* p_open) float fixed_step = 100.0f; if (type == 0) ImGui::SetNextWindowSizeConstraints(ImVec2(100, 100), ImVec2(500, 500)); // Between 100x100 and 500x500 if (type == 1) ImGui::SetNextWindowSizeConstraints(ImVec2(100, 100), ImVec2(FLT_MAX, FLT_MAX)); // Width > 100, Height > 100 - if (type == 2) ImGui::SetNextWindowSizeConstraints(ImVec2(-1, 0), ImVec2(-1, FLT_MAX)); // Vertical only - if (type == 3) ImGui::SetNextWindowSizeConstraints(ImVec2(0, -1), ImVec2(FLT_MAX, -1)); // Horizontal only + if (type == 2) ImGui::SetNextWindowSizeConstraints(ImVec2(-1, 0), ImVec2(-1, FLT_MAX)); // Resize vertical + lock current width + if (type == 3) ImGui::SetNextWindowSizeConstraints(ImVec2(0, -1), ImVec2(FLT_MAX, -1)); // Resize horizontal + lock current height if (type == 4) ImGui::SetNextWindowSizeConstraints(ImVec2(400, -1), ImVec2(500, -1)); // Width Between and 400 and 500 - if (type == 5) ImGui::SetNextWindowSizeConstraints(ImVec2(0, 0), ImVec2(FLT_MAX, FLT_MAX), CustomConstraints::AspectRatio, (void*)&aspect_ratio); // Aspect ratio - if (type == 6) ImGui::SetNextWindowSizeConstraints(ImVec2(0, 0), ImVec2(FLT_MAX, FLT_MAX), CustomConstraints::Square); // Always Square - if (type == 7) ImGui::SetNextWindowSizeConstraints(ImVec2(0, 0), ImVec2(FLT_MAX, FLT_MAX), CustomConstraints::Step, (void*)&fixed_step); // Fixed Step + if (type == 5) ImGui::SetNextWindowSizeConstraints(ImVec2(-1, 500), ImVec2(-1, FLT_MAX)); // Height at least 400 + if (type == 6) ImGui::SetNextWindowSizeConstraints(ImVec2(0, 0), ImVec2(FLT_MAX, FLT_MAX), CustomConstraints::AspectRatio, (void*)&aspect_ratio); // Aspect ratio + if (type == 7) ImGui::SetNextWindowSizeConstraints(ImVec2(0, 0), ImVec2(FLT_MAX, FLT_MAX), CustomConstraints::Square); // Always Square + if (type == 8) ImGui::SetNextWindowSizeConstraints(ImVec2(0, 0), ImVec2(FLT_MAX, FLT_MAX), CustomConstraints::Step, (void*)&fixed_step); // Fixed Step // Submit window if (!window_padding) @@ -7367,6 +7916,8 @@ static void ShowExampleAppConstrainedResize(bool* p_open) else { ImGui::Text("(Hold SHIFT to display a dummy viewport)"); + if (ImGui::IsWindowDocked()) + ImGui::Text("Warning: Sizing Constraints won't work if the window is docked!"); if (ImGui::Button("Set 200x200")) { ImGui::SetWindowSize(ImVec2(200, 200)); } ImGui::SameLine(); if (ImGui::Button("Set 500x500")) { ImGui::SetWindowSize(ImVec2(500, 500)); } ImGui::SameLine(); if (ImGui::Button("Set 800x200")) { ImGui::SetWindowSize(ImVec2(800, 200)); } @@ -7393,7 +7944,7 @@ static void ShowExampleAppSimpleOverlay(bool* p_open) { static int location = 0; ImGuiIO& io = ImGui::GetIO(); - ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav; + ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav; if (location >= 0) { const float PAD = 10.0f; @@ -7406,6 +7957,7 @@ static void ShowExampleAppSimpleOverlay(bool* p_open) window_pos_pivot.x = (location & 1) ? 1.0f : 0.0f; window_pos_pivot.y = (location & 2) ? 1.0f : 0.0f; ImGui::SetNextWindowPos(window_pos, ImGuiCond_Always, window_pos_pivot); + ImGui::SetNextWindowViewport(viewport->ID); window_flags |= ImGuiWindowFlags_NoMove; } else if (location == -2) @@ -7587,6 +8139,9 @@ static void ShowExampleAppCustomRendering(bool* p_open) const float rounding = sz / 5.0f; const int circle_segments = circle_segments_override ? circle_segments_override_v : 0; const int curve_segments = curve_segments_override ? curve_segments_override_v : 0; + const ImVec2 cp3[3] = { ImVec2(0.0f, sz * 0.6f), ImVec2(sz * 0.5f, -sz * 0.4f), ImVec2(sz, sz) }; // Control points for curves + const ImVec2 cp4[4] = { ImVec2(0.0f, 0.0f), ImVec2(sz * 1.3f, sz * 0.3f), ImVec2(sz - sz * 1.3f, sz - sz * 0.3f), ImVec2(sz, sz) }; + float x = p.x + 4.0f; float y = p.y + 4.0f; for (int n = 0; n < 2; n++) @@ -7595,6 +8150,7 @@ static void ShowExampleAppCustomRendering(bool* p_open) float th = (n == 0) ? 1.0f : thickness; draw_list->AddNgon(ImVec2(x + sz*0.5f, y + sz*0.5f), sz*0.5f, col, ngon_sides, th); x += sz + spacing; // N-gon draw_list->AddCircle(ImVec2(x + sz*0.5f, y + sz*0.5f), sz*0.5f, col, circle_segments, th); x += sz + spacing; // Circle + draw_list->AddEllipse(ImVec2(x + sz*0.5f, y + sz*0.5f), sz*0.5f, sz*0.3f, col, -0.3f, circle_segments, th); x += sz + spacing; // Ellipse draw_list->AddRect(ImVec2(x, y), ImVec2(x + sz, y + sz), col, 0.0f, ImDrawFlags_None, th); x += sz + spacing; // Square draw_list->AddRect(ImVec2(x, y), ImVec2(x + sz, y + sz), col, rounding, ImDrawFlags_None, th); x += sz + spacing; // Square with all rounded corners draw_list->AddRect(ImVec2(x, y), ImVec2(x + sz, y + sz), col, rounding, corners_tl_br, th); x += sz + spacing; // Square with two rounded corners @@ -7604,19 +8160,26 @@ static void ShowExampleAppCustomRendering(bool* p_open) draw_list->AddLine(ImVec2(x, y), ImVec2(x, y + sz), col, th); x += spacing; // Vertical line (note: drawing a filled rectangle will be faster!) draw_list->AddLine(ImVec2(x, y), ImVec2(x + sz, y + sz), col, th); x += sz + spacing; // Diagonal line + // Path + draw_list->PathArcTo(ImVec2(x + sz*0.5f, y + sz*0.5f), sz*0.5f, 3.141592f, 3.141592f * -0.5f); + draw_list->PathStroke(col, ImDrawFlags_None, th); + x += sz + spacing; + // Quadratic Bezier Curve (3 control points) - ImVec2 cp3[3] = { ImVec2(x, y + sz * 0.6f), ImVec2(x + sz * 0.5f, y - sz * 0.4f), ImVec2(x + sz, y + sz) }; - draw_list->AddBezierQuadratic(cp3[0], cp3[1], cp3[2], col, th, curve_segments); x += sz + spacing; + draw_list->AddBezierQuadratic(ImVec2(x + cp3[0].x, y + cp3[0].y), ImVec2(x + cp3[1].x, y + cp3[1].y), ImVec2(x + cp3[2].x, y + cp3[2].y), col, th, curve_segments); + x += sz + spacing; // Cubic Bezier Curve (4 control points) - ImVec2 cp4[4] = { ImVec2(x, y), ImVec2(x + sz * 1.3f, y + sz * 0.3f), ImVec2(x + sz - sz * 1.3f, y + sz - sz * 0.3f), ImVec2(x + sz, y + sz) }; - draw_list->AddBezierCubic(cp4[0], cp4[1], cp4[2], cp4[3], col, th, curve_segments); + draw_list->AddBezierCubic(ImVec2(x + cp4[0].x, y + cp4[0].y), ImVec2(x + cp4[1].x, y + cp4[1].y), ImVec2(x + cp4[2].x, y + cp4[2].y), ImVec2(x + cp4[3].x, y + cp4[3].y), col, th, curve_segments); x = p.x + 4; y += sz + spacing; } - draw_list->AddNgonFilled(ImVec2(x + sz * 0.5f, y + sz * 0.5f), sz*0.5f, col, ngon_sides); x += sz + spacing; // N-gon - draw_list->AddCircleFilled(ImVec2(x + sz*0.5f, y + sz*0.5f), sz*0.5f, col, circle_segments); x += sz + spacing; // Circle + + // Filled shapes + draw_list->AddNgonFilled(ImVec2(x + sz * 0.5f, y + sz * 0.5f), sz * 0.5f, col, ngon_sides); x += sz + spacing; // N-gon + draw_list->AddCircleFilled(ImVec2(x + sz * 0.5f, y + sz * 0.5f), sz * 0.5f, col, circle_segments); x += sz + spacing; // Circle + draw_list->AddEllipseFilled(ImVec2(x + sz * 0.5f, y + sz * 0.5f), sz * 0.5f, sz * 0.3f, col, -0.3f, circle_segments); x += sz + spacing;// Ellipse draw_list->AddRectFilled(ImVec2(x, y), ImVec2(x + sz, y + sz), col); x += sz + spacing; // Square draw_list->AddRectFilled(ImVec2(x, y), ImVec2(x + sz, y + sz), col, 10.0f); x += sz + spacing; // Square with all rounded corners draw_list->AddRectFilled(ImVec2(x, y), ImVec2(x + sz, y + sz), col, 10.0f, corners_tl_br); x += sz + spacing; // Square with two rounded corners @@ -7625,9 +8188,27 @@ static void ShowExampleAppCustomRendering(bool* p_open) draw_list->AddRectFilled(ImVec2(x, y), ImVec2(x + sz, y + thickness), col); x += sz + spacing; // Horizontal line (faster than AddLine, but only handle integer thickness) draw_list->AddRectFilled(ImVec2(x, y), ImVec2(x + thickness, y + sz), col); x += spacing * 2.0f;// Vertical line (faster than AddLine, but only handle integer thickness) draw_list->AddRectFilled(ImVec2(x, y), ImVec2(x + 1, y + 1), col); x += sz; // Pixel (faster than AddLine) + + // Path + draw_list->PathArcTo(ImVec2(x + sz * 0.5f, y + sz * 0.5f), sz * 0.5f, 3.141592f * -0.5f, 3.141592f); + draw_list->PathFillConvex(col); + x += sz + spacing; + + // Quadratic Bezier Curve (3 control points) + draw_list->PathLineTo(ImVec2(x + cp3[0].x, y + cp3[0].y)); + draw_list->PathBezierQuadraticCurveTo(ImVec2(x + cp3[1].x, y + cp3[1].y), ImVec2(x + cp3[2].x, y + cp3[2].y), curve_segments); + draw_list->PathFillConvex(col); + x += sz + spacing; + + // Cubic Bezier Curve (4 control points): this is concave so not drawing it yet + //draw_list->PathLineTo(ImVec2(x + cp4[0].x, y + cp4[0].y)); + //draw_list->PathBezierCubicCurveTo(ImVec2(x + cp4[1].x, y + cp4[1].y), ImVec2(x + cp4[2].x, y + cp4[2].y), ImVec2(x + cp4[3].x, y + cp4[3].y), curve_segments); + //draw_list->PathFillConvex(col); + //x += sz + spacing; + draw_list->AddRectFilledMultiColor(ImVec2(x, y), ImVec2(x + sz, y + sz), IM_COL32(0, 0, 0, 255), IM_COL32(255, 0, 0, 255), IM_COL32(255, 255, 0, 255), IM_COL32(0, 255, 0, 255)); - ImGui::Dummy(ImVec2((sz + spacing) * 10.2f, (sz + spacing) * 3.0f)); + ImGui::Dummy(ImVec2((sz + spacing) * 12.2f, (sz + spacing) * 3.0f)); ImGui::PopItemWidth(); ImGui::EndTabItem(); } @@ -7649,7 +8230,7 @@ static void ShowExampleAppCustomRendering(bool* p_open) // To use a child window instead we could use, e.g: // ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); // Disable padding // ImGui::PushStyleColor(ImGuiCol_ChildBg, IM_COL32(50, 50, 50, 255)); // Set a background color - // ImGui::BeginChild("canvas", ImVec2(0.0f, 0.0f), true, ImGuiWindowFlags_NoMove); + // ImGui::BeginChild("canvas", ImVec2(0.0f, 0.0f), ImGuiChildFlags_Border, ImGuiWindowFlags_NoMove); // ImGui::PopStyleColor(); // ImGui::PopStyleVar(); // [...] @@ -7747,12 +8328,177 @@ static void ShowExampleAppCustomRendering(bool* p_open) ImGui::EndTabItem(); } + // Demonstrate out-of-order rendering via channels splitting + // We use functions in ImDrawList as each draw list contains a convenience splitter, + // but you can also instantiate your own ImDrawListSplitter if you need to nest them. + if (ImGui::BeginTabItem("Draw Channels")) + { + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + { + ImGui::Text("Blue shape is drawn first: appears in back"); + ImGui::Text("Red shape is drawn after: appears in front"); + ImVec2 p0 = ImGui::GetCursorScreenPos(); + draw_list->AddRectFilled(ImVec2(p0.x, p0.y), ImVec2(p0.x + 50, p0.y + 50), IM_COL32(0, 0, 255, 255)); // Blue + draw_list->AddRectFilled(ImVec2(p0.x + 25, p0.y + 25), ImVec2(p0.x + 75, p0.y + 75), IM_COL32(255, 0, 0, 255)); // Red + ImGui::Dummy(ImVec2(75, 75)); + } + ImGui::Separator(); + { + ImGui::Text("Blue shape is drawn first, into channel 1: appears in front"); + ImGui::Text("Red shape is drawn after, into channel 0: appears in back"); + ImVec2 p1 = ImGui::GetCursorScreenPos(); + + // Create 2 channels and draw a Blue shape THEN a Red shape. + // You can create any number of channels. Tables API use 1 channel per column in order to better batch draw calls. + draw_list->ChannelsSplit(2); + draw_list->ChannelsSetCurrent(1); + draw_list->AddRectFilled(ImVec2(p1.x, p1.y), ImVec2(p1.x + 50, p1.y + 50), IM_COL32(0, 0, 255, 255)); // Blue + draw_list->ChannelsSetCurrent(0); + draw_list->AddRectFilled(ImVec2(p1.x + 25, p1.y + 25), ImVec2(p1.x + 75, p1.y + 75), IM_COL32(255, 0, 0, 255)); // Red + + // Flatten/reorder channels. Red shape is in channel 0 and it appears below the Blue shape in channel 1. + // This works by copying draw indices only (vertices are not copied). + draw_list->ChannelsMerge(); + ImGui::Dummy(ImVec2(75, 75)); + ImGui::Text("After reordering, contents of channel 0 appears below channel 1."); + } + ImGui::EndTabItem(); + } + ImGui::EndTabBar(); } ImGui::End(); } +//----------------------------------------------------------------------------- +// [SECTION] Example App: Docking, DockSpace / ShowExampleAppDockSpace() +//----------------------------------------------------------------------------- + +// Demonstrate using DockSpace() to create an explicit docking node within an existing window. +// Note: You can use most Docking facilities without calling any API. You DO NOT need to call DockSpace() to use Docking! +// - Drag from window title bar or their tab to dock/undock. Hold SHIFT to disable docking. +// - Drag from window menu button (upper-left button) to undock an entire node (all windows). +// - When io.ConfigDockingWithShift == true, you instead need to hold SHIFT to enable docking. +// About dockspaces: +// - Use DockSpace() to create an explicit dock node _within_ an existing window. +// - Use DockSpaceOverViewport() to create an explicit dock node covering the screen or a specific viewport. +// This is often used with ImGuiDockNodeFlags_PassthruCentralNode. +// - Important: Dockspaces need to be submitted _before_ any window they can host. Submit it early in your frame! (*) +// - Important: Dockspaces need to be kept alive if hidden, otherwise windows docked into it will be undocked. +// e.g. if you have multiple tabs with a dockspace inside each tab: submit the non-visible dockspaces with ImGuiDockNodeFlags_KeepAliveOnly. +// (*) because of this constraint, the implicit \"Debug\" window can not be docked into an explicit DockSpace() node, +// because that window is submitted as part of the part of the NewFrame() call. An easy workaround is that you can create +// your own implicit "Debug##2" window after calling DockSpace() and leave it in the window stack for anyone to use. +void ShowExampleAppDockSpace(bool* p_open) +{ + // READ THIS !!! + // TL;DR; this demo is more complicated than what most users you would normally use. + // If we remove all options we are showcasing, this demo would become: + // void ShowExampleAppDockSpace() + // { + // ImGui::DockSpaceOverViewport(ImGui::GetMainViewport()); + // } + // In most cases you should be able to just call DockSpaceOverViewport() and ignore all the code below! + // In this specific demo, we are not using DockSpaceOverViewport() because: + // - (1) we allow the host window to be floating/moveable instead of filling the viewport (when opt_fullscreen == false) + // - (2) we allow the host window to have padding (when opt_padding == true) + // - (3) we expose many flags and need a way to have them visible. + // - (4) we have a local menu bar in the host window (vs. you could use BeginMainMenuBar() + DockSpaceOverViewport() + // in your code, but we don't here because we allow the window to be floating) + + static bool opt_fullscreen = true; + static bool opt_padding = false; + static ImGuiDockNodeFlags dockspace_flags = ImGuiDockNodeFlags_None; + + // We are using the ImGuiWindowFlags_NoDocking flag to make the parent window not dockable into, + // because it would be confusing to have two docking targets within each others. + ImGuiWindowFlags window_flags = ImGuiWindowFlags_MenuBar | ImGuiWindowFlags_NoDocking; + if (opt_fullscreen) + { + const ImGuiViewport* viewport = ImGui::GetMainViewport(); + ImGui::SetNextWindowPos(viewport->WorkPos); + ImGui::SetNextWindowSize(viewport->WorkSize); + ImGui::SetNextWindowViewport(viewport->ID); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); + window_flags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove; + window_flags |= ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoNavFocus; + } + else + { + dockspace_flags &= ~ImGuiDockNodeFlags_PassthruCentralNode; + } + + // When using ImGuiDockNodeFlags_PassthruCentralNode, DockSpace() will render our background + // and handle the pass-thru hole, so we ask Begin() to not render a background. + if (dockspace_flags & ImGuiDockNodeFlags_PassthruCentralNode) + window_flags |= ImGuiWindowFlags_NoBackground; + + // Important: note that we proceed even if Begin() returns false (aka window is collapsed). + // This is because we want to keep our DockSpace() active. If a DockSpace() is inactive, + // all active windows docked into it will lose their parent and become undocked. + // We cannot preserve the docking relationship between an active window and an inactive docking, otherwise + // any change of dockspace/settings would lead to windows being stuck in limbo and never being visible. + if (!opt_padding) + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); + ImGui::Begin("DockSpace Demo", p_open, window_flags); + if (!opt_padding) + ImGui::PopStyleVar(); + + if (opt_fullscreen) + ImGui::PopStyleVar(2); + + // Submit the DockSpace + ImGuiIO& io = ImGui::GetIO(); + if (io.ConfigFlags & ImGuiConfigFlags_DockingEnable) + { + ImGuiID dockspace_id = ImGui::GetID("MyDockSpace"); + ImGui::DockSpace(dockspace_id, ImVec2(0.0f, 0.0f), dockspace_flags); + } + else + { + ShowDockingDisabledMessage(); + } + + if (ImGui::BeginMenuBar()) + { + if (ImGui::BeginMenu("Options")) + { + // Disabling fullscreen would allow the window to be moved to the front of other windows, + // which we can't undo at the moment without finer window depth/z control. + ImGui::MenuItem("Fullscreen", NULL, &opt_fullscreen); + ImGui::MenuItem("Padding", NULL, &opt_padding); + ImGui::Separator(); + + if (ImGui::MenuItem("Flag: NoDockingOverCentralNode", "", (dockspace_flags & ImGuiDockNodeFlags_NoDockingOverCentralNode) != 0)) { dockspace_flags ^= ImGuiDockNodeFlags_NoDockingOverCentralNode; } + if (ImGui::MenuItem("Flag: NoDockingSplit", "", (dockspace_flags & ImGuiDockNodeFlags_NoDockingSplit) != 0)) { dockspace_flags ^= ImGuiDockNodeFlags_NoDockingSplit; } + if (ImGui::MenuItem("Flag: NoUndocking", "", (dockspace_flags & ImGuiDockNodeFlags_NoUndocking) != 0)) { dockspace_flags ^= ImGuiDockNodeFlags_NoUndocking; } + if (ImGui::MenuItem("Flag: NoResize", "", (dockspace_flags & ImGuiDockNodeFlags_NoResize) != 0)) { dockspace_flags ^= ImGuiDockNodeFlags_NoResize; } + if (ImGui::MenuItem("Flag: AutoHideTabBar", "", (dockspace_flags & ImGuiDockNodeFlags_AutoHideTabBar) != 0)) { dockspace_flags ^= ImGuiDockNodeFlags_AutoHideTabBar; } + if (ImGui::MenuItem("Flag: PassthruCentralNode", "", (dockspace_flags & ImGuiDockNodeFlags_PassthruCentralNode) != 0, opt_fullscreen)) { dockspace_flags ^= ImGuiDockNodeFlags_PassthruCentralNode; } + ImGui::Separator(); + + if (ImGui::MenuItem("Close", NULL, false, p_open != NULL)) + *p_open = false; + ImGui::EndMenu(); + } + HelpMarker( + "When docking is enabled, you can ALWAYS dock MOST window into another! Try it now!" "\n" + "- Drag from window title bar or their tab to dock/undock." "\n" + "- Drag from window menu button (upper-left button) to undock an entire node (all windows)." "\n" + "- Hold SHIFT to disable docking (if io.ConfigDockingWithShift == false, default)" "\n" + "- Hold SHIFT to enable docking (if io.ConfigDockingWithShift == true)" "\n" + "This demo app has nothing to do with enabling docking!" "\n\n" + "This demo app only demonstrate the use of ImGui::DockSpace() which allows you to manually create a docking node _within_ another window." "\n\n" + "Read comments in ShowExampleAppDockSpace() for more details."); + + ImGui::EndMenuBar(); + } + + ImGui::End(); +} + //----------------------------------------------------------------------------- // [SECTION] Example App: Documents Handling / ShowExampleAppDocuments() //----------------------------------------------------------------------------- @@ -7838,12 +8584,11 @@ struct ExampleAppDocuments // Note that this completely optional, and only affect tab bars with the ImGuiTabBarFlags_Reorderable flag. static void NotifyOfDocumentsClosedElsewhere(ExampleAppDocuments& app) { - for (int doc_n = 0; doc_n < app.Documents.Size; doc_n++) + for (MyDocument& doc : app.Documents) { - MyDocument* doc = &app.Documents[doc_n]; - if (!doc->Open && doc->OpenPrev) - ImGui::SetTabItemClosed(doc->Name); - doc->OpenPrev = doc->Open; + if (!doc.Open && doc.OpenPrev) + ImGui::SetTabItemClosed(doc.Name); + doc.OpenPrev = doc.Open; } } @@ -7852,11 +8597,25 @@ void ShowExampleAppDocuments(bool* p_open) static ExampleAppDocuments app; // Options + enum Target + { + Target_None, + Target_Tab, // Create documents as local tab into a local tab bar + Target_DockSpaceAndWindow // Create documents as regular windows, and create an embedded dockspace + }; + static Target opt_target = Target_Tab; static bool opt_reorderable = true; static ImGuiTabBarFlags opt_fitting_flags = ImGuiTabBarFlags_FittingPolicyDefault_; + // When (opt_target == Target_DockSpaceAndWindow) there is the possibily that one of our child Document window (e.g. "Eggplant") + // that we emit gets docked into the same spot as the parent window ("Example: Documents"). + // This would create a problematic feedback loop because selecting the "Eggplant" tab would make the "Example: Documents" tab + // not visible, which in turn would stop submitting the "Eggplant" window. + // We avoid this problem by submitting our documents window even if our parent window is not currently visible. + // Another solution may be to make the "Example: Documents" window use the ImGuiWindowFlags_NoDocking. + bool window_contents_visible = ImGui::Begin("Example: Documents", p_open, ImGuiWindowFlags_MenuBar); - if (!window_contents_visible) + if (!window_contents_visible && opt_target != Target_DockSpaceAndWindow) { ImGui::End(); return; @@ -7868,23 +8627,19 @@ void ShowExampleAppDocuments(bool* p_open) if (ImGui::BeginMenu("File")) { int open_count = 0; - for (int doc_n = 0; doc_n < app.Documents.Size; doc_n++) - open_count += app.Documents[doc_n].Open ? 1 : 0; + for (MyDocument& doc : app.Documents) + open_count += doc.Open ? 1 : 0; if (ImGui::BeginMenu("Open", open_count < app.Documents.Size)) { - for (int doc_n = 0; doc_n < app.Documents.Size; doc_n++) - { - MyDocument* doc = &app.Documents[doc_n]; - if (!doc->Open) - if (ImGui::MenuItem(doc->Name)) - doc->DoOpen(); - } + for (MyDocument& doc : app.Documents) + if (!doc.Open && ImGui::MenuItem(doc.Name)) + doc.DoOpen(); ImGui::EndMenu(); } if (ImGui::MenuItem("Close All Documents", NULL, false, open_count > 0)) - for (int doc_n = 0; doc_n < app.Documents.Size; doc_n++) - app.Documents[doc_n].DoQueueClose(); + for (MyDocument& doc : app.Documents) + doc.DoQueueClose(); if (ImGui::MenuItem("Exit", "Ctrl+F4") && p_open) *p_open = false; ImGui::EndMenu(); @@ -7895,15 +8650,21 @@ void ShowExampleAppDocuments(bool* p_open) // [Debug] List documents with one checkbox for each for (int doc_n = 0; doc_n < app.Documents.Size; doc_n++) { - MyDocument* doc = &app.Documents[doc_n]; + MyDocument& doc = app.Documents[doc_n]; if (doc_n > 0) ImGui::SameLine(); - ImGui::PushID(doc); - if (ImGui::Checkbox(doc->Name, &doc->Open)) - if (!doc->Open) - doc->DoForceClose(); + ImGui::PushID(&doc); + if (ImGui::Checkbox(doc.Name, &doc.Open)) + if (!doc.Open) + doc.DoForceClose(); ImGui::PopID(); } + ImGui::PushItemWidth(ImGui::GetFontSize() * 12); + ImGui::Combo("Output", (int*)&opt_target, "None\0TabBar+Tabs\0DockSpace+Window\0"); + ImGui::PopItemWidth(); + bool redock_all = false; + if (opt_target == Target_Tab) { ImGui::SameLine(); ImGui::Checkbox("Reorderable Tabs", &opt_reorderable); } + if (opt_target == Target_DockSpaceAndWindow) { ImGui::SameLine(); redock_all = ImGui::Button("Redock all"); } ImGui::Separator(); @@ -7917,7 +8678,8 @@ void ShowExampleAppDocuments(bool* p_open) // hole for one-frame, both in the tab-bar and in tab-contents when closing a tab/window. // The rarely used SetTabItemClosed() function is a way to notify of programmatic closure to avoid the one-frame hole. - // Submit Tab Bar and Tabs + // Tabs + if (opt_target == Target_Tab) { ImGuiTabBarFlags tab_bar_flags = (opt_fitting_flags) | (opt_reorderable ? ImGuiTabBarFlags_Reorderable : 0); if (ImGui::BeginTabBar("##tabs", tab_bar_flags)) @@ -7930,14 +8692,52 @@ void ShowExampleAppDocuments(bool* p_open) //if (ImGui::GetIO().KeyCtrl) ImGui::SetTabItemSelected(docs[1].Name); // [DEBUG] Test SetTabItemSelected(), probably not very useful as-is anyway.. // Submit Tabs + for (MyDocument& doc : app.Documents) + { + if (!doc.Open) + continue; + + ImGuiTabItemFlags tab_flags = (doc.Dirty ? ImGuiTabItemFlags_UnsavedDocument : 0); + bool visible = ImGui::BeginTabItem(doc.Name, &doc.Open, tab_flags); + + // Cancel attempt to close when unsaved add to save queue so we can display a popup. + if (!doc.Open && doc.Dirty) + { + doc.Open = true; + doc.DoQueueClose(); + } + + MyDocument::DisplayContextMenu(&doc); + if (visible) + { + MyDocument::DisplayContents(&doc); + ImGui::EndTabItem(); + } + } + + ImGui::EndTabBar(); + } + } + else if (opt_target == Target_DockSpaceAndWindow) + { + if (ImGui::GetIO().ConfigFlags & ImGuiConfigFlags_DockingEnable) + { + NotifyOfDocumentsClosedElsewhere(app); + + // Create a DockSpace node where any window can be docked + ImGuiID dockspace_id = ImGui::GetID("MyDockSpace"); + ImGui::DockSpace(dockspace_id); + + // Create Windows for (int doc_n = 0; doc_n < app.Documents.Size; doc_n++) { MyDocument* doc = &app.Documents[doc_n]; if (!doc->Open) continue; - ImGuiTabItemFlags tab_flags = (doc->Dirty ? ImGuiTabItemFlags_UnsavedDocument : 0); - bool visible = ImGui::BeginTabItem(doc->Name, &doc->Open, tab_flags); + ImGui::SetNextWindowDockID(dockspace_id, redock_all ? ImGuiCond_Always : ImGuiCond_FirstUseEver); + ImGuiWindowFlags window_flags = (doc->Dirty ? ImGuiWindowFlags_UnsavedDocument : 0); + bool visible = ImGui::Begin(doc->Name, &doc->Open, window_flags); // Cancel attempt to close when unsaved add to save queue so we can display a popup. if (!doc->Open && doc->Dirty) @@ -7948,14 +8748,22 @@ void ShowExampleAppDocuments(bool* p_open) MyDocument::DisplayContextMenu(doc); if (visible) - { MyDocument::DisplayContents(doc); - ImGui::EndTabItem(); - } - } - ImGui::EndTabBar(); + ImGui::End(); + } } + else + { + ShowDockingDisabledMessage(); + } + } + + // Early out other contents + if (!window_contents_visible) + { + ImGui::End(); + return; } // Update closing queue @@ -7963,15 +8771,12 @@ void ShowExampleAppDocuments(bool* p_open) if (close_queue.empty()) { // Close queue is locked once we started a popup - for (int doc_n = 0; doc_n < app.Documents.Size; doc_n++) - { - MyDocument* doc = &app.Documents[doc_n]; - if (doc->WantClose) + for (MyDocument& doc : app.Documents) + if (doc.WantClose) { - doc->WantClose = false; - close_queue.push_back(doc); + doc.WantClose = false; + close_queue.push_back(&doc); } - } } // Display closing confirmation UI @@ -7997,13 +8802,13 @@ void ShowExampleAppDocuments(bool* p_open) { ImGui::Text("Save change to the following items?"); float item_height = ImGui::GetTextLineHeightWithSpacing(); - if (ImGui::BeginChildFrame(ImGui::GetID("frame"), ImVec2(-FLT_MIN, 6.25f * item_height))) + if (ImGui::BeginChild(ImGui::GetID("frame"), ImVec2(-FLT_MIN, 6.25f * item_height), ImGuiChildFlags_FrameStyle)) { for (int n = 0; n < close_queue.Size; n++) if (close_queue[n]->Dirty) ImGui::Text("%s", close_queue[n]->Name); - ImGui::EndChildFrame(); } + ImGui::EndChild(); ImVec2 button_size(ImGui::GetFontSize() * 7.0f, 0.0f); if (ImGui::Button("Yes", button_size)) diff --git a/libs/imgui/imgui_draw.cpp b/libs/imgui/imgui_draw.cpp index 0280c6e..659d578 100644 --- a/libs/imgui/imgui_draw.cpp +++ b/libs/imgui/imgui_draw.cpp @@ -1,4 +1,4 @@ -// dear imgui, v1.89.6 +// dear imgui, v1.90.4 // (drawing and font code) /* @@ -63,6 +63,7 @@ Index of this file: #pragma clang diagnostic ignored "-Wreserved-id-macro" // warning: macro name is a reserved identifier #pragma clang diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function // using printf() is a misery with this as C++ va_arg ellipsis changes float to double. #pragma clang diagnostic ignored "-Wimplicit-int-float-conversion" // warning: implicit conversion from 'xxx' to 'float' may lose precision +#pragma clang diagnostic ignored "-Wreserved-identifier" // warning: identifier '_Xxx' is reserved because it starts with '_' followed by a capital letter #elif defined(__GNUC__) #pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind #pragma GCC diagnostic ignored "-Wunused-function" // warning: 'xxxx' defined but not used @@ -134,7 +135,7 @@ namespace IMGUI_STB_NAMESPACE #define STBTT_sqrt(x) ImSqrt(x) #define STBTT_pow(x,y) ImPow(x,y) #define STBTT_fabs(x) ImFabs(x) -#define STBTT_ifloor(x) ((int)ImFloorSigned(x)) +#define STBTT_ifloor(x) ((int)ImFloor(x)) #define STBTT_iceil(x) ((int)ImCeil(x)) #define STBTT_STATIC #define STB_TRUETYPE_IMPLEMENTATION @@ -213,6 +214,8 @@ void ImGui::StyleColorsDark(ImGuiStyle* dst) colors[ImGuiCol_TabActive] = ImLerp(colors[ImGuiCol_HeaderActive], colors[ImGuiCol_TitleBgActive], 0.60f); colors[ImGuiCol_TabUnfocused] = ImLerp(colors[ImGuiCol_Tab], colors[ImGuiCol_TitleBg], 0.80f); colors[ImGuiCol_TabUnfocusedActive] = ImLerp(colors[ImGuiCol_TabActive], colors[ImGuiCol_TitleBg], 0.40f); + colors[ImGuiCol_DockingPreview] = colors[ImGuiCol_HeaderActive] * ImVec4(1.0f, 1.0f, 1.0f, 0.7f); + colors[ImGuiCol_DockingEmptyBg] = ImVec4(0.20f, 0.20f, 0.20f, 1.00f); colors[ImGuiCol_PlotLines] = ImVec4(0.61f, 0.61f, 0.61f, 1.00f); colors[ImGuiCol_PlotLinesHovered] = ImVec4(1.00f, 0.43f, 0.35f, 1.00f); colors[ImGuiCol_PlotHistogram] = ImVec4(0.90f, 0.70f, 0.00f, 1.00f); @@ -273,6 +276,8 @@ void ImGui::StyleColorsClassic(ImGuiStyle* dst) colors[ImGuiCol_TabActive] = ImLerp(colors[ImGuiCol_HeaderActive], colors[ImGuiCol_TitleBgActive], 0.60f); colors[ImGuiCol_TabUnfocused] = ImLerp(colors[ImGuiCol_Tab], colors[ImGuiCol_TitleBg], 0.80f); colors[ImGuiCol_TabUnfocusedActive] = ImLerp(colors[ImGuiCol_TabActive], colors[ImGuiCol_TitleBg], 0.40f); + colors[ImGuiCol_DockingPreview] = colors[ImGuiCol_Header] * ImVec4(1.0f, 1.0f, 1.0f, 0.7f); + colors[ImGuiCol_DockingEmptyBg] = ImVec4(0.20f, 0.20f, 0.20f, 1.00f); colors[ImGuiCol_PlotLines] = ImVec4(1.00f, 1.00f, 1.00f, 1.00f); colors[ImGuiCol_PlotLinesHovered] = ImVec4(0.90f, 0.70f, 0.00f, 1.00f); colors[ImGuiCol_PlotHistogram] = ImVec4(0.90f, 0.70f, 0.00f, 1.00f); @@ -334,6 +339,8 @@ void ImGui::StyleColorsLight(ImGuiStyle* dst) colors[ImGuiCol_TabActive] = ImLerp(colors[ImGuiCol_HeaderActive], colors[ImGuiCol_TitleBgActive], 0.60f); colors[ImGuiCol_TabUnfocused] = ImLerp(colors[ImGuiCol_Tab], colors[ImGuiCol_TitleBg], 0.80f); colors[ImGuiCol_TabUnfocusedActive] = ImLerp(colors[ImGuiCol_TabActive], colors[ImGuiCol_TitleBg], 0.40f); + colors[ImGuiCol_DockingPreview] = colors[ImGuiCol_Header] * ImVec4(1.0f, 1.0f, 1.0f, 0.7f); + colors[ImGuiCol_DockingEmptyBg] = ImVec4(0.20f, 0.20f, 0.20f, 1.00f); colors[ImGuiCol_PlotLines] = ImVec4(0.39f, 0.39f, 0.39f, 1.00f); colors[ImGuiCol_PlotLinesHovered] = ImVec4(1.00f, 0.43f, 0.35f, 1.00f); colors[ImGuiCol_PlotHistogram] = ImVec4(0.90f, 0.70f, 0.00f, 1.00f); @@ -385,9 +392,9 @@ void ImDrawListSharedData::SetCircleTessellationMaxError(float max_error) void ImDrawList::_ResetForNewFrame() { // Verify that the ImDrawCmd fields we want to memcmp() are contiguous in memory. - IM_STATIC_ASSERT(IM_OFFSETOF(ImDrawCmd, ClipRect) == 0); - IM_STATIC_ASSERT(IM_OFFSETOF(ImDrawCmd, TextureId) == sizeof(ImVec4)); - IM_STATIC_ASSERT(IM_OFFSETOF(ImDrawCmd, VtxOffset) == sizeof(ImVec4) + sizeof(ImTextureID)); + IM_STATIC_ASSERT(offsetof(ImDrawCmd, ClipRect) == 0); + IM_STATIC_ASSERT(offsetof(ImDrawCmd, TextureId) == sizeof(ImVec4)); + IM_STATIC_ASSERT(offsetof(ImDrawCmd, VtxOffset) == sizeof(ImVec4) + sizeof(ImTextureID)); if (_Splitter._Count > 1) _Splitter.Merge(this); @@ -474,7 +481,7 @@ void ImDrawList::AddCallback(ImDrawCallback callback, void* callback_data) } // Compare ClipRect, TextureId and VtxOffset with a single memcmp() -#define ImDrawCmd_HeaderSize (IM_OFFSETOF(ImDrawCmd, VtxOffset) + sizeof(unsigned int)) +#define ImDrawCmd_HeaderSize (offsetof(ImDrawCmd, VtxOffset) + sizeof(unsigned int)) #define ImDrawCmd_HeaderCompare(CMD_LHS, CMD_RHS) (memcmp(CMD_LHS, CMD_RHS, ImDrawCmd_HeaderSize)) // Compare ClipRect, TextureId, VtxOffset #define ImDrawCmd_HeaderCopy(CMD_DST, CMD_SRC) (memcpy(CMD_DST, CMD_SRC, ImDrawCmd_HeaderSize)) // Copy ClipRect, TextureId, VtxOffset #define ImDrawCmd_AreSequentialIdxOffset(CMD_0, CMD_1) (CMD_0->IdxOffset + CMD_0->ElemCount == CMD_1->IdxOffset) @@ -560,7 +567,7 @@ int ImDrawList::_CalcCircleAutoSegmentCount(float radius) const { // Automatic segment count const int radius_idx = (int)(radius + 0.999999f); // ceil to never reduce accuracy - if (radius_idx < IM_ARRAYSIZE(_Data->CircleSegmentCounts)) + if (radius_idx >= 0 && radius_idx < IM_ARRAYSIZE(_Data->CircleSegmentCounts)) return _Data->CircleSegmentCounts[radius_idx]; // Use cached value else return IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC(radius, _Data->CircleSegmentMaxError); @@ -640,7 +647,7 @@ void ImDrawList::PrimReserve(int idx_count, int vtx_count) _IdxWritePtr = IdxBuffer.Data + idx_buffer_old_size; } -// Release the a number of reserved vertices/indices from the end of the last reservation made with PrimReserve(). +// Release the number of reserved vertices/indices from the end of the last reservation made with PrimReserve(). void ImDrawList::PrimUnreserve(int idx_count, int vtx_count) { IM_ASSERT_PARANOID(idx_count >= 0 && vtx_count >= 0); @@ -1190,8 +1197,8 @@ void ImDrawList::PathArcTo(const ImVec2& center, float radius, float a_min, floa const float a_min_sample_f = IM_DRAWLIST_ARCFAST_SAMPLE_MAX * a_min / (IM_PI * 2.0f); const float a_max_sample_f = IM_DRAWLIST_ARCFAST_SAMPLE_MAX * a_max / (IM_PI * 2.0f); - const int a_min_sample = a_is_reverse ? (int)ImFloorSigned(a_min_sample_f) : (int)ImCeil(a_min_sample_f); - const int a_max_sample = a_is_reverse ? (int)ImCeil(a_max_sample_f) : (int)ImFloorSigned(a_max_sample_f); + const int a_min_sample = a_is_reverse ? (int)ImFloor(a_min_sample_f) : (int)ImCeil(a_min_sample_f); + const int a_max_sample = a_is_reverse ? (int)ImCeil(a_max_sample_f) : (int)ImFloor(a_max_sample_f); const int a_mid_samples = a_is_reverse ? ImMax(a_min_sample - a_max_sample, 0) : ImMax(a_max_sample - a_min_sample, 0); const float a_min_segment_angle = a_min_sample * IM_PI * 2.0f / IM_DRAWLIST_ARCFAST_SAMPLE_MAX; @@ -1216,6 +1223,27 @@ void ImDrawList::PathArcTo(const ImVec2& center, float radius, float a_min, floa } } +void ImDrawList::PathEllipticalArcTo(const ImVec2& center, float radius_x, float radius_y, float rot, float a_min, float a_max, int num_segments) +{ + if (num_segments <= 0) + num_segments = _CalcCircleAutoSegmentCount(ImMax(radius_x, radius_y)); // A bit pessimistic, maybe there's a better computation to do here. + + _Path.reserve(_Path.Size + (num_segments + 1)); + + const float cos_rot = ImCos(rot); + const float sin_rot = ImSin(rot); + for (int i = 0; i <= num_segments; i++) + { + const float a = a_min + ((float)i / (float)num_segments) * (a_max - a_min); + ImVec2 point(ImCos(a) * radius_x, ImSin(a) * radius_y); + const float rel_x = (point.x * cos_rot) - (point.y * sin_rot); + const float rel_y = (point.x * sin_rot) + (point.y * cos_rot); + point.x = rel_x + center.x; + point.y = rel_y + center.y; + _Path.push_back(point); + } +} + ImVec2 ImBezierCubicCalc(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, float t) { float u = 1.0f - t; @@ -1311,33 +1339,22 @@ void ImDrawList::PathBezierQuadraticCurveTo(const ImVec2& p2, const ImVec2& p3, } } -IM_STATIC_ASSERT(ImDrawFlags_RoundCornersTopLeft == (1 << 4)); static inline ImDrawFlags FixRectCornerFlags(ImDrawFlags flags) { + /* + IM_STATIC_ASSERT(ImDrawFlags_RoundCornersTopLeft == (1 << 4)); #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS - // Obsoleted in 1.82 (from February 2021) - // Legacy Support for hard coded ~0 (used to be a suggested equivalent to ImDrawCornerFlags_All) - // ~0 --> ImDrawFlags_RoundCornersAll or 0 - if (flags == ~0) - return ImDrawFlags_RoundCornersAll; - - // Legacy Support for hard coded 0x01 to 0x0F (matching 15 out of 16 old flags combinations) - // 0x01 --> ImDrawFlags_RoundCornersTopLeft (VALUE 0x01 OVERLAPS ImDrawFlags_Closed but ImDrawFlags_Closed is never valid in this path!) - // 0x02 --> ImDrawFlags_RoundCornersTopRight - // 0x03 --> ImDrawFlags_RoundCornersTopLeft | ImDrawFlags_RoundCornersTopRight - // 0x04 --> ImDrawFlags_RoundCornersBotLeft - // 0x05 --> ImDrawFlags_RoundCornersTopLeft | ImDrawFlags_RoundCornersBotLeft - // ... - // 0x0F --> ImDrawFlags_RoundCornersAll or 0 - // (See all values in ImDrawCornerFlags_) - if (flags >= 0x01 && flags <= 0x0F) - return (flags << 4); - + // Obsoleted in 1.82 (from February 2021). This code was stripped/simplified and mostly commented in 1.90 (from September 2023) + // - Legacy Support for hard coded ~0 (used to be a suggested equivalent to ImDrawCornerFlags_All) + if (flags == ~0) { return ImDrawFlags_RoundCornersAll; } + // - Legacy Support for hard coded 0x01 to 0x0F (matching 15 out of 16 old flags combinations). Read details in older version of this code. + if (flags >= 0x01 && flags <= 0x0F) { return (flags << 4); } // We cannot support hard coded 0x00 with 'float rounding > 0.0f' --> replace with ImDrawFlags_RoundCornersNone or use 'float rounding = 0.0f' #endif - - // If this triggers, please update your code replacing hardcoded values with new ImDrawFlags_RoundCorners* values. - // Note that ImDrawFlags_Closed (== 0x01) is an invalid flag for AddRect(), AddRectFilled(), PathRect() etc... + */ + // If this assert triggers, please update your code replacing hardcoded values with new ImDrawFlags_RoundCorners* values. + // Note that ImDrawFlags_Closed (== 0x01) is an invalid flag for AddRect(), AddRectFilled(), PathRect() etc. anyway. + // See details in 1.82 Changelog as well as 2021/03/12 and 2023/09/08 entries in "API BREAKING CHANGES" section. IM_ASSERT((flags & 0x0F) == 0 && "Misuse of legacy hardcoded ImDrawCornerFlags values!"); if ((flags & ImDrawFlags_RoundCornersMask_) == 0) @@ -1348,10 +1365,12 @@ static inline ImDrawFlags FixRectCornerFlags(ImDrawFlags flags) void ImDrawList::PathRect(const ImVec2& a, const ImVec2& b, float rounding, ImDrawFlags flags) { - flags = FixRectCornerFlags(flags); - rounding = ImMin(rounding, ImFabs(b.x - a.x) * ( ((flags & ImDrawFlags_RoundCornersTop) == ImDrawFlags_RoundCornersTop) || ((flags & ImDrawFlags_RoundCornersBottom) == ImDrawFlags_RoundCornersBottom) ? 0.5f : 1.0f ) - 1.0f); - rounding = ImMin(rounding, ImFabs(b.y - a.y) * ( ((flags & ImDrawFlags_RoundCornersLeft) == ImDrawFlags_RoundCornersLeft) || ((flags & ImDrawFlags_RoundCornersRight) == ImDrawFlags_RoundCornersRight) ? 0.5f : 1.0f ) - 1.0f); - + if (rounding >= 0.5f) + { + flags = FixRectCornerFlags(flags); + rounding = ImMin(rounding, ImFabs(b.x - a.x) * (((flags & ImDrawFlags_RoundCornersTop) == ImDrawFlags_RoundCornersTop) || ((flags & ImDrawFlags_RoundCornersBottom) == ImDrawFlags_RoundCornersBottom) ? 0.5f : 1.0f) - 1.0f); + rounding = ImMin(rounding, ImFabs(b.y - a.y) * (((flags & ImDrawFlags_RoundCornersLeft) == ImDrawFlags_RoundCornersLeft) || ((flags & ImDrawFlags_RoundCornersRight) == ImDrawFlags_RoundCornersRight) ? 0.5f : 1.0f) - 1.0f); + } if (rounding < 0.5f || (flags & ImDrawFlags_RoundCornersMask_) == ImDrawFlags_RoundCornersNone) { PathLineTo(a); @@ -1544,6 +1563,35 @@ void ImDrawList::AddNgonFilled(const ImVec2& center, float radius, ImU32 col, in PathFillConvex(col); } +// Ellipse +void ImDrawList::AddEllipse(const ImVec2& center, float radius_x, float radius_y, ImU32 col, float rot, int num_segments, float thickness) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + if (num_segments <= 0) + num_segments = _CalcCircleAutoSegmentCount(ImMax(radius_x, radius_y)); // A bit pessimistic, maybe there's a better computation to do here. + + // Because we are filling a closed shape we remove 1 from the count of segments/points + const float a_max = IM_PI * 2.0f * ((float)num_segments - 1.0f) / (float)num_segments; + PathEllipticalArcTo(center, radius_x, radius_y, rot, 0.0f, a_max, num_segments - 1); + PathStroke(col, true, thickness); +} + +void ImDrawList::AddEllipseFilled(const ImVec2& center, float radius_x, float radius_y, ImU32 col, float rot, int num_segments) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + if (num_segments <= 0) + num_segments = _CalcCircleAutoSegmentCount(ImMax(radius_x, radius_y)); // A bit pessimistic, maybe there's a better computation to do here. + + // Because we are filling a closed shape we remove 1 from the count of segments/points + const float a_max = IM_PI * 2.0f * ((float)num_segments - 1.0f) / (float)num_segments; + PathEllipticalArcTo(center, radius_x, radius_y, rot, 0.0f, a_max, num_segments - 1); + PathFillConvex(col); +} + // Cubic Bezier takes 4 controls points void ImDrawList::AddBezierCubic(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, ImU32 col, float thickness, int num_segments) { @@ -1808,6 +1856,63 @@ void ImDrawListSplitter::SetCurrentChannel(ImDrawList* draw_list, int idx) // [SECTION] ImDrawData //----------------------------------------------------------------------------- +void ImDrawData::Clear() +{ + Valid = false; + CmdListsCount = TotalIdxCount = TotalVtxCount = 0; + CmdLists.resize(0); // The ImDrawList are NOT owned by ImDrawData but e.g. by ImGuiContext, so we don't clear them. + DisplayPos = DisplaySize = FramebufferScale = ImVec2(0.0f, 0.0f); + OwnerViewport = NULL; +} + +// Important: 'out_list' is generally going to be draw_data->CmdLists, but may be another temporary list +// as long at it is expected that the result will be later merged into draw_data->CmdLists[]. +void ImGui::AddDrawListToDrawDataEx(ImDrawData* draw_data, ImVector* out_list, ImDrawList* draw_list) +{ + if (draw_list->CmdBuffer.Size == 0) + return; + if (draw_list->CmdBuffer.Size == 1 && draw_list->CmdBuffer[0].ElemCount == 0 && draw_list->CmdBuffer[0].UserCallback == NULL) + return; + + // Draw list sanity check. Detect mismatch between PrimReserve() calls and incrementing _VtxCurrentIdx, _VtxWritePtr etc. + // May trigger for you if you are using PrimXXX functions incorrectly. + IM_ASSERT(draw_list->VtxBuffer.Size == 0 || draw_list->_VtxWritePtr == draw_list->VtxBuffer.Data + draw_list->VtxBuffer.Size); + IM_ASSERT(draw_list->IdxBuffer.Size == 0 || draw_list->_IdxWritePtr == draw_list->IdxBuffer.Data + draw_list->IdxBuffer.Size); + if (!(draw_list->Flags & ImDrawListFlags_AllowVtxOffset)) + IM_ASSERT((int)draw_list->_VtxCurrentIdx == draw_list->VtxBuffer.Size); + + // Check that draw_list doesn't use more vertices than indexable (default ImDrawIdx = unsigned short = 2 bytes = 64K vertices per ImDrawList = per window) + // If this assert triggers because you are drawing lots of stuff manually: + // - First, make sure you are coarse clipping yourself and not trying to draw many things outside visible bounds. + // Be mindful that the lower-level ImDrawList API doesn't filter vertices. Use the Metrics/Debugger window to inspect draw list contents. + // - If you want large meshes with more than 64K vertices, you can either: + // (A) Handle the ImDrawCmd::VtxOffset value in your renderer backend, and set 'io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset'. + // Most example backends already support this from 1.71. Pre-1.71 backends won't. + // Some graphics API such as GL ES 1/2 don't have a way to offset the starting vertex so it is not supported for them. + // (B) Or handle 32-bit indices in your renderer backend, and uncomment '#define ImDrawIdx unsigned int' line in imconfig.h. + // Most example backends already support this. For example, the OpenGL example code detect index size at compile-time: + // glDrawElements(GL_TRIANGLES, (GLsizei)pcmd->ElemCount, sizeof(ImDrawIdx) == 2 ? GL_UNSIGNED_SHORT : GL_UNSIGNED_INT, idx_buffer_offset); + // Your own engine or render API may use different parameters or function calls to specify index sizes. + // 2 and 4 bytes indices are generally supported by most graphics API. + // - If for some reason neither of those solutions works for you, a workaround is to call BeginChild()/EndChild() before reaching + // the 64K limit to split your draw commands in multiple draw lists. + if (sizeof(ImDrawIdx) == 2) + IM_ASSERT(draw_list->_VtxCurrentIdx < (1 << 16) && "Too many vertices in ImDrawList using 16-bit indices. Read comment above"); + + // Add to output list + records state in ImDrawData + out_list->push_back(draw_list); + draw_data->CmdListsCount++; + draw_data->TotalVtxCount += draw_list->VtxBuffer.Size; + draw_data->TotalIdxCount += draw_list->IdxBuffer.Size; +} + +void ImDrawData::AddDrawList(ImDrawList* draw_list) +{ + IM_ASSERT(CmdLists.Size == CmdListsCount); + draw_list->_PopUnusedDrawCmd(); + ImGui::AddDrawListToDrawDataEx(this, &CmdLists, draw_list); +} + // For backward compatibility: convert all buffers from indexed to de-indexed, in case you cannot render indexed. Note: this is slow and most likely a waste of resources. Always prefer indexed rendering! void ImDrawData::DeIndexAllBuffers() { @@ -1832,15 +1937,9 @@ void ImDrawData::DeIndexAllBuffers() // or if there is a difference between your window resolution and framebuffer resolution. void ImDrawData::ScaleClipRects(const ImVec2& fb_scale) { - for (int i = 0; i < CmdListsCount; i++) - { - ImDrawList* cmd_list = CmdLists[i]; - for (int cmd_i = 0; cmd_i < cmd_list->CmdBuffer.Size; cmd_i++) - { - ImDrawCmd* cmd = &cmd_list->CmdBuffer[cmd_i]; - cmd->ClipRect = ImVec4(cmd->ClipRect.x * fb_scale.x, cmd->ClipRect.y * fb_scale.y, cmd->ClipRect.z * fb_scale.x, cmd->ClipRect.w * fb_scale.y); - } - } + for (ImDrawList* draw_list : CmdLists) + for (ImDrawCmd& cmd : draw_list->CmdBuffer) + cmd.ClipRect = ImVec4(cmd.ClipRect.x * fb_scale.x, cmd.ClipRect.y * fb_scale.y, cmd.ClipRect.z * fb_scale.x, cmd.ClipRect.w * fb_scale.y); } //----------------------------------------------------------------------------- @@ -1896,6 +1995,14 @@ void ImGui::ShadeVertsLinearUV(ImDrawList* draw_list, int vert_start_idx, int ve } } +void ImGui::ShadeVertsTransformPos(ImDrawList* draw_list, int vert_start_idx, int vert_end_idx, const ImVec2& pivot_in, float cos_a, float sin_a, const ImVec2& pivot_out) +{ + ImDrawVert* vert_start = draw_list->VtxBuffer.Data + vert_start_idx; + ImDrawVert* vert_end = draw_list->VtxBuffer.Data + vert_end_idx; + for (ImDrawVert* vertex = vert_start; vertex < vert_end; ++vertex) + vertex->pos = ImRotate(vertex->pos- pivot_in, cos_a, sin_a) + pivot_out; +} + //----------------------------------------------------------------------------- // [SECTION] ImFontConfig //----------------------------------------------------------------------------- @@ -1904,10 +2011,11 @@ ImFontConfig::ImFontConfig() { memset(this, 0, sizeof(*this)); FontDataOwnedByAtlas = true; - OversampleH = 3; // FIXME: 2 may be a better default? + OversampleH = 2; OversampleV = 1; GlyphMaxAdvanceX = FLT_MAX; RasterizerMultiply = 1.0f; + RasterizerDensity = 1.0f; EllipsisChar = (ImWchar)-1; } @@ -1981,19 +2089,19 @@ ImFontAtlas::~ImFontAtlas() void ImFontAtlas::ClearInputData() { IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas between NewFrame() and EndFrame/Render()!"); - for (int i = 0; i < ConfigData.Size; i++) - if (ConfigData[i].FontData && ConfigData[i].FontDataOwnedByAtlas) + for (ImFontConfig& font_cfg : ConfigData) + if (font_cfg.FontData && font_cfg.FontDataOwnedByAtlas) { - IM_FREE(ConfigData[i].FontData); - ConfigData[i].FontData = NULL; + IM_FREE(font_cfg.FontData); + font_cfg.FontData = NULL; } // When clearing this we lose access to the font name and other information used to build the font. - for (int i = 0; i < Fonts.Size; i++) - if (Fonts[i]->ConfigData >= ConfigData.Data && Fonts[i]->ConfigData < ConfigData.Data + ConfigData.Size) + for (ImFont* font : Fonts) + if (font->ConfigData >= ConfigData.Data && font->ConfigData < ConfigData.Data + ConfigData.Size) { - Fonts[i]->ConfigData = NULL; - Fonts[i]->ConfigDataCount = 0; + font->ConfigData = NULL; + font->ConfigDataCount = 0; } ConfigData.clear(); CustomRects.clear(); @@ -2090,6 +2198,8 @@ ImFont* ImFontAtlas::AddFont(const ImFontConfig* font_cfg) if (new_font_cfg.DstFont->EllipsisChar == (ImWchar)-1) new_font_cfg.DstFont->EllipsisChar = font_cfg->EllipsisChar; + ImFontAtlasUpdateConfigDataPointers(this); + // Invalidate texture TexReady = false; ClearTexData(); @@ -2126,7 +2236,7 @@ ImFont* ImFontAtlas::AddFontDefault(const ImFontConfig* font_cfg_template) if (font_cfg.Name[0] == '\0') ImFormatString(font_cfg.Name, IM_ARRAYSIZE(font_cfg.Name), "ProggyClean.ttf, %dpx", (int)font_cfg.SizePixels); font_cfg.EllipsisChar = (ImWchar)0x0085; - font_cfg.GlyphOffset.y = 1.0f * IM_FLOOR(font_cfg.SizePixels / 13.0f); // Add +1 offset per 13 units + font_cfg.GlyphOffset.y = 1.0f * IM_TRUNC(font_cfg.SizePixels / 13.0f); // Add +1 offset per 13 units const char* ttf_compressed_base85 = GetDefaultCompressedFontDataTTFBase85(); const ImWchar* glyph_ranges = font_cfg.GlyphRanges != NULL ? font_cfg.GlyphRanges : GetGlyphRangesDefault(); @@ -2156,13 +2266,14 @@ ImFont* ImFontAtlas::AddFontFromFileTTF(const char* filename, float size_pixels, } // NB: Transfer ownership of 'ttf_data' to ImFontAtlas, unless font_cfg_template->FontDataOwnedByAtlas == false. Owned TTF buffer will be deleted after Build(). -ImFont* ImFontAtlas::AddFontFromMemoryTTF(void* ttf_data, int ttf_size, float size_pixels, const ImFontConfig* font_cfg_template, const ImWchar* glyph_ranges) +ImFont* ImFontAtlas::AddFontFromMemoryTTF(void* font_data, int font_data_size, float size_pixels, const ImFontConfig* font_cfg_template, const ImWchar* glyph_ranges) { IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas between NewFrame() and EndFrame/Render()!"); ImFontConfig font_cfg = font_cfg_template ? *font_cfg_template : ImFontConfig(); IM_ASSERT(font_cfg.FontData == NULL); - font_cfg.FontData = ttf_data; - font_cfg.FontDataSize = ttf_size; + IM_ASSERT(font_data_size > 100 && "Incorrect value for font_data_size!"); // Heuristic to prevent accidentally passing a wrong value to font_data_size. + font_cfg.FontData = font_data; + font_cfg.FontDataSize = font_data_size; font_cfg.SizePixels = size_pixels > 0.0f ? size_pixels : font_cfg.SizePixels; if (glyph_ranges) font_cfg.GlyphRanges = glyph_ranges; @@ -2377,7 +2488,10 @@ static bool ImFontAtlasBuildWithStbTruetype(ImFontAtlas* atlas) const int font_offset = stbtt_GetFontOffsetForIndex((unsigned char*)cfg.FontData, cfg.FontNo); IM_ASSERT(font_offset >= 0 && "FontData is incorrect, or FontNo cannot be found."); if (!stbtt_InitFont(&src_tmp.FontInfo, (unsigned char*)cfg.FontData, font_offset)) + { + IM_ASSERT(0 && "stbtt_InitFont(): failed to parse FontData. It is correct and complete? Check FontDataSize."); return false; + } // Measure highest codepoints ImFontBuildDstData& dst_tmp = dst_tmp_array[src_tmp.DstIndex]; @@ -2459,7 +2573,7 @@ static bool ImFontAtlasBuildWithStbTruetype(ImFontAtlas* atlas) // Convert our ranges in the format stb_truetype wants ImFontConfig& cfg = atlas->ConfigData[src_i]; - src_tmp.PackRange.font_size = cfg.SizePixels; + src_tmp.PackRange.font_size = cfg.SizePixels * cfg.RasterizerDensity; src_tmp.PackRange.first_unicode_codepoint_in_range = 0; src_tmp.PackRange.array_of_unicode_codepoints = src_tmp.GlyphsList.Data; src_tmp.PackRange.num_chars = src_tmp.GlyphsList.Size; @@ -2468,7 +2582,7 @@ static bool ImFontAtlasBuildWithStbTruetype(ImFontAtlas* atlas) src_tmp.PackRange.v_oversample = (unsigned char)cfg.OversampleV; // Gather the sizes of all rectangles we will need to pack (this loop is based on stbtt_PackFontRangesGatherRects) - const float scale = (cfg.SizePixels > 0) ? stbtt_ScaleForPixelHeight(&src_tmp.FontInfo, cfg.SizePixels) : stbtt_ScaleForMappingEmToPixels(&src_tmp.FontInfo, -cfg.SizePixels); + const float scale = (cfg.SizePixels > 0.0f) ? stbtt_ScaleForPixelHeight(&src_tmp.FontInfo, cfg.SizePixels * cfg.RasterizerDensity) : stbtt_ScaleForMappingEmToPixels(&src_tmp.FontInfo, -cfg.SizePixels * cfg.RasterizerDensity); const int padding = atlas->TexGlyphPadding; for (int glyph_i = 0; glyph_i < src_tmp.GlyphsList.Size; glyph_i++) { @@ -2564,12 +2678,14 @@ static bool ImFontAtlasBuildWithStbTruetype(ImFontAtlas* atlas) int unscaled_ascent, unscaled_descent, unscaled_line_gap; stbtt_GetFontVMetrics(&src_tmp.FontInfo, &unscaled_ascent, &unscaled_descent, &unscaled_line_gap); - const float ascent = ImFloor(unscaled_ascent * font_scale + ((unscaled_ascent > 0.0f) ? +1 : -1)); - const float descent = ImFloor(unscaled_descent * font_scale + ((unscaled_descent > 0.0f) ? +1 : -1)); + const float ascent = ImTrunc(unscaled_ascent * font_scale + ((unscaled_ascent > 0.0f) ? +1 : -1)); + const float descent = ImTrunc(unscaled_descent * font_scale + ((unscaled_descent > 0.0f) ? +1 : -1)); ImFontAtlasBuildSetupFont(atlas, dst_font, &cfg, ascent, descent); const float font_off_x = cfg.GlyphOffset.x; const float font_off_y = cfg.GlyphOffset.y + IM_ROUND(dst_font->Ascent); + const float inv_rasterization_scale = 1.0f / cfg.RasterizerDensity; + for (int glyph_i = 0; glyph_i < src_tmp.GlyphsCount; glyph_i++) { // Register glyph @@ -2578,7 +2694,11 @@ static bool ImFontAtlasBuildWithStbTruetype(ImFontAtlas* atlas) stbtt_aligned_quad q; float unused_x = 0.0f, unused_y = 0.0f; stbtt_GetPackedQuad(src_tmp.PackedChars, atlas->TexWidth, atlas->TexHeight, glyph_i, &unused_x, &unused_y, &q, 0); - dst_font->AddGlyph(&cfg, (ImWchar)codepoint, q.x0 + font_off_x, q.y0 + font_off_y, q.x1 + font_off_x, q.y1 + font_off_y, q.s0, q.t0, q.s1, q.t1, pc.xadvance); + float x0 = q.x0 * inv_rasterization_scale + font_off_x; + float y0 = q.y0 * inv_rasterization_scale + font_off_y; + float x1 = q.x1 * inv_rasterization_scale + font_off_x; + float y1 = q.y1 * inv_rasterization_scale + font_off_y; + dst_font->AddGlyph(&cfg, (ImWchar)codepoint, x0, y0, x1, y1, q.s0, q.t0, q.s1, q.t1, pc.xadvance * inv_rasterization_scale); } } @@ -2598,19 +2718,31 @@ const ImFontBuilderIO* ImFontAtlasGetBuilderForStbTruetype() #endif // IMGUI_ENABLE_STB_TRUETYPE +void ImFontAtlasUpdateConfigDataPointers(ImFontAtlas* atlas) +{ + for (ImFontConfig& font_cfg : atlas->ConfigData) + { + ImFont* font = font_cfg.DstFont; + if (!font_cfg.MergeMode) + { + font->ConfigData = &font_cfg; + font->ConfigDataCount = 0; + } + font->ConfigDataCount++; + } +} + void ImFontAtlasBuildSetupFont(ImFontAtlas* atlas, ImFont* font, ImFontConfig* font_config, float ascent, float descent) { if (!font_config->MergeMode) { font->ClearOutputData(); font->FontSize = font_config->SizePixels; - font->ConfigData = font_config; - font->ConfigDataCount = 0; + IM_ASSERT(font->ConfigData == font_config); font->ContainerAtlas = atlas; font->Ascent = ascent; font->Descent = descent; } - font->ConfigDataCount++; } void ImFontAtlasBuildPackCustomRects(ImFontAtlas* atlas, void* stbrp_context_opaque) @@ -2757,6 +2889,13 @@ static void ImFontAtlasBuildRenderLinesTexData(ImFontAtlas* atlas) // Note: this is called / shared by both the stb_truetype and the FreeType builder void ImFontAtlasBuildInit(ImFontAtlas* atlas) { + // Round font size + // - We started rounding in 1.90 WIP (18991) as our layout system currently doesn't support non-rounded font size well yet. + // - Note that using io.FontGlobalScale or SetWindowFontScale(), with are legacy-ish, partially supported features, can still lead to unrounded sizes. + // - We may support it better later and remove this rounding. + for (ImFontConfig& cfg : atlas->ConfigData) + cfg.SizePixels = ImTrunc(cfg.SizePixels); + // Register texture region for mouse cursors or standard white pixels if (atlas->PackIdMouseCursors < 0) { @@ -2798,9 +2937,9 @@ void ImFontAtlasBuildFinish(ImFontAtlas* atlas) } // Build all fonts lookup tables - for (int i = 0; i < atlas->Fonts.Size; i++) - if (atlas->Fonts[i]->DirtyLookupTables) - atlas->Fonts[i]->BuildLookupTable(); + for (ImFont* font : atlas->Fonts) + if (font->DirtyLookupTables) + font->BuildLookupTable(); atlas->TexReady = true; } @@ -3165,6 +3304,7 @@ void ImFont::BuildLookupTable() max_codepoint = ImMax(max_codepoint, (int)Glyphs[i].Codepoint); // Build lookup table + IM_ASSERT(Glyphs.Size > 0 && "Font has not loaded glyph!"); IM_ASSERT(Glyphs.Size < 0xFFFF); // -1 is reserved IndexAdvanceX.clear(); IndexLookup.clear(); @@ -3281,7 +3421,7 @@ void ImFont::AddGlyph(const ImFontConfig* cfg, ImWchar codepoint, float x0, floa advance_x = ImClamp(advance_x, cfg->GlyphMinAdvanceX, cfg->GlyphMaxAdvanceX); if (advance_x != advance_x_original) { - float char_off_x = cfg->PixelSnapH ? ImFloor((advance_x - advance_x_original) * 0.5f) : (advance_x - advance_x_original) * 0.5f; + float char_off_x = cfg->PixelSnapH ? ImTrunc((advance_x - advance_x_original) * 0.5f) : (advance_x - advance_x_original) * 0.5f; x0 += char_off_x; x1 += char_off_x; } @@ -3549,8 +3689,8 @@ void ImFont::RenderChar(ImDrawList* draw_list, float size, const ImVec2& pos, Im if (glyph->Colored) col |= ~IM_COL32_A_MASK; float scale = (size >= 0.0f) ? (size / FontSize) : 1.0f; - float x = IM_FLOOR(pos.x); - float y = IM_FLOOR(pos.y); + float x = IM_TRUNC(pos.x); + float y = IM_TRUNC(pos.y); draw_list->PrimReserve(6, 4); draw_list->PrimRectUV(ImVec2(x + glyph->X0 * scale, y + glyph->Y0 * scale), ImVec2(x + glyph->X1 * scale, y + glyph->Y1 * scale), ImVec2(glyph->U0, glyph->V0), ImVec2(glyph->U1, glyph->V1), col); } @@ -3562,8 +3702,8 @@ void ImFont::RenderText(ImDrawList* draw_list, float size, const ImVec2& pos, Im text_end = text_begin + strlen(text_begin); // ImGui:: functions generally already provides a valid text_end, so this is merely to handle direct calls. // Align to be pixel perfect - float x = IM_FLOOR(pos.x); - float y = IM_FLOOR(pos.y); + float x = IM_TRUNC(pos.x); + float y = IM_TRUNC(pos.y); if (y > clip_rect.w) return; @@ -3747,6 +3887,7 @@ void ImFont::RenderText(ImDrawList* draw_list, float size, const ImVec2& pos, Im // - RenderArrow() // - RenderBullet() // - RenderCheckMark() +// - RenderArrowDockMenu() // - RenderArrowPointingAt() // - RenderRectFilledRangeH() // - RenderRectFilledWithHole() @@ -3821,6 +3962,14 @@ void ImGui::RenderArrowPointingAt(ImDrawList* draw_list, ImVec2 pos, ImVec2 half } } +// This is less wide than RenderArrow() and we use in dock nodes instead of the regular RenderArrow() to denote a change of functionality, +// and because the saved space means that the left-most tab label can stay at exactly the same position as the label of a loose window. +void ImGui::RenderArrowDockMenu(ImDrawList* draw_list, ImVec2 p_min, float sz, ImU32 col) +{ + draw_list->AddRectFilled(p_min + ImVec2(sz * 0.20f, sz * 0.15f), p_min + ImVec2(sz * 0.80f, sz * 0.30f), col); + RenderArrowPointingAt(draw_list, p_min + ImVec2(sz * 0.50f, sz * 0.85f), ImVec2(sz * 0.30f, sz * 0.40f), ImGuiDir_Down, col); +} + static inline float ImAcos01(float x) { if (x <= 0.0f) return IM_PI * 0.5f; @@ -3863,8 +4012,8 @@ void ImGui::RenderRectFilledRangeH(ImDrawList* draw_list, const ImRect& rect, Im } else { - draw_list->PathArcTo(ImVec2(x0, p1.y - rounding), rounding, IM_PI - arc0_e, IM_PI - arc0_b, 3); // BL - draw_list->PathArcTo(ImVec2(x0, p0.y + rounding), rounding, IM_PI + arc0_b, IM_PI + arc0_e, 3); // TR + draw_list->PathArcTo(ImVec2(x0, p1.y - rounding), rounding, IM_PI - arc0_e, IM_PI - arc0_b); // BL + draw_list->PathArcTo(ImVec2(x0, p0.y + rounding), rounding, IM_PI + arc0_b, IM_PI + arc0_e); // TR } if (p1.x > rect.Min.x + rounding) { @@ -3883,8 +4032,8 @@ void ImGui::RenderRectFilledRangeH(ImDrawList* draw_list, const ImRect& rect, Im } else { - draw_list->PathArcTo(ImVec2(x1, p0.y + rounding), rounding, -arc1_e, -arc1_b, 3); // TR - draw_list->PathArcTo(ImVec2(x1, p1.y - rounding), rounding, +arc1_b, +arc1_e, 3); // BR + draw_list->PathArcTo(ImVec2(x1, p0.y + rounding), rounding, -arc1_e, -arc1_b); // TR + draw_list->PathArcTo(ImVec2(x1, p1.y - rounding), rounding, +arc1_b, +arc1_e); // BR } } draw_list->PathFillConvex(col); @@ -3906,6 +4055,17 @@ void ImGui::RenderRectFilledWithHole(ImDrawList* draw_list, const ImRect& outer, if (fill_R && fill_D) draw_list->AddRectFilled(ImVec2(inner.Max.x, inner.Max.y), ImVec2(outer.Max.x, outer.Max.y), col, rounding, ImDrawFlags_RoundCornersBottomRight); } +ImDrawFlags ImGui::CalcRoundingFlagsForRectInRect(const ImRect& r_in, const ImRect& r_outer, float threshold) +{ + bool round_l = r_in.Min.x <= r_outer.Min.x + threshold; + bool round_r = r_in.Max.x >= r_outer.Max.x - threshold; + bool round_t = r_in.Min.y <= r_outer.Min.y + threshold; + bool round_b = r_in.Max.y >= r_outer.Max.y - threshold; + return ImDrawFlags_RoundCornersNone + | ((round_t && round_l) ? ImDrawFlags_RoundCornersTopLeft : 0) | ((round_t && round_r) ? ImDrawFlags_RoundCornersTopRight : 0) + | ((round_b && round_l) ? ImDrawFlags_RoundCornersBottomLeft : 0) | ((round_b && round_r) ? ImDrawFlags_RoundCornersBottomRight : 0); +} + // Helper for ColorPicker4() // NB: This is rather brittle and will show artifact when rounding this enabled if rounded corners overlap multiple cells. Caller currently responsible for avoiding that. // Spent a non reasonable amount of time trying to getting this right for ColorButton with rounding+anti-aliasing+ImGuiColorEditFlags_HalfAlphaPreview flag + various grid sizes and offsets, and eventually gave up... probably more reasonable to disable rounding altogether. @@ -4071,8 +4231,8 @@ static unsigned int stb_decompress(unsigned char *output, const unsigned char *i //----------------------------------------------------------------------------- // ProggyClean.ttf // Copyright (c) 2004, 2005 Tristan Grimmer -// MIT license (see License.txt in http://www.upperbounds.net/download/ProggyClean.ttf.zip) -// Download and more information at http://upperbounds.net +// MIT license (see License.txt in http://www.proggyfonts.net/index.php?menu=download) +// Download and more information at http://www.proggyfonts.net or http://upperboundsinteractive.com/fonts.php //----------------------------------------------------------------------------- // File: 'ProggyClean.ttf' (41208 bytes) // Exported using misc/fonts/binary_to_compressed_c.cpp (with compression + base85 string encoding). diff --git a/libs/imgui/imgui_internal.h b/libs/imgui/imgui_internal.h index 3311d65..2d243a1 100644 --- a/libs/imgui/imgui_internal.h +++ b/libs/imgui/imgui_internal.h @@ -1,12 +1,7 @@ -// dear imgui, v1.89.6 +// dear imgui, v1.90.4 // (internal structures/api) // You may use this file to debug, understand or extend Dear ImGui features but we don't provide any guarantee of forward compatibility. -// To implement maths operators for ImVec2 (disabled by default to not conflict with using IM_VEC2_CLASS_EXTRA with your own math types+operators), use: -/* -#define IMGUI_DEFINE_MATH_OPERATORS -#include "imgui_internal.h" -*/ /* @@ -20,9 +15,12 @@ Index of this file: // [SECTION] Generic helpers // [SECTION] ImDrawList support // [SECTION] Widgets support: flags, enums, data structures +// [SECTION] Data types support +// [SECTION] Popup support // [SECTION] Inputs support // [SECTION] Clipper support // [SECTION] Navigation support +// [SECTION] Typing-select support // [SECTION] Columns support // [SECTION] Multi-select support // [SECTION] Docking support @@ -81,7 +79,7 @@ Index of this file: #pragma clang diagnostic ignored "-Wunknown-warning-option" // warning: unknown warning group 'xxx' #endif #pragma clang diagnostic ignored "-Wunknown-pragmas" // warning: unknown warning group 'xxx' -#pragma clang diagnostic ignored "-Wfloat-equal" // warning: comparing floating point with == or != is unsafe // storing and comparing against same constants ok, for ImFloorSigned() +#pragma clang diagnostic ignored "-Wfloat-equal" // warning: comparing floating point with == or != is unsafe // storing and comparing against same constants ok, for ImFloor() #pragma clang diagnostic ignored "-Wunused-function" // for stb_textedit.h #pragma clang diagnostic ignored "-Wmissing-prototypes" // for stb_textedit.h #pragma clang diagnostic ignored "-Wold-style-cast" @@ -128,6 +126,10 @@ struct ImGuiContext; // Main Dear ImGui context struct ImGuiContextHook; // Hook for extensions like ImGuiTestEngine struct ImGuiDataVarInfo; // Variable information (e.g. to avoid style variables from an enum) struct ImGuiDataTypeInfo; // Type information associated to a ImGuiDataType enum +struct ImGuiDockContext; // Docking system context +struct ImGuiDockRequest; // Docking system dock/undock queued request +struct ImGuiDockNode; // Docking system node (hold a list of Windows OR two child dock nodes) +struct ImGuiDockNodeSettings; // Storage for a dock node in .ini file (we preserve those even if the associated dock node isn't active during the session) struct ImGuiGroupData; // Stacked storage data for BeginGroup()/EndGroup() struct ImGuiInputTextState; // Internal state of the currently focused/edited text input box struct ImGuiInputTextDeactivateData;// Short term storage to backup text of a deactivating InputText() while another is stealing active id @@ -135,6 +137,7 @@ struct ImGuiLastItemData; // Status storage for last submitted items struct ImGuiLocEntry; // A localization entry. struct ImGuiMenuColumns; // Simple column measurement, currently used for MenuItem() only struct ImGuiNavItemData; // Result of a gamepad/keyboard directional navigation move query result +struct ImGuiNavTreeNodeData; // Temporary storage for last TreeNode() being a Left arrow landing candidate. struct ImGuiMetricsConfig; // Storage for ShowMetricsWindow() and DebugNodeXXX() functions struct ImGuiNextWindowData; // Storage for SetNextWindow** functions struct ImGuiNextItemData; // Storage for SetNextItem** functions @@ -152,6 +155,8 @@ struct ImGuiTableInstanceData; // Storage for one instance of a same table struct ImGuiTableTempData; // Temporary storage for one table (one per table in the stack), shared between tables. struct ImGuiTableSettings; // Storage for a table .ini settings struct ImGuiTableColumnsSettings; // Storage for a column .ini settings +struct ImGuiTypingSelectState; // Storage for GetTypingSelectRequest() +struct ImGuiTypingSelectRequest; // Storage for GetTypingSelectRequest() (aimed to be public) struct ImGuiWindow; // Storage for one window struct ImGuiWindowTempData; // Temporary storage for one window (that's the data which in theory we could ditch at the end of the frame, in practice we currently keep it for each window) struct ImGuiWindowSettings; // Storage for a window .ini settings (we keep one of those even if the actual window wasn't instanced during this session) @@ -159,6 +164,7 @@ struct ImGuiWindowSettings; // Storage for a window .ini settings (we ke // Enumerations // Use your programming IDE "Go to definition" facility on the names of the center columns to find the actual flags/enum lists. enum ImGuiLocKey : int; // -> enum ImGuiLocKey // Enum: a localization entry for translation. +typedef int ImGuiDataAuthority; // -> enum ImGuiDataAuthority_ // Enum: for storing the source authority (dock node vs window) of a field typedef int ImGuiLayoutType; // -> enum ImGuiLayoutType_ // Enum: Horizontal or vertical // Flags @@ -177,6 +183,7 @@ typedef int ImGuiScrollFlags; // -> enum ImGuiScrollFlags_ // F typedef int ImGuiSeparatorFlags; // -> enum ImGuiSeparatorFlags_ // Flags: for SeparatorEx() typedef int ImGuiTextFlags; // -> enum ImGuiTextFlags_ // Flags: for TextEx() typedef int ImGuiTooltipFlags; // -> enum ImGuiTooltipFlags_ // Flags: for BeginTooltipEx() +typedef int ImGuiTypingSelectFlags; // -> enum ImGuiTypingSelectFlags_ // Flags: for GetTypingSelectRequest() typedef void (*ImGuiErrorLogCallback)(void* user_data, const char* fmt, ...); @@ -196,13 +203,13 @@ extern IMGUI_API ImGuiContext* GImGui; // Current implicit context pointer namespace ImStb { -#undef STB_TEXTEDIT_STRING -#undef STB_TEXTEDIT_CHARTYPE -#define STB_TEXTEDIT_STRING ImGuiInputTextState -#define STB_TEXTEDIT_CHARTYPE ImWchar -#define STB_TEXTEDIT_GETWIDTH_NEWLINE (-1.0f) -#define STB_TEXTEDIT_UNDOSTATECOUNT 99 -#define STB_TEXTEDIT_UNDOCHARCOUNT 999 +#undef IMSTB_TEXTEDIT_STRING +#undef IMSTB_TEXTEDIT_CHARTYPE +#define IMSTB_TEXTEDIT_STRING ImGuiInputTextState +#define IMSTB_TEXTEDIT_CHARTYPE ImWchar +#define IMSTB_TEXTEDIT_GETWIDTH_NEWLINE (-1.0f) +#define IMSTB_TEXTEDIT_UNDOSTATECOUNT 99 +#define IMSTB_TEXTEDIT_UNDOCHARCOUNT 999 #include "imstb_textedit.h" } // namespace ImStb @@ -211,6 +218,9 @@ namespace ImStb // [SECTION] Macros //----------------------------------------------------------------------------- +// Internal Drag and Drop payload types. String starting with '_' are reserved for Dear ImGui. +#define IMGUI_PAYLOAD_TYPE_WINDOW "_IMWINDOW" // Payload == ImGuiWindow* + // Debug Printing Into TTY // (since IMGUI_VERSION_NUM >= 18729: IMGUI_DEBUG_LOG was reworked into IMGUI_DEBUG_PRINTF (and removed framecount from it). If you were using a #define IMGUI_DEBUG_LOG please rename) #ifndef IMGUI_DEBUG_PRINTF @@ -227,13 +237,16 @@ namespace ImStb #else #define IMGUI_DEBUG_LOG(...) ((void)0) #endif -#define IMGUI_DEBUG_LOG_ACTIVEID(...) do { if (g.DebugLogFlags & ImGuiDebugLogFlags_EventActiveId) IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0) -#define IMGUI_DEBUG_LOG_FOCUS(...) do { if (g.DebugLogFlags & ImGuiDebugLogFlags_EventFocus) IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0) -#define IMGUI_DEBUG_LOG_POPUP(...) do { if (g.DebugLogFlags & ImGuiDebugLogFlags_EventPopup) IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0) -#define IMGUI_DEBUG_LOG_NAV(...) do { if (g.DebugLogFlags & ImGuiDebugLogFlags_EventNav) IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0) -#define IMGUI_DEBUG_LOG_SELECTION(...) do { if (g.DebugLogFlags & ImGuiDebugLogFlags_EventSelection)IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0) -#define IMGUI_DEBUG_LOG_CLIPPER(...) do { if (g.DebugLogFlags & ImGuiDebugLogFlags_EventClipper) IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0) -#define IMGUI_DEBUG_LOG_IO(...) do { if (g.DebugLogFlags & ImGuiDebugLogFlags_EventIO) IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0) +#define IMGUI_DEBUG_LOG_ACTIVEID(...) do { if (g.DebugLogFlags & ImGuiDebugLogFlags_EventActiveId) IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0) +#define IMGUI_DEBUG_LOG_FOCUS(...) do { if (g.DebugLogFlags & ImGuiDebugLogFlags_EventFocus) IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0) +#define IMGUI_DEBUG_LOG_POPUP(...) do { if (g.DebugLogFlags & ImGuiDebugLogFlags_EventPopup) IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0) +#define IMGUI_DEBUG_LOG_NAV(...) do { if (g.DebugLogFlags & ImGuiDebugLogFlags_EventNav) IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0) +#define IMGUI_DEBUG_LOG_SELECTION(...) do { if (g.DebugLogFlags & ImGuiDebugLogFlags_EventSelection) IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0) +#define IMGUI_DEBUG_LOG_CLIPPER(...) do { if (g.DebugLogFlags & ImGuiDebugLogFlags_EventClipper) IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0) +#define IMGUI_DEBUG_LOG_IO(...) do { if (g.DebugLogFlags & ImGuiDebugLogFlags_EventIO) IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0) +#define IMGUI_DEBUG_LOG_INPUTROUTING(...) do{if (g.DebugLogFlags & ImGuiDebugLogFlags_EventInputRouting)IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0) +#define IMGUI_DEBUG_LOG_DOCKING(...) do { if (g.DebugLogFlags & ImGuiDebugLogFlags_EventDocking) IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0) +#define IMGUI_DEBUG_LOG_VIEWPORT(...) do { if (g.DebugLogFlags & ImGuiDebugLogFlags_EventViewport) IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0) // Static Asserts #define IM_STATIC_ASSERT(_COND) static_assert(_COND, "") @@ -266,8 +279,13 @@ namespace ImStb #define IM_MEMALIGN(_OFF,_ALIGN) (((_OFF) + ((_ALIGN) - 1)) & ~((_ALIGN) - 1)) // Memory align e.g. IM_ALIGN(0,4)=0, IM_ALIGN(1,4)=4, IM_ALIGN(4,4)=4, IM_ALIGN(5,4)=8 #define IM_F32_TO_INT8_UNBOUND(_VAL) ((int)((_VAL) * 255.0f + ((_VAL)>=0 ? 0.5f : -0.5f))) // Unsaturated, for display purpose #define IM_F32_TO_INT8_SAT(_VAL) ((int)(ImSaturate(_VAL) * 255.0f + 0.5f)) // Saturated, always output 0..255 -#define IM_FLOOR(_VAL) ((float)(int)(_VAL)) // ImFloor() is not inlined in MSVC debug builds +#define IM_TRUNC(_VAL) ((float)(int)(_VAL)) // ImTrunc() is not inlined in MSVC debug builds #define IM_ROUND(_VAL) ((float)(int)((_VAL) + 0.5f)) // +#define IM_STRINGIFY_HELPER(_X) #_X +#define IM_STRINGIFY(_X) IM_STRINGIFY_HELPER(_X) // Preprocessor idiom to stringify e.g. an integer. +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS +#define IM_FLOOR IM_TRUNC +#endif // Enforce cdecl calling convention for functions called by the standard library, in case compilation settings changed the default to e.g. __vectorcall #ifdef _MSC_VER @@ -292,16 +310,28 @@ namespace ImStb #elif defined(__clang__) #define IM_DEBUG_BREAK() __builtin_debugtrap() #elif defined(__GNUC__) && (defined(__i386__) || defined(__x86_64__)) -#define IM_DEBUG_BREAK() __asm__ volatile("int $0x03") +#define IM_DEBUG_BREAK() __asm__ volatile("int3;nop") #elif defined(__GNUC__) && defined(__thumb__) #define IM_DEBUG_BREAK() __asm__ volatile(".inst 0xde01") #elif defined(__GNUC__) && defined(__arm__) && !defined(__thumb__) -#define IM_DEBUG_BREAK() __asm__ volatile(".inst 0xe7f001f0"); +#define IM_DEBUG_BREAK() __asm__ volatile(".inst 0xe7f001f0") #else #define IM_DEBUG_BREAK() IM_ASSERT(0) // It is expected that you define IM_DEBUG_BREAK() into something that will break nicely in a debugger! #endif #endif // #ifndef IM_DEBUG_BREAK +// Format specifiers, printing 64-bit hasn't been decently standardized... +// In a real application you should be using PRId64 and PRIu64 from (non-windows) and on Windows define them yourself. +#if defined(_MSC_VER) && !defined(__clang__) +#define IM_PRId64 "I64d" +#define IM_PRIu64 "I64u" +#define IM_PRIX64 "I64X" +#else +#define IM_PRId64 "lld" +#define IM_PRIu64 "llu" +#define IM_PRIX64 "llX" +#endif + //----------------------------------------------------------------------------- // [SECTION] Generic helpers // Note that the ImXXX helpers functions are lower-level than ImGui functions. @@ -345,18 +375,18 @@ static inline bool ImIsPowerOfTwo(ImU64 v) { return v != 0 && (v & static inline int ImUpperPowerOfTwo(int v) { v--; v |= v >> 1; v |= v >> 2; v |= v >> 4; v |= v >> 8; v |= v >> 16; v++; return v; } // Helpers: String -IMGUI_API int ImStricmp(const char* str1, const char* str2); -IMGUI_API int ImStrnicmp(const char* str1, const char* str2, size_t count); -IMGUI_API void ImStrncpy(char* dst, const char* src, size_t count); -IMGUI_API char* ImStrdup(const char* str); -IMGUI_API char* ImStrdupcpy(char* dst, size_t* p_dst_size, const char* str); -IMGUI_API const char* ImStrchrRange(const char* str_begin, const char* str_end, char c); -IMGUI_API int ImStrlenW(const ImWchar* str); +IMGUI_API int ImStricmp(const char* str1, const char* str2); // Case insensitive compare. +IMGUI_API int ImStrnicmp(const char* str1, const char* str2, size_t count); // Case insensitive compare to a certain count. +IMGUI_API void ImStrncpy(char* dst, const char* src, size_t count); // Copy to a certain count and always zero terminate (strncpy doesn't). +IMGUI_API char* ImStrdup(const char* str); // Duplicate a string. +IMGUI_API char* ImStrdupcpy(char* dst, size_t* p_dst_size, const char* str); // Copy in provided buffer, recreate buffer if needed. +IMGUI_API const char* ImStrchrRange(const char* str_begin, const char* str_end, char c); // Find first occurrence of 'c' in string range. IMGUI_API const char* ImStreolRange(const char* str, const char* str_end); // End end-of-line -IMGUI_API const ImWchar*ImStrbolW(const ImWchar* buf_mid_line, const ImWchar* buf_begin); // Find beginning-of-line -IMGUI_API const char* ImStristr(const char* haystack, const char* haystack_end, const char* needle, const char* needle_end); -IMGUI_API void ImStrTrimBlanks(char* str); -IMGUI_API const char* ImStrSkipBlank(const char* str); +IMGUI_API const char* ImStristr(const char* haystack, const char* haystack_end, const char* needle, const char* needle_end); // Find a substring in a string range. +IMGUI_API void ImStrTrimBlanks(char* str); // Remove leading and trailing blanks from a buffer. +IMGUI_API const char* ImStrSkipBlank(const char* str); // Find first non-blank character. +IMGUI_API int ImStrlenW(const ImWchar* str); // Computer string length (ImWchar string) +IMGUI_API const ImWchar*ImStrbolW(const ImWchar* buf_mid_line, const ImWchar* buf_begin); // Find beginning-of-line (ImWchar string) IM_MSVC_RUNTIME_CHECKS_OFF static inline char ImToUpper(char c) { return (c >= 'a' && c <= 'z') ? c &= ~32 : c; } static inline bool ImCharIsBlankA(char c) { return c == ' ' || c == '\t'; } @@ -383,6 +413,7 @@ IMGUI_API int ImTextStrFromUtf8(ImWchar* out_buf, int out_buf_size, co IMGUI_API int ImTextCountCharsFromUtf8(const char* in_text, const char* in_text_end); // return number of UTF-8 code-points (NOT bytes count) IMGUI_API int ImTextCountUtf8BytesFromChar(const char* in_text, const char* in_text_end); // return number of bytes to express one char in UTF-8 IMGUI_API int ImTextCountUtf8BytesFromStr(const ImWchar* in_text, const ImWchar* in_text_end); // return number of bytes to express string in UTF-8 +IMGUI_API const char* ImTextFindPreviousUtf8Codepoint(const char* in_text_start, const char* in_text_curr); // return previous UTF-8 code-point. // Helpers: File System #ifdef IMGUI_DISABLE_FILE_FUNCTIONS @@ -418,7 +449,6 @@ IM_MSVC_RUNTIME_CHECKS_OFF #define ImAcos(X) acosf(X) #define ImAtan2(Y, X) atan2f((Y), (X)) #define ImAtof(STR) atof(STR) -//#define ImFloorStd(X) floorf(X) // We use our own, see ImFloor() and ImFloorSigned() #define ImCeil(X) ceilf(X) static inline float ImPow(float x, float y) { return powf(x, y); } // DragBehaviorT/SliderBehaviorT uses ImPow with either float/double and need the precision static inline double ImPow(double x, double y) { return pow(x, y); } @@ -456,10 +486,10 @@ static inline float ImSaturate(float f) static inline float ImLengthSqr(const ImVec2& lhs) { return (lhs.x * lhs.x) + (lhs.y * lhs.y); } static inline float ImLengthSqr(const ImVec4& lhs) { return (lhs.x * lhs.x) + (lhs.y * lhs.y) + (lhs.z * lhs.z) + (lhs.w * lhs.w); } static inline float ImInvLength(const ImVec2& lhs, float fail_value) { float d = (lhs.x * lhs.x) + (lhs.y * lhs.y); if (d > 0.0f) return ImRsqrt(d); return fail_value; } -static inline float ImFloor(float f) { return (float)(int)(f); } -static inline float ImFloorSigned(float f) { return (float)((f >= 0 || (float)(int)f == f) ? (int)f : (int)f - 1); } // Decent replacement for floorf() -static inline ImVec2 ImFloor(const ImVec2& v) { return ImVec2((float)(int)(v.x), (float)(int)(v.y)); } -static inline ImVec2 ImFloorSigned(const ImVec2& v) { return ImVec2(ImFloorSigned(v.x), ImFloorSigned(v.y)); } +static inline float ImTrunc(float f) { return (float)(int)(f); } +static inline ImVec2 ImTrunc(const ImVec2& v) { return ImVec2((float)(int)(v.x), (float)(int)(v.y)); } +static inline float ImFloor(float f) { return (float)((f >= 0 || (float)(int)f == f) ? (int)f : (int)f - 1); } // Decent replacement for floorf() +static inline ImVec2 ImFloor(const ImVec2& v) { return ImVec2(ImFloor(v.x), ImFloor(v.y)); } static inline int ImModPositive(int a, int b) { return (a + b) % b; } static inline float ImDot(const ImVec2& a, const ImVec2& b) { return a.x * b.x + a.y * b.y; } static inline ImVec2 ImRotate(const ImVec2& v, float cos_a, float sin_a) { return ImVec2(v.x * cos_a - v.y * sin_a, v.x * sin_a + v.y * cos_a); } @@ -522,6 +552,7 @@ struct IMGUI_API ImRect ImVec2 GetBR() const { return Max; } // Bottom-right bool Contains(const ImVec2& p) const { return p.x >= Min.x && p.y >= Min.y && p.x < Max.x && p.y < Max.y; } bool Contains(const ImRect& r) const { return r.Min.x >= Min.x && r.Min.y >= Min.y && r.Max.x <= Max.x && r.Max.y <= Max.y; } + bool ContainsWithPad(const ImVec2& p, const ImVec2& pad) const { return p.x >= Min.x - pad.x && p.y >= Min.y - pad.y && p.x < Max.x + pad.x && p.y < Max.y + pad.y; } bool Overlaps(const ImRect& r) const { return r.Min.y < Max.y && r.Max.y > Min.y && r.Min.x < Max.x && r.Max.x > Min.x; } void Add(const ImVec2& p) { if (Min.x > p.x) Min.x = p.x; if (Min.y > p.y) Min.y = p.y; if (Max.x < p.x) Max.x = p.x; if (Max.y < p.y) Max.y = p.y; } void Add(const ImRect& r) { if (Min.x > r.Min.x) Min.x = r.Min.x; if (Min.y > r.Min.y) Min.y = r.Min.y; if (Max.x < r.Max.x) Max.x = r.Max.x; if (Max.y < r.Max.y) Max.y = r.Max.y; } @@ -532,7 +563,7 @@ struct IMGUI_API ImRect void TranslateY(float dy) { Min.y += dy; Max.y += dy; } void ClipWith(const ImRect& r) { Min = ImMax(Min, r.Min); Max = ImMin(Max, r.Max); } // Simple version, may lead to an inverted rectangle, which is fine for Contains/Overlaps test but not for display. void ClipWithFull(const ImRect& r) { Min = ImClamp(Min, r.Min, r.Max); Max = ImClamp(Max, r.Min, r.Max); } // Full version, ensure both points are fully clipped. - void Floor() { Min.x = IM_FLOOR(Min.x); Min.y = IM_FLOOR(Min.y); Max.x = IM_FLOOR(Max.x); Max.y = IM_FLOOR(Max.y); } + void Floor() { Min.x = IM_TRUNC(Min.x); Min.y = IM_TRUNC(Min.y); Max.x = IM_TRUNC(Max.x); Max.y = IM_TRUNC(Max.y); } bool IsInverted() const { return Min.x > Max.x || Min.y > Max.y; } ImVec4 ToVec4() const { return ImVec4(Min.x, Min.y, Max.x, Max.y); } }; @@ -671,9 +702,6 @@ struct ImPool int GetBufSize() const { return Buf.Size; } int GetMapSize() const { return Map.Data.Size; } // It is the map we need iterate to find valid items, since we don't have "alive" storage anywhere T* TryGetMapData(ImPoolIdx n) { int idx = Map.Data[n].val_i; if (idx == -1) return NULL; return GetByIndex(idx); } -#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS - int GetSize() { return GetMapSize(); } // For ImPlot: should use GetMapSize() from (IMGUI_VERSION_NUM >= 18304) -#endif }; // Helper: ImChunkStream<> @@ -697,7 +725,6 @@ struct ImChunkStream int offset_from_ptr(const T* p) { IM_ASSERT(p >= begin() && p < end()); const ptrdiff_t off = (const char*)p - Buf.Data; return (int)off; } T* ptr_from_offset(int off) { IM_ASSERT(off >= 4 && off < Buf.Size); return (T*)(void*)(Buf.Data + off); } void swap(ImChunkStream& rhs) { rhs.Buf.swap(Buf); } - }; // Helper: ImGuiTextIndex<> @@ -770,12 +797,10 @@ struct IMGUI_API ImDrawListSharedData struct ImDrawDataBuilder { - ImVector Layers[2]; // Global layers for: regular, tooltip + ImVector* Layers[2]; // Pointers to global layers for: regular, tooltip. LayersP[0] is owned by DrawData. + ImVector LayerData1; - void Clear() { for (int n = 0; n < IM_ARRAYSIZE(Layers); n++) Layers[n].resize(0); } - void ClearFreeMemory() { for (int n = 0; n < IM_ARRAYSIZE(Layers); n++) Layers[n].clear(); } - int GetDrawListCount() const { int count = 0; for (int n = 0; n < IM_ARRAYSIZE(Layers); n++) count += Layers[n].Size; return count; } - IMGUI_API void FlattenIntoSingleLayer(); + ImDrawDataBuilder() { memset(this, 0, sizeof(*this)); } }; //----------------------------------------------------------------------------- @@ -800,9 +825,11 @@ enum ImGuiItemFlags_ ImGuiItemFlags_MixedValue = 1 << 6, // false // [BETA] Represent a mixed/indeterminate value, generally multi-selection where values differ. Currently only supported by Checkbox() (later should support all sorts of widgets) ImGuiItemFlags_ReadOnly = 1 << 7, // false // [ALPHA] Allow hovering interactions but underlying value is not changed. ImGuiItemFlags_NoWindowHoverableCheck = 1 << 8, // false // Disable hoverable check in ItemHoverable() + ImGuiItemFlags_AllowOverlap = 1 << 9, // false // Allow being overlapped by another widget. Not-hovered to Hovered transition deferred by a frame. // Controlled by widget code ImGuiItemFlags_Inputable = 1 << 10, // false // [WIP] Auto-activate input mode when tab focused. Currently only used and supported by a few items before it becomes a generic feature. + ImGuiItemFlags_HasSelectionUserData = 1 << 11, // false // Set by SetNextItemSelectionUserData() }; // Status flags for an already submitted item @@ -818,8 +845,8 @@ enum ImGuiItemStatusFlags_ ImGuiItemStatusFlags_HasDeactivated = 1 << 5, // Set if the widget/group is able to provide data for the ImGuiItemStatusFlags_Deactivated flag. ImGuiItemStatusFlags_Deactivated = 1 << 6, // Only valid if ImGuiItemStatusFlags_HasDeactivated is set. ImGuiItemStatusFlags_HoveredWindow = 1 << 7, // Override the HoveredWindow test to allow cross-window hover testing. - ImGuiItemStatusFlags_FocusedByTabbing = 1 << 8, // Set when the Focusable item just got focused by Tabbing (FIXME: to be removed soon) - ImGuiItemStatusFlags_Visible = 1 << 9, // [WIP] Set when item is overlapping the current clipping rectangle (Used internally. Please don't use yet: API/system will change as we refactor Itemadd()). + ImGuiItemStatusFlags_Visible = 1 << 8, // [WIP] Set when item is overlapping the current clipping rectangle (Used internally. Please don't use yet: API/system will change as we refactor Itemadd()). + ImGuiItemStatusFlags_HasClipRect = 1 << 9, // g.LastItemData.ClipRect is valid // Additional status + semantic for ImGuiTestEngine #ifdef IMGUI_ENABLE_TEST_ENGINE @@ -831,6 +858,14 @@ enum ImGuiItemStatusFlags_ #endif }; +// Extend ImGuiHoveredFlags_ +enum ImGuiHoveredFlagsPrivate_ +{ + ImGuiHoveredFlags_DelayMask_ = ImGuiHoveredFlags_DelayNone | ImGuiHoveredFlags_DelayShort | ImGuiHoveredFlags_DelayNormal | ImGuiHoveredFlags_NoSharedDelay, + ImGuiHoveredFlags_AllowedMaskForIsWindowHovered = ImGuiHoveredFlags_ChildWindows | ImGuiHoveredFlags_RootWindow | ImGuiHoveredFlags_AnyWindow | ImGuiHoveredFlags_NoPopupHierarchy | ImGuiHoveredFlags_DockHierarchy | ImGuiHoveredFlags_AllowWhenBlockedByPopup | ImGuiHoveredFlags_AllowWhenBlockedByActiveItem | ImGuiHoveredFlags_ForTooltip | ImGuiHoveredFlags_Stationary, + ImGuiHoveredFlags_AllowedMaskForIsItemHovered = ImGuiHoveredFlags_AllowWhenBlockedByPopup | ImGuiHoveredFlags_AllowWhenBlockedByActiveItem | ImGuiHoveredFlags_AllowWhenOverlapped | ImGuiHoveredFlags_AllowWhenDisabled | ImGuiHoveredFlags_NoNavOverride | ImGuiHoveredFlags_ForTooltip | ImGuiHoveredFlags_Stationary | ImGuiHoveredFlags_DelayMask_, +}; + // Extend ImGuiInputTextFlags_ enum ImGuiInputTextFlagsPrivate_ { @@ -851,7 +886,7 @@ enum ImGuiButtonFlagsPrivate_ ImGuiButtonFlags_PressedOnDragDropHold = 1 << 9, // return true when held into while we are drag and dropping another item (used by e.g. tree nodes, collapsing headers) ImGuiButtonFlags_Repeat = 1 << 10, // hold to repeat ImGuiButtonFlags_FlattenChildren = 1 << 11, // allow interactions even if a child window is overlapping - ImGuiButtonFlags_AllowItemOverlap = 1 << 12, // require previous frame HoveredId to either match id or be null before being usable, use along with SetItemAllowOverlap() + ImGuiButtonFlags_AllowOverlap = 1 << 12, // require previous frame HoveredId to either match id or be null before being usable. ImGuiButtonFlags_DontClosePopups = 1 << 13, // disable automatically closing parent popup on press // [UNUSED] //ImGuiButtonFlags_Disabled = 1 << 14, // disable interactions -> use BeginDisabled() or ImGuiItemFlags_Disabled ImGuiButtonFlags_AlignTextBaseLine = 1 << 15, // vertically align button to match text baseline - ButtonEx() only // FIXME: Should be removed and handled by SmallButton(), not possible currently because of DC.CursorPosPrevLine @@ -875,7 +910,7 @@ enum ImGuiComboFlagsPrivate_ enum ImGuiSliderFlagsPrivate_ { ImGuiSliderFlags_Vertical = 1 << 20, // Should this slider be orientated vertically? - ImGuiSliderFlags_ReadOnly = 1 << 21, + ImGuiSliderFlags_ReadOnly = 1 << 21, // Consider using g.NextItemData.ItemFlags |= ImGuiItemFlags_ReadOnly instead. }; // Extend ImGuiSelectableFlags_ @@ -896,6 +931,7 @@ enum ImGuiSelectableFlagsPrivate_ enum ImGuiTreeNodeFlagsPrivate_ { ImGuiTreeNodeFlags_ClipLabelForTrailingButton = 1 << 20, + ImGuiTreeNodeFlags_UpsideDownArrow = 1 << 21,// (FIXME-WIP) Turn Down arrow into an Up arrow, but reversed trees (#6517) }; enum ImGuiSeparatorFlags_ @@ -925,7 +961,7 @@ enum ImGuiTextFlags_ enum ImGuiTooltipFlags_ { ImGuiTooltipFlags_None = 0, - ImGuiTooltipFlags_OverridePreviousTooltip = 1 << 0, // Override will clear/ignore previously submitted tooltip (defaults to append) + ImGuiTooltipFlags_OverridePrevious = 1 << 1, // Clear/ignore previously submitted tooltip (defaults to append) }; // FIXME: this is in development, not exposed/functional as a generic feature yet. @@ -959,43 +995,6 @@ enum ImGuiPlotType ImGuiPlotType_Histogram, }; -enum ImGuiPopupPositionPolicy -{ - ImGuiPopupPositionPolicy_Default, - ImGuiPopupPositionPolicy_ComboBox, - ImGuiPopupPositionPolicy_Tooltip, -}; - -struct ImGuiDataVarInfo -{ - ImGuiDataType Type; - ImU32 Count; // 1+ - ImU32 Offset; // Offset in parent structure - void* GetVarPtr(void* parent) const { return (void*)((unsigned char*)parent + Offset); } -}; - -struct ImGuiDataTypeTempStorage -{ - ImU8 Data[8]; // Can fit any data up to ImGuiDataType_COUNT -}; - -// Type information associated to one ImGuiDataType. Retrieve with DataTypeGetInfo(). -struct ImGuiDataTypeInfo -{ - size_t Size; // Size in bytes - const char* Name; // Short descriptive name for the type, for debugging - const char* PrintFmt; // Default printf format for the type - const char* ScanFmt; // Default scanf format for the type -}; - -// Extend ImGuiDataType_ -enum ImGuiDataTypePrivate_ -{ - ImGuiDataType_String = ImGuiDataType_COUNT + 1, - ImGuiDataType_Pointer, - ImGuiDataType_ID, -}; - // Stacked color modifier, backup of modified data so we can restore it struct ImGuiColorMod { @@ -1032,6 +1031,7 @@ struct IMGUI_API ImGuiGroupData ImGuiID WindowID; ImVec2 BackupCursorPos; ImVec2 BackupCursorMaxPos; + ImVec2 BackupCursorPosPrevLine; ImVec1 BackupIndent; ImVec1 BackupGroupOffset; ImVec2 BackupCurrLineSize; @@ -1039,6 +1039,7 @@ struct IMGUI_API ImGuiGroupData ImGuiID BackupActiveIdIsAlive; bool BackupActiveIdPreviousFrameIsAlive; bool BackupHoveredIdIsAlive; + bool BackupIsSameLine; bool EmitItem; }; @@ -1078,7 +1079,7 @@ struct IMGUI_API ImGuiInputTextState int CurLenW, CurLenA; // we need to maintain our buffer length in both UTF-8 and wchar format. UTF-8 length is valid even if TextA is not. ImVector TextW; // edit buffer, we need to persist but can't guarantee the persistence of the user-provided buffer. so we copy into own buffer. ImVector TextA; // temporary UTF8 buffer for callbacks and other operations. this is not updated in every code-path! size=capacity. - ImVector InitialTextA; // backup of end-user buffer at the time of focus (in UTF-8, unaltered) + ImVector InitialTextA; // value to revert to when pressing Escape = backup of end-user buffer at the time of focus (in UTF-8, unaltered) bool TextAIsValid; // temporary UTF8 buffer is not initially valid before we make the widget active (until then we pull the data from user argument) int BufCapacityA; // end-user buffer capacity float ScrollX; // horizontal scrolling/offset @@ -1088,12 +1089,15 @@ struct IMGUI_API ImGuiInputTextState bool SelectedAllMouseLock; // after a double-click to select all, we ignore further mouse drags to update selection bool Edited; // edited this frame ImGuiInputTextFlags Flags; // copy of InputText() flags. may be used to check if e.g. ImGuiInputTextFlags_Password is set. + bool ReloadUserBuf; // force a reload of user buf so it may be modified externally. may be automatic in future version. + int ReloadSelectionStart; // POSITIONS ARE IN IMWCHAR units *NOT* UTF-8 this is why this is not exposed yet. + int ReloadSelectionEnd; ImGuiInputTextState() { memset(this, 0, sizeof(*this)); } void ClearText() { CurLenW = CurLenA = 0; TextW[0] = 0; TextA[0] = 0; CursorClamp(); } void ClearFreeMemory() { TextW.clear(); TextA.clear(); InitialTextA.clear(); } int GetUndoAvailCount() const { return Stb.undostate.undo_point; } - int GetRedoAvailCount() const { return STB_TEXTEDIT_UNDOSTATECOUNT - Stb.undostate.redo_point; } + int GetRedoAvailCount() const { return IMSTB_TEXTEDIT_UNDOSTATECOUNT - Stb.undostate.redo_point; } void OnKeyPressed(int key); // Cannot be inline because we call in code in stb_textedit.h implementation // Cursor & Selection @@ -1105,21 +1109,16 @@ struct IMGUI_API ImGuiInputTextState int GetSelectionStart() const { return Stb.select_start; } int GetSelectionEnd() const { return Stb.select_end; } void SelectAll() { Stb.select_start = 0; Stb.cursor = Stb.select_end = CurLenW; Stb.has_preferred_x = 0; } -}; -// Storage for current popup stack -struct ImGuiPopupData -{ - ImGuiID PopupId; // Set on OpenPopup() - ImGuiWindow* Window; // Resolved on BeginPopup() - may stay unresolved if user never calls OpenPopup() - ImGuiWindow* BackupNavWindow;// Set on OpenPopup(), a NavWindow that will be restored on popup close - int ParentNavLayer; // Resolved on BeginPopup(). Actually a ImGuiNavLayer type (declared down below), initialized to -1 which is not part of an enum, but serves well-enough as "not any of layers" value - int OpenFrameCount; // Set on OpenPopup() - ImGuiID OpenParentId; // Set on OpenPopup(), we need this to differentiate multiple menu sets from each others (e.g. inside menu bar vs loose menu items) - ImVec2 OpenPopupPos; // Set on OpenPopup(), preferred popup position (typically == OpenMousePos when using mouse) - ImVec2 OpenMousePos; // Set on OpenPopup(), copy of mouse position at the time of opening popup + // Reload user buf (WIP #2890) + // If you modify underlying user-passed const char* while active you need to call this (InputText V2 may lift this) + // strcpy(my_buf, "hello"); + // if (ImGuiInputTextState* state = ImGui::GetInputTextState(id)) // id may be ImGui::GetItemID() is last item + // state->ReloadUserBufAndSelectAll(); + void ReloadUserBufAndSelectAll() { ReloadUserBuf = true; ReloadSelectionStart = 0; ReloadSelectionEnd = INT_MAX; } + void ReloadUserBufAndKeepSelection() { ReloadUserBuf = true; ReloadSelectionStart = Stb.select_start; ReloadSelectionEnd = Stb.select_end; } + void ReloadUserBufAndMoveToEnd() { ReloadUserBuf = true; ReloadSelectionStart = ReloadSelectionEnd = INT_MAX; } - ImGuiPopupData() { memset(this, 0, sizeof(*this)); ParentNavLayer = OpenFrameCount = -1; } }; enum ImGuiNextWindowDataFlags_ @@ -1133,6 +1132,10 @@ enum ImGuiNextWindowDataFlags_ ImGuiNextWindowDataFlags_HasFocus = 1 << 5, ImGuiNextWindowDataFlags_HasBgAlpha = 1 << 6, ImGuiNextWindowDataFlags_HasScroll = 1 << 7, + ImGuiNextWindowDataFlags_HasChildFlags = 1 << 8, + ImGuiNextWindowDataFlags_HasViewport = 1 << 9, + ImGuiNextWindowDataFlags_HasDock = 1 << 10, + ImGuiNextWindowDataFlags_HasWindowClass = 1 << 11, }; // Storage for SetNexWindow** functions @@ -1142,39 +1145,53 @@ struct ImGuiNextWindowData ImGuiCond PosCond; ImGuiCond SizeCond; ImGuiCond CollapsedCond; + ImGuiCond DockCond; ImVec2 PosVal; ImVec2 PosPivotVal; ImVec2 SizeVal; ImVec2 ContentSizeVal; ImVec2 ScrollVal; + ImGuiChildFlags ChildFlags; + bool PosUndock; bool CollapsedVal; ImRect SizeConstraintRect; ImGuiSizeCallback SizeCallback; void* SizeCallbackUserData; float BgAlphaVal; // Override background alpha + ImGuiID ViewportId; + ImGuiID DockId; + ImGuiWindowClass WindowClass; ImVec2 MenuBarOffsetMinVal; // (Always on) This is not exposed publicly, so we don't clear it and it doesn't have a corresponding flag (could we? for consistency?) ImGuiNextWindowData() { memset(this, 0, sizeof(*this)); } inline void ClearFlags() { Flags = ImGuiNextWindowDataFlags_None; } }; +// Multi-Selection item index or identifier when using SetNextItemSelectionUserData()/BeginMultiSelect() +// (Most users are likely to use this store an item INDEX but this may be used to store a POINTER as well.) +typedef ImS64 ImGuiSelectionUserData; + enum ImGuiNextItemDataFlags_ { - ImGuiNextItemDataFlags_None = 0, - ImGuiNextItemDataFlags_HasWidth = 1 << 0, - ImGuiNextItemDataFlags_HasOpen = 1 << 1, + ImGuiNextItemDataFlags_None = 0, + ImGuiNextItemDataFlags_HasWidth = 1 << 0, + ImGuiNextItemDataFlags_HasOpen = 1 << 1, + ImGuiNextItemDataFlags_HasShortcut = 1 << 2, }; struct ImGuiNextItemData { ImGuiNextItemDataFlags Flags; - float Width; // Set by SetNextItemWidth() - ImGuiID FocusScopeId; // Set by SetNextItemMultiSelectData() (!= 0 signify value has been set, so it's an alternate version of HasSelectionData, we don't use Flags for this because they are cleared too early. This is mostly used for debugging) - ImGuiCond OpenCond; - bool OpenVal; // Set by SetNextItemOpen() + ImGuiItemFlags ItemFlags; // Currently only tested/used for ImGuiItemFlags_AllowOverlap. + // Non-flags members are NOT cleared by ItemAdd() meaning they are still valid during NavProcessItem() + ImGuiSelectionUserData SelectionUserData; // Set by SetNextItemSelectionUserData() (note that NULL/0 is a valid value, we use -1 == ImGuiSelectionUserData_Invalid to mark invalid values) + float Width; // Set by SetNextItemWidth() + ImGuiKeyChord Shortcut; // Set by SetNextItemShortcut() + bool OpenVal; // Set by SetNextItemOpen() + ImGuiCond OpenCond : 8; - ImGuiNextItemData() { memset(this, 0, sizeof(*this)); } - inline void ClearFlags() { Flags = ImGuiNextItemDataFlags_None; } // Also cleared manually by ItemAdd()! + ImGuiNextItemData() { memset(this, 0, sizeof(*this)); SelectionUserData = -1; } + inline void ClearFlags() { Flags = ImGuiNextItemDataFlags_None; ItemFlags = ImGuiItemFlags_None; } // Also cleared manually by ItemAdd()! }; // Status storage for the last submitted item @@ -1185,11 +1202,23 @@ struct ImGuiLastItemData ImGuiItemStatusFlags StatusFlags; // See ImGuiItemStatusFlags_ ImRect Rect; // Full rectangle ImRect NavRect; // Navigation scoring rectangle (not displayed) - ImRect DisplayRect; // Display rectangle (only if ImGuiItemStatusFlags_HasDisplayRect is set) + // Rarely used fields are not explicitly cleared, only valid when the corresponding ImGuiItemStatusFlags is set. + ImRect DisplayRect; // Display rectangle (ONLY VALID IF ImGuiItemStatusFlags_HasDisplayRect is set) + ImRect ClipRect; // Clip rectangle at the time of submitting item (ONLY VALID IF ImGuiItemStatusFlags_HasClipRect is set) ImGuiLastItemData() { memset(this, 0, sizeof(*this)); } }; +// Store data emitted by TreeNode() for usage by TreePop() to implement ImGuiTreeNodeFlags_NavLeftJumpsBackHere. +// This is the minimum amount of data that we need to perform the equivalent of NavApplyItemToResult() and which we can't infer in TreePop() +// Only stored when the node is a potential candidate for landing on a Left arrow jump. +struct ImGuiNavTreeNodeData +{ + ImGuiID ID; + ImGuiItemFlags InFlags; + ImRect NavRect; +}; + struct IMGUI_API ImGuiStackSizes { short SizeOfIDStack; @@ -1210,9 +1239,9 @@ struct IMGUI_API ImGuiStackSizes // Data saved for each window pushed into the stack struct ImGuiWindowStackData { - ImGuiWindow* Window; - ImGuiLastItemData ParentLastItemDataBackup; - ImGuiStackSizes StackSizesOnBegin; // Store size of various stacks for asserting + ImGuiWindow* Window; + ImGuiLastItemData ParentLastItemDataBackup; + ImGuiStackSizes StackSizesOnBegin; // Store size of various stacks for asserting }; struct ImGuiShrinkWidthItem @@ -1231,6 +1260,66 @@ struct ImGuiPtrOrIndex ImGuiPtrOrIndex(int index) { Ptr = NULL; Index = index; } }; +//----------------------------------------------------------------------------- +// [SECTION] Data types support +//----------------------------------------------------------------------------- + +struct ImGuiDataVarInfo +{ + ImGuiDataType Type; + ImU32 Count; // 1+ + ImU32 Offset; // Offset in parent structure + void* GetVarPtr(void* parent) const { return (void*)((unsigned char*)parent + Offset); } +}; + +struct ImGuiDataTypeTempStorage +{ + ImU8 Data[8]; // Can fit any data up to ImGuiDataType_COUNT +}; + +// Type information associated to one ImGuiDataType. Retrieve with DataTypeGetInfo(). +struct ImGuiDataTypeInfo +{ + size_t Size; // Size in bytes + const char* Name; // Short descriptive name for the type, for debugging + const char* PrintFmt; // Default printf format for the type + const char* ScanFmt; // Default scanf format for the type +}; + +// Extend ImGuiDataType_ +enum ImGuiDataTypePrivate_ +{ + ImGuiDataType_String = ImGuiDataType_COUNT + 1, + ImGuiDataType_Pointer, + ImGuiDataType_ID, +}; + +//----------------------------------------------------------------------------- +// [SECTION] Popup support +//----------------------------------------------------------------------------- + +enum ImGuiPopupPositionPolicy +{ + ImGuiPopupPositionPolicy_Default, + ImGuiPopupPositionPolicy_ComboBox, + ImGuiPopupPositionPolicy_Tooltip, +}; + +// Storage for popup stacks (g.OpenPopupStack and g.BeginPopupStack) +struct ImGuiPopupData +{ + ImGuiID PopupId; // Set on OpenPopup() + ImGuiWindow* Window; // Resolved on BeginPopup() - may stay unresolved if user never calls OpenPopup() + ImGuiWindow* BackupNavWindow;// Set on OpenPopup(), a NavWindow that will be restored on popup close + int ParentNavLayer; // Resolved on BeginPopup(). Actually a ImGuiNavLayer type (declared down below), initialized to -1 which is not part of an enum, but serves well-enough as "not any of layers" value + int OpenFrameCount; // Set on OpenPopup() + ImGuiID OpenParentId; // Set on OpenPopup(), we need this to differentiate multiple menu sets from each others (e.g. inside menu bar vs loose menu items) + ImVec2 OpenPopupPos; // Set on OpenPopup(), preferred popup position (typically == OpenMousePos when using mouse) + ImVec2 OpenMousePos; // Set on OpenPopup(), copy of mouse position at the time of opening popup + + ImGuiPopupData() { memset(this, 0, sizeof(*this)); ParentNavLayer = OpenFrameCount = -1; } +}; + //----------------------------------------------------------------------------- // [SECTION] Inputs support //----------------------------------------------------------------------------- @@ -1266,6 +1355,7 @@ enum ImGuiInputEventType ImGuiInputEventType_MousePos, ImGuiInputEventType_MouseWheel, ImGuiInputEventType_MouseButton, + ImGuiInputEventType_MouseViewport, ImGuiInputEventType_Key, ImGuiInputEventType_Text, ImGuiInputEventType_Focus, @@ -1287,6 +1377,7 @@ enum ImGuiInputSource struct ImGuiInputEventMousePos { float PosX, PosY; ImGuiMouseSource MouseSource; }; struct ImGuiInputEventMouseWheel { float WheelX, WheelY; ImGuiMouseSource MouseSource; }; struct ImGuiInputEventMouseButton { int Button; bool Down; ImGuiMouseSource MouseSource; }; +struct ImGuiInputEventMouseViewport { ImGuiID HoveredViewportID; }; struct ImGuiInputEventKey { ImGuiKey Key; bool Down; float AnalogValue; }; struct ImGuiInputEventText { unsigned int Char; }; struct ImGuiInputEventAppFocused { bool Focused; }; @@ -1301,6 +1392,7 @@ struct ImGuiInputEvent ImGuiInputEventMousePos MousePos; // if Type == ImGuiInputEventType_MousePos ImGuiInputEventMouseWheel MouseWheel; // if Type == ImGuiInputEventType_MouseWheel ImGuiInputEventMouseButton MouseButton; // if Type == ImGuiInputEventType_MouseButton + ImGuiInputEventMouseViewport MouseViewport; // if Type == ImGuiInputEventType_MouseViewport ImGuiInputEventKey Key; // if Type == ImGuiInputEventType_Key ImGuiInputEventText Text; // if Type == ImGuiInputEventType_Text ImGuiInputEventAppFocused AppFocused; // if Type == ImGuiInputEventType_Focus @@ -1321,11 +1413,12 @@ struct ImGuiKeyRoutingData { ImGuiKeyRoutingIndex NextEntryIndex; ImU16 Mods; // Technically we'd only need 4-bits but for simplify we store ImGuiMod_ values which need 16-bits. ImGuiMod_Shortcut is already translated to Ctrl/Super. + ImU8 RoutingCurrScore; // [DEBUG] For debug display ImU8 RoutingNextScore; // Lower is better (0: perfect score) ImGuiID RoutingCurr; ImGuiID RoutingNext; - ImGuiKeyRoutingData() { NextEntryIndex = -1; Mods = 0; RoutingNextScore = 255; RoutingCurr = RoutingNext = ImGuiKeyOwner_None; } + ImGuiKeyRoutingData() { NextEntryIndex = -1; Mods = 0; RoutingCurrScore = RoutingNextScore = 255; RoutingCurr = RoutingNext = ImGuiKeyOwner_None; } }; // Routing table: maintain a desired owner for each possible key-chord (key + mods), and setup owner in NewFrame() when mods are matching. @@ -1353,49 +1446,69 @@ struct ImGuiKeyOwnerData }; // Flags for extended versions of IsKeyPressed(), IsMouseClicked(), Shortcut(), SetKeyOwner(), SetItemKeyOwner() -// Don't mistake with ImGuiInputTextFlags! (for ImGui::InputText() function) +// Don't mistake with ImGuiInputTextFlags! (which is for ImGui::InputText() function) enum ImGuiInputFlags_ { - // Flags for IsKeyPressed(), IsMouseClicked(), Shortcut() + // Flags for IsKeyPressed(), IsKeyChordPressed(), IsMouseClicked(), Shortcut() ImGuiInputFlags_None = 0, - ImGuiInputFlags_Repeat = 1 << 0, // Return true on successive repeats. Default for legacy IsKeyPressed(). NOT Default for legacy IsMouseClicked(). MUST BE == 1. + + // Repeat mode + ImGuiInputFlags_Repeat = 1 << 0, // Enable repeat. Return true on successive repeats. Default for legacy IsKeyPressed(). NOT Default for legacy IsMouseClicked(). MUST BE == 1. ImGuiInputFlags_RepeatRateDefault = 1 << 1, // Repeat rate: Regular (default) ImGuiInputFlags_RepeatRateNavMove = 1 << 2, // Repeat rate: Fast ImGuiInputFlags_RepeatRateNavTweak = 1 << 3, // Repeat rate: Faster - ImGuiInputFlags_RepeatRateMask_ = ImGuiInputFlags_RepeatRateDefault | ImGuiInputFlags_RepeatRateNavMove | ImGuiInputFlags_RepeatRateNavTweak, + + // Repeat mode: Specify when repeating key pressed can be interrupted. + // In theory ImGuiInputFlags_RepeatUntilOtherKeyPress may be a desirable default, but it would break too many behavior so everything is opt-in. + ImGuiInputFlags_RepeatUntilRelease = 1 << 4, // Stop repeating when released (default for all functions except Shortcut). This only exists to allow overriding Shortcut() default behavior. + ImGuiInputFlags_RepeatUntilKeyModsChange = 1 << 5, // Stop repeating when released OR if keyboard mods are changed (default for Shortcut) + ImGuiInputFlags_RepeatUntilKeyModsChangeFromNone = 1 << 6, // Stop repeating when released OR if keyboard mods are leaving the None state. Allows going from Mod+Key to Key by releasing Mod. + ImGuiInputFlags_RepeatUntilOtherKeyPress = 1 << 7, // Stop repeating when released OR if any other keyboard key is pressed during the repeat // Flags for SetItemKeyOwner() - ImGuiInputFlags_CondHovered = 1 << 4, // Only set if item is hovered (default to both) - ImGuiInputFlags_CondActive = 1 << 5, // Only set if item is active (default to both) + ImGuiInputFlags_CondHovered = 1 << 8, // Only set if item is hovered (default to both) + ImGuiInputFlags_CondActive = 1 << 9, // Only set if item is active (default to both) ImGuiInputFlags_CondDefault_ = ImGuiInputFlags_CondHovered | ImGuiInputFlags_CondActive, - ImGuiInputFlags_CondMask_ = ImGuiInputFlags_CondHovered | ImGuiInputFlags_CondActive, // Flags for SetKeyOwner(), SetItemKeyOwner() - ImGuiInputFlags_LockThisFrame = 1 << 6, // Access to key data will require EXPLICIT owner ID (ImGuiKeyOwner_Any/0 will NOT accepted for polling). Cleared at end of frame. This is useful to make input-owner-aware code steal keys from non-input-owner-aware code. - ImGuiInputFlags_LockUntilRelease = 1 << 7, // Access to key data will require EXPLICIT owner ID (ImGuiKeyOwner_Any/0 will NOT accepted for polling). Cleared when the key is released or at end of each frame if key is released. This is useful to make input-owner-aware code steal keys from non-input-owner-aware code. + // Locking is useful to make input-owner-aware code steal keys from non-input-owner-aware code. If all code is input-owner-aware locking would never be necessary. + ImGuiInputFlags_LockThisFrame = 1 << 10, // Further accesses to key data will require EXPLICIT owner ID (ImGuiKeyOwner_Any/0 will NOT accepted for polling). Cleared at end of frame. + ImGuiInputFlags_LockUntilRelease = 1 << 11, // Further accesses to key data will require EXPLICIT owner ID (ImGuiKeyOwner_Any/0 will NOT accepted for polling). Cleared when the key is released or at end of each frame if key is released. // Routing policies for Shortcut() + low-level SetShortcutRouting() // - The general idea is that several callers register interest in a shortcut, and only one owner gets it. - // - When a policy (other than _RouteAlways) is set, Shortcut() will register itself with SetShortcutRouting(), + // Parent -> call Shortcut(Ctrl+S) // When Parent is focused, Parent gets the shortcut. + // Child1 -> call Shortcut(Ctrl+S) // When Child1 is focused, Child1 gets the shortcut (Child1 overrides Parent shortcuts) + // Child2 -> no call // When Child2 is focused, Parent gets the shortcut. + // The whole system is order independent, so if Child1 does it calls before Parent results will be identical. + // This is an important property as it facilitate working with foreign code or larger codebase. + // - Visualize registered routes in 'Metrics->Inputs' and submitted routes in 'Debug Log->InputRouting'. + // - When a policy (except for _RouteAlways *) is set, Shortcut() will register itself with SetShortcutRouting(), // allowing the system to decide where to route the input among other route-aware calls. - // - Shortcut() uses ImGuiInputFlags_RouteFocused by default: meaning that a simple Shortcut() poll - // will register a route and only succeed when parent window is in the focus stack and if no-one - // with a higher priority is claiming the shortcut. - // - Using ImGuiInputFlags_RouteAlways is roughly equivalent to doing e.g. IsKeyPressed(key) + testing mods. + // (* Using ImGuiInputFlags_RouteAlways is roughly equivalent to calling IsKeyChordPressed(key)). + // - Shortcut() uses ImGuiInputFlags_RouteFocused by default. Meaning that a Shortcut() call will register + // a route and only succeed when parent window is in the focus-stack and if no-one with a higher priority + // is claiming the same shortcut. + // - You can chain two unrelated windows in the focus stack using SetWindowParentWindowForFocusRoute(). // - Priorities: GlobalHigh > Focused (when owner is active item) > Global > Focused (when focused window) > GlobalLow. // - Can select only 1 policy among all available. - ImGuiInputFlags_RouteFocused = 1 << 8, // (Default) Register focused route: Accept inputs if window is in focus stack. Deep-most focused window takes inputs. ActiveId takes inputs over deep-most focused window. - ImGuiInputFlags_RouteGlobalLow = 1 << 9, // Register route globally (lowest priority: unless a focused window or active item registered the route) -> recommended Global priority. - ImGuiInputFlags_RouteGlobal = 1 << 10, // Register route globally (medium priority: unless an active item registered the route, e.g. CTRL+A registered by InputText). - ImGuiInputFlags_RouteGlobalHigh = 1 << 11, // Register route globally (highest priority: unlikely you need to use that: will interfere with every active items) - ImGuiInputFlags_RouteMask_ = ImGuiInputFlags_RouteFocused | ImGuiInputFlags_RouteGlobal | ImGuiInputFlags_RouteGlobalLow | ImGuiInputFlags_RouteGlobalHigh, // _Always not part of this! - ImGuiInputFlags_RouteAlways = 1 << 12, // Do not register route, poll keys directly. - ImGuiInputFlags_RouteUnlessBgFocused= 1 << 13, // Global routes will not be applied if underlying background/void is focused (== no Dear ImGui windows are focused). Useful for overlay applications. - ImGuiInputFlags_RouteExtraMask_ = ImGuiInputFlags_RouteAlways | ImGuiInputFlags_RouteUnlessBgFocused, + ImGuiInputFlags_RouteFocused = 1 << 12, // (Default) Honor focus route: Accept inputs if window is in focus stack. Deep-most focused window takes inputs. ActiveId takes inputs over deep-most focused window. + ImGuiInputFlags_RouteGlobalLow = 1 << 13, // Register route globally (lowest priority: unless a focused window or active item registered the route) -> recommended Global priority IF you need a Global priority. + ImGuiInputFlags_RouteGlobal = 1 << 14, // Register route globally (medium priority: unless an active item registered the route, e.g. CTRL+A registered by InputText will take priority over this). + ImGuiInputFlags_RouteGlobalHigh = 1 << 15, // Register route globally (higher priority: unlikely you need to use that: will interfere with every active items, e.g. CTRL+A registered by InputText will be overriden by this) + ImGuiInputFlags_RouteAlways = 1 << 16, // Do not register route, poll keys directly. + // Routing polices: extra options + ImGuiInputFlags_RouteUnlessBgFocused= 1 << 17, // Global routes will not be applied if underlying background/void is focused (== no Dear ImGui windows are focused). Useful for overlay applications. // [Internal] Mask of which function support which flags - ImGuiInputFlags_SupportedByIsKeyPressed = ImGuiInputFlags_Repeat | ImGuiInputFlags_RepeatRateMask_, - ImGuiInputFlags_SupportedByShortcut = ImGuiInputFlags_Repeat | ImGuiInputFlags_RepeatRateMask_ | ImGuiInputFlags_RouteMask_ | ImGuiInputFlags_RouteExtraMask_, + ImGuiInputFlags_RepeatRateMask_ = ImGuiInputFlags_RepeatRateDefault | ImGuiInputFlags_RepeatRateNavMove | ImGuiInputFlags_RepeatRateNavTweak, + ImGuiInputFlags_RepeatUntilMask_ = ImGuiInputFlags_RepeatUntilRelease | ImGuiInputFlags_RepeatUntilKeyModsChange | ImGuiInputFlags_RepeatUntilKeyModsChangeFromNone | ImGuiInputFlags_RepeatUntilOtherKeyPress, + ImGuiInputFlags_RepeatMask_ = ImGuiInputFlags_Repeat | ImGuiInputFlags_RepeatRateMask_ | ImGuiInputFlags_RepeatUntilMask_, + ImGuiInputFlags_CondMask_ = ImGuiInputFlags_CondHovered | ImGuiInputFlags_CondActive, + ImGuiInputFlags_RouteMask_ = ImGuiInputFlags_RouteFocused | ImGuiInputFlags_RouteGlobal | ImGuiInputFlags_RouteGlobalLow | ImGuiInputFlags_RouteGlobalHigh, // _Always not part of this! + ImGuiInputFlags_SupportedByIsKeyPressed = ImGuiInputFlags_RepeatMask_, + ImGuiInputFlags_SupportedByIsMouseClicked = ImGuiInputFlags_Repeat, + ImGuiInputFlags_SupportedByShortcut = ImGuiInputFlags_RepeatMask_ | ImGuiInputFlags_RouteMask_ | ImGuiInputFlags_RouteAlways | ImGuiInputFlags_RouteUnlessBgFocused, ImGuiInputFlags_SupportedBySetKeyOwner = ImGuiInputFlags_LockThisFrame | ImGuiInputFlags_LockUntilRelease, ImGuiInputFlags_SupportedBySetItemKeyOwner = ImGuiInputFlags_SupportedBySetKeyOwner | ImGuiInputFlags_CondMask_, }; @@ -1440,6 +1553,8 @@ enum ImGuiActivateFlags_ ImGuiActivateFlags_PreferInput = 1 << 0, // Favor activation that requires keyboard text input (e.g. for Slider/Drag). Default for Enter key. ImGuiActivateFlags_PreferTweak = 1 << 1, // Favor activation for tweaking with arrows or gamepad (e.g. for Slider/Drag). Default for Space key and if keyboard is not used. ImGuiActivateFlags_TryToPreserveState = 1 << 2, // Request widget to preserve state if it can (e.g. InputText will try to preserve cursor/selection) + ImGuiActivateFlags_FromTabbing = 1 << 3, // Activation requested by a tabbing request + ImGuiActivateFlags_FromShortcut = 1 << 4, // Activation requested by an item shortcut via SetNextItemShortcut() function. }; // Early work-in-progress API for ScrollToItem() @@ -1460,8 +1575,7 @@ enum ImGuiScrollFlags_ enum ImGuiNavHighlightFlags_ { ImGuiNavHighlightFlags_None = 0, - ImGuiNavHighlightFlags_TypeDefault = 1 << 0, - ImGuiNavHighlightFlags_TypeThin = 1 << 1, + ImGuiNavHighlightFlags_Compact = 1 << 1, // Compact highlight, no padding ImGuiNavHighlightFlags_AlwaysDraw = 1 << 2, // Draw rectangular highlight if (g.NavId == id) _even_ when using the mouse. ImGuiNavHighlightFlags_NoRounding = 1 << 3, }; @@ -1479,10 +1593,12 @@ enum ImGuiNavMoveFlags_ ImGuiNavMoveFlags_ScrollToEdgeY = 1 << 6, // Force scrolling to min/max (used by Home/End) // FIXME-NAV: Aim to remove or reword, probably unnecessary ImGuiNavMoveFlags_Forwarded = 1 << 7, ImGuiNavMoveFlags_DebugNoResult = 1 << 8, // Dummy scoring for debug purpose, don't apply result - ImGuiNavMoveFlags_FocusApi = 1 << 9, - ImGuiNavMoveFlags_Tabbing = 1 << 10, // == Focus + Activate if item is Inputable + DontChangeNavHighlight - ImGuiNavMoveFlags_Activate = 1 << 11, - ImGuiNavMoveFlags_DontSetNavHighlight = 1 << 12, // Do not alter the visible state of keyboard vs mouse nav highlight + ImGuiNavMoveFlags_FocusApi = 1 << 9, // Requests from focus API can land/focus/activate items even if they are marked with _NoTabStop (see NavProcessItemForTabbingRequest() for details) + ImGuiNavMoveFlags_IsTabbing = 1 << 10, // == Focus + Activate if item is Inputable + DontChangeNavHighlight + ImGuiNavMoveFlags_IsPageMove = 1 << 11, // Identify a PageDown/PageUp request. + ImGuiNavMoveFlags_Activate = 1 << 12, // Activate/select target item. + ImGuiNavMoveFlags_NoSelect = 1 << 13, // Don't trigger selection by not setting g.NavJustMovedTo + ImGuiNavMoveFlags_NoSetNavHighlight = 1 << 14, // Do not alter the visible state of keyboard vs mouse nav highlight }; enum ImGuiNavLayer @@ -1499,19 +1615,63 @@ struct ImGuiNavItemData ImGuiID FocusScopeId; // Init,Move // Best candidate focus scope ID ImRect RectRel; // Init,Move // Best candidate bounding box in window relative space ImGuiItemFlags InFlags; // ????,Move // Best candidate item flags + ImGuiSelectionUserData SelectionUserData;//I+Mov // Best candidate SetNextItemSelectionData() value. float DistBox; // Move // Best candidate box distance to current NavId float DistCenter; // Move // Best candidate center distance to current NavId float DistAxial; // Move // Best candidate axial distance to current NavId ImGuiNavItemData() { Clear(); } - void Clear() { Window = NULL; ID = FocusScopeId = 0; InFlags = 0; DistBox = DistCenter = DistAxial = FLT_MAX; } + void Clear() { Window = NULL; ID = FocusScopeId = 0; InFlags = 0; SelectionUserData = -1; DistBox = DistCenter = DistAxial = FLT_MAX; } +}; + +struct ImGuiFocusScopeData +{ + ImGuiID ID; + ImGuiID WindowID; +}; + +//----------------------------------------------------------------------------- +// [SECTION] Typing-select support +//----------------------------------------------------------------------------- + +// Flags for GetTypingSelectRequest() +enum ImGuiTypingSelectFlags_ +{ + ImGuiTypingSelectFlags_None = 0, + ImGuiTypingSelectFlags_AllowBackspace = 1 << 0, // Backspace to delete character inputs. If using: ensure GetTypingSelectRequest() is not called more than once per frame (filter by e.g. focus state) + ImGuiTypingSelectFlags_AllowSingleCharMode = 1 << 1, // Allow "single char" search mode which is activated when pressing the same character multiple times. +}; + +// Returned by GetTypingSelectRequest(), designed to eventually be public. +struct IMGUI_API ImGuiTypingSelectRequest +{ + ImGuiTypingSelectFlags Flags; // Flags passed to GetTypingSelectRequest() + int SearchBufferLen; + const char* SearchBuffer; // Search buffer contents (use full string. unless SingleCharMode is set, in which case use SingleCharSize). + bool SelectRequest; // Set when buffer was modified this frame, requesting a selection. + bool SingleCharMode; // Notify when buffer contains same character repeated, to implement special mode. In this situation it preferred to not display any on-screen search indication. + ImS8 SingleCharSize; // Length in bytes of first letter codepoint (1 for ascii, 2-4 for UTF-8). If (SearchBufferLen==RepeatCharSize) only 1 letter has been input. +}; + +// Storage for GetTypingSelectRequest() +struct IMGUI_API ImGuiTypingSelectState +{ + ImGuiTypingSelectRequest Request; // User-facing data + char SearchBuffer[64]; // Search buffer: no need to make dynamic as this search is very transient. + ImGuiID FocusScope; + int LastRequestFrame = 0; + float LastRequestTime = 0.0f; + bool SingleCharModeLock = false; // After a certain single char repeat count we lock into SingleCharMode. Two benefits: 1) buffer never fill, 2) we can provide an immediate SingleChar mode without timer elapsing. + + ImGuiTypingSelectState() { memset(this, 0, sizeof(*this)); } + void Clear() { SearchBuffer[0] = 0; SingleCharModeLock = false; } // We preserve remaining data for easier debugging }; //----------------------------------------------------------------------------- // [SECTION] Columns support //----------------------------------------------------------------------------- -// Flags for internal's BeginColumns(). Prefix using BeginTable() nowadays! +// Flags for internal's BeginColumns(). This is an obsolete API. Prefer using BeginTable() nowadays! enum ImGuiOldColumnFlags_ { ImGuiOldColumnFlags_None = 0, @@ -1519,16 +1679,16 @@ enum ImGuiOldColumnFlags_ ImGuiOldColumnFlags_NoResize = 1 << 1, // Disable resizing columns when clicking on the dividers ImGuiOldColumnFlags_NoPreserveWidths = 1 << 2, // Disable column width preservation when adjusting columns ImGuiOldColumnFlags_NoForceWithinWindow = 1 << 3, // Disable forcing columns to fit within window - ImGuiOldColumnFlags_GrowParentContentsSize = 1 << 4, // (WIP) Restore pre-1.51 behavior of extending the parent window contents size but _without affecting the columns width at all_. Will eventually remove. + ImGuiOldColumnFlags_GrowParentContentsSize = 1 << 4, // Restore pre-1.51 behavior of extending the parent window contents size but _without affecting the columns width at all_. Will eventually remove. // Obsolete names (will be removed) #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS - ImGuiColumnsFlags_None = ImGuiOldColumnFlags_None, - ImGuiColumnsFlags_NoBorder = ImGuiOldColumnFlags_NoBorder, - ImGuiColumnsFlags_NoResize = ImGuiOldColumnFlags_NoResize, - ImGuiColumnsFlags_NoPreserveWidths = ImGuiOldColumnFlags_NoPreserveWidths, - ImGuiColumnsFlags_NoForceWithinWindow = ImGuiOldColumnFlags_NoForceWithinWindow, - ImGuiColumnsFlags_GrowParentContentsSize = ImGuiOldColumnFlags_GrowParentContentsSize, + //ImGuiColumnsFlags_None = ImGuiOldColumnFlags_None, + //ImGuiColumnsFlags_NoBorder = ImGuiOldColumnFlags_NoBorder, + //ImGuiColumnsFlags_NoResize = ImGuiOldColumnFlags_NoResize, + //ImGuiColumnsFlags_NoPreserveWidths = ImGuiOldColumnFlags_NoPreserveWidths, + //ImGuiColumnsFlags_NoForceWithinWindow = ImGuiOldColumnFlags_NoForceWithinWindow, + //ImGuiColumnsFlags_GrowParentContentsSize = ImGuiOldColumnFlags_GrowParentContentsSize, #endif }; @@ -1567,6 +1727,9 @@ struct ImGuiOldColumns // [SECTION] Multi-select support //----------------------------------------------------------------------------- +// We always assume that -1 is an invalid value (which works for indices and pointers) +#define ImGuiSelectionUserData_Invalid ((ImGuiSelectionUserData)-1) + #ifdef IMGUI_HAS_MULTI_SELECT // #endif // #ifdef IMGUI_HAS_MULTI_SELECT @@ -1575,8 +1738,149 @@ struct ImGuiOldColumns // [SECTION] Docking support //----------------------------------------------------------------------------- +#define DOCKING_HOST_DRAW_CHANNEL_BG 0 // Dock host: background fill +#define DOCKING_HOST_DRAW_CHANNEL_FG 1 // Dock host: decorations and contents + #ifdef IMGUI_HAS_DOCK -// + +// Extend ImGuiDockNodeFlags_ +enum ImGuiDockNodeFlagsPrivate_ +{ + // [Internal] + ImGuiDockNodeFlags_DockSpace = 1 << 10, // Saved // A dockspace is a node that occupy space within an existing user window. Otherwise the node is floating and create its own window. + ImGuiDockNodeFlags_CentralNode = 1 << 11, // Saved // The central node has 2 main properties: stay visible when empty, only use "remaining" spaces from its neighbor. + ImGuiDockNodeFlags_NoTabBar = 1 << 12, // Saved // Tab bar is completely unavailable. No triangle in the corner to enable it back. + ImGuiDockNodeFlags_HiddenTabBar = 1 << 13, // Saved // Tab bar is hidden, with a triangle in the corner to show it again (NB: actual tab-bar instance may be destroyed as this is only used for single-window tab bar) + ImGuiDockNodeFlags_NoWindowMenuButton = 1 << 14, // Saved // Disable window/docking menu (that one that appears instead of the collapse button) + ImGuiDockNodeFlags_NoCloseButton = 1 << 15, // Saved // Disable close button + ImGuiDockNodeFlags_NoResizeX = 1 << 16, // // + ImGuiDockNodeFlags_NoResizeY = 1 << 17, // // + ImGuiDockNodeFlags_DockedWindowsInFocusRoute= 1 << 18, // // Any docked window will be automatically be focus-route chained (window->ParentWindowForFocusRoute set to this) so Shortcut() in this window can run when any docked window is focused. + // Disable docking/undocking actions in this dockspace or individual node (existing docked nodes will be preserved) + // Those are not exposed in public because the desirable sharing/inheriting/copy-flag-on-split behaviors are quite difficult to design and understand. + // The two public flags ImGuiDockNodeFlags_NoDockingOverCentralNode/ImGuiDockNodeFlags_NoDockingSplit don't have those issues. + ImGuiDockNodeFlags_NoDockingSplitOther = 1 << 19, // // Disable this node from splitting other windows/nodes. + ImGuiDockNodeFlags_NoDockingOverMe = 1 << 20, // // Disable other windows/nodes from being docked over this node. + ImGuiDockNodeFlags_NoDockingOverOther = 1 << 21, // // Disable this node from being docked over another window or non-empty node. + ImGuiDockNodeFlags_NoDockingOverEmpty = 1 << 22, // // Disable this node from being docked over an empty node (e.g. DockSpace with no other windows) + ImGuiDockNodeFlags_NoDocking = ImGuiDockNodeFlags_NoDockingOverMe | ImGuiDockNodeFlags_NoDockingOverOther | ImGuiDockNodeFlags_NoDockingOverEmpty | ImGuiDockNodeFlags_NoDockingSplit | ImGuiDockNodeFlags_NoDockingSplitOther, + // Masks + ImGuiDockNodeFlags_SharedFlagsInheritMask_ = ~0, + ImGuiDockNodeFlags_NoResizeFlagsMask_ = ImGuiDockNodeFlags_NoResize | ImGuiDockNodeFlags_NoResizeX | ImGuiDockNodeFlags_NoResizeY, + // When splitting, those local flags are moved to the inheriting child, never duplicated + ImGuiDockNodeFlags_LocalFlagsTransferMask_ = ImGuiDockNodeFlags_NoDockingSplit | ImGuiDockNodeFlags_NoResizeFlagsMask_ | ImGuiDockNodeFlags_AutoHideTabBar | ImGuiDockNodeFlags_CentralNode | ImGuiDockNodeFlags_NoTabBar | ImGuiDockNodeFlags_HiddenTabBar | ImGuiDockNodeFlags_NoWindowMenuButton | ImGuiDockNodeFlags_NoCloseButton, + ImGuiDockNodeFlags_SavedFlagsMask_ = ImGuiDockNodeFlags_NoResizeFlagsMask_ | ImGuiDockNodeFlags_DockSpace | ImGuiDockNodeFlags_CentralNode | ImGuiDockNodeFlags_NoTabBar | ImGuiDockNodeFlags_HiddenTabBar | ImGuiDockNodeFlags_NoWindowMenuButton | ImGuiDockNodeFlags_NoCloseButton, +}; + +// Store the source authority (dock node vs window) of a field +enum ImGuiDataAuthority_ +{ + ImGuiDataAuthority_Auto, + ImGuiDataAuthority_DockNode, + ImGuiDataAuthority_Window, +}; + +enum ImGuiDockNodeState +{ + ImGuiDockNodeState_Unknown, + ImGuiDockNodeState_HostWindowHiddenBecauseSingleWindow, + ImGuiDockNodeState_HostWindowHiddenBecauseWindowsAreResizing, + ImGuiDockNodeState_HostWindowVisible, +}; + +// sizeof() 156~192 +struct IMGUI_API ImGuiDockNode +{ + ImGuiID ID; + ImGuiDockNodeFlags SharedFlags; // (Write) Flags shared by all nodes of a same dockspace hierarchy (inherited from the root node) + ImGuiDockNodeFlags LocalFlags; // (Write) Flags specific to this node + ImGuiDockNodeFlags LocalFlagsInWindows; // (Write) Flags specific to this node, applied from windows + ImGuiDockNodeFlags MergedFlags; // (Read) Effective flags (== SharedFlags | LocalFlagsInNode | LocalFlagsInWindows) + ImGuiDockNodeState State; + ImGuiDockNode* ParentNode; + ImGuiDockNode* ChildNodes[2]; // [Split node only] Child nodes (left/right or top/bottom). Consider switching to an array. + ImVector Windows; // Note: unordered list! Iterate TabBar->Tabs for user-order. + ImGuiTabBar* TabBar; + ImVec2 Pos; // Current position + ImVec2 Size; // Current size + ImVec2 SizeRef; // [Split node only] Last explicitly written-to size (overridden when using a splitter affecting the node), used to calculate Size. + ImGuiAxis SplitAxis; // [Split node only] Split axis (X or Y) + ImGuiWindowClass WindowClass; // [Root node only] + ImU32 LastBgColor; + + ImGuiWindow* HostWindow; + ImGuiWindow* VisibleWindow; // Generally point to window which is ID is == SelectedTabID, but when CTRL+Tabbing this can be a different window. + ImGuiDockNode* CentralNode; // [Root node only] Pointer to central node. + ImGuiDockNode* OnlyNodeWithWindows; // [Root node only] Set when there is a single visible node within the hierarchy. + int CountNodeWithWindows; // [Root node only] + int LastFrameAlive; // Last frame number the node was updated or kept alive explicitly with DockSpace() + ImGuiDockNodeFlags_KeepAliveOnly + int LastFrameActive; // Last frame number the node was updated. + int LastFrameFocused; // Last frame number the node was focused. + ImGuiID LastFocusedNodeId; // [Root node only] Which of our child docking node (any ancestor in the hierarchy) was last focused. + ImGuiID SelectedTabId; // [Leaf node only] Which of our tab/window is selected. + ImGuiID WantCloseTabId; // [Leaf node only] Set when closing a specific tab/window. + ImGuiID RefViewportId; // Reference viewport ID from visible window when HostWindow == NULL. + ImGuiDataAuthority AuthorityForPos :3; + ImGuiDataAuthority AuthorityForSize :3; + ImGuiDataAuthority AuthorityForViewport :3; + bool IsVisible :1; // Set to false when the node is hidden (usually disabled as it has no active window) + bool IsFocused :1; + bool IsBgDrawnThisFrame :1; + bool HasCloseButton :1; // Provide space for a close button (if any of the docked window has one). Note that button may be hidden on window without one. + bool HasWindowMenuButton :1; + bool HasCentralNodeChild :1; + bool WantCloseAll :1; // Set when closing all tabs at once. + bool WantLockSizeOnce :1; + bool WantMouseMove :1; // After a node extraction we need to transition toward moving the newly created host window + bool WantHiddenTabBarUpdate :1; + bool WantHiddenTabBarToggle :1; + + ImGuiDockNode(ImGuiID id); + ~ImGuiDockNode(); + bool IsRootNode() const { return ParentNode == NULL; } + bool IsDockSpace() const { return (MergedFlags & ImGuiDockNodeFlags_DockSpace) != 0; } + bool IsFloatingNode() const { return ParentNode == NULL && (MergedFlags & ImGuiDockNodeFlags_DockSpace) == 0; } + bool IsCentralNode() const { return (MergedFlags & ImGuiDockNodeFlags_CentralNode) != 0; } + bool IsHiddenTabBar() const { return (MergedFlags & ImGuiDockNodeFlags_HiddenTabBar) != 0; } // Hidden tab bar can be shown back by clicking the small triangle + bool IsNoTabBar() const { return (MergedFlags & ImGuiDockNodeFlags_NoTabBar) != 0; } // Never show a tab bar + bool IsSplitNode() const { return ChildNodes[0] != NULL; } + bool IsLeafNode() const { return ChildNodes[0] == NULL; } + bool IsEmpty() const { return ChildNodes[0] == NULL && Windows.Size == 0; } + ImRect Rect() const { return ImRect(Pos.x, Pos.y, Pos.x + Size.x, Pos.y + Size.y); } + + void SetLocalFlags(ImGuiDockNodeFlags flags) { LocalFlags = flags; UpdateMergedFlags(); } + void UpdateMergedFlags() { MergedFlags = SharedFlags | LocalFlags | LocalFlagsInWindows; } +}; + +// List of colors that are stored at the time of Begin() into Docked Windows. +// We currently store the packed colors in a simple array window->DockStyle.Colors[]. +// A better solution may involve appending into a log of colors in ImGuiContext + store offsets into those arrays in ImGuiWindow, +// but it would be more complex as we'd need to double-buffer both as e.g. drop target may refer to window from last frame. +enum ImGuiWindowDockStyleCol +{ + ImGuiWindowDockStyleCol_Text, + ImGuiWindowDockStyleCol_Tab, + ImGuiWindowDockStyleCol_TabHovered, + ImGuiWindowDockStyleCol_TabActive, + ImGuiWindowDockStyleCol_TabUnfocused, + ImGuiWindowDockStyleCol_TabUnfocusedActive, + ImGuiWindowDockStyleCol_COUNT +}; + +struct ImGuiWindowDockStyle +{ + ImU32 Colors[ImGuiWindowDockStyleCol_COUNT]; +}; + +struct ImGuiDockContext +{ + ImGuiStorage Nodes; // Map ID -> ImGuiDockNode*: Active nodes + ImVector Requests; + ImVector NodesSettings; + bool WantFullRebuild; + ImGuiDockContext() { memset(this, 0, sizeof(*this)); } +}; + #endif // #ifdef IMGUI_HAS_DOCK //----------------------------------------------------------------------------- @@ -1587,18 +1891,31 @@ struct ImGuiOldColumns // Every instance of ImGuiViewport is in fact a ImGuiViewportP. struct ImGuiViewportP : public ImGuiViewport { - int DrawListsLastFrame[2]; // Last frame number the background (0) and foreground (1) draw lists were used - ImDrawList* DrawLists[2]; // Convenience background (0) and foreground (1) draw lists. We use them to draw software mouser cursor when io.MouseDrawCursor is set and to draw most debug overlays. + ImGuiWindow* Window; // Set when the viewport is owned by a window (and ImGuiViewportFlags_CanHostOtherWindows is NOT set) + int Idx; + int LastFrameActive; // Last frame number this viewport was activated by a window + int LastFocusedStampCount; // Last stamp number from when a window hosted by this viewport was focused (by comparing this value between two viewport we have an implicit viewport z-order we use as fallback) + ImGuiID LastNameHash; + ImVec2 LastPos; + float Alpha; // Window opacity (when dragging dockable windows/viewports we make them transparent) + float LastAlpha; + bool LastFocusedHadNavWindow;// Instead of maintaining a LastFocusedWindow (which may harder to correctly maintain), we merely store weither NavWindow != NULL last time the viewport was focused. + short PlatformMonitor; + int BgFgDrawListsLastFrame[2]; // Last frame number the background (0) and foreground (1) draw lists were used + ImDrawList* BgFgDrawLists[2]; // Convenience background (0) and foreground (1) draw lists. We use them to draw software mouser cursor when io.MouseDrawCursor is set and to draw most debug overlays. ImDrawData DrawDataP; - ImDrawDataBuilder DrawDataBuilder; - + ImDrawDataBuilder DrawDataBuilder; // Temporary data while building final ImDrawData + ImVec2 LastPlatformPos; + ImVec2 LastPlatformSize; + ImVec2 LastRendererSize; ImVec2 WorkOffsetMin; // Work Area: Offset from Pos to top-left corner of Work Area. Generally (0,0) or (0,+main_menu_bar_height). Work Area is Full Area but without menu-bars/status-bars (so WorkArea always fit inside Pos/Size!) ImVec2 WorkOffsetMax; // Work Area: Offset from Pos+Size to bottom-right corner of Work Area. Generally (0,0) or (0,-status_bar_height). ImVec2 BuildWorkOffsetMin; // Work Area: Offset being built during current frame. Generally >= 0.0f. ImVec2 BuildWorkOffsetMax; // Work Area: Offset being built during current frame. Generally <= 0.0f. - ImGuiViewportP() { DrawListsLastFrame[0] = DrawListsLastFrame[1] = -1; DrawLists[0] = DrawLists[1] = NULL; } - ~ImGuiViewportP() { if (DrawLists[0]) IM_DELETE(DrawLists[0]); if (DrawLists[1]) IM_DELETE(DrawLists[1]); } + ImGuiViewportP() { Window = NULL; Idx = -1; LastFrameActive = BgFgDrawListsLastFrame[0] = BgFgDrawListsLastFrame[1] = LastFocusedStampCount = -1; LastNameHash = 0; Alpha = LastAlpha = 1.0f; LastFocusedHadNavWindow = false; PlatformMonitor = -1; BgFgDrawLists[0] = BgFgDrawLists[1] = NULL; LastPlatformPos = LastPlatformSize = LastRendererSize = ImVec2(FLT_MAX, FLT_MAX); } + ~ImGuiViewportP() { if (BgFgDrawLists[0]) IM_DELETE(BgFgDrawLists[0]); if (BgFgDrawLists[1]) IM_DELETE(BgFgDrawLists[1]); } + void ClearRequestFlags() { PlatformRequestClose = PlatformRequestMove = PlatformRequestResize = false; } // Calculate work rect pos/size given a set of offset (we have 1 pair of offset for rect locked from last frame data, and 1 pair for currently building rect) ImVec2 CalcWorkRectPos(const ImVec2& off_min) const { return ImVec2(Pos.x + off_min.x, Pos.y + off_min.y); } @@ -1621,13 +1938,19 @@ struct ImGuiViewportP : public ImGuiViewport struct ImGuiWindowSettings { ImGuiID ID; - ImVec2ih Pos; + ImVec2ih Pos; // NB: Settings position are stored RELATIVE to the viewport! Whereas runtime ones are absolute positions. ImVec2ih Size; + ImVec2ih ViewportPos; + ImGuiID ViewportId; + ImGuiID DockId; // ID of last known DockNode (even if the DockNode is invisible because it has only 1 active window), or 0 if none. + ImGuiID ClassId; // ID of window class if specified + short DockOrder; // Order of the last time the window was visible within its DockNode. This is used to reorder windows that are reappearing on the same frame. Same value between windows that were active and windows that were none are possible. bool Collapsed; + bool IsChild; bool WantApply; // Set when loaded from .ini data (to enable merging/loading .ini data into an already running context) bool WantDelete; // Set to invalidate/delete the settings entry - ImGuiWindowSettings() { memset(this, 0, sizeof(*this)); } + ImGuiWindowSettings() { memset(this, 0, sizeof(*this)); DockOrder = -1; } char* GetName() { return (char*)(this + 1); } }; @@ -1653,6 +1976,7 @@ struct ImGuiSettingsHandler // This is experimental and not officially supported, it'll probably fall short of features, if/when it does we may backtrack. enum ImGuiLocKey : int { + ImGuiLocKey_VersionStr, ImGuiLocKey_TableSizeOne, ImGuiLocKey_TableSizeAllFit, ImGuiLocKey_TableSizeAllDefault, @@ -1660,6 +1984,9 @@ enum ImGuiLocKey : int ImGuiLocKey_WindowingMainMenuBar, ImGuiLocKey_WindowingPopup, ImGuiLocKey_WindowingUntitled, + ImGuiLocKey_DockingHideTabBar, + ImGuiLocKey_DockingHoldShiftToDock, + ImGuiLocKey_DockingDragToUndockOrMoveNode, ImGuiLocKey_COUNT }; @@ -1677,30 +2004,56 @@ struct ImGuiLocEntry enum ImGuiDebugLogFlags_ { // Event types - ImGuiDebugLogFlags_None = 0, - ImGuiDebugLogFlags_EventActiveId = 1 << 0, - ImGuiDebugLogFlags_EventFocus = 1 << 1, - ImGuiDebugLogFlags_EventPopup = 1 << 2, - ImGuiDebugLogFlags_EventNav = 1 << 3, - ImGuiDebugLogFlags_EventClipper = 1 << 4, - ImGuiDebugLogFlags_EventSelection = 1 << 5, - ImGuiDebugLogFlags_EventIO = 1 << 6, - ImGuiDebugLogFlags_EventMask_ = ImGuiDebugLogFlags_EventActiveId | ImGuiDebugLogFlags_EventFocus | ImGuiDebugLogFlags_EventPopup | ImGuiDebugLogFlags_EventNav | ImGuiDebugLogFlags_EventClipper | ImGuiDebugLogFlags_EventSelection | ImGuiDebugLogFlags_EventIO, - ImGuiDebugLogFlags_OutputToTTY = 1 << 10, // Also send output to TTY + ImGuiDebugLogFlags_None = 0, + ImGuiDebugLogFlags_EventActiveId = 1 << 0, + ImGuiDebugLogFlags_EventFocus = 1 << 1, + ImGuiDebugLogFlags_EventPopup = 1 << 2, + ImGuiDebugLogFlags_EventNav = 1 << 3, + ImGuiDebugLogFlags_EventClipper = 1 << 4, + ImGuiDebugLogFlags_EventSelection = 1 << 5, + ImGuiDebugLogFlags_EventIO = 1 << 6, + ImGuiDebugLogFlags_EventInputRouting = 1 << 7, + ImGuiDebugLogFlags_EventDocking = 1 << 8, + ImGuiDebugLogFlags_EventViewport = 1 << 9, + + ImGuiDebugLogFlags_EventMask_ = ImGuiDebugLogFlags_EventActiveId | ImGuiDebugLogFlags_EventFocus | ImGuiDebugLogFlags_EventPopup | ImGuiDebugLogFlags_EventNav | ImGuiDebugLogFlags_EventClipper | ImGuiDebugLogFlags_EventSelection | ImGuiDebugLogFlags_EventIO | ImGuiDebugLogFlags_EventInputRouting | ImGuiDebugLogFlags_EventDocking | ImGuiDebugLogFlags_EventViewport, + ImGuiDebugLogFlags_OutputToTTY = 1 << 20, // Also send output to TTY + ImGuiDebugLogFlags_OutputToTestEngine = 1 << 21, // Also send output to Test Engine +}; + +struct ImGuiDebugAllocEntry +{ + int FrameCount; + ImS16 AllocCount; + ImS16 FreeCount; +}; + +struct ImGuiDebugAllocInfo +{ + int TotalAllocCount; // Number of call to MemAlloc(). + int TotalFreeCount; + ImS16 LastEntriesIdx; // Current index in buffer + ImGuiDebugAllocEntry LastEntriesBuf[6]; // Track last 6 frames that had allocations + + ImGuiDebugAllocInfo() { memset(this, 0, sizeof(*this)); } }; struct ImGuiMetricsConfig { bool ShowDebugLog = false; - bool ShowStackTool = false; + bool ShowIDStackTool = false; bool ShowWindowsRects = false; bool ShowWindowsBeginOrder = false; bool ShowTablesRects = false; bool ShowDrawCmdMesh = true; bool ShowDrawCmdBoundingBoxes = true; + bool ShowTextEncodingViewer = false; bool ShowAtlasTintedWithTextColor = false; + bool ShowDockingNodes = false; int ShowWindowsRectsType = -1; int ShowTablesRectsType = -1; + int HighlightMonitorIdx = -1; + ImGuiID HighlightViewportID = 0; }; struct ImGuiStackLevelInfo @@ -1714,8 +2067,8 @@ struct ImGuiStackLevelInfo ImGuiStackLevelInfo() { memset(this, 0, sizeof(*this)); } }; -// State for Stack tool queries -struct ImGuiStackTool +// State for ID Stack tool queries +struct ImGuiIDStackTool { int LastActiveFrame; int StackLevel; // -1: query stack and resize Results, >= 0: individual stack level @@ -1724,7 +2077,7 @@ struct ImGuiStackTool bool CopyToClipboardOnCtrlC; float CopyToClipboardLastTime; - ImGuiStackTool() { memset(this, 0, sizeof(*this)); CopyToClipboardLastTime = -FLT_MAX; } + ImGuiIDStackTool() { memset(this, 0, sizeof(*this)); CopyToClipboardLastTime = -FLT_MAX; } }; //----------------------------------------------------------------------------- @@ -1754,7 +2107,10 @@ struct ImGuiContext bool Initialized; bool FontAtlasOwnedByContext; // IO.Fonts-> is owned by the ImGuiContext and will be destructed along with it. ImGuiIO IO; + ImGuiPlatformIO PlatformIO; ImGuiStyle Style; + ImGuiConfigFlags ConfigFlagsCurrFrame; // = g.IO.ConfigFlags at the time of NewFrame() + ImGuiConfigFlags ConfigFlagsLastFrame; ImFont* Font; // (Shortcut) == FontStack.empty() ? IO.Font : FontStack.back() float FontSize; // (Shortcut) == FontBaseSize * g.CurrentWindow->FontWindowScale == window->FontSize(). Text height for current window. float FontBaseSize; // (Shortcut) == IO.FontGlobalScale * Font->Scale * Font->FontSize. Base text height. @@ -1762,6 +2118,7 @@ struct ImGuiContext double Time; int FrameCount; int FrameCountEnded; + int FrameCountPlatformEnded; int FrameCountRendered; bool WithinFrameScope; // Set by NewFrame(), cleared by EndFrame() bool WithinFrameScopeWithImplicitWindow; // Set by NewFrame(), cleared by EndFrame() when the implicit debug window has been pushed @@ -1784,19 +2141,21 @@ struct ImGuiContext ImGuiStorage WindowsById; // Map window's ImGuiID to ImGuiWindow* int WindowsActiveCount; // Number of unique windows submitted by frame ImVec2 WindowsHoverPadding; // Padding around resizable windows for which hovering on counts as hovering the window == ImMax(style.TouchExtraPadding, WINDOWS_HOVER_PADDING) + ImGuiID DebugBreakInWindow; // Set to break in Begin() call. ImGuiWindow* CurrentWindow; // Window being drawn into ImGuiWindow* HoveredWindow; // Window the mouse is hovering. Will typically catch mouse inputs. ImGuiWindow* HoveredWindowUnderMovingWindow; // Hovered window ignoring MovingWindow. Only set if MovingWindow is set. - ImGuiWindow* MovingWindow; // Track the window we clicked on (in order to preserve focus). The actual window that is moved is generally MovingWindow->RootWindow. + ImGuiWindow* MovingWindow; // Track the window we clicked on (in order to preserve focus). The actual window that is moved is generally MovingWindow->RootWindowDockTree. ImGuiWindow* WheelingWindow; // Track the window we started mouse-wheeling on. Until a timer elapse or mouse has moved, generally keep scrolling the same window even if during the course of scrolling the mouse ends up hovering a child window. ImVec2 WheelingWindowRefMousePos; int WheelingWindowStartFrame; // This may be set one frame before WheelingWindow is != NULL + int WheelingWindowScrolledFrame; float WheelingWindowReleaseTimer; ImVec2 WheelingWindowWheelRemainder; ImVec2 WheelingAxisAvg; // Item/widgets state and tracking information - ImGuiID DebugHookIdInfo; // Will call core hooks: DebugHookIdInfo() from GetID functions, used by Stack Tool [next HoveredId/ActiveId to not pull in an extra cache-line] + ImGuiID DebugHookIdInfo; // Will call core hooks: DebugHookIdInfo() from GetID functions, used by ID Stack Tool [next HoveredId/ActiveId to not pull in an extra cache-line] ImGuiID HoveredId; // Hovered widget, filled during the frame ImGuiID HoveredIdPreviousFrame; bool HoveredIdAllowOverlap; @@ -1812,10 +2171,11 @@ struct ImGuiContext bool ActiveIdHasBeenPressedBefore; // Track whether the active id led to a press (this is to allow changing between PressOnClick and PressOnRelease without pressing twice). Used by range_select branch. bool ActiveIdHasBeenEditedBefore; // Was the value associated to the widget Edited over the course of the Active state. bool ActiveIdHasBeenEditedThisFrame; + bool ActiveIdFromShortcut; + int ActiveIdMouseButton : 8; ImVec2 ActiveIdClickOffset; // Clicked offset from upper-left corner, if applicable (currently only set by ButtonBehavior) ImGuiWindow* ActiveIdWindow; ImGuiInputSource ActiveIdSource; // Activating source: ImGuiInputSource_Mouse OR ImGuiInputSource_Keyboard OR ImGuiInputSource_Gamepad - int ActiveIdMouseButton; ImGuiID ActiveIdPreviousFrame; bool ActiveIdPreviousFrameIsAlive; bool ActiveIdPreviousFrameHasBeenEditedBefore; @@ -1827,44 +2187,64 @@ struct ImGuiContext // - The idea is that instead of "eating" a given key, we can link to an owner. // - Input query can then read input by specifying ImGuiKeyOwner_Any (== 0), ImGuiKeyOwner_None (== -1) or a custom ID. // - Routing is requested ahead of time for a given chord (Key + Mods) and granted in NewFrame(). + double LastKeyModsChangeTime; // Record the last time key mods changed (affect repeat delay when using shortcut logic) + double LastKeyModsChangeFromNoneTime; // Record the last time key mods changed away from being 0 (affect repeat delay when using shortcut logic) + double LastKeyboardKeyPressTime; // Record the last time a keyboard key (ignore mouse/gamepad ones) was pressed. + ImBitArrayForNamedKeys KeysMayBeCharInput; // Lookup to tell if a key can emit char input, see IsKeyChordPotentiallyCharInput(). sizeof() = 20 bytes ImGuiKeyOwnerData KeysOwnerData[ImGuiKey_NamedKey_COUNT]; ImGuiKeyRoutingTable KeysRoutingTable; ImU32 ActiveIdUsingNavDirMask; // Active widget will want to read those nav move requests (e.g. can activate a button and move away from it) bool ActiveIdUsingAllKeyboardKeys; // Active widget will want to read all keyboard keys inputs. (FIXME: This is a shortcut for not taking ownership of 100+ keys but perhaps best to not have the inconsistency) + ImGuiKeyChord DebugBreakInShortcutRouting; // Set to break in SetShortcutRouting()/Shortcut() calls. #ifndef IMGUI_DISABLE_OBSOLETE_KEYIO ImU32 ActiveIdUsingNavInputMask; // If you used this. Since (IMGUI_VERSION_NUM >= 18804) : 'g.ActiveIdUsingNavInputMask |= (1 << ImGuiNavInput_Cancel);' becomes 'SetKeyOwner(ImGuiKey_Escape, g.ActiveId) and/or SetKeyOwner(ImGuiKey_NavGamepadCancel, g.ActiveId);' #endif // Next window/item data - ImGuiID CurrentFocusScopeId; // == g.FocusScopeStack.back() - ImGuiItemFlags CurrentItemFlags; // == g.ItemFlagsStack.back() + ImGuiID CurrentFocusScopeId; // Value for currently appending items == g.FocusScopeStack.back(). Not to be mistaken with g.NavFocusScopeId. + ImGuiItemFlags CurrentItemFlags; // Value for currently appending items == g.ItemFlagsStack.back() ImGuiID DebugLocateId; // Storage for DebugLocateItemOnHover() feature: this is read by ItemAdd() so we keep it in a hot/cached location ImGuiNextItemData NextItemData; // Storage for SetNextItem** functions ImGuiLastItemData LastItemData; // Storage for last submitted item (setup by ItemAdd) ImGuiNextWindowData NextWindowData; // Storage for SetNextWindow** functions + bool DebugShowGroupRects; // Shared stacks - ImVector ColorStack; // Stack for PushStyleColor()/PopStyleColor() - inherited by Begin() - ImVector StyleVarStack; // Stack for PushStyleVar()/PopStyleVar() - inherited by Begin() - ImVector FontStack; // Stack for PushFont()/PopFont() - inherited by Begin() - ImVector FocusScopeStack; // Stack for PushFocusScope()/PopFocusScope() - inherited by BeginChild(), pushed into by Begin() - ImVectorItemFlagsStack; // Stack for PushItemFlag()/PopItemFlag() - inherited by Begin() - ImVectorGroupStack; // Stack for BeginGroup()/EndGroup() - not inherited by Begin() - ImVectorOpenPopupStack; // Which popups are open (persistent) - ImVectorBeginPopupStack; // Which level of BeginPopup() we are in (reset every frame) - int BeginMenuCount; + ImGuiCol DebugFlashStyleColorIdx; // (Keep close to ColorStack to share cache line) + ImVector ColorStack; // Stack for PushStyleColor()/PopStyleColor() - inherited by Begin() + ImVector StyleVarStack; // Stack for PushStyleVar()/PopStyleVar() - inherited by Begin() + ImVector FontStack; // Stack for PushFont()/PopFont() - inherited by Begin() + ImVector FocusScopeStack; // Stack for PushFocusScope()/PopFocusScope() - inherited by BeginChild(), pushed into by Begin() + ImVector ItemFlagsStack; // Stack for PushItemFlag()/PopItemFlag() - inherited by Begin() + ImVector GroupStack; // Stack for BeginGroup()/EndGroup() - not inherited by Begin() + ImVector OpenPopupStack; // Which popups are open (persistent) + ImVector BeginPopupStack; // Which level of BeginPopup() we are in (reset every frame) + ImVector NavTreeNodeStack; // Stack for TreeNode() when a NavLeft requested is emitted. // Viewports - ImVector Viewports; // Active viewports (Size==1 in 'master' branch). Each viewports hold their copy of ImDrawData. + ImVector Viewports; // Active viewports (always 1+, and generally 1 unless multi-viewports are enabled). Each viewports hold their copy of ImDrawData. + float CurrentDpiScale; // == CurrentViewport->DpiScale + ImGuiViewportP* CurrentViewport; // We track changes of viewport (happening in Begin) so we can call Platform_OnChangedViewport() + ImGuiViewportP* MouseViewport; + ImGuiViewportP* MouseLastHoveredViewport; // Last known viewport that was hovered by mouse (even if we are not hovering any viewport any more) + honoring the _NoInputs flag. + ImGuiID PlatformLastFocusedViewportId; + ImGuiPlatformMonitor FallbackMonitor; // Virtual monitor used as fallback if backend doesn't provide monitor information. + ImRect PlatformMonitorsFullWorkRect; // Bounding box of all platform monitors + int ViewportCreatedCount; // Unique sequential creation counter (mostly for testing/debugging) + int PlatformWindowsCreatedCount; // Unique sequential creation counter (mostly for testing/debugging) + int ViewportFocusedStampCount; // Every time the front-most window changes, we stamp its viewport with an incrementing counter // Gamepad/keyboard Navigation ImGuiWindow* NavWindow; // Focused window for navigation. Could be called 'FocusedWindow' ImGuiID NavId; // Focused item for navigation - ImGuiID NavFocusScopeId; // Identify a selection scope (selection code often wants to "clear other items" when landing on an item of the selection set) + ImGuiID NavFocusScopeId; // Focused focus scope (e.g. selection code often wants to "clear other items" when landing on an item of the same scope) + ImVector NavFocusRoute; // Reversed copy focus scope stack for NavId (should contains NavFocusScopeId). This essentially follow the window->ParentWindowForFocusRoute chain. ImGuiID NavActivateId; // ~~ (g.ActiveId == 0) && (IsKeyPressed(ImGuiKey_Space) || IsKeyDown(ImGuiKey_Enter) || IsKeyPressed(ImGuiKey_NavGamepadActivate)) ? NavId : 0, also set when calling ActivateItem() ImGuiID NavActivateDownId; // ~~ IsKeyDown(ImGuiKey_Space) || IsKeyDown(ImGuiKey_Enter) || IsKeyDown(ImGuiKey_NavGamepadActivate) ? NavId : 0 ImGuiID NavActivatePressedId; // ~~ IsKeyPressed(ImGuiKey_Space) || IsKeyPressed(ImGuiKey_Enter) || IsKeyPressed(ImGuiKey_NavGamepadActivate) ? NavId : 0 (no repeat) ImGuiActivateFlags NavActivateFlags; + ImGuiID NavHighlightActivatedId; + float NavHighlightActivatedTimer; ImGuiID NavJustMovedToId; // Just navigated to this id (result of a successfully MoveRequest). ImGuiID NavJustMovedToFocusScopeId; // Just navigated to this focus scope id (result of a successfully MoveRequest). ImGuiKeyChord NavJustMovedToKeyMods; @@ -1872,6 +2252,7 @@ struct ImGuiContext ImGuiActivateFlags NavNextActivateFlags; ImGuiInputSource NavInputSource; // Keyboard or Gamepad mode? THIS CAN ONLY BE ImGuiInputSource_Keyboard or ImGuiInputSource_Mouse ImGuiNavLayer NavLayer; // Layer we are navigating on. For now the system is hard-coded for 0=main contents and 1=menu/title bar, may expose layers later. + ImGuiSelectionUserData NavLastValidSelectionUserData; // Last valid data passed to SetNextItemSelectionUser(), or -1. For current window. Not reset when focusing an item that doesn't have selection data. bool NavIdIsAlive; // Nav widget has been seen this frame ~~ NavRectRel is valid bool NavMousePosDirty; // When set we will update mouse position if (io.ConfigFlags & ImGuiConfigFlags_NavEnableSetMousePos) if set (NB: this not enabled by default) bool NavDisableHighlight; // When user starts using mouse, we hide gamepad/keyboard highlight (NB: but they are still available, which is why NavDisableHighlight isn't always != NavDisableMouseHover) @@ -1910,12 +2291,12 @@ struct ImGuiContext float NavWindowingTimer; float NavWindowingHighlightAlpha; bool NavWindowingToggleLayer; + ImGuiKey NavWindowingToggleKey; ImVec2 NavWindowingAccumDeltaPos; ImVec2 NavWindowingAccumDeltaSize; // Render float DimBgRatio; // 0.0..1.0 animation when fading in a dimming background (for modal window and CTRL+TAB list) - ImGuiMouseCursor MouseCursor; // Drag and Drop bool DragDropActive; @@ -1926,6 +2307,7 @@ struct ImGuiContext int DragDropMouseButton; ImGuiPayload DragDropPayload; ImRect DragDropTargetRect; // Store rectangle of current target candidate (we favor small targets when overlapping) + ImRect DragDropTargetClipRect; // Store ClipRect at the time of item's drawing ImGuiID DragDropTargetId; ImGuiDragDropFlags DragDropAcceptFlags; float DragDropAcceptIdCurrRectSurface; // Target item surface (we resolve overlapping targets by prioritizing the smaller surface) @@ -1942,6 +2324,7 @@ struct ImGuiContext // Tables ImGuiTable* CurrentTable; + ImGuiID DebugBreakInTable; // Set to break in BeginTable() call. int TablesTempDataStacked; // Temporary table data size (because we leave previous instances undestructed, we generally don't use TablesTempData.Size) ImVector TablesTempData; // Temporary table data (buffers reused/shared across instances, support nesting) ImPool Tables; // Persistent table data @@ -1955,17 +2338,25 @@ struct ImGuiContext ImVector ShrinkWidthBuffer; // Hover Delay system - ImGuiID HoverDelayId; - ImGuiID HoverDelayIdPreviousFrame; - float HoverDelayTimer; // Currently used IsItemHovered(), generally inferred from g.HoveredIdTimer but kept uncleared until clear timer elapse. - float HoverDelayClearTimer; // Currently used IsItemHovered(): grace time before g.TooltipHoverTimer gets cleared. + ImGuiID HoverItemDelayId; + ImGuiID HoverItemDelayIdPreviousFrame; + float HoverItemDelayTimer; // Currently used by IsItemHovered() + float HoverItemDelayClearTimer; // Currently used by IsItemHovered(): grace time before g.TooltipHoverTimer gets cleared. + ImGuiID HoverItemUnlockedStationaryId; // Mouse has once been stationary on this item. Only reset after departing the item. + ImGuiID HoverWindowUnlockedStationaryId; // Mouse has once been stationary on this window. Only reset after departing the window. + + // Mouse state + ImGuiMouseCursor MouseCursor; + float MouseStationaryTimer; // Time the mouse has been stationary (with some loose heuristic) + ImVec2 MouseLastValidPos; // Widget state - ImVec2 MouseLastValidPos; ImGuiInputTextState InputTextState; ImGuiInputTextDeactivatedState InputTextDeactivatedState; ImFont InputTextPasswordFont; ImGuiID TempInputId; // Temporary text input when CTRL+clicking on a slider, etc. + int BeginMenuDepth; + int BeginComboDepth; ImGuiColorEditFlags ColorEditOptions; // Store user options for color edit widgets ImGuiID ColorEditCurrentID; // Set temporarily while inside of the parent-most ColorEdit4/ColorPicker4 (because they call each others). ImGuiID ColorEditSavedID; // ID we are saving/restoring HS for @@ -1974,6 +2365,8 @@ struct ImGuiContext ImU32 ColorEditSavedColor; // RGB value with alpha set to 0. ImVec4 ColorPickerRef; // Initial/reference color at the time of opening the color picker. ImGuiComboPreviewData ComboPreviewData; + ImRect WindowResizeBorderExpectedRect; // Expected border rect, switch to relative edit if moving + bool WindowResizeRelativeMode; float SliderGrabClickOffset; float SliderCurrentAccum; // Accumulated slider delta when using navigation controls. bool SliderCurrentAccumDirty; // Has the accumulated slider delta changed since last time we tried to apply it? @@ -1983,14 +2376,21 @@ struct ImGuiContext float ScrollbarClickDeltaToGrabCenter; // Distance between mouse and center of grab box, normalized in parent space. Use storage? float DisabledAlphaBackup; // Backup for style.Alpha for BeginDisabled() short DisabledStackSize; + short LockMarkEdited; short TooltipOverrideCount; ImVector ClipboardHandlerData; // If no custom clipboard handler is defined ImVector MenusIdSubmittedThisFrame; // A list of menu IDs that were rendered at least once + ImGuiTypingSelectState TypingSelectState; // State for GetTypingSelectRequest() // Platform support ImGuiPlatformImeData PlatformImeData; // Data updated by current frame ImGuiPlatformImeData PlatformImeDataPrev; // Previous frame data (when changing we will call io.SetPlatformImeDataFn - char PlatformLocaleDecimalPoint; // '.' or *localeconv()->decimal_point + ImGuiID PlatformImeViewport; + + // Extensions + // FIXME: We could provide an API to register one slot in an array held in ImGuiContext? + ImGuiDockContext DockContext; + void (*DockNodeWindowMenuHandler)(ImGuiContext* ctx, ImGuiDockNode* node, ImGuiTabBar* tab_bar); // Settings bool SettingsLoaded; @@ -2019,17 +2419,25 @@ struct ImGuiContext int LogDepthToExpandDefault; // Default/stored value for LogDepthMaxExpand if not specified in the LogXXX function call. // Debug Tools + // (some of the highly frequently used data are interleaved in other structures above: DebugBreakXXX fields, DebugHookIdInfo, DebugLocateId etc.) ImGuiDebugLogFlags DebugLogFlags; ImGuiTextBuffer DebugLogBuf; ImGuiTextIndex DebugLogIndex; - ImU8 DebugLogClipperAutoDisableFrames; + ImGuiDebugLogFlags DebugLogAutoDisableFlags; + ImU8 DebugLogAutoDisableFrames; ImU8 DebugLocateFrames; // For DebugLocateItemOnHover(). This is used together with DebugLocateId which is in a hot/cached spot above. + bool DebugBreakInLocateId; // Debug break in ItemAdd() call for g.DebugLocateId. + ImGuiKeyChord DebugBreakKeyChord; // = ImGuiKey_Pause ImS8 DebugBeginReturnValueCullDepth; // Cycle between 0..9 then wrap around. bool DebugItemPickerActive; // Item picker is active (started with DebugStartItemPicker()) ImU8 DebugItemPickerMouseButton; ImGuiID DebugItemPickerBreakId; // Will call IM_DEBUG_BREAK() when encountering this ID + float DebugFlashStyleColorTime; + ImVec4 DebugFlashStyleColorBackup; ImGuiMetricsConfig DebugMetricsConfig; - ImGuiStackTool DebugStackTool; + ImGuiIDStackTool DebugIDStackTool; + ImGuiDebugAllocInfo DebugAllocInfo; + ImGuiDockNode* DebugHoveredDockNode; // Hovered dock node. // Misc float FramerateSecPerFrame[60]; // Calculate estimate of framerate for user over the last 60 frames.. @@ -2040,6 +2448,7 @@ struct ImGuiContext int WantCaptureKeyboardNextFrame; // " int WantTextInputNextFrame; ImVector TempBuffer; // Temporary text buffer + char TempKeychordName[64]; ImGuiContext(ImFontAtlas* shared_font_atlas) { @@ -2047,13 +2456,14 @@ struct ImGuiContext InputTextState.Ctx = this; Initialized = false; + ConfigFlagsCurrFrame = ConfigFlagsLastFrame = ImGuiConfigFlags_None; FontAtlasOwnedByContext = shared_font_atlas ? false : true; Font = NULL; FontSize = FontBaseSize = 0.0f; IO.Fonts = shared_font_atlas ? shared_font_atlas : IM_NEW(ImFontAtlas)(); Time = 0.0f; FrameCount = 0; - FrameCountEnded = FrameCountRendered = -1; + FrameCountEnded = FrameCountPlatformEnded = FrameCountRendered = -1; WithinFrameScope = WithinFrameScopeWithImplicitWindow = WithinEndChild = false; GcCompactAll = false; TestEngineHookItems = false; @@ -2068,7 +2478,7 @@ struct ImGuiContext HoveredWindowUnderMovingWindow = NULL; MovingWindow = NULL; WheelingWindow = NULL; - WheelingWindowStartFrame = -1; + WheelingWindowStartFrame = WheelingWindowScrolledFrame = -1; WheelingWindowReleaseTimer = 0.0f; DebugHookIdInfo = 0; @@ -2085,6 +2495,7 @@ struct ImGuiContext ActiveIdHasBeenPressedBefore = false; ActiveIdHasBeenEditedBefore = false; ActiveIdHasBeenEditedThisFrame = false; + ActiveIdFromShortcut = false; ActiveIdClickOffset = ImVec2(-1, -1); ActiveIdWindow = NULL; ActiveIdSource = ImGuiInputSource_None; @@ -2096,6 +2507,8 @@ struct ImGuiContext LastActiveId = 0; LastActiveIdTimer = 0.0f; + LastKeyboardKeyPressTime = LastKeyModsChangeTime = LastKeyModsChangeFromNoneTime = -1.0; + ActiveIdUsingNavDirMask = 0x00; ActiveIdUsingAllKeyboardKeys = false; #ifndef IMGUI_DISABLE_OBSOLETE_KEYIO @@ -2104,15 +2517,25 @@ struct ImGuiContext CurrentFocusScopeId = 0; CurrentItemFlags = ImGuiItemFlags_None; - BeginMenuCount = 0; + DebugShowGroupRects = false; + + CurrentDpiScale = 0.0f; + CurrentViewport = NULL; + MouseViewport = MouseLastHoveredViewport = NULL; + PlatformLastFocusedViewportId = 0; + ViewportCreatedCount = PlatformWindowsCreatedCount = 0; + ViewportFocusedStampCount = 0; NavWindow = NULL; NavId = NavFocusScopeId = NavActivateId = NavActivateDownId = NavActivatePressedId = 0; NavJustMovedToId = NavJustMovedToFocusScopeId = NavNextActivateId = 0; NavActivateFlags = NavNextActivateFlags = ImGuiActivateFlags_None; + NavHighlightActivatedId = 0; + NavHighlightActivatedTimer = 0.0f; NavJustMovedToKeyMods = ImGuiMod_None; NavInputSource = ImGuiInputSource_Keyboard; NavLayer = ImGuiNavLayer_Main; + NavLastValidSelectionUserData = ImGuiSelectionUserData_Invalid; NavIdIsAlive = false; NavMousePosDirty = false; NavDisableHighlight = true; @@ -2136,9 +2559,9 @@ struct ImGuiContext NavWindowingTarget = NavWindowingTargetAnim = NavWindowingListWindow = NULL; NavWindowingTimer = NavWindowingHighlightAlpha = 0.0f; NavWindowingToggleLayer = false; + NavWindowingToggleKey = ImGuiKey_None; DimBgRatio = 0.0f; - MouseCursor = ImGuiMouseCursor_Arrow; DragDropActive = DragDropWithinSource = DragDropWithinTarget = false; DragDropSourceFlags = ImGuiDragDropFlags_None; @@ -2158,14 +2581,19 @@ struct ImGuiContext TablesTempDataStacked = 0; CurrentTabBar = NULL; - HoverDelayId = HoverDelayIdPreviousFrame = 0; - HoverDelayTimer = HoverDelayClearTimer = 0.0f; + HoverItemDelayId = HoverItemDelayIdPreviousFrame = HoverItemUnlockedStationaryId = HoverWindowUnlockedStationaryId = 0; + HoverItemDelayTimer = HoverItemDelayClearTimer = 0.0f; + + MouseCursor = ImGuiMouseCursor_Arrow; + MouseStationaryTimer = 0.0f; TempInputId = 0; + BeginMenuDepth = BeginComboDepth = 0; ColorEditOptions = ImGuiColorEditFlags_DefaultOptions_; ColorEditCurrentID = ColorEditSavedID = 0; ColorEditSavedHue = ColorEditSavedSat = 0.0f; ColorEditSavedColor = 0; + WindowResizeRelativeMode = false; SliderGrabClickOffset = 0.0f; SliderCurrentAccum = 0.0f; SliderCurrentAccumDirty = false; @@ -2175,11 +2603,14 @@ struct ImGuiContext ScrollbarClickDeltaToGrabCenter = 0.0f; DisabledAlphaBackup = 0.0f; DisabledStackSize = 0; + LockMarkEdited = 0; TooltipOverrideCount = 0; PlatformImeData.InputPos = ImVec2(0.0f, 0.0f); PlatformImeDataPrev.InputPos = ImVec2(-1.0f, -1.0f); // Different to ensure initial submission - PlatformLocaleDecimalPoint = '.'; + PlatformImeViewport = 0; + + DockNodeWindowMenuHandler = NULL; SettingsLoaded = false; SettingsDirtyTimer = 0.0f; @@ -2198,17 +2629,29 @@ struct ImGuiContext DebugLogFlags = ImGuiDebugLogFlags_OutputToTTY; DebugLocateId = 0; - DebugLogClipperAutoDisableFrames = 0; + DebugLogAutoDisableFlags = ImGuiDebugLogFlags_None; + DebugLogAutoDisableFrames = 0; DebugLocateFrames = 0; DebugBeginReturnValueCullDepth = -1; DebugItemPickerActive = false; DebugItemPickerMouseButton = ImGuiMouseButton_Left; DebugItemPickerBreakId = 0; + DebugFlashStyleColorTime = 0.0f; + DebugFlashStyleColorIdx = ImGuiCol_COUNT; + DebugHoveredDockNode = NULL; + + // Same as DebugBreakClearData(). Those fields are scattered in their respective subsystem to stay in hot-data locations + DebugBreakInWindow = 0; + DebugBreakInTable = 0; + DebugBreakInLocateId = false; + DebugBreakKeyChord = ImGuiKey_Pause; + DebugBreakInShortcutRouting = ImGuiKey_None; memset(FramerateSecPerFrame, 0, sizeof(FramerateSecPerFrame)); FramerateSecPerFrameIdx = FramerateSecPerFrameCount = 0; FramerateSecPerFrameAccum = 0.0f; WantCaptureMouseNextFrame = WantCaptureKeyboardNextFrame = WantTextInputNextFrame = -1; + memset(TempKeychordName, 0, sizeof(TempKeychordName)); } }; @@ -2258,6 +2701,7 @@ struct IMGUI_API ImGuiWindowTempData int CurrentTableIdx; // Current table index (into g.Tables) ImGuiLayoutType LayoutType; ImGuiLayoutType ParentLayoutType; // Layout type of parent window at the time of Begin() + ImU32 ModalDimBgColor; // Local parameters stacks // We store the current settings outside of the vectors to increase memory locality (reduce cache misses). The vectors are rarely modified. Also it allows us to not heap allocate for short-lived windows which are not using those settings. @@ -2273,8 +2717,13 @@ struct IMGUI_API ImGuiWindow ImGuiContext* Ctx; // Parent UI context (needs to be set explicitly by parent). char* Name; // Window name, owned by the window. ImGuiID ID; // == ImHashStr(Name) - ImGuiWindowFlags Flags; // See enum ImGuiWindowFlags_ + ImGuiWindowFlags Flags, FlagsPreviousFrame; // See enum ImGuiWindowFlags_ + ImGuiChildFlags ChildFlags; // Set when window is a child window. See enum ImGuiChildFlags_ + ImGuiWindowClass WindowClass; // Advanced users only. Set with SetNextWindowClass() ImGuiViewportP* Viewport; // Always set in Begin(). Inactive windows may have a NULL value here if their viewport was discarded. + ImGuiID ViewportId; // We backup the viewport id (since the viewport may disappear or never be created if the window is inactive) + ImVec2 ViewportPos; // We backup the viewport position (since the viewport may disappear or never be created if the window is inactive) + int ViewportAllowPlatformMonitorExtend; // Reset to -1 every frame (index is guaranteed to be valid between NewFrame..EndFrame), only used in the Appearing frame of a tooltip/popup to enforce clamping to a given monitor ImVec2 Pos; // Position (always rounded-up to nearest pixel) ImVec2 Size; // Current size (==SizeFull or collapsed title bar size) ImVec2 SizeFull; // Size when non collapsed @@ -2289,6 +2738,7 @@ struct IMGUI_API ImGuiWindow float DecoInnerSizeX1, DecoInnerSizeY1; // Applied AFTER/OVER InnerRect. Specialized for Tables as they use specialized form of clipping and frozen rows/columns are inside InnerRect (and not part of regular decoration sizes). int NameBufLen; // Size of buffer storing Name. May be larger than strlen(Name)! ImGuiID MoveId; // == window->GetID("#MOVE") + ImGuiID TabId; // == window->GetID("#TAB") ImGuiID ChildId; // ID of corresponding item in parent window (for navigation to return from child window to parent window) ImVec2 Scroll; ImVec2 ScrollMax; @@ -2297,6 +2747,7 @@ struct IMGUI_API ImGuiWindow ImVec2 ScrollTargetEdgeSnapDist; // 0.0f = no snapping, >0.0f snapping threshold ImVec2 ScrollbarSizes; // Size taken by each scrollbars on their smaller axis. Pay attention! ScrollbarSizes.x == width of the vertical scrollbar, ScrollbarSizes.y = height of the horizontal scrollbar. bool ScrollbarX, ScrollbarY; // Are scrollbars visible? + bool ViewportOwned; bool Active; // Set to true on Begin(), unless Collapsed bool WasActive; bool WriteAccessed; // Set to true when any widget access the current window @@ -2308,6 +2759,7 @@ struct IMGUI_API ImGuiWindow bool IsFallbackWindow; // Set on the "Debug##Default" window. bool IsExplicitChild; // Set when passed _ChildWindow, left to false by BeginDocked() bool HasCloseButton; // Set when the window has a close button (p_open != NULL) + signed char ResizeBorderHovered; // Current border being hovered for resize (-1: none, otherwise 0-3) signed char ResizeBorderHeld; // Current border being held for resize (-1: none, otherwise 0-3) short BeginCount; // Number of Begin() during the current frame (generally 0 or 1, 1+ if appending via multiple Begin/End pairs) short BeginCountPreviousFrame; // Number of Begin() during the previous frame @@ -2316,7 +2768,6 @@ struct IMGUI_API ImGuiWindow short FocusOrder; // Order within WindowsFocusOrder[], altered when windows are focused. ImGuiID PopupId; // ID in the popup stack when this window is used as a popup/menu (because we use generic Name/ID for recycling) ImS8 AutoFitFramesX, AutoFitFramesY; - ImS8 AutoFitChildAxises; bool AutoFitOnlyGrows; ImGuiDir AutoPosLastDirection; ImS8 HiddenFramesCanSkipItems; // Hide the window for N frames @@ -2326,6 +2777,7 @@ struct IMGUI_API ImGuiWindow ImGuiCond SetWindowPosAllowFlags : 8; // store acceptable condition flags for SetNextWindowPos() use. ImGuiCond SetWindowSizeAllowFlags : 8; // store acceptable condition flags for SetNextWindowSize() use. ImGuiCond SetWindowCollapsedAllowFlags : 8; // store acceptable condition flags for SetNextWindowCollapsed() use. + ImGuiCond SetWindowDockAllowFlags : 8; // store acceptable condition flags for SetNextWindowDock() use. ImVec2 SetWindowPosVal; // store window position when using a non-zero Pivot (position set needs to be processed when we know the window size) ImVec2 SetWindowPosPivot; // store window pivot for positioning. ImVec2(0, 0) when positioning from top-left corner; ImVec2(0.5f, 0.5f) for centering; ImVec2(1, 1) for bottom right. @@ -2345,11 +2797,13 @@ struct IMGUI_API ImGuiWindow ImVec2ih HitTestHoleOffset; int LastFrameActive; // Last frame number the window was Active. + int LastFrameJustFocused; // Last frame number the window was made Focused. float LastTimeActive; // Last timestamp the window was Active (using float as we don't need high precision there) float ItemWidthDefault; ImGuiStorage StateStorage; ImVector ColumnsStorage; float FontWindowScale; // User scale multiplier per-window, via SetWindowFontScale() + float FontDpiScale; int SettingsOffset; // Offset into SettingsWindows[] (offsets are always valid as we only grow the array from the back) ImDrawList* DrawList; // == &DrawListInst (for backward compatibility reason with code using imgui_internal.h we keep this a pointer) @@ -2358,8 +2812,10 @@ struct IMGUI_API ImGuiWindow ImGuiWindow* ParentWindowInBeginStack; ImGuiWindow* RootWindow; // Point to ourself or first ancestor that is not a child window. Doesn't cross through popups/dock nodes. ImGuiWindow* RootWindowPopupTree; // Point to ourself or first ancestor that is not a child window. Cross through popups parent<>child. + ImGuiWindow* RootWindowDockTree; // Point to ourself or first ancestor that is not a child window. Cross through dock nodes. ImGuiWindow* RootWindowForTitleBarHighlight; // Point to ourself or first ancestor which will display TitleBgActive color when this window is active. ImGuiWindow* RootWindowForNav; // Point to ourself or first ancestor which doesn't have the NavFlattened flag. + ImGuiWindow* ParentWindowForFocusRoute; // Set to manual link a window to its logical parent so that Shortcut() chain are honoerd (e.g. Tool linked to Document) ImGuiWindow* NavLastChildNavWindow; // When going to the menu bar, we remember the child window we came from. (This could probably be made implicit if we kept g.Windows sorted by last focused including child window.) ImGuiID NavLastIds[ImGuiNavLayer_COUNT]; // Last known NavId for this window, per layer (0/1) @@ -2371,6 +2827,19 @@ struct IMGUI_API ImGuiWindow int MemoryDrawListVtxCapacity; bool MemoryCompacted; // Set when window extraneous data have been garbage collected + // Docking + bool DockIsActive :1; // When docking artifacts are actually visible. When this is set, DockNode is guaranteed to be != NULL. ~~ (DockNode != NULL) && (DockNode->Windows.Size > 1). + bool DockNodeIsVisible :1; + bool DockTabIsVisible :1; // Is our window visible this frame? ~~ is the corresponding tab selected? + bool DockTabWantClose :1; + short DockOrder; // Order of the last time the window was visible within its DockNode. This is used to reorder windows that are reappearing on the same frame. Same value between windows that were active and windows that were none are possible. + ImGuiWindowDockStyle DockStyle; + ImGuiDockNode* DockNode; // Which node are we docked into. Important: Prefer testing DockIsActive in many cases as this will still be set when the dock node is hidden. + ImGuiDockNode* DockNodeAsHost; // Which node are we owning (for parent windows) + ImGuiID DockId; // Backup of last valid DockNode->ID, so single window remember their dock node id even when they are not bound any more + ImGuiItemStatusFlags DockTabItemStatusFlags; + ImRect DockTabItemRect; + public: ImGuiWindow(ImGuiContext* context, const char* name); ~ImGuiWindow(); @@ -2382,7 +2851,7 @@ public: // We don't use g.FontSize because the window may be != g.CurrentWindow. ImRect Rect() const { return ImRect(Pos.x, Pos.y, Pos.x + Size.x, Pos.y + Size.y); } - float CalcFontSize() const { ImGuiContext& g = *Ctx; float scale = g.FontBaseSize * FontWindowScale; if (ParentWindow) scale *= ParentWindow->FontWindowScale; return scale; } + float CalcFontSize() const { ImGuiContext& g = *Ctx; float scale = g.FontBaseSize * FontWindowScale * FontDpiScale; if (ParentWindow) scale *= ParentWindow->FontWindowScale; return scale; } float TitleBarHeight() const { ImGuiContext& g = *Ctx; return (Flags & ImGuiWindowFlags_NoTitleBar) ? 0.0f : CalcFontSize() + g.Style.FramePadding.y * 2.0f; } ImRect TitleBarRect() const { return ImRect(Pos, ImVec2(Pos.x + SizeFull.x, Pos.y + TitleBarHeight())); } float MenuBarHeight() const { ImGuiContext& g = *Ctx; return (Flags & ImGuiWindowFlags_MenuBar) ? DC.MenuBarOffset.y + CalcFontSize() + g.Style.FramePadding.y * 2.0f : 0.0f; } @@ -2407,13 +2876,15 @@ enum ImGuiTabItemFlagsPrivate_ ImGuiTabItemFlags_SectionMask_ = ImGuiTabItemFlags_Leading | ImGuiTabItemFlags_Trailing, ImGuiTabItemFlags_NoCloseButton = 1 << 20, // Track whether p_open was set or not (we'll need this info on the next frame to recompute ContentWidth during layout) ImGuiTabItemFlags_Button = 1 << 21, // Used by TabItemButton, change the tab item behavior to mimic a button + ImGuiTabItemFlags_Unsorted = 1 << 22, // [Docking] Trailing tabs with the _Unsorted flag will be sorted based on the DockOrder of their Window. }; -// Storage for one active tab item (sizeof() 40 bytes) +// Storage for one active tab item (sizeof() 48 bytes) struct ImGuiTabItem { ImGuiID ID; ImGuiTabItemFlags Flags; + ImGuiWindow* Window; // When TabItem is part of a DockNode's TabBar, we hold on to a window. int LastFrameVisible; int LastFrameSelected; // This allows us to infer an ordered list of the last activated tabs with little maintenance float Offset; // Position relative to beginning of tab @@ -2450,6 +2921,8 @@ struct IMGUI_API ImGuiTabBar float ScrollingSpeed; float ScrollingRectMinX; float ScrollingRectMaxX; + float SeparatorMinX; + float SeparatorMaxX; ImGuiID ReorderRequestTabId; ImS16 ReorderRequestOffset; ImS8 BeginCount; @@ -2547,14 +3020,17 @@ struct ImGuiTableCellData }; // Per-instance data that needs preserving across frames (seemingly most others do not need to be preserved aside from debug needs. Does that means they could be moved to ImGuiTableTempData?) +// sizeof() ~ 24 bytes struct ImGuiTableInstanceData { ImGuiID TableInstanceID; float LastOuterHeight; // Outer height from last frame - float LastFirstRowHeight; // Height of first row from last frame (FIXME: this is used as "header height" and may be reworked) + float LastTopHeadersRowHeight; // Height of first consecutive header rows from last frame (FIXME: this is used assuming consecutive headers are in same frozen set) float LastFrozenHeight; // Height of frozen section from last frame + int HoveredRowLast; // Index of row which was hovered last frame. + int HoveredRowNext; // Index of row hovered this frame, set after encountering it. - ImGuiTableInstanceData() { TableInstanceID = 0; LastOuterHeight = LastFirstRowHeight = LastFrozenHeight = 0.0f; } + ImGuiTableInstanceData() { TableInstanceID = 0; LastOuterHeight = LastTopHeadersRowHeight = LastFrozenHeight = 0.0f; HoveredRowLast = HoveredRowNext = -1; } }; // FIXME-TABLE: more transient data could be stored in a stacked ImGuiTableTempData: e.g. SortSpecs, incoming RowData @@ -2582,6 +3058,7 @@ struct IMGUI_API ImGuiTable float RowPosY1; float RowPosY2; float RowMinHeight; // Height submitted to TableNextRow() + float RowCellPaddingY; // Top and bottom padding. Reloaded during row change. float RowTextBaseline; float RowIndentOffsetX; ImGuiTableRowFlags RowFlags : 16; // Current row flags, see ImGuiTableRowFlags_ @@ -2595,9 +3072,8 @@ struct IMGUI_API ImGuiTable float HostIndentX; float MinColumnWidth; float OuterPaddingX; - float CellPaddingX; // Padding from each borders - float CellPaddingY; - float CellSpacingX1; // Spacing between non-bordered cells + float CellPaddingX; // Padding from each borders. Locked in BeginTable()/Layout. + float CellSpacingX1; // Spacing between non-bordered cells. Locked in BeginTable()/Layout. float CellSpacingX2; float InnerWidth; // User value passed to BeginTable(), see comments at the top of BeginTable() for details. float ColumnsGivenWidth; // Sum of current column width @@ -2606,6 +3082,8 @@ struct IMGUI_API ImGuiTable float ResizedColumnNextWidth; float ResizeLockMinContentsX2; // Lock minimum contents width while resizing down in order to not create feedback loops. But we allow growing the table. float RefScale; // Reference scale to be able to rescale columns on font/dpi changes. + float AngledHeadersHeight; // Set by TableAngledHeadersRow(), used in TableUpdateLayout() + float AngledHeadersSlope; // Set by TableAngledHeadersRow(), used in TableUpdateLayout() ImRect OuterRect; // Note: for non-scrolling table, OuterRect.Max.y is often FLT_MAX until EndTable(), unless a height has been specified in BeginTable(). ImRect InnerRect; // InnerRect but without decoration. As with OuterRect, for non-scrolling tables, InnerRect.Max.y is ImRect WorkRect; @@ -2628,8 +3106,10 @@ struct IMGUI_API ImGuiTable ImGuiTableColumnIdx ColumnsEnabledCount; // Number of enabled columns (<= ColumnsCount) ImGuiTableColumnIdx ColumnsEnabledFixedCount; // Number of enabled columns (<= ColumnsCount) ImGuiTableColumnIdx DeclColumnsCount; // Count calls to TableSetupColumn() + ImGuiTableColumnIdx AngledHeadersCount; // Count columns with angled headers ImGuiTableColumnIdx HoveredColumnBody; // Index of column whose visible region is being hovered. Important: == ColumnsCount when hovering empty region after the right-most column! ImGuiTableColumnIdx HoveredColumnBorder; // Index of column whose right-border is being hovered (for resizing). + ImGuiTableColumnIdx HighlightColumnHeader; // Index of column which should be highlighted. ImGuiTableColumnIdx AutoFitSingleColumn; // Index of single column requesting auto-fit. ImGuiTableColumnIdx ResizedColumn; // Index of column being resized. Reset when InstanceCurrent==0. ImGuiTableColumnIdx LastResizedColumn; // Index of column being resized from previous frame. @@ -2655,6 +3135,7 @@ struct IMGUI_API ImGuiTable bool IsSortSpecsDirty; bool IsUsingHeaders; // Set when the first row had the ImGuiTableRowFlags_Headers flag. bool IsContextPopupOpen; // Set when default context menu is open (also see: ContextPopupColumn, InstanceInteracted). + bool DisableDefaultContextMenu; // Disable default context menu contents. You may submit your own using TableBeginContextMenuPopup()/EndPopup() bool IsSettingsRequestLoad; bool IsSettingsDirty; // Set when table settings have changed and needs to be reported into ImGuiTableSetttings data. bool IsDefaultDisplayOrder; // Set when display order is unchanged from default (DisplayOrder contains 0...Count-1) @@ -2662,6 +3143,8 @@ struct IMGUI_API ImGuiTable bool IsResetDisplayOrderRequest; bool IsUnfrozenRows; // Set when we got past the frozen row. bool IsDefaultSizingPolicy; // Set if user didn't explicitly set a sizing policy in BeginTable() + bool IsActiveIdAliveBeforeTable; + bool IsActiveIdInTable; bool HasScrollbarYCurr; // Whether ANY instance of this table had a vertical scrollbar during the current frame. bool HasScrollbarYPrev; // Whether ANY instance of this table had a vertical scrollbar during the previous. bool MemoryCompacted; @@ -2674,11 +3157,12 @@ struct IMGUI_API ImGuiTable // Transient data that are only needed between BeginTable() and EndTable(), those buffers are shared (1 per level of stacked table). // - Accessing those requires chasing an extra pointer so for very frequently used data we leave them in the main table structure. // - We also leave out of this structure data that tend to be particularly useful for debugging/metrics. -// sizeof() ~ 112 bytes. +// sizeof() ~ 120 bytes. struct IMGUI_API ImGuiTableTempData { int TableIndex; // Index in g.Tables.Buf[] pool float LastTimeActive; // Last timestamp this structure was used + float AngledHeadersExtraWidth; // Used in EndTable() ImVec2 UserOuterSize; // outer_size.x passed to BeginTable() ImDrawListSplitter DrawSplitter; @@ -2751,7 +3235,7 @@ namespace ImGui IMGUI_API ImGuiWindow* FindWindowByName(const char* name); IMGUI_API void UpdateWindowParentAndRootLinks(ImGuiWindow* window, ImGuiWindowFlags flags, ImGuiWindow* parent_window); IMGUI_API ImVec2 CalcWindowNextAutoFitSize(ImGuiWindow* window); - IMGUI_API bool IsWindowChildOf(ImGuiWindow* window, ImGuiWindow* potential_parent, bool popup_hierarchy); + IMGUI_API bool IsWindowChildOf(ImGuiWindow* window, ImGuiWindow* potential_parent, bool popup_hierarchy, bool dock_hierarchy); IMGUI_API bool IsWindowWithinBeginStackOf(ImGuiWindow* window, ImGuiWindow* potential_parent); IMGUI_API bool IsWindowAbove(ImGuiWindow* potential_above, ImGuiWindow* potential_below); IMGUI_API bool IsWindowNavFocusable(ImGuiWindow* window); @@ -2759,7 +3243,8 @@ namespace ImGui IMGUI_API void SetWindowSize(ImGuiWindow* window, const ImVec2& size, ImGuiCond cond = 0); IMGUI_API void SetWindowCollapsed(ImGuiWindow* window, bool collapsed, ImGuiCond cond = 0); IMGUI_API void SetWindowHitTestHole(ImGuiWindow* window, const ImVec2& pos, const ImVec2& size); - IMGUI_API void SetWindowHiddendAndSkipItemsForCurrentFrame(ImGuiWindow* window); + IMGUI_API void SetWindowHiddenAndSkipItemsForCurrentFrame(ImGuiWindow* window); + inline void SetWindowParentWindowForFocusRoute(ImGuiWindow* window, ImGuiWindow* parent_window) { window->ParentWindowForFocusRoute = parent_window; } // You may also use SetNextWindowClass()'s FocusRouteParentWindowId field. inline ImRect WindowRectAbsToRel(ImGuiWindow* window, const ImRect& r) { ImVec2 off = window->DC.CursorStartPos; return ImRect(r.Min.x - off.x, r.Min.y - off.y, r.Max.x - off.x, r.Max.y - off.y); } inline ImRect WindowRectRelToAbs(ImGuiWindow* window, const ImRect& r) { ImVec2 off = window->DC.CursorStartPos; return ImRect(r.Min.x + off.x, r.Min.y + off.y, r.Max.x + off.x, r.Max.y + off.y); } inline ImVec2 WindowPosRelToAbs(ImGuiWindow* window, const ImVec2& p) { ImVec2 off = window->DC.CursorStartPos; return ImVec2(p.x + off.x, p.y + off.y); } @@ -2777,9 +3262,8 @@ namespace ImGui // Fonts, drawing IMGUI_API void SetCurrentFont(ImFont* font); inline ImFont* GetDefaultFont() { ImGuiContext& g = *GImGui; return g.IO.FontDefault ? g.IO.FontDefault : g.IO.Fonts->Fonts[0]; } - inline ImDrawList* GetForegroundDrawList(ImGuiWindow* window) { IM_UNUSED(window); return GetForegroundDrawList(); } // This seemingly unnecessary wrapper simplifies compatibility between the 'master' and 'docking' branches. - IMGUI_API ImDrawList* GetBackgroundDrawList(ImGuiViewport* viewport); // get background draw list for the given viewport. this draw list will be the first rendering one. Useful to quickly draw shapes/text behind dear imgui contents. - IMGUI_API ImDrawList* GetForegroundDrawList(ImGuiViewport* viewport); // get foreground draw list for the given viewport. this draw list will be the last rendered one. Useful to quickly draw shapes/text over dear imgui contents. + inline ImDrawList* GetForegroundDrawList(ImGuiWindow* window) { return GetForegroundDrawList(window->Viewport); } + IMGUI_API void AddDrawListToDrawDataEx(ImDrawData* draw_data, ImVector* out_list, ImDrawList* draw_list); // Init IMGUI_API void Initialize(); @@ -2789,6 +3273,7 @@ namespace ImGui IMGUI_API void UpdateInputEvents(bool trickle_fast_inputs); IMGUI_API void UpdateHoveredWindowAndCaptureFlags(); IMGUI_API void StartMouseMovingWindow(ImGuiWindow* window); + IMGUI_API void StartMouseMovingWindowOrNode(ImGuiWindow* window, ImGuiDockNode* node, bool undock); IMGUI_API void UpdateMouseMovingWindowNewFrame(); IMGUI_API void UpdateMouseMovingWindowEndFrame(); @@ -2798,7 +3283,13 @@ namespace ImGui IMGUI_API void CallContextHooks(ImGuiContext* context, ImGuiContextHookType type); // Viewports + IMGUI_API void TranslateWindowsInViewport(ImGuiViewportP* viewport, const ImVec2& old_pos, const ImVec2& new_pos); + IMGUI_API void ScaleWindowsInViewport(ImGuiViewportP* viewport, float scale); + IMGUI_API void DestroyPlatformWindow(ImGuiViewportP* viewport); IMGUI_API void SetWindowViewport(ImGuiWindow* window, ImGuiViewportP* viewport); + IMGUI_API void SetCurrentViewport(ImGuiWindow* window, ImGuiViewportP* viewport); + IMGUI_API const ImGuiPlatformMonitor* GetViewportPlatformMonitor(ImGuiViewport* viewport); + IMGUI_API ImGuiViewportP* FindHoveredViewportFromPlatformWindowStack(const ImVec2& mouse_platform_pos); // Settings IMGUI_API void MarkIniSettingsDirty(); @@ -2852,7 +3343,7 @@ namespace ImGui IMGUI_API void ItemSize(const ImVec2& size, float text_baseline_y = -1.0f); inline void ItemSize(const ImRect& bb, float text_baseline_y = -1.0f) { ItemSize(bb.GetSize(), text_baseline_y); } // FIXME: This is a misleading API since we expect CursorPos to be bb.Min. IMGUI_API bool ItemAdd(const ImRect& bb, ImGuiID id, const ImRect* nav_bb = NULL, ImGuiItemFlags extra_flags = 0); - IMGUI_API bool ItemHoverable(const ImRect& bb, ImGuiID id); + IMGUI_API bool ItemHoverable(const ImRect& bb, ImGuiID id, ImGuiItemFlags item_flags); IMGUI_API bool IsWindowContentHoverable(ImGuiWindow* window, ImGuiHoveredFlags flags = 0); IMGUI_API bool IsClippedEx(const ImRect& bb, ImGuiID id); IMGUI_API void SetLastItemData(ImGuiID item_id, ImGuiItemFlags in_flags, ImGuiItemStatusFlags status_flags, const ImRect& item_rect); @@ -2875,7 +3366,7 @@ namespace ImGui IMGUI_API void LogSetNextTextDecoration(const char* prefix, const char* suffix); // Popups, Modals, Tooltips - IMGUI_API bool BeginChildEx(const char* name, ImGuiID id, const ImVec2& size_arg, bool border, ImGuiWindowFlags flags); + IMGUI_API bool BeginChildEx(const char* name, ImGuiID id, const ImVec2& size_arg, ImGuiChildFlags child_flags, ImGuiWindowFlags window_flags); IMGUI_API void OpenPopupEx(ImGuiID id, ImGuiPopupFlags popup_flags = ImGuiPopupFlags_None); IMGUI_API void ClosePopupToLevel(int remaining, bool restore_focus_to_window_under_popup); IMGUI_API void ClosePopupsOverWindow(ImGuiWindow* ref_window, bool restore_focus_to_window_under_popup); @@ -2883,6 +3374,7 @@ namespace ImGui IMGUI_API bool IsPopupOpen(ImGuiID id, ImGuiPopupFlags popup_flags); IMGUI_API bool BeginPopupEx(ImGuiID id, ImGuiWindowFlags extra_flags); IMGUI_API bool BeginTooltipEx(ImGuiTooltipFlags tooltip_flags, ImGuiWindowFlags extra_window_flags); + IMGUI_API bool BeginTooltipHidden(); IMGUI_API ImRect GetPopupAllowedExtentRect(ImGuiWindow* window); IMGUI_API ImGuiWindow* GetTopMostPopupModal(); IMGUI_API ImGuiWindow* GetTopMostAndVisiblePopupModal(); @@ -2907,14 +3399,23 @@ namespace ImGui IMGUI_API void NavMoveRequestSubmit(ImGuiDir move_dir, ImGuiDir clip_dir, ImGuiNavMoveFlags move_flags, ImGuiScrollFlags scroll_flags); IMGUI_API void NavMoveRequestForward(ImGuiDir move_dir, ImGuiDir clip_dir, ImGuiNavMoveFlags move_flags, ImGuiScrollFlags scroll_flags); IMGUI_API void NavMoveRequestResolveWithLastItem(ImGuiNavItemData* result); + IMGUI_API void NavMoveRequestResolveWithPastTreeNode(ImGuiNavItemData* result, ImGuiNavTreeNodeData* tree_node_data); IMGUI_API void NavMoveRequestCancel(); IMGUI_API void NavMoveRequestApplyResult(); IMGUI_API void NavMoveRequestTryWrapping(ImGuiWindow* window, ImGuiNavMoveFlags move_flags); + IMGUI_API void NavHighlightActivated(ImGuiID id); IMGUI_API void NavClearPreferredPosForAxis(ImGuiAxis axis); + IMGUI_API void NavRestoreHighlightAfterMove(); IMGUI_API void NavUpdateCurrentWindowIsScrollPushableX(); - IMGUI_API void ActivateItem(ImGuiID id); // Remotely activate a button, checkbox, tree node etc. given its unique ID. activation is queued and processed on the next frame when the item is encountered again. IMGUI_API void SetNavWindow(ImGuiWindow* window); IMGUI_API void SetNavID(ImGuiID id, ImGuiNavLayer nav_layer, ImGuiID focus_scope_id, const ImRect& rect_rel); + IMGUI_API void SetNavFocusScope(ImGuiID focus_scope_id); + + // Focus/Activation + // This should be part of a larger set of API: FocusItem(offset = -1), FocusItemByID(id), ActivateItem(offset = -1), ActivateItemByID(id) etc. which are + // much harder to design and implement than expected. I have a couple of private branches on this matter but it's not simple. For now implementing the easy ones. + IMGUI_API void FocusItem(); // Focus last item (no selection/activation). + IMGUI_API void ActivateItemByID(ImGuiID id); // Activate an item by ID (button, checkbox, tree node etc.). Activation is queued and processed on the next frame when the item is encountered again. // Inputs // FIXME: Eventually we should aim to move e.g. IsActiveIdUsingKey() into IsKeyXXX functions. @@ -2925,7 +3426,8 @@ namespace ImGui inline bool IsGamepadKey(ImGuiKey key) { return key >= ImGuiKey_Gamepad_BEGIN && key < ImGuiKey_Gamepad_END; } inline bool IsMouseKey(ImGuiKey key) { return key >= ImGuiKey_Mouse_BEGIN && key < ImGuiKey_Mouse_END; } inline bool IsAliasKey(ImGuiKey key) { return key >= ImGuiKey_Aliases_BEGIN && key < ImGuiKey_Aliases_END; } - inline ImGuiKeyChord ConvertShortcutMod(ImGuiKeyChord key_chord) { ImGuiContext& g = *GImGui; IM_ASSERT_PARANOID(key_chord & ImGuiMod_Shortcut); return (key_chord & ~ImGuiMod_Shortcut) | (g.IO.ConfigMacOSXBehaviors ? ImGuiMod_Super : ImGuiMod_Ctrl); } + inline bool IsModKey(ImGuiKey key) { return key >= ImGuiKey_LeftCtrl && key <= ImGuiKey_RightSuper; } + ImGuiKeyChord FixupKeyChord(ImGuiContext* ctx, ImGuiKeyChord key_chord); inline ImGuiKey ConvertSingleModFlagToKey(ImGuiContext* ctx, ImGuiKey key) { ImGuiContext& g = *ctx; @@ -2939,13 +3441,14 @@ namespace ImGui IMGUI_API ImGuiKeyData* GetKeyData(ImGuiContext* ctx, ImGuiKey key); inline ImGuiKeyData* GetKeyData(ImGuiKey key) { ImGuiContext& g = *GImGui; return GetKeyData(&g, key); } - IMGUI_API void GetKeyChordName(ImGuiKeyChord key_chord, char* out_buf, int out_buf_size); + IMGUI_API const char* GetKeyChordName(ImGuiKeyChord key_chord); inline ImGuiKey MouseButtonToKey(ImGuiMouseButton button) { IM_ASSERT(button >= 0 && button < ImGuiMouseButton_COUNT); return (ImGuiKey)(ImGuiKey_MouseLeft + button); } IMGUI_API bool IsMouseDragPastThreshold(ImGuiMouseButton button, float lock_threshold = -1.0f); IMGUI_API ImVec2 GetKeyMagnitude2d(ImGuiKey key_left, ImGuiKey key_right, ImGuiKey key_up, ImGuiKey key_down); IMGUI_API float GetNavTweakPressedAmount(ImGuiAxis axis); IMGUI_API int CalcTypematicRepeatAmount(float t0, float t1, float repeat_delay, float repeat_rate); IMGUI_API void GetTypematicRepeatRate(ImGuiInputFlags flags, float* repeat_delay, float* repeat_rate); + IMGUI_API void TeleportMousePos(const ImVec2& pos); IMGUI_API void SetActiveIdUsingAllKeyboardKeys(); inline bool IsActiveIdUsingNavDir(ImGuiDir dir) { ImGuiContext& g = *GImGui; return (g.ActiveIdUsingNavDirMask & (1 << dir)) != 0; } @@ -2979,6 +3482,7 @@ namespace ImGui IMGUI_API bool IsMouseDown(ImGuiMouseButton button, ImGuiID owner_id); IMGUI_API bool IsMouseClicked(ImGuiMouseButton button, ImGuiID owner_id, ImGuiInputFlags flags = 0); IMGUI_API bool IsMouseReleased(ImGuiMouseButton button, ImGuiID owner_id); + IMGUI_API bool IsMouseDoubleClicked(ImGuiMouseButton button, ImGuiID owner_id); // [EXPERIMENTAL] Shortcut Routing // - ImGuiKeyChord = a ImGuiKey optionally OR-red with ImGuiMod_Alt/ImGuiMod_Ctrl/ImGuiMod_Shift/ImGuiMod_Super. @@ -2990,11 +3494,71 @@ namespace ImGui // - Route is granted to a single owner. When multiple requests are made we have policies to select the winning route. // - Multiple read sites may use the same owner id and will all get the granted route. // - For routing: when owner_id is 0 we use the current Focus Scope ID as a default owner in order to identify our location. + // - TL;DR; + // - IsKeyChordPressed() compares mods + call IsKeyPressed() -> function has no side-effect. + // - Shortcut() submits a route then if currently can be routed calls IsKeyChordPressed() -> function has (desirable) side-effects. + IMGUI_API bool IsKeyChordPressed(ImGuiKeyChord key_chord, ImGuiID owner_id, ImGuiInputFlags flags = 0); + IMGUI_API void SetNextItemShortcut(ImGuiKeyChord key_chord); IMGUI_API bool Shortcut(ImGuiKeyChord key_chord, ImGuiID owner_id = 0, ImGuiInputFlags flags = 0); - IMGUI_API bool SetShortcutRouting(ImGuiKeyChord key_chord, ImGuiID owner_id = 0, ImGuiInputFlags flags = 0); + IMGUI_API bool SetShortcutRouting(ImGuiKeyChord key_chord, ImGuiID owner_id, ImGuiInputFlags flags = 0); // owner_id needs to be explicit and cannot be 0 IMGUI_API bool TestShortcutRouting(ImGuiKeyChord key_chord, ImGuiID owner_id); IMGUI_API ImGuiKeyRoutingData* GetShortcutRoutingData(ImGuiKeyChord key_chord); + // Docking + // (some functions are only declared in imgui.cpp, see Docking section) + IMGUI_API void DockContextInitialize(ImGuiContext* ctx); + IMGUI_API void DockContextShutdown(ImGuiContext* ctx); + IMGUI_API void DockContextClearNodes(ImGuiContext* ctx, ImGuiID root_id, bool clear_settings_refs); // Use root_id==0 to clear all + IMGUI_API void DockContextRebuildNodes(ImGuiContext* ctx); + IMGUI_API void DockContextNewFrameUpdateUndocking(ImGuiContext* ctx); + IMGUI_API void DockContextNewFrameUpdateDocking(ImGuiContext* ctx); + IMGUI_API void DockContextEndFrame(ImGuiContext* ctx); + IMGUI_API ImGuiID DockContextGenNodeID(ImGuiContext* ctx); + IMGUI_API void DockContextQueueDock(ImGuiContext* ctx, ImGuiWindow* target, ImGuiDockNode* target_node, ImGuiWindow* payload, ImGuiDir split_dir, float split_ratio, bool split_outer); + IMGUI_API void DockContextQueueUndockWindow(ImGuiContext* ctx, ImGuiWindow* window); + IMGUI_API void DockContextQueueUndockNode(ImGuiContext* ctx, ImGuiDockNode* node); + IMGUI_API void DockContextProcessUndockWindow(ImGuiContext* ctx, ImGuiWindow* window, bool clear_persistent_docking_ref = true); + IMGUI_API void DockContextProcessUndockNode(ImGuiContext* ctx, ImGuiDockNode* node); + IMGUI_API bool DockContextCalcDropPosForDocking(ImGuiWindow* target, ImGuiDockNode* target_node, ImGuiWindow* payload_window, ImGuiDockNode* payload_node, ImGuiDir split_dir, bool split_outer, ImVec2* out_pos); + IMGUI_API ImGuiDockNode*DockContextFindNodeByID(ImGuiContext* ctx, ImGuiID id); + IMGUI_API void DockNodeWindowMenuHandler_Default(ImGuiContext* ctx, ImGuiDockNode* node, ImGuiTabBar* tab_bar); + IMGUI_API bool DockNodeBeginAmendTabBar(ImGuiDockNode* node); + IMGUI_API void DockNodeEndAmendTabBar(); + inline ImGuiDockNode* DockNodeGetRootNode(ImGuiDockNode* node) { while (node->ParentNode) node = node->ParentNode; return node; } + inline bool DockNodeIsInHierarchyOf(ImGuiDockNode* node, ImGuiDockNode* parent) { while (node) { if (node == parent) return true; node = node->ParentNode; } return false; } + inline int DockNodeGetDepth(const ImGuiDockNode* node) { int depth = 0; while (node->ParentNode) { node = node->ParentNode; depth++; } return depth; } + inline ImGuiID DockNodeGetWindowMenuButtonId(const ImGuiDockNode* node) { return ImHashStr("#COLLAPSE", 0, node->ID); } + inline ImGuiDockNode* GetWindowDockNode() { ImGuiContext& g = *GImGui; return g.CurrentWindow->DockNode; } + IMGUI_API bool GetWindowAlwaysWantOwnTabBar(ImGuiWindow* window); + IMGUI_API void BeginDocked(ImGuiWindow* window, bool* p_open); + IMGUI_API void BeginDockableDragDropSource(ImGuiWindow* window); + IMGUI_API void BeginDockableDragDropTarget(ImGuiWindow* window); + IMGUI_API void SetWindowDock(ImGuiWindow* window, ImGuiID dock_id, ImGuiCond cond); + + // Docking - Builder function needs to be generally called before the node is used/submitted. + // - The DockBuilderXXX functions are designed to _eventually_ become a public API, but it is too early to expose it and guarantee stability. + // - Do not hold on ImGuiDockNode* pointers! They may be invalidated by any split/merge/remove operation and every frame. + // - To create a DockSpace() node, make sure to set the ImGuiDockNodeFlags_DockSpace flag when calling DockBuilderAddNode(). + // You can create dockspace nodes (attached to a window) _or_ floating nodes (carry its own window) with this API. + // - DockBuilderSplitNode() create 2 child nodes within 1 node. The initial node becomes a parent node. + // - If you intend to split the node immediately after creation using DockBuilderSplitNode(), make sure + // to call DockBuilderSetNodeSize() beforehand. If you don't, the resulting split sizes may not be reliable. + // - Call DockBuilderFinish() after you are done. + IMGUI_API void DockBuilderDockWindow(const char* window_name, ImGuiID node_id); + IMGUI_API ImGuiDockNode*DockBuilderGetNode(ImGuiID node_id); + inline ImGuiDockNode* DockBuilderGetCentralNode(ImGuiID node_id) { ImGuiDockNode* node = DockBuilderGetNode(node_id); if (!node) return NULL; return DockNodeGetRootNode(node)->CentralNode; } + IMGUI_API ImGuiID DockBuilderAddNode(ImGuiID node_id = 0, ImGuiDockNodeFlags flags = 0); + IMGUI_API void DockBuilderRemoveNode(ImGuiID node_id); // Remove node and all its child, undock all windows + IMGUI_API void DockBuilderRemoveNodeDockedWindows(ImGuiID node_id, bool clear_settings_refs = true); + IMGUI_API void DockBuilderRemoveNodeChildNodes(ImGuiID node_id); // Remove all split/hierarchy. All remaining docked windows will be re-docked to the remaining root node (node_id). + IMGUI_API void DockBuilderSetNodePos(ImGuiID node_id, ImVec2 pos); + IMGUI_API void DockBuilderSetNodeSize(ImGuiID node_id, ImVec2 size); + IMGUI_API ImGuiID DockBuilderSplitNode(ImGuiID node_id, ImGuiDir split_dir, float size_ratio_for_node_at_dir, ImGuiID* out_id_at_dir, ImGuiID* out_id_at_opposite_dir); // Create 2 child nodes in this parent node. + IMGUI_API void DockBuilderCopyDockSpace(ImGuiID src_dockspace_id, ImGuiID dst_dockspace_id, ImVector* in_window_remap_pairs); + IMGUI_API void DockBuilderCopyNode(ImGuiID src_node_id, ImGuiID dst_node_id, ImVector* out_node_remap_pairs); + IMGUI_API void DockBuilderCopyWindowSettings(const char* src_name, const char* dst_name); + IMGUI_API void DockBuilderFinish(ImGuiID node_id); + // [EXPERIMENTAL] Focus Scope // This is generally used to identify a unique input location (for e.g. a selection set) // There is one per window (automatically set in Begin), but: @@ -3012,7 +3576,13 @@ namespace ImGui IMGUI_API bool BeginDragDropTargetCustom(const ImRect& bb, ImGuiID id); IMGUI_API void ClearDragDrop(); IMGUI_API bool IsDragDropPayloadBeingAccepted(); - IMGUI_API void RenderDragDropTargetRect(const ImRect& bb); + IMGUI_API void RenderDragDropTargetRect(const ImRect& bb, const ImRect& item_clip_rect); + + // Typing-Select API + IMGUI_API ImGuiTypingSelectRequest* GetTypingSelectRequest(ImGuiTypingSelectFlags flags = ImGuiTypingSelectFlags_None); + IMGUI_API int TypingSelectFindMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data, int nav_item_idx); + IMGUI_API int TypingSelectFindNextSingleCharMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data, int nav_item_idx); + IMGUI_API int TypingSelectFindBestLeadingMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data); // Internal Columns API (this is not exposed because we will encourage transitioning to the Tables API) IMGUI_API void SetWindowClipRectBeforeSetChannel(ImGuiWindow* window, const ImRect& clip_rect); @@ -3030,10 +3600,13 @@ namespace ImGui IMGUI_API void TableOpenContextMenu(int column_n = -1); IMGUI_API void TableSetColumnWidth(int column_n, float width); IMGUI_API void TableSetColumnSortDirection(int column_n, ImGuiSortDirection sort_direction, bool append_to_sort_specs); - IMGUI_API int TableGetHoveredColumn(); // May use (TableGetColumnFlags() & ImGuiTableColumnFlags_IsHovered) instead. Return hovered column. return -1 when table is not hovered. return columns_count if the unused space at the right of visible columns is hovered. + IMGUI_API int TableGetHoveredColumn(); // May use (TableGetColumnFlags() & ImGuiTableColumnFlags_IsHovered) instead. Return hovered column. return -1 when table is not hovered. return columns_count if the unused space at the right of visible columns is hovered. + IMGUI_API int TableGetHoveredRow(); // Retrieve *PREVIOUS FRAME* hovered row. This difference with TableGetHoveredColumn() is the reason why this is not public yet. IMGUI_API float TableGetHeaderRowHeight(); + IMGUI_API float TableGetHeaderAngledMaxLabelWidth(); IMGUI_API void TablePushBackgroundChannel(); IMGUI_API void TablePopBackgroundChannel(); + IMGUI_API void TableAngledHeadersRowEx(float angle, float max_label_width = 0.0f); // Tables: Internals inline ImGuiTable* GetCurrentTable() { ImGuiContext& g = *GImGui; return g.CurrentTable; } @@ -3046,7 +3619,7 @@ namespace ImGui IMGUI_API void TableUpdateBorders(ImGuiTable* table); IMGUI_API void TableUpdateColumnsWeightFromWidth(ImGuiTable* table); IMGUI_API void TableDrawBorders(ImGuiTable* table); - IMGUI_API void TableDrawContextMenu(ImGuiTable* table); + IMGUI_API void TableDrawDefaultContextMenu(ImGuiTable* table, ImGuiTableFlags flags_for_section_to_display); IMGUI_API bool TableBeginContextMenuPopup(ImGuiTable* table); IMGUI_API void TableMergeDrawChannels(ImGuiTable* table); inline ImGuiTableInstanceData* TableGetInstanceData(ImGuiTable* table, int instance_no) { if (instance_no == 0) return &table->InstanceDataFirst; return &table->InstanceDataExtra[instance_no - 1]; } @@ -3085,9 +3658,11 @@ namespace ImGui IMGUI_API bool BeginTabBarEx(ImGuiTabBar* tab_bar, const ImRect& bb, ImGuiTabBarFlags flags); IMGUI_API ImGuiTabItem* TabBarFindTabByID(ImGuiTabBar* tab_bar, ImGuiID tab_id); IMGUI_API ImGuiTabItem* TabBarFindTabByOrder(ImGuiTabBar* tab_bar, int order); + IMGUI_API ImGuiTabItem* TabBarFindMostRecentlySelectedTabForActiveWindow(ImGuiTabBar* tab_bar); IMGUI_API ImGuiTabItem* TabBarGetCurrentTab(ImGuiTabBar* tab_bar); inline int TabBarGetTabOrder(ImGuiTabBar* tab_bar, ImGuiTabItem* tab) { return tab_bar->Tabs.index_from_ptr(tab); } IMGUI_API const char* TabBarGetTabName(ImGuiTabBar* tab_bar, ImGuiTabItem* tab); + IMGUI_API void TabBarAddTab(ImGuiTabBar* tab_bar, ImGuiTabItemFlags tab_flags, ImGuiWindow* window); IMGUI_API void TabBarRemoveTab(ImGuiTabBar* tab_bar, ImGuiID tab_id); IMGUI_API void TabBarCloseTab(ImGuiTabBar* tab_bar, ImGuiTabItem* tab); IMGUI_API void TabBarQueueFocus(ImGuiTabBar* tab_bar, ImGuiTabItem* tab); @@ -3111,7 +3686,7 @@ namespace ImGui IMGUI_API void RenderFrame(ImVec2 p_min, ImVec2 p_max, ImU32 fill_col, bool border = true, float rounding = 0.0f); IMGUI_API void RenderFrameBorder(ImVec2 p_min, ImVec2 p_max, float rounding = 0.0f); IMGUI_API void RenderColorRectWithAlphaCheckerboard(ImDrawList* draw_list, ImVec2 p_min, ImVec2 p_max, ImU32 fill_col, float grid_step, ImVec2 grid_off, float rounding = 0.0f, ImDrawFlags flags = 0); - IMGUI_API void RenderNavHighlight(const ImRect& bb, ImGuiID id, ImGuiNavHighlightFlags flags = ImGuiNavHighlightFlags_TypeDefault); // Navigation highlight + IMGUI_API void RenderNavHighlight(const ImRect& bb, ImGuiID id, ImGuiNavHighlightFlags flags = ImGuiNavHighlightFlags_None); // Navigation highlight IMGUI_API const char* FindRenderedTextEnd(const char* text, const char* text_end = NULL); // Find the optional ## from which we stop displaying text. IMGUI_API void RenderMouseCursor(ImVec2 pos, float scale, ImGuiMouseCursor mouse_cursor, ImU32 col_fill, ImU32 col_border, ImU32 col_shadow); @@ -3120,14 +3695,16 @@ namespace ImGui IMGUI_API void RenderBullet(ImDrawList* draw_list, ImVec2 pos, ImU32 col); IMGUI_API void RenderCheckMark(ImDrawList* draw_list, ImVec2 pos, ImU32 col, float sz); IMGUI_API void RenderArrowPointingAt(ImDrawList* draw_list, ImVec2 pos, ImVec2 half_sz, ImGuiDir direction, ImU32 col); + IMGUI_API void RenderArrowDockMenu(ImDrawList* draw_list, ImVec2 p_min, float sz, ImU32 col); IMGUI_API void RenderRectFilledRangeH(ImDrawList* draw_list, const ImRect& rect, ImU32 col, float x_start_norm, float x_end_norm, float rounding); IMGUI_API void RenderRectFilledWithHole(ImDrawList* draw_list, const ImRect& outer, const ImRect& inner, ImU32 col, float rounding); + IMGUI_API ImDrawFlags CalcRoundingFlagsForRectInRect(const ImRect& r_in, const ImRect& r_outer, float threshold); // Widgets IMGUI_API void TextEx(const char* text, const char* text_end = NULL, ImGuiTextFlags flags = 0); IMGUI_API bool ButtonEx(const char* label, const ImVec2& size_arg = ImVec2(0, 0), ImGuiButtonFlags flags = 0); IMGUI_API bool ArrowButtonEx(const char* str_id, ImGuiDir dir, ImVec2 size_arg, ImGuiButtonFlags flags = 0); - IMGUI_API bool ImageButtonEx(ImGuiID id, ImTextureID texture_id, const ImVec2& size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& bg_col, const ImVec4& tint_col, ImGuiButtonFlags flags = 0); + IMGUI_API bool ImageButtonEx(ImGuiID id, ImTextureID texture_id, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& bg_col, const ImVec4& tint_col, ImGuiButtonFlags flags = 0); IMGUI_API void SeparatorEx(ImGuiSeparatorFlags flags, float thickness = 1.0f); IMGUI_API void SeparatorTextEx(ImGuiID id, const char* label, const char* label_end, float extra_width); IMGUI_API bool CheckboxFlags(const char* label, ImS64* flags, ImS64 flags_value); @@ -3135,7 +3712,7 @@ namespace ImGui // Widgets: Window Decorations IMGUI_API bool CloseButton(ImGuiID id, const ImVec2& pos); - IMGUI_API bool CollapseButton(ImGuiID id, const ImVec2& pos); + IMGUI_API bool CollapseButton(ImGuiID id, const ImVec2& pos, ImGuiDockNode* dock_node); IMGUI_API void Scrollbar(ImGuiAxis axis); IMGUI_API bool ScrollbarEx(const ImRect& bb, ImGuiID id, ImGuiAxis axis, ImS64* p_scroll_v, ImS64 avail_v, ImS64 contents_v, ImDrawFlags flags); IMGUI_API ImRect GetWindowScrollbarRect(ImGuiWindow* window, ImGuiAxis axis); @@ -3152,6 +3729,7 @@ namespace ImGui IMGUI_API void TreePushOverrideID(ImGuiID id); IMGUI_API void TreeNodeSetOpen(ImGuiID id, bool open); IMGUI_API bool TreeNodeUpdateNextOpen(ImGuiID id, ImGuiTreeNodeFlags flags); // Return open state. Consume previous SetNextItemOpen() data, if any. May return true when logging. + IMGUI_API void SetNextItemSelectionUserData(ImGuiSelectionUserData selection_user_data); // Template functions are instantiated in imgui_widgets.cpp for a finite number of types. // To use them externally (for custom widget) you may need an "extern template" statement in your code in order to link to existing instances and silence Clang warnings (see #2036). @@ -3190,6 +3768,7 @@ namespace ImGui // Shade functions (write over already created vertices) IMGUI_API void ShadeVertsLinearColorGradientKeepAlpha(ImDrawList* draw_list, int vert_start_idx, int vert_end_idx, ImVec2 gradient_p0, ImVec2 gradient_p1, ImU32 col0, ImU32 col1); IMGUI_API void ShadeVertsLinearUV(ImDrawList* draw_list, int vert_start_idx, int vert_end_idx, const ImVec2& a, const ImVec2& b, const ImVec2& uv_a, const ImVec2& uv_b, bool clamp); + IMGUI_API void ShadeVertsTransformPos(ImDrawList* draw_list, int vert_start_idx, int vert_end_idx, const ImVec2& pivot_in, float cos_a, float sin_a, const ImVec2& pivot_out); // Garbage collection IMGUI_API void GcCompactTransientMiscBuffers(); @@ -3199,20 +3778,26 @@ namespace ImGui // Debug Log IMGUI_API void DebugLog(const char* fmt, ...) IM_FMTARGS(1); IMGUI_API void DebugLogV(const char* fmt, va_list args) IM_FMTLIST(1); + IMGUI_API void DebugAllocHook(ImGuiDebugAllocInfo* info, int frame_count, void* ptr, size_t size); // size >= 0 : alloc, size = -1 : free // Debug Tools IMGUI_API void ErrorCheckEndFrameRecover(ImGuiErrorLogCallback log_callback, void* user_data = NULL); IMGUI_API void ErrorCheckEndWindowRecover(ImGuiErrorLogCallback log_callback, void* user_data = NULL); IMGUI_API void ErrorCheckUsingSetCursorPosToExtendParentBoundaries(); + IMGUI_API void DebugDrawCursorPos(ImU32 col = IM_COL32(255, 0, 0, 255)); + IMGUI_API void DebugDrawLineExtents(ImU32 col = IM_COL32(255, 0, 0, 255)); + IMGUI_API void DebugDrawItemRect(ImU32 col = IM_COL32(255, 0, 0, 255)); IMGUI_API void DebugLocateItem(ImGuiID target_id); // Call sparingly: only 1 at the same time! IMGUI_API void DebugLocateItemOnHover(ImGuiID target_id); // Only call on reaction to a mouse Hover: because only 1 at the same time! IMGUI_API void DebugLocateItemResolveWithLastItem(); - inline void DebugDrawItemRect(ImU32 col = IM_COL32(255,0,0,255)) { ImGuiContext& g = *GImGui; ImGuiWindow* window = g.CurrentWindow; GetForegroundDrawList(window)->AddRect(g.LastItemData.Rect.Min, g.LastItemData.Rect.Max, col); } - inline void DebugStartItemPicker() { ImGuiContext& g = *GImGui; g.DebugItemPickerActive = true; } + IMGUI_API void DebugBreakClearData(); + IMGUI_API bool DebugBreakButton(const char* label, const char* description_of_location); + IMGUI_API void DebugBreakButtonTooltip(bool keyboard_only, const char* description_of_location); IMGUI_API void ShowFontAtlas(ImFontAtlas* atlas); IMGUI_API void DebugHookIdInfo(ImGuiID id, ImGuiDataType data_type, const void* data_id, const void* data_id_end); IMGUI_API void DebugNodeColumns(ImGuiOldColumns* columns); - IMGUI_API void DebugNodeDrawList(ImGuiWindow* window, const ImDrawList* draw_list, const char* label); + IMGUI_API void DebugNodeDockNode(ImGuiDockNode* node, const char* label); + IMGUI_API void DebugNodeDrawList(ImGuiWindow* window, ImGuiViewportP* viewport, const ImDrawList* draw_list, const char* label); IMGUI_API void DebugNodeDrawCmdShowMeshAndBoundingBox(ImDrawList* out_draw_list, const ImDrawList* draw_list, const ImDrawCmd* draw_cmd, bool show_mesh, bool show_aabb); IMGUI_API void DebugNodeFont(ImFont* font); IMGUI_API void DebugNodeFontGlyph(ImFont* font, const ImFontGlyph* glyph); @@ -3221,6 +3806,7 @@ namespace ImGui IMGUI_API void DebugNodeTable(ImGuiTable* table); IMGUI_API void DebugNodeTableSettings(ImGuiTableSettings* settings); IMGUI_API void DebugNodeInputTextState(ImGuiInputTextState* state); + IMGUI_API void DebugNodeTypingSelectState(ImGuiTypingSelectState* state); IMGUI_API void DebugNodeWindow(ImGuiWindow* window, const char* label); IMGUI_API void DebugNodeWindowSettings(ImGuiWindowSettings* settings); IMGUI_API void DebugNodeWindowsList(ImVector* windows, const char* label); @@ -3234,13 +3820,12 @@ namespace ImGui inline void SetItemUsingMouseWheel() { SetItemKeyOwner(ImGuiKey_MouseWheelY); } // Changed in 1.89 inline bool TreeNodeBehaviorIsOpen(ImGuiID id, ImGuiTreeNodeFlags flags = 0) { return TreeNodeUpdateNextOpen(id, flags); } // Renamed in 1.89 - // Refactored focus/nav/tabbing system in 1.82 and 1.84. If you have old/custom copy-and-pasted widgets that used FocusableItemRegister(): + // Refactored focus/nav/tabbing system in 1.82 and 1.84. If you have old/custom copy-and-pasted widgets which used FocusableItemRegister(): // (Old) IMGUI_VERSION_NUM < 18209: using 'ItemAdd(....)' and 'bool tab_focused = FocusableItemRegister(...)' - // (Old) IMGUI_VERSION_NUM >= 18209: using 'ItemAdd(..., ImGuiItemAddFlags_Focusable)' and 'bool tab_focused = (GetItemStatusFlags() & ImGuiItemStatusFlags_Focused) != 0' - // (New) IMGUI_VERSION_NUM >= 18413: using 'ItemAdd(..., ImGuiItemFlags_Inputable)' and 'bool tab_focused = (GetItemStatusFlags() & ImGuiItemStatusFlags_FocusedTabbing) != 0 || (g.NavActivateId == id && (g.NavActivateFlags & ImGuiActivateFlags_PreferInput))' (WIP) - // Widget code are simplified as there's no need to call FocusableItemUnregister() while managing the transition from regular widget to TempInputText() - inline bool FocusableItemRegister(ImGuiWindow* window, ImGuiID id) { IM_ASSERT(0); IM_UNUSED(window); IM_UNUSED(id); return false; } // -> pass ImGuiItemAddFlags_Inputable flag to ItemAdd() - inline void FocusableItemUnregister(ImGuiWindow* window) { IM_ASSERT(0); IM_UNUSED(window); } // -> unnecessary: TempInputText() uses ImGuiInputTextFlags_MergedItem + // (Old) IMGUI_VERSION_NUM >= 18209: using 'ItemAdd(..., ImGuiItemAddFlags_Focusable)' and 'bool tab_focused = (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Focused) != 0' + // (New) IMGUI_VERSION_NUM >= 18413: using 'ItemAdd(..., ImGuiItemFlags_Inputable)' and 'bool tab_focused = (g.NavActivateId == id && (g.NavActivateFlags & ImGuiActivateFlags_PreferInput))' + //inline bool FocusableItemRegister(ImGuiWindow* window, ImGuiID id) // -> pass ImGuiItemAddFlags_Inputable flag to ItemAdd() + //inline void FocusableItemUnregister(ImGuiWindow* window) // -> unnecessary: TempInputText() uses ImGuiInputTextFlags_MergedItem #endif #ifndef IMGUI_DISABLE_OBSOLETE_KEYIO inline bool IsKeyPressedMap(ImGuiKey key, bool repeat = true) { IM_ASSERT(IsNamedKey(key)); return IsKeyPressed(key, repeat); } // Removed in 1.87: Mapping from named key is always identity! @@ -3263,6 +3848,7 @@ struct ImFontBuilderIO #ifdef IMGUI_ENABLE_STB_TRUETYPE IMGUI_API const ImFontBuilderIO* ImFontAtlasGetBuilderForStbTruetype(); #endif +IMGUI_API void ImFontAtlasUpdateConfigDataPointers(ImFontAtlas* atlas); IMGUI_API void ImFontAtlasBuildInit(ImFontAtlas* atlas); IMGUI_API void ImFontAtlasBuildSetupFont(ImFontAtlas* atlas, ImFont* font, ImFontConfig* font_config, float ascent, float descent); IMGUI_API void ImFontAtlasBuildPackCustomRects(ImFontAtlas* atlas, void* stbrp_context_opaque); diff --git a/libs/imgui/imgui_tables.cpp b/libs/imgui/imgui_tables.cpp index 8850094..260df1a 100644 --- a/libs/imgui/imgui_tables.cpp +++ b/libs/imgui/imgui_tables.cpp @@ -1,4 +1,4 @@ -// dear imgui, v1.89.6 +// dear imgui, v1.90.4 // (tables and columns code) /* @@ -48,7 +48,8 @@ Index of this file: // - TableUpdateLayout() [Internal] followup to BeginTable(): setup everything: widths, columns positions, clipping rectangles. Automatically called by the FIRST call to TableNextRow() or TableHeadersRow(). // | TableSetupDrawChannels() - setup ImDrawList channels // | TableUpdateBorders() - detect hovering columns for resize, ahead of contents submission -// | TableDrawContextMenu() - draw right-click context menu +// | TableBeginContextMenuPopup() +// | - TableDrawDefaultContextMenu() - draw right-click context menu contents //----------------------------------------------------------------------------- // - TableHeadersRow() or TableHeader() user submit a headers row (optional) // | TableSortSpecsClickColumn() - when left-clicked: alter sort order and sort direction @@ -82,7 +83,7 @@ Index of this file: // Y with ScrollX/ScrollY disabled: we output table directly in current window // - outer_size.y < 0.0f -> Bottom-align (but will auto extend, unless _NoHostExtendY is set). Not meaningful if parent window can vertically scroll. // - outer_size.y = 0.0f -> No minimum height (but will auto extend, unless _NoHostExtendY is set) -// - outer_size.y > 0.0f -> Set Minimum height (but will auto extend, unless _NoHostExtenY is set) +// - outer_size.y > 0.0f -> Set Minimum height (but will auto extend, unless _NoHostExtendY is set) // Y with ScrollX/ScrollY enabled: using a child window for scrolling // - outer_size.y < 0.0f -> Bottom-align. Not meaningful if parent window can vertically scroll. // - outer_size.y = 0.0f -> Bottom-align, consistent with BeginChild(). Not recommended unless table is last item in parent window. @@ -198,11 +199,7 @@ Index of this file: #include "imgui_internal.h" // System includes -#if defined(_MSC_VER) && _MSC_VER <= 1500 // MSVC 2008 or earlier -#include // intptr_t -#else #include // intptr_t -#endif // Visual Studio warnings #ifdef _MSC_VER @@ -323,14 +320,19 @@ bool ImGui::BeginTableEx(const char* name, ImGuiID id, int columns_count, ImG // If an outer size is specified ahead we will be able to early out when not visible. Exact clipping criteria may evolve. const bool use_child_window = (flags & (ImGuiTableFlags_ScrollX | ImGuiTableFlags_ScrollY)) != 0; const ImVec2 avail_size = GetContentRegionAvail(); - ImVec2 actual_outer_size = CalcItemSize(outer_size, ImMax(avail_size.x, 1.0f), use_child_window ? ImMax(avail_size.y, 1.0f) : 0.0f); - ImRect outer_rect(outer_window->DC.CursorPos, outer_window->DC.CursorPos + actual_outer_size); - if (use_child_window && IsClippedEx(outer_rect, 0)) + const ImVec2 actual_outer_size = CalcItemSize(outer_size, ImMax(avail_size.x, 1.0f), use_child_window ? ImMax(avail_size.y, 1.0f) : 0.0f); + const ImRect outer_rect(outer_window->DC.CursorPos, outer_window->DC.CursorPos + actual_outer_size); + const bool outer_window_is_measuring_size = (outer_window->AutoFitFramesX > 0) || (outer_window->AutoFitFramesY > 0); // Doesn't apply to AlwaysAutoResize windows! + if (use_child_window && IsClippedEx(outer_rect, 0) && !outer_window_is_measuring_size) { ItemSize(outer_rect); return false; } + // [DEBUG] Debug break requested by user + if (g.DebugBreakInTable == id) + IM_DEBUG_BREAK(); + // Acquire storage for the table ImGuiTable* table = g.Tables.GetOrAddByKey(id); const ImGuiTableFlags table_last_flags = table->Flags; @@ -349,7 +351,8 @@ bool ImGui::BeginTableEx(const char* name, ImGuiID id, int columns_count, ImG flags = TableFixFlags(flags, outer_window); // Initialize - const int instance_no = (table->LastFrameActive != g.FrameCount) ? 0 : table->InstanceCurrent + 1; + const int previous_frame_active = table->LastFrameActive; + const int instance_no = (previous_frame_active != g.FrameCount) ? 0 : table->InstanceCurrent + 1; table->ID = id; table->Flags = flags; table->LastFrameActive = g.FrameCount; @@ -408,13 +411,17 @@ bool ImGui::BeginTableEx(const char* name, ImGuiID id, int columns_count, ImG table->InnerRect = table->InnerWindow->InnerRect; IM_ASSERT(table->InnerWindow->WindowPadding.x == 0.0f && table->InnerWindow->WindowPadding.y == 0.0f && table->InnerWindow->WindowBorderSize == 0.0f); + // Allow submitting when host is measuring + if (table->InnerWindow->SkipItems && outer_window_is_measuring_size) + table->InnerWindow->SkipItems = false; + // When using multiple instances, ensure they have the same amount of horizontal decorations (aka vertical scrollbar) so stretched columns can be aligned) if (instance_no == 0) { table->HasScrollbarYPrev = table->HasScrollbarYCurr; table->HasScrollbarYCurr = false; } - table->HasScrollbarYCurr |= (table->InnerWindow->ScrollMax.y > 0.0f); + table->HasScrollbarYCurr |= table->InnerWindow->ScrollbarY; } else { @@ -443,6 +450,18 @@ bool ImGui::BeginTableEx(const char* name, ImGuiID id, int columns_count, ImG temp_data->HostBackupItemWidthStackSize = outer_window->DC.ItemWidthStack.Size; inner_window->DC.PrevLineSize = inner_window->DC.CurrLineSize = ImVec2(0.0f, 0.0f); + // Make left and top borders not overlap our contents by offsetting HostClipRect (#6765) + // (we normally shouldn't alter HostClipRect as we rely on TableMergeDrawChannels() expanding non-clipped column toward the + // limits of that rectangle, in order for ImDrawListSplitter::Merge() to merge the draw commands. However since the overlap + // problem only affect scrolling tables in this case we can get away with doing it without extra cost). + if (inner_window != outer_window) + { + if (flags & ImGuiTableFlags_BordersOuterV) + table->HostClipRect.Min.x = ImMin(table->HostClipRect.Min.x + TABLE_BORDER_SIZE, table->HostClipRect.Max.x); + if (flags & ImGuiTableFlags_BordersOuterH) + table->HostClipRect.Min.y = ImMin(table->HostClipRect.Min.y + TABLE_BORDER_SIZE, table->HostClipRect.Max.y); + } + // Padding and Spacing // - None ........Content..... Pad .....Content........ // - PadOuter | Pad ..Content..... Pad .....Content.. Pad | @@ -456,7 +475,6 @@ bool ImGui::BeginTableEx(const char* name, ImGuiID id, int columns_count, ImG table->CellSpacingX1 = inner_spacing_explicit + inner_spacing_for_border; table->CellSpacingX2 = inner_spacing_explicit; table->CellPaddingX = inner_padding_explicit; - table->CellPaddingY = g.Style.CellPadding.y; const float outer_padding_for_border = (flags & ImGuiTableFlags_BordersOuterV) ? TABLE_BORDER_SIZE : 0.0f; const float outer_padding_explicit = pad_outer_x ? g.Style.CellPadding.x : 0.0f; @@ -473,10 +491,14 @@ bool ImGui::BeginTableEx(const char* name, ImGuiID id, int columns_count, ImG table->RowPosY1 = table->RowPosY2 = table->WorkRect.Min.y; // This is needed somehow table->RowTextBaseline = 0.0f; // This will be cleared again by TableBeginRow() + table->RowCellPaddingY = 0.0f; table->FreezeRowsRequest = table->FreezeRowsCount = 0; // This will be setup by TableSetupScrollFreeze(), if any table->FreezeColumnsRequest = table->FreezeColumnsCount = 0; table->IsUnfrozenRows = true; - table->DeclColumnsCount = 0; + table->DeclColumnsCount = table->AngledHeadersCount = 0; + if (previous_frame_active + 1 < g.FrameCount) + table->IsActiveIdInTable = false; + temp_data->AngledHeadersExtraWidth = 0.0f; // Using opaque colors facilitate overlapping lines of the grid, otherwise we'd need to improve TableDrawBorders() table->BorderColorStrong = GetColorU32(ImGuiCol_TableBorderStrong); @@ -850,8 +872,8 @@ void ImGui::TableUpdateLayout(ImGuiTable* table) table->RightMostEnabledColumn = (ImGuiTableColumnIdx)prev_visible_column_idx; IM_ASSERT(table->LeftMostEnabledColumn >= 0 && table->RightMostEnabledColumn >= 0); - // [Part 2] Disable child window clipping while fitting columns. This is not strictly necessary but makes it possible - // to avoid the column fitting having to wait until the first visible frame of the child container (may or not be a good thing). + // [Part 2] Disable child window clipping while fitting columns. This is not strictly necessary but makes it possible to avoid + // the column fitting having to wait until the first visible frame of the child container (may or not be a good thing). Also see #6510. // FIXME-TABLE: for always auto-resizing columns may not want to do that all the time. if (has_auto_fit_request && table->OuterWindow != table->InnerWindow) table->InnerWindow->SkipItems = false; @@ -936,7 +958,7 @@ void ImGui::TableUpdateLayout(ImGuiTable* table) if (column->Flags & ImGuiTableColumnFlags_WidthStretch) { float weight_ratio = column->StretchWeight / stretch_sum_weights; - column->WidthRequest = IM_FLOOR(ImMax(width_avail_for_stretched_columns * weight_ratio, table->MinColumnWidth) + 0.01f); + column->WidthRequest = IM_TRUNC(ImMax(width_avail_for_stretched_columns * weight_ratio, table->MinColumnWidth) + 0.01f); width_remaining_for_stretched_columns -= column->WidthRequest; } @@ -946,7 +968,7 @@ void ImGui::TableUpdateLayout(ImGuiTable* table) column->Flags |= ImGuiTableColumnFlags_NoDirectResize_; // Assign final width, record width in case we will need to shrink - column->WidthGiven = ImFloor(ImMax(column->WidthRequest, table->MinColumnWidth)); + column->WidthGiven = ImTrunc(ImMax(column->WidthRequest, table->MinColumnWidth)); table->ColumnsGivenWidth += column->WidthGiven; } @@ -971,17 +993,25 @@ void ImGui::TableUpdateLayout(ImGuiTable* table) // clear ActiveId, which is equivalent to the change provided by _AllowWhenBLockedByActiveItem). // - This allows columns to be marked as hovered when e.g. clicking a button inside the column, or using drag and drop. ImGuiTableInstanceData* table_instance = TableGetInstanceData(table, table->InstanceCurrent); - table->HoveredColumnBody = -1; - table->HoveredColumnBorder = -1; + table_instance->HoveredRowLast = table_instance->HoveredRowNext; + table_instance->HoveredRowNext = -1; + table->HoveredColumnBody = table->HoveredColumnBorder = -1; const ImRect mouse_hit_rect(table->OuterRect.Min.x, table->OuterRect.Min.y, table->OuterRect.Max.x, ImMax(table->OuterRect.Max.y, table->OuterRect.Min.y + table_instance->LastOuterHeight)); const ImGuiID backup_active_id = g.ActiveId; g.ActiveId = 0; - const bool is_hovering_table = ItemHoverable(mouse_hit_rect, 0); + const bool is_hovering_table = ItemHoverable(mouse_hit_rect, 0, ImGuiItemFlags_None); g.ActiveId = backup_active_id; + // Determine skewed MousePos.x to support angled headers. + float mouse_skewed_x = g.IO.MousePos.x; + if (table->AngledHeadersHeight > 0.0f) + if (g.IO.MousePos.y >= table->OuterRect.Min.y && g.IO.MousePos.y <= table->OuterRect.Min.y + table->AngledHeadersHeight) + mouse_skewed_x += ImTrunc((table->OuterRect.Min.y + table->AngledHeadersHeight - g.IO.MousePos.y) * table->AngledHeadersSlope); + // [Part 6] Setup final position, offset, skip/clip states and clipping rectangles, detect hovered column // Process columns in their visible orders as we are comparing the visible order and adjusting host_clip_rect while looping. int visible_n = 0; + bool has_at_least_one_column_requesting_output = false; bool offset_x_frozen = (table->FreezeColumnsCount > 0); float offset_x = ((table->FreezeColumnsCount > 0) ? table->OuterRect.Min.x : work_rect.Min.x) + table->OuterPaddingX - table->CellSpacingX1; ImRect host_clip_rect = table->InnerClipRect; @@ -1019,7 +1049,7 @@ void ImGui::TableUpdateLayout(ImGuiTable* table) } // Detect hovered column - if (is_hovering_table && g.IO.MousePos.x >= column->ClipRect.Min.x && g.IO.MousePos.x < column->ClipRect.Max.x) + if (is_hovering_table && mouse_skewed_x >= column->ClipRect.Min.x && mouse_skewed_x < column->ClipRect.Max.x) table->HoveredColumnBody = (ImGuiTableColumnIdx)column_n; // Lock start position @@ -1038,7 +1068,7 @@ void ImGui::TableUpdateLayout(ImGuiTable* table) // - FIXME-TABLE: We want equal width columns to have equal (ClipRect.Max.x - WorkMinX) width, which means ClipRect.max.x cannot stray off host_clip_rect.Max.x else right-most column may appear shorter. column->WorkMinX = column->MinX + table->CellPaddingX + table->CellSpacingX1; column->WorkMaxX = column->MaxX - table->CellPaddingX - table->CellSpacingX2; // Expected max - column->ItemWidth = ImFloor(column->WidthGiven * 0.65f); + column->ItemWidth = ImTrunc(column->WidthGiven * 0.65f); column->ClipRect.Min.x = column->MinX; column->ClipRect.Min.y = work_rect.Min.y; column->ClipRect.Max.x = column->MaxX; //column->WorkMaxX; @@ -1062,9 +1092,12 @@ void ImGui::TableUpdateLayout(ImGuiTable* table) column->IsRequestOutput = is_visible || column->AutoFitQueue != 0 || column->CannotSkipItemsQueue != 0; // Mark column as SkipItems (ignoring all items/layout) + // (table->HostSkipItems is a copy of inner_window->SkipItems before we cleared it above in Part 2) column->IsSkipItems = !column->IsEnabled || table->HostSkipItems; if (column->IsSkipItems) IM_ASSERT(!is_visible); + if (column->IsRequestOutput && !column->IsSkipItems) + has_at_least_one_column_requesting_output = true; // Update status flags column->Flags |= ImGuiTableColumnFlags_IsEnabled; @@ -1102,18 +1135,26 @@ void ImGui::TableUpdateLayout(ImGuiTable* table) visible_n++; } + // In case the table is visible (e.g. decorations) but all columns clipped, we keep a column visible. + // Else if give no chance to a clipper-savy user to submit rows and therefore total contents height used by scrollbar. + if (has_at_least_one_column_requesting_output == false) + { + table->Columns[table->LeftMostEnabledColumn].IsRequestOutput = true; + table->Columns[table->LeftMostEnabledColumn].IsSkipItems = false; + } + // [Part 7] Detect/store when we are hovering the unused space after the right-most column (so e.g. context menus can react on it) // Clear Resizable flag if none of our column are actually resizable (either via an explicit _NoResize flag, either // because of using _WidthAuto/_WidthStretch). This will hide the resizing option from the context menu. const float unused_x1 = ImMax(table->WorkRect.Min.x, table->Columns[table->RightMostEnabledColumn].ClipRect.Max.x); if (is_hovering_table && table->HoveredColumnBody == -1) - { - if (g.IO.MousePos.x >= unused_x1) + if (mouse_skewed_x >= unused_x1) table->HoveredColumnBody = (ImGuiTableColumnIdx)table->ColumnsCount; - } if (has_resizable == false && (table->Flags & ImGuiTableFlags_Resizable)) table->Flags &= ~ImGuiTableFlags_Resizable; + table->IsActiveIdAliveBeforeTable = (g.ActiveIdIsAlive != 0); + // [Part 8] Lock actual OuterRect/WorkRect right-most position. // This is done late to handle the case of fixed-columns tables not claiming more widths that they need. // Because of this we are careful with uses of WorkRect and InnerClipRect before this point. @@ -1125,8 +1166,16 @@ void ImGui::TableUpdateLayout(ImGuiTable* table) table->InnerClipRect.Max.x = ImMin(table->InnerClipRect.Max.x, unused_x1); } table->InnerWindow->ParentWorkRect = table->WorkRect; - table->BorderX1 = table->InnerClipRect.Min.x;// +((table->Flags & ImGuiTableFlags_BordersOuter) ? 0.0f : -1.0f); - table->BorderX2 = table->InnerClipRect.Max.x;// +((table->Flags & ImGuiTableFlags_BordersOuter) ? 0.0f : +1.0f); + table->BorderX1 = table->InnerClipRect.Min.x; + table->BorderX2 = table->InnerClipRect.Max.x; + + // Setup window's WorkRect.Max.y for GetContentRegionAvail(). Other values will be updated in each TableBeginCell() call. + float window_content_max_y; + if (table->Flags & ImGuiTableFlags_NoHostExtendY) + window_content_max_y = table->OuterRect.Max.y; + else + window_content_max_y = ImMax(table->InnerWindow->ContentRegionRect.Max.y, (table->Flags & ImGuiTableFlags_ScrollY) ? 0.0f : table->OuterRect.Max.y); + table->InnerWindow->WorkRect.Max.y = ImClamp(window_content_max_y - g.Style.CellPadding.y, table->InnerWindow->WorkRect.Min.y, table->InnerWindow->WorkRect.Max.y); // [Part 9] Allocate draw channels and setup background cliprect TableSetupDrawChannels(table); @@ -1134,14 +1183,26 @@ void ImGui::TableUpdateLayout(ImGuiTable* table) // [Part 10] Hit testing on borders if (table->Flags & ImGuiTableFlags_Resizable) TableUpdateBorders(table); - table_instance->LastFirstRowHeight = 0.0f; + table_instance->LastTopHeadersRowHeight = 0.0f; table->IsLayoutLocked = true; table->IsUsingHeaders = false; - // [Part 11] Context menu - if (TableBeginContextMenuPopup(table)) + // Highlight header + table->HighlightColumnHeader = -1; + if (table->IsContextPopupOpen && table->ContextPopupColumn != -1 && table->InstanceInteracted == table->InstanceCurrent) + table->HighlightColumnHeader = table->ContextPopupColumn; + else if ((table->Flags & ImGuiTableFlags_HighlightHoveredColumn) && table->HoveredColumnBody != -1 && table->HoveredColumnBody != table->ColumnsCount && table->HoveredColumnBorder == -1) + if (g.ActiveId == 0 || (table->IsActiveIdInTable || g.DragDropActive)) + table->HighlightColumnHeader = table->HoveredColumnBody; + + // [Part 11] Default context menu + // - To append to this menu: you can call TableBeginContextMenuPopup()/.../EndPopup(). + // - To modify or replace this: set table->IsContextPopupNoDefaultContents = true, then call TableBeginContextMenuPopup()/.../EndPopup(). + // - You may call TableDrawDefaultContextMenu() with selected flags to display specific sections of the default menu, + // e.g. TableDrawDefaultContextMenu(table, table->Flags & ~ImGuiTableFlags_Hideable) will display everything EXCEPT columns visibility options. + if (table->DisableDefaultContextMenu == false && TableBeginContextMenuPopup(table)) { - TableDrawContextMenu(table); + TableDrawDefaultContextMenu(table, table->Flags); EndPopup(); } @@ -1167,8 +1228,6 @@ void ImGui::TableUpdateLayout(ImGuiTable* table) // Process hit-testing on resizing borders. Actual size change will be applied in EndTable() // - Set table->HoveredColumnBorder with a short delay/timer to reduce visual feedback noise. -// - Submit ahead of table contents and header, use ImGuiButtonFlags_AllowItemOverlap to prioritize -// widgets overlapping the same area. void ImGui::TableUpdateBorders(ImGuiTable* table) { ImGuiContext& g = *GImGui; @@ -1180,9 +1239,9 @@ void ImGui::TableUpdateBorders(ImGuiTable* table) // Actual columns highlight/render will be performed in EndTable() and not be affected. ImGuiTableInstanceData* table_instance = TableGetInstanceData(table, table->InstanceCurrent); const float hit_half_width = TABLE_RESIZE_SEPARATOR_HALF_THICKNESS; - const float hit_y1 = table->OuterRect.Min.y; + const float hit_y1 = (table->FreezeRowsCount >= 1 ? table->OuterRect.Min.y : table->WorkRect.Min.y) + table->AngledHeadersHeight; const float hit_y2_body = ImMax(table->OuterRect.Max.y, hit_y1 + table_instance->LastOuterHeight); - const float hit_y2_head = hit_y1 + table_instance->LastFirstRowHeight; + const float hit_y2_head = hit_y1 + table_instance->LastTopHeadersRowHeight; for (int order_n = 0; order_n < table->ColumnsCount; order_n++) { @@ -1208,12 +1267,12 @@ void ImGui::TableUpdateBorders(ImGuiTable* table) //GetForegroundDrawList()->AddRect(hit_rect.Min, hit_rect.Max, IM_COL32(255, 0, 0, 100)); bool hovered = false, held = false; - bool pressed = ButtonBehavior(hit_rect, column_id, &hovered, &held, ImGuiButtonFlags_FlattenChildren | ImGuiButtonFlags_AllowItemOverlap | ImGuiButtonFlags_PressedOnClick | ImGuiButtonFlags_PressedOnDoubleClick | ImGuiButtonFlags_NoNavFocus); + bool pressed = ButtonBehavior(hit_rect, column_id, &hovered, &held, ImGuiButtonFlags_FlattenChildren | ImGuiButtonFlags_PressedOnClick | ImGuiButtonFlags_PressedOnDoubleClick | ImGuiButtonFlags_NoNavFocus); if (pressed && IsMouseDoubleClicked(0)) { TableSetColumnWidthAutoSingle(table, column_n); ClearActiveID(); - held = hovered = false; + held = false; } if (held) { @@ -1285,7 +1344,7 @@ void ImGui::EndTable() max_pos_x = ImMax(max_pos_x, table->Columns[table->RightMostEnabledColumn].WorkMaxX + table->CellPaddingX + table->OuterPaddingX - outer_padding_for_border); if (table->ResizedColumn != -1) max_pos_x = ImMax(max_pos_x, table->ResizeLockMinContentsX2); - table->InnerWindow->DC.CursorMaxPos.x = max_pos_x; + table->InnerWindow->DC.CursorMaxPos.x = max_pos_x + table->TempData->AngledHeadersExtraWidth; } // Pop clipping rect @@ -1357,10 +1416,12 @@ void ImGui::EndTable() { ImGuiTableColumn* column = &table->Columns[table->ResizedColumn]; const float new_x2 = (g.IO.MousePos.x - g.ActiveIdClickOffset.x + TABLE_RESIZE_SEPARATOR_HALF_THICKNESS); - const float new_width = ImFloor(new_x2 - column->MinX - table->CellSpacingX1 - table->CellPaddingX * 2.0f); + const float new_width = ImTrunc(new_x2 - column->MinX - table->CellSpacingX1 - table->CellPaddingX * 2.0f); table->ResizedColumnNextWidth = new_width; } + table->IsActiveIdInTable = (g.ActiveIdIsAlive != 0 && table->IsActiveIdAliveBeforeTable == false); + // Pop from id stack IM_ASSERT_USER_ERROR(inner_window->IDStack.back() == table_instance->TableInstanceID, "Mismatching PushID/PopID!"); IM_ASSERT_USER_ERROR(outer_window->DC.ItemWidthStack.Size >= temp_data->HostBackupItemWidthStackSize, "Too many PopItemWidth!"); @@ -1401,7 +1462,7 @@ void ImGui::EndTable() } else if (temp_data->UserOuterSize.x <= 0.0f) { - const float decoration_size = (table->Flags & ImGuiTableFlags_ScrollX) ? inner_window->ScrollbarSizes.x : 0.0f; + const float decoration_size = table->TempData->AngledHeadersExtraWidth + ((table->Flags & ImGuiTableFlags_ScrollX) ? inner_window->ScrollbarSizes.x : 0.0f); outer_window->DC.IdealMaxPos.x = ImMax(outer_window->DC.IdealMaxPos.x, table->OuterRect.Min.x + table->ColumnsAutoFitWidth + decoration_size - temp_data->UserOuterSize.x); outer_window->DC.CursorMaxPos.x = ImMax(backup_outer_max_pos.x, ImMin(table->OuterRect.Max.x, table->OuterRect.Min.x + table->ColumnsAutoFitWidth)); } @@ -1440,7 +1501,7 @@ void ImGui::EndTable() NavUpdateCurrentWindowIsScrollPushableX(); } -// See "COLUMN SIZING POLICIES" comments at the top of this file +// See "COLUMNS SIZING POLICIES" comments at the top of this file // If (init_width_or_weight <= 0.0f) it is ignored void ImGui::TableSetupColumn(const char* label, ImGuiTableColumnFlags flags, float init_width_or_weight, ImGuiID user_id) { @@ -1468,6 +1529,11 @@ void ImGui::TableSetupColumn(const char* label, ImGuiTableColumnFlags flags, flo if ((flags & ImGuiTableColumnFlags_WidthMask_) == 0 && init_width_or_weight > 0.0f) if ((table->Flags & ImGuiTableFlags_SizingMask_) == ImGuiTableFlags_SizingFixedFit || (table->Flags & ImGuiTableFlags_SizingMask_) == ImGuiTableFlags_SizingFixedSame) flags |= ImGuiTableColumnFlags_WidthFixed; + if (flags & ImGuiTableColumnFlags_AngledHeader) + { + flags |= ImGuiTableColumnFlags_NoHeaderLabel; + table->AngledHeadersCount++; + } TableSetupColumnFlags(table, column, flags); column->UserID = user_id; @@ -1501,6 +1567,7 @@ void ImGui::TableSetupColumn(const char* label, ImGuiTableColumnFlags flags, flo } // Store name (append with zero-terminator in contiguous buffer) + // FIXME: If we recorded the number of \n in names we could compute header row height column->NameOffset = -1; if (label != NULL && label[0] != 0) { @@ -1549,6 +1616,7 @@ void ImGui::TableSetupScrollFreeze(int columns, int rows) // - TableGetCellBgRect() [Internal] // - TableGetColumnResizeID() [Internal] // - TableGetHoveredColumn() [Internal] +// - TableGetHoveredRow() [Internal] // - TableSetBgColor() //----------------------------------------------------------------------------- @@ -1653,6 +1721,19 @@ int ImGui::TableGetHoveredColumn() return (int)table->HoveredColumnBody; } +// Return -1 when table is not hovered. Return maxrow+1 if in table but below last submitted row. +// *IMPORTANT* Unlike TableGetHoveredColumn(), this has a one frame latency in updating the value. +// This difference with is the reason why this is not public yet. +int ImGui::TableGetHoveredRow() +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + if (!table) + return -1; + ImGuiTableInstanceData* table_instance = TableGetInstanceData(table, table->InstanceCurrent); + return (int)table_instance->HoveredRowLast; +} + void ImGui::TableSetBgColor(ImGuiTableBgTarget target, ImU32 color, int column_n) { ImGuiContext& g = *GImGui; @@ -1727,19 +1808,20 @@ void ImGui::TableNextRow(ImGuiTableRowFlags row_flags, float row_min_height) table->LastRowFlags = table->RowFlags; table->RowFlags = row_flags; + table->RowCellPaddingY = g.Style.CellPadding.y; table->RowMinHeight = row_min_height; TableBeginRow(table); // We honor min_row_height requested by user, but cannot guarantee per-row maximum height, // because that would essentially require a unique clipping rectangle per-cell. - table->RowPosY2 += table->CellPaddingY * 2.0f; + table->RowPosY2 += table->RowCellPaddingY * 2.0f; table->RowPosY2 = ImMax(table->RowPosY2, table->RowPosY1 + row_min_height); // Disable output until user calls TableNextColumn() table->InnerWindow->SkipItems = true; } -// [Internal] Called by TableNextRow() +// [Internal] Only called by TableNextRow() void ImGui::TableBeginRow(ImGuiTable* table) { ImGuiWindow* window = table->InnerWindow; @@ -1760,8 +1842,10 @@ void ImGui::TableBeginRow(ImGuiTable* table) table->RowPosY1 = table->RowPosY2 = next_y1; table->RowTextBaseline = 0.0f; table->RowIndentOffsetX = window->DC.Indent.x - table->HostIndentX; // Lock indent + window->DC.PrevLineTextBaseOffset = 0.0f; - window->DC.CurrLineSize = ImVec2(0.0f, 0.0f); + window->DC.CursorPosPrevLine = ImVec2(window->DC.CursorPos.x, window->DC.CursorPos.y + table->RowCellPaddingY); // This allows users to call SameLine() to share LineSize between columns. + window->DC.PrevLineSize = window->DC.CurrLineSize = ImVec2(0.0f, 0.0f); // This allows users to call SameLine() to share LineSize between columns, and to call it from first column too. window->DC.IsSameLine = window->DC.IsSetPos = false; window->DC.CursorMaxPos.y = next_y1; @@ -1798,12 +1882,17 @@ void ImGui::TableEndRow(ImGuiTable* table) const float bg_y2 = table->RowPosY2; const bool unfreeze_rows_actual = (table->CurrentRow + 1 == table->FreezeRowsCount); const bool unfreeze_rows_request = (table->CurrentRow + 1 == table->FreezeRowsRequest); - if (table->CurrentRow == 0) - TableGetInstanceData(table, table->InstanceCurrent)->LastFirstRowHeight = bg_y2 - bg_y1; + ImGuiTableInstanceData* table_instance = TableGetInstanceData(table, table->InstanceCurrent); + if ((table->RowFlags & ImGuiTableRowFlags_Headers) && (table->CurrentRow == 0 || (table->LastRowFlags & ImGuiTableRowFlags_Headers))) + table_instance->LastTopHeadersRowHeight += bg_y2 - bg_y1; const bool is_visible = (bg_y2 >= table->InnerClipRect.Min.y && bg_y1 <= table->InnerClipRect.Max.y); if (is_visible) { + // Update data for TableGetHoveredRow() + if (table->HoveredColumnBody != -1 && g.IO.MousePos.y >= bg_y1 && g.IO.MousePos.y < bg_y2) + table_instance->HoveredRowNext = table->CurrentRow; + // Decide of background color for the row ImU32 bg_col0 = 0; ImU32 bg_col1 = 0; @@ -1815,15 +1904,14 @@ void ImGui::TableEndRow(ImGuiTable* table) bg_col1 = table->RowBgColor[1]; // Decide of top border color - ImU32 border_col = 0; + ImU32 top_border_col = 0; const float border_size = TABLE_BORDER_SIZE; - if (table->CurrentRow > 0 || table->InnerWindow == table->OuterWindow) - if (table->Flags & ImGuiTableFlags_BordersInnerH) - border_col = (table->LastRowFlags & ImGuiTableRowFlags_Headers) ? table->BorderColorStrong : table->BorderColorLight; + if (table->CurrentRow > 0 && (table->Flags & ImGuiTableFlags_BordersInnerH)) + top_border_col = (table->LastRowFlags & ImGuiTableRowFlags_Headers) ? table->BorderColorStrong : table->BorderColorLight; const bool draw_cell_bg_color = table->RowCellDataCurrent >= 0; const bool draw_strong_bottom_border = unfreeze_rows_actual; - if ((bg_col0 | bg_col1 | border_col) != 0 || draw_strong_bottom_border || draw_cell_bg_color) + if ((bg_col0 | bg_col1 | top_border_col) != 0 || draw_strong_bottom_border || draw_cell_bg_color) { // In theory we could call SetWindowClipRectBeforeSetChannel() but since we know TableEndRow() is // always followed by a change of clipping rectangle we perform the smallest overwrite possible here. @@ -1862,8 +1950,8 @@ void ImGui::TableEndRow(ImGuiTable* table) } // Draw top border - if (border_col && bg_y1 >= table->BgClipRect.Min.y && bg_y1 < table->BgClipRect.Max.y) - window->DrawList->AddLine(ImVec2(table->BorderX1, bg_y1), ImVec2(table->BorderX2, bg_y1), border_col, border_size); + if (top_border_col && bg_y1 >= table->BgClipRect.Min.y && bg_y1 < table->BgClipRect.Max.y) + window->DrawList->AddLine(ImVec2(table->BorderX1, bg_y1), ImVec2(table->BorderX2, bg_y1), top_border_col, border_size); // Draw bottom border at the row unfreezing mark (always strong) if (draw_strong_bottom_border && bg_y2 >= table->BgClipRect.Min.y && bg_y2 < table->BgClipRect.Max.y) @@ -1881,7 +1969,7 @@ void ImGui::TableEndRow(ImGuiTable* table) IM_ASSERT(table->IsUnfrozenRows == false); const float y0 = ImMax(table->RowPosY2 + 1, window->InnerClipRect.Min.y); table->IsUnfrozenRows = true; - TableGetInstanceData(table, table->InstanceCurrent)->LastFrozenHeight = y0 - table->OuterRect.Min.y; + table_instance->LastFrozenHeight = y0 - table->OuterRect.Min.y; // BgClipRect starts as table->InnerClipRect, reduce it now and make BgClipRectForDrawCmd == BgClipRect table->BgClipRect.Min.y = table->Bg2ClipRectForDrawCmd.Min.y = ImMin(y0, window->InnerClipRect.Max.y); @@ -1991,12 +2079,14 @@ void ImGui::TableBeginCell(ImGuiTable* table, int column_n) start_x += table->RowIndentOffsetX; // ~~ += window.DC.Indent.x - table->HostIndentX, except we locked it for the row. window->DC.CursorPos.x = start_x; - window->DC.CursorPos.y = table->RowPosY1 + table->CellPaddingY; + window->DC.CursorPos.y = table->RowPosY1 + table->RowCellPaddingY; window->DC.CursorMaxPos.x = window->DC.CursorPos.x; window->DC.ColumnsOffset.x = start_x - window->Pos.x - window->DC.Indent.x; // FIXME-WORKRECT + window->DC.CursorPosPrevLine.x = window->DC.CursorPos.x; // PrevLine.y is preserved. This allows users to call SameLine() to share LineSize between columns. window->DC.CurrLineTextBaseOffset = table->RowTextBaseline; window->DC.NavLayerCurrent = (ImGuiNavLayer)column->NavLayerCurrent; + // Note how WorkRect.Max.y is only set once during layout window->WorkRect.Min.y = window->DC.CursorPos.y; window->WorkRect.Min.x = column->WorkMinX; window->WorkRect.Max.x = column->WorkMaxX; @@ -2047,7 +2137,7 @@ void ImGui::TableEndCell(ImGuiTable* table) p_max_pos_x = table->IsUnfrozenRows ? &column->ContentMaxXUnfrozen : &column->ContentMaxXFrozen; *p_max_pos_x = ImMax(*p_max_pos_x, window->DC.CursorMaxPos.x); if (column->IsEnabled) - table->RowPosY2 = ImMax(table->RowPosY2, window->DC.CursorMaxPos.y + table->CellPaddingY); + table->RowPosY2 = ImMax(table->RowPosY2, window->DC.CursorMaxPos.y + table->RowCellPaddingY); column->ItemWidth = window->DC.ItemWidth; // Propagate text baseline for the entire row @@ -2065,6 +2155,8 @@ void ImGui::TableEndCell(ImGuiTable* table) // - TableSetColumnWidthAutoAll() [Internal] // - TableUpdateColumnsWeightFromWidth() [Internal] //------------------------------------------------------------------------- +// Note that actual columns widths are computed in TableUpdateLayout(). +//------------------------------------------------------------------------- // Maximum column content width given current layout. Use column->MinX so this value on a per-column basis. float ImGui::TableGetMaxColumnWidth(const ImGuiTable* table, int column_n) @@ -2257,6 +2349,7 @@ void ImGui::TableUpdateColumnsWeightFromWidth(ImGuiTable* table) // - TablePopBackgroundChannel() [Internal] // - TableSetupDrawChannels() [Internal] // - TableMergeDrawChannels() [Internal] +// - TableGetColumnBorderCol() [Internal] // - TableDrawBorders() [Internal] //------------------------------------------------------------------------- @@ -2540,6 +2633,18 @@ void ImGui::TableMergeDrawChannels(ImGuiTable* table) } } +static ImU32 TableGetColumnBorderCol(ImGuiTable* table, int order_n, int column_n) +{ + const bool is_hovered = (table->HoveredColumnBorder == column_n); + const bool is_resized = (table->ResizedColumn == column_n) && (table->InstanceInteracted == table->InstanceCurrent); + const bool is_frozen_separator = (table->FreezeColumnsCount == order_n + 1); + if (is_resized || is_hovered) + return ImGui::GetColorU32(is_resized ? ImGuiCol_SeparatorActive : ImGuiCol_SeparatorHovered); + if (is_frozen_separator || (table->Flags & (ImGuiTableFlags_NoBordersInBody | ImGuiTableFlags_NoBordersInBodyUntilResize))) + return table->BorderColorStrong; + return table->BorderColorLight; +} + // FIXME-TABLE: This is a mess, need to redesign how we render borders (as some are also done in TableEndRow) void ImGui::TableDrawBorders(ImGuiTable* table) { @@ -2554,9 +2659,9 @@ void ImGui::TableDrawBorders(ImGuiTable* table) // Draw inner border and resizing feedback ImGuiTableInstanceData* table_instance = TableGetInstanceData(table, table->InstanceCurrent); const float border_size = TABLE_BORDER_SIZE; - const float draw_y1 = table->InnerRect.Min.y; + const float draw_y1 = ImMax(table->InnerRect.Min.y, (table->FreezeRowsCount >= 1 ? table->InnerRect.Min.y : table->WorkRect.Min.y) + table->AngledHeadersHeight) + ((table->Flags & ImGuiTableFlags_BordersOuterH) ? 1.0f : 0.0f); const float draw_y2_body = table->InnerRect.Max.y; - const float draw_y2_head = table->IsUsingHeaders ? ImMin(table->InnerRect.Max.y, (table->FreezeRowsCount >= 1 ? table->InnerRect.Min.y : table->WorkRect.Min.y) + table_instance->LastFirstRowHeight) : draw_y1; + const float draw_y2_head = table->IsUsingHeaders ? ImMin(table->InnerRect.Max.y, (table->FreezeRowsCount >= 1 ? table->InnerRect.Min.y : table->WorkRect.Min.y) + table_instance->LastTopHeadersRowHeight) : draw_y1; if (table->Flags & ImGuiTableFlags_BordersInnerV) { for (int order_n = 0; order_n < table->ColumnsCount; order_n++) @@ -2582,21 +2687,9 @@ void ImGui::TableDrawBorders(ImGuiTable* table) // Draw in outer window so right-most column won't be clipped // Always draw full height border when being resized/hovered, or on the delimitation of frozen column scrolling. - ImU32 col; - float draw_y2; - if (is_hovered || is_resized || is_frozen_separator) - { - draw_y2 = draw_y2_body; - col = is_resized ? GetColorU32(ImGuiCol_SeparatorActive) : is_hovered ? GetColorU32(ImGuiCol_SeparatorHovered) : table->BorderColorStrong; - } - else - { - draw_y2 = (table->Flags & (ImGuiTableFlags_NoBordersInBody | ImGuiTableFlags_NoBordersInBodyUntilResize)) ? draw_y2_head : draw_y2_body; - col = (table->Flags & (ImGuiTableFlags_NoBordersInBody | ImGuiTableFlags_NoBordersInBodyUntilResize)) ? table->BorderColorStrong : table->BorderColorLight; - } - + float draw_y2 = (is_hovered || is_resized || is_frozen_separator || (table->Flags & (ImGuiTableFlags_NoBordersInBody | ImGuiTableFlags_NoBordersInBodyUntilResize)) == 0) ? draw_y2_body : draw_y2_head; if (draw_y2 > draw_y1) - inner_drawlist->AddLine(ImVec2(column->MaxX, draw_y1), ImVec2(column->MaxX, draw_y2), col, border_size); + inner_drawlist->AddLine(ImVec2(column->MaxX, draw_y1), ImVec2(column->MaxX, draw_y2), TableGetColumnBorderCol(table, order_n, column_n), border_size); } } @@ -2613,7 +2706,7 @@ void ImGui::TableDrawBorders(ImGuiTable* table) const ImU32 outer_col = table->BorderColorStrong; if ((table->Flags & ImGuiTableFlags_BordersOuter) == ImGuiTableFlags_BordersOuter) { - inner_drawlist->AddRect(outer_border.Min, outer_border.Max, outer_col, 0.0f, 0, border_size); + inner_drawlist->AddRect(outer_border.Min, outer_border.Max + ImVec2(1, 1), outer_col, 0.0f, 0, border_size); } else if (table->Flags & ImGuiTableFlags_BordersOuterV) { @@ -2628,7 +2721,7 @@ void ImGui::TableDrawBorders(ImGuiTable* table) } if ((table->Flags & ImGuiTableFlags_BordersInnerH) && table->RowPosY2 < table->OuterRect.Max.y) { - // Draw bottom-most row border + // Draw bottom-most row border between it is above outer border. const float border_y = table->RowPosY2; if (border_y >= table->BgClipRect.Min.y && border_y < table->BgClipRect.Max.y) inner_drawlist->AddLine(ImVec2(table->BorderX1, border_y), ImVec2(table->BorderX2, border_y), table->BorderColorLight, border_size); @@ -2649,8 +2742,9 @@ void ImGui::TableDrawBorders(ImGuiTable* table) //------------------------------------------------------------------------- // Return NULL if no sort specs (most often when ImGuiTableFlags_Sortable is not set) -// You can sort your data again when 'SpecsChanged == true'. It will be true with sorting specs have changed since -// last call, or the first time. +// When 'sort_specs->SpecsDirty == true' you should sort your data. It will be true when sorting specs have +// changed since last call, or the first time. Make sure to set 'SpecsDirty = false' after sorting, +// else you may wastefully sort your data every frame! // Lifetime: don't hold on this pointer over multiple frames or past any subsequent call to BeginTable()! ImGuiTableSortSpecs* ImGui::TableGetSortSpecs() { @@ -2836,8 +2930,11 @@ void ImGui::TableSortSpecsBuild(ImGuiTable* table) // [SECTION] Tables: Headers //------------------------------------------------------------------------- // - TableGetHeaderRowHeight() [Internal] +// - TableGetHeaderAngledMaxLabelWidth() [Internal] // - TableHeadersRow() // - TableHeader() +// - TableAngledHeadersRow() +// - TableAngledHeadersRowEx() [Internal] //------------------------------------------------------------------------- float ImGui::TableGetHeaderRowHeight() @@ -2846,16 +2943,26 @@ float ImGui::TableGetHeaderRowHeight() // Calculate row height, for the unlikely case that some labels may be taller than others. // If we didn't do that, uneven header height would highlight but smaller one before the tallest wouldn't catch input for all height. // In your custom header row you may omit this all together and just call TableNextRow() without a height... - float row_height = GetTextLineHeight(); - int columns_count = TableGetColumnCount(); - for (int column_n = 0; column_n < columns_count; column_n++) - { - ImGuiTableColumnFlags flags = TableGetColumnFlags(column_n); - if ((flags & ImGuiTableColumnFlags_IsEnabled) && !(flags & ImGuiTableColumnFlags_NoHeaderLabel)) - row_height = ImMax(row_height, CalcTextSize(TableGetColumnName(column_n)).y); - } - row_height += GetStyle().CellPadding.y * 2.0f; - return row_height; + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + float row_height = g.FontSize; + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + if (IM_BITARRAY_TESTBIT(table->EnabledMaskByIndex, column_n)) + if ((table->Columns[column_n].Flags & ImGuiTableColumnFlags_NoHeaderLabel) == 0) + row_height = ImMax(row_height, CalcTextSize(TableGetColumnName(table, column_n)).y); + return row_height + g.Style.CellPadding.y * 2.0f; +} + +float ImGui::TableGetHeaderAngledMaxLabelWidth() +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + float width = 0.0f; + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + if (IM_BITARRAY_TESTBIT(table->EnabledMaskByIndex, column_n)) + if (table->Columns[column_n].Flags & ImGuiTableColumnFlags_AngledHeader) + width = ImMax(width, CalcTextSize(TableGetColumnName(table, column_n), NULL, true).x); + return width + g.Style.CellPadding.y * 2.0f; // Swap padding } // [Public] This is a helper to output TableHeader() calls based on the column names declared in TableSetupColumn(). @@ -2875,9 +2982,9 @@ void ImGui::TableHeadersRow() TableUpdateLayout(table); // Open row - const float row_y1 = GetCursorScreenPos().y; const float row_height = TableGetHeaderRowHeight(); TableNextRow(ImGuiTableRowFlags_Headers, row_height); + const float row_y1 = GetCursorScreenPos().y; if (table->HostSkipItems) // Merely an optimization, you may skip in your own code. return; @@ -2899,7 +3006,7 @@ void ImGui::TableHeadersRow() ImVec2 mouse_pos = ImGui::GetMousePos(); if (IsMouseReleased(1) && TableGetHoveredColumn() == columns_count) if (mouse_pos.y >= row_y1 && mouse_pos.y < row_y1 + row_height) - TableOpenContextMenu(-1); // Will open a non-column-specific popup. + TableOpenContextMenu(columns_count); // Will open a non-column-specific popup. } // Emit a column header (text + optional sort order) @@ -2928,16 +3035,19 @@ void ImGui::TableHeader(const char* label) // If we already got a row height, there's use that. // FIXME-TABLE: Padding problem if the correct outer-padding CellBgRect strays off our ClipRect? ImRect cell_r = TableGetCellBgRect(table, column_n); - float label_height = ImMax(label_size.y, table->RowMinHeight - table->CellPaddingY * 2.0f); + float label_height = ImMax(label_size.y, table->RowMinHeight - table->RowCellPaddingY * 2.0f); // Calculate ideal size for sort order arrow float w_arrow = 0.0f; float w_sort_text = 0.0f; + bool sort_arrow = false; char sort_order_suf[4] = ""; const float ARROW_SCALE = 0.65f; if ((table->Flags & ImGuiTableFlags_Sortable) && !(column->Flags & ImGuiTableColumnFlags_NoSort)) { - w_arrow = ImFloor(g.FontSize * ARROW_SCALE + g.Style.FramePadding.x); + w_arrow = ImTrunc(g.FontSize * ARROW_SCALE + g.Style.FramePadding.x); + if (column->SortOrder != -1) + sort_arrow = true; if (column->SortOrder > 0) { ImFormatString(sort_order_suf, IM_ARRAYSIZE(sort_order_suf), "%d", column->SortOrder + 1); @@ -2945,13 +3055,12 @@ void ImGui::TableHeader(const char* label) } } - // We feed our unclipped width to the column without writing on CursorMaxPos, so that column is still considering for merging. + // We feed our unclipped width to the column without writing on CursorMaxPos, so that column is still considered for merging. float max_pos_x = label_pos.x + label_size.x + w_sort_text + w_arrow; - column->ContentMaxXHeadersUsed = ImMax(column->ContentMaxXHeadersUsed, column->WorkMaxX); + column->ContentMaxXHeadersUsed = ImMax(column->ContentMaxXHeadersUsed, sort_arrow ? cell_r.Max.x : ImMin(max_pos_x, cell_r.Max.x)); column->ContentMaxXHeadersIdeal = ImMax(column->ContentMaxXHeadersIdeal, max_pos_x); // Keep header highlighted when context menu is open. - const bool selected = (table->IsContextPopupOpen && table->ContextPopupColumn == column_n && table->InstanceInteracted == table->InstanceCurrent); ImGuiID id = window->GetID(label); ImRect bb(cell_r.Min.x, cell_r.Min.y, cell_r.Max.x, ImMax(cell_r.Max.y, cell_r.Min.y + label_height + g.Style.CellPadding.y * 2.0f)); ItemSize(ImVec2(0.0f, label_height)); // Don't declare unclipped width, it'll be fed ContentMaxPosHeadersIdeal @@ -2961,12 +3070,11 @@ void ImGui::TableHeader(const char* label) //GetForegroundDrawList()->AddRect(cell_r.Min, cell_r.Max, IM_COL32(255, 0, 0, 255)); // [DEBUG] //GetForegroundDrawList()->AddRect(bb.Min, bb.Max, IM_COL32(255, 0, 0, 255)); // [DEBUG] - // Using AllowItemOverlap mode because we cover the whole cell, and we want user to be able to submit subsequent items. + // Using AllowOverlap mode because we cover the whole cell, and we want user to be able to submit subsequent items. + const bool highlight = (table->HighlightColumnHeader == column_n); bool hovered, held; - bool pressed = ButtonBehavior(bb, id, &hovered, &held, ImGuiButtonFlags_AllowItemOverlap); - if (g.ActiveId != id) - SetItemAllowOverlap(); - if (held || hovered || selected) + bool pressed = ButtonBehavior(bb, id, &hovered, &held, ImGuiButtonFlags_AllowOverlap); + if (held || hovered || highlight) { const ImU32 col = GetColorU32(held ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header); //RenderFrame(bb.Min, bb.Max, col, false, 0.0f); @@ -2978,7 +3086,7 @@ void ImGui::TableHeader(const char* label) if ((table->RowFlags & ImGuiTableRowFlags_Headers) == 0) TableSetBgColor(ImGuiTableBgTarget_CellBg, GetColorU32(ImGuiCol_TableHeaderBg), table->CurrentColumn); } - RenderNavHighlight(bb, id, ImGuiNavHighlightFlags_TypeThin | ImGuiNavHighlightFlags_NoRounding); + RenderNavHighlight(bb, id, ImGuiNavHighlightFlags_Compact | ImGuiNavHighlightFlags_NoRounding); if (held) table->HeldHeaderColumn = (ImGuiTableColumnIdx)column_n; window->DC.CursorPos.y -= g.Style.ItemSpacing.y * 0.5f; @@ -3036,19 +3144,156 @@ void ImGui::TableHeader(const char* label) RenderTextEllipsis(window->DrawList, label_pos, ImVec2(ellipsis_max, label_pos.y + label_height + g.Style.FramePadding.y), ellipsis_max, ellipsis_max, label, label_end, &label_size); const bool text_clipped = label_size.x > (ellipsis_max - label_pos.x); - if (text_clipped && hovered && g.ActiveId == 0 && IsItemHovered(ImGuiHoveredFlags_DelayNormal)) - SetTooltip("%.*s", (int)(label_end - label), label); + if (text_clipped && hovered && g.ActiveId == 0) + SetItemTooltip("%.*s", (int)(label_end - label), label); // We don't use BeginPopupContextItem() because we want the popup to stay up even after the column is hidden if (IsMouseReleased(1) && IsItemHovered()) TableOpenContextMenu(column_n); } +// Unlike TableHeadersRow() it is not expected that you can reimplement or customize this with custom widgets. +// FIXME: highlight without ImGuiTableFlags_HighlightHoveredColumn +// FIXME: No hit-testing/button on the angled header. +void ImGui::TableAngledHeadersRow() +{ + ImGuiContext& g = *GImGui; + TableAngledHeadersRowEx(g.Style.TableAngledHeadersAngle, 0.0f); +} + +void ImGui::TableAngledHeadersRowEx(float angle, float max_label_width) +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + ImGuiWindow* window = g.CurrentWindow; + ImDrawList* draw_list = window->DrawList; + IM_ASSERT(table != NULL && "Need to call TableHeadersRow() after BeginTable()!"); + IM_ASSERT(table->CurrentRow == -1 && "Must be first row"); + + if (max_label_width == 0.0f) + max_label_width = TableGetHeaderAngledMaxLabelWidth(); + + // Angle argument expressed in (-IM_PI/2 .. +IM_PI/2) as it is easier to think about for user. + const bool flip_label = (angle < 0.0f); + angle -= IM_PI * 0.5f; + const float cos_a = ImCos(angle); + const float sin_a = ImSin(angle); + const float label_cos_a = flip_label ? ImCos(angle + IM_PI) : cos_a; + const float label_sin_a = flip_label ? ImSin(angle + IM_PI) : sin_a; + const ImVec2 unit_right = ImVec2(cos_a, sin_a); + + // Calculate our base metrics and set angled headers data _before_ the first call to TableNextRow() + // FIXME-STYLE: Would it be better for user to submit 'max_label_width' or 'row_height' ? One can be derived from the other. + const float header_height = g.FontSize + g.Style.CellPadding.x * 2.0f; + const float row_height = ImFabs(ImRotate(ImVec2(max_label_width, flip_label ? +header_height : -header_height), cos_a, sin_a).y); + table->AngledHeadersHeight = row_height; + table->AngledHeadersSlope = (sin_a != 0.0f) ? (cos_a / sin_a) : 0.0f; + const ImVec2 header_angled_vector = unit_right * (row_height / -sin_a); // vector from bottom-left to top-left, and from bottom-right to top-right + + // Declare row, override and draw our own background + TableNextRow(ImGuiTableRowFlags_Headers, row_height); + TableNextColumn(); + const ImRect row_r(table->WorkRect.Min.x, table->BgClipRect.Min.y, table->WorkRect.Max.x, table->RowPosY2); + table->DrawSplitter->SetCurrentChannel(draw_list, TABLE_DRAW_CHANNEL_BG0); + float clip_rect_min_x = table->BgClipRect.Min.x; + if (table->FreezeColumnsCount > 0) + clip_rect_min_x = ImMax(clip_rect_min_x, table->Columns[table->FreezeColumnsCount - 1].MaxX); + TableSetBgColor(ImGuiTableBgTarget_RowBg0, 0); // Cancel + PushClipRect(table->BgClipRect.Min, table->BgClipRect.Max, false); // Span all columns + draw_list->AddRectFilled(ImVec2(table->BgClipRect.Min.x, row_r.Min.y), ImVec2(table->BgClipRect.Max.x, row_r.Max.y), GetColorU32(ImGuiCol_TableHeaderBg, 0.25f)); // FIXME-STYLE: Change row background with an arbitrary color. + PushClipRect(ImVec2(clip_rect_min_x, table->BgClipRect.Min.y), table->BgClipRect.Max, true); // Span all columns + + const ImGuiID row_id = GetID("##AngledHeaders"); + ButtonBehavior(row_r, row_id, NULL, NULL); + KeepAliveID(row_id); + + ImGuiTableInstanceData* table_instance = TableGetInstanceData(table, table->InstanceCurrent); + int highlight_column_n = table->HighlightColumnHeader; + if (highlight_column_n == -1 && table->HoveredColumnBody != -1) + if (table_instance->HoveredRowLast == 0 && table->HoveredColumnBorder == -1 && (g.ActiveId == 0 || g.ActiveId == row_id || (table->IsActiveIdInTable || g.DragDropActive))) + highlight_column_n = table->HoveredColumnBody; + + // Draw background and labels in first pass, then all borders. + float max_x = 0.0f; + ImVec2 padding = g.Style.CellPadding; // We will always use swapped component + for (int pass = 0; pass < 2; pass++) + for (int order_n = 0; order_n < table->ColumnsCount; order_n++) + { + if (!IM_BITARRAY_TESTBIT(table->EnabledMaskByDisplayOrder, order_n)) + continue; + const int column_n = table->DisplayOrderToIndex[order_n]; + ImGuiTableColumn* column = &table->Columns[column_n]; + if ((column->Flags & ImGuiTableColumnFlags_AngledHeader) == 0) // Note: can't rely on ImGuiTableColumnFlags_IsVisible test here. + continue; + + ImVec2 bg_shape[4]; + bg_shape[0] = ImVec2(column->MaxX, row_r.Max.y); + bg_shape[1] = ImVec2(column->MinX, row_r.Max.y); + bg_shape[2] = bg_shape[1] + header_angled_vector; + bg_shape[3] = bg_shape[0] + header_angled_vector; + if (pass == 0) + { + // Draw shape + draw_list->AddQuadFilled(bg_shape[0], bg_shape[1], bg_shape[2], bg_shape[3], GetColorU32(ImGuiCol_TableHeaderBg)); + if (column_n == highlight_column_n) + draw_list->AddQuadFilled(bg_shape[0], bg_shape[1], bg_shape[2], bg_shape[3], GetColorU32(ImGuiCol_Header)); // Highlight on hover + max_x = ImMax(max_x, bg_shape[3].x); + + // Draw label + // - First draw at an offset where RenderTextXXX() function won't meddle with applying current ClipRect, then transform to final offset. + // - Handle multiple lines manually, as we want each lines to follow on the horizontal border, rather than see a whole block rotated. + const char* label_name = TableGetColumnName(table, column_n); + const char* label_name_end = FindRenderedTextEnd(label_name); + const float line_off_step_x = g.FontSize / -sin_a; + float line_off_curr_x = 0.0f; + while (label_name < label_name_end) + { + const char* label_name_eol = strchr(label_name, '\n'); + if (label_name_eol == NULL) + label_name_eol = label_name_end; + + // FIXME: Individual line clipping for right-most column is broken for negative angles. + ImVec2 label_size = CalcTextSize(label_name, label_name_eol); + float clip_width = max_label_width - padding.y; // Using padding.y*2.0f would be symetrical but hide more text. + float clip_height = ImMin(label_size.y, column->ClipRect.Max.x - column->WorkMinX - line_off_curr_x); + ImRect clip_r(window->ClipRect.Min, window->ClipRect.Min + ImVec2(clip_width, clip_height)); + int vtx_idx_begin = draw_list->_VtxCurrentIdx; + RenderTextEllipsis(draw_list, clip_r.Min, clip_r.Max, clip_r.Max.x, clip_r.Max.x, label_name, label_name_eol, &label_size); + int vtx_idx_end = draw_list->_VtxCurrentIdx; + + // Rotate and offset label + ImVec2 pivot_in = ImVec2(window->ClipRect.Min.x, window->ClipRect.Min.y + label_size.y); + ImVec2 pivot_out = ImVec2(column->WorkMinX, row_r.Max.y); + line_off_curr_x += line_off_step_x; + pivot_out += unit_right * padding.y; + if (flip_label) + pivot_out += unit_right * (clip_width - ImMax(0.0f, clip_width - label_size.x)); + pivot_out.x += flip_label ? line_off_curr_x - line_off_step_x : line_off_curr_x; + ShadeVertsTransformPos(draw_list, vtx_idx_begin, vtx_idx_end, pivot_in, label_cos_a, label_sin_a, pivot_out); // Rotate and offset + //if (g.IO.KeyShift) { ImDrawList* fg_dl = GetForegroundDrawList(); vtx_idx_begin = fg_dl->_VtxCurrentIdx; fg_dl->AddRect(clip_r.Min, clip_r.Max, IM_COL32(0, 255, 0, 255), 0.0f, 0, 2.0f); ShadeVertsTransformPos(fg_dl, vtx_idx_begin, fg_dl->_VtxCurrentIdx, pivot_in, label_cos_a, label_sin_a, pivot_out); } + + // Register header width + column->ContentMaxXHeadersUsed = column->ContentMaxXHeadersIdeal = column->WorkMinX + ImCeil(line_off_curr_x); + label_name = label_name_eol + 1; + } + } + if (pass == 1) + { + // Draw border + draw_list->AddLine(bg_shape[0], bg_shape[3], TableGetColumnBorderCol(table, order_n, column_n)); + } + } + PopClipRect(); + PopClipRect(); + table->TempData->AngledHeadersExtraWidth = ImMax(0.0f, max_x - table->Columns[table->RightMostEnabledColumn].MaxX); +} + //------------------------------------------------------------------------- // [SECTION] Tables: Context Menu //------------------------------------------------------------------------- // - TableOpenContextMenu() [Internal] -// - TableDrawContextMenu() [Internal] +// - TableBeginContextMenuPopup() [Internal] +// - TableDrawDefaultContextMenu() [Internal] //------------------------------------------------------------------------- // Use -1 to open menu not specific to a given column. @@ -3084,7 +3329,13 @@ bool ImGui::TableBeginContextMenuPopup(ImGuiTable* table) // Output context menu into current window (generally a popup) // FIXME-TABLE: Ideally this should be writable by the user. Full programmatic access to that data? -void ImGui::TableDrawContextMenu(ImGuiTable* table) +// Sections to display are pulled from 'flags_for_section_to_display', which is typically == table->Flags. +// - ImGuiTableFlags_Resizable -> display Sizing menu items +// - ImGuiTableFlags_Reorderable -> display "Reset Order" +////- ImGuiTableFlags_Sortable -> display sorting options (disabled) +// - ImGuiTableFlags_Hideable -> display columns visibility menu items +// It means if you have a custom context menus you can call this section and omit some sections, and add your own. +void ImGui::TableDrawDefaultContextMenu(ImGuiTable* table, ImGuiTableFlags flags_for_section_to_display) { ImGuiContext& g = *GImGui; ImGuiWindow* window = g.CurrentWindow; @@ -3096,7 +3347,7 @@ void ImGui::TableDrawContextMenu(ImGuiTable* table) ImGuiTableColumn* column = (column_n != -1) ? &table->Columns[column_n] : NULL; // Sizing - if (table->Flags & ImGuiTableFlags_Resizable) + if (flags_for_section_to_display & ImGuiTableFlags_Resizable) { if (column != NULL) { @@ -3116,7 +3367,7 @@ void ImGui::TableDrawContextMenu(ImGuiTable* table) } // Ordering - if (table->Flags & ImGuiTableFlags_Reorderable) + if (flags_for_section_to_display & ImGuiTableFlags_Reorderable) { if (MenuItem(LocalizeGetMsg(ImGuiLocKey_TableResetOrder), NULL, false, !table->IsDefaultDisplayOrder)) table->IsResetDisplayOrderRequest = true; @@ -3130,7 +3381,7 @@ void ImGui::TableDrawContextMenu(ImGuiTable* table) // Sorting // (modify TableOpenContextMenu() to add _Sortable flag if enabling this) #if 0 - if ((table->Flags & ImGuiTableFlags_Sortable) && column != NULL && (column->Flags & ImGuiTableColumnFlags_NoSort) == 0) + if ((flags_for_section_to_display & ImGuiTableFlags_Sortable) && column != NULL && (column->Flags & ImGuiTableColumnFlags_NoSort) == 0) { if (want_separator) Separator(); @@ -3145,7 +3396,7 @@ void ImGui::TableDrawContextMenu(ImGuiTable* table) #endif // Hiding / Visibility - if (table->Flags & ImGuiTableFlags_Hideable) + if (flags_for_section_to_display & ImGuiTableFlags_Hideable) { if (want_separator) Separator(); @@ -3581,7 +3832,8 @@ static const char* DebugNodeTableGetSizingPolicyDesc(ImGuiTableFlags sizing_poli void ImGui::DebugNodeTable(ImGuiTable* table) { - const bool is_active = (table->LastFrameActive >= GetFrameCount() - 2); // Note that fully clipped early out scrolling tables will appear as inactive here. + ImGuiContext& g = *GImGui; + const bool is_active = (table->LastFrameActive >= g.FrameCount - 2); // Note that fully clipped early out scrolling tables will appear as inactive here. if (!is_active) { PushStyleColor(ImGuiCol_Text, GetStyleColorVec4(ImGuiCol_TextDisabled)); } bool open = TreeNode(table, "Table 0x%08X (%d columns, in '%s')%s", table->ID, table->ColumnsCount, table->OuterWindow->Name, is_active ? "" : " *Inactive*"); if (!is_active) { PopStyleColor(); } @@ -3593,12 +3845,24 @@ void ImGui::DebugNodeTable(ImGuiTable* table) return; if (table->InstanceCurrent > 0) Text("** %d instances of same table! Some data below will refer to last instance.", table->InstanceCurrent + 1); + if (g.IO.ConfigDebugIsDebuggerPresent) + { + if (DebugBreakButton("**DebugBreak**", "in BeginTable()")) + g.DebugBreakInTable = table->ID; + SameLine(); + } + bool clear_settings = SmallButton("Clear settings"); BulletText("OuterRect: Pos: (%.1f,%.1f) Size: (%.1f,%.1f) Sizing: '%s'", table->OuterRect.Min.x, table->OuterRect.Min.y, table->OuterRect.GetWidth(), table->OuterRect.GetHeight(), DebugNodeTableGetSizingPolicyDesc(table->Flags)); BulletText("ColumnsGivenWidth: %.1f, ColumnsAutoFitWidth: %.1f, InnerWidth: %.1f%s", table->ColumnsGivenWidth, table->ColumnsAutoFitWidth, table->InnerWidth, table->InnerWidth == 0.0f ? " (auto)" : ""); BulletText("CellPaddingX: %.1f, CellSpacingX: %.1f/%.1f, OuterPaddingX: %.1f", table->CellPaddingX, table->CellSpacingX1, table->CellSpacingX2, table->OuterPaddingX); BulletText("HoveredColumnBody: %d, HoveredColumnBorder: %d", table->HoveredColumnBody, table->HoveredColumnBorder); BulletText("ResizedColumn: %d, ReorderColumn: %d, HeldHeaderColumn: %d", table->ResizedColumn, table->ReorderColumn, table->HeldHeaderColumn); + for (int n = 0; n < table->InstanceCurrent + 1; n++) + { + ImGuiTableInstanceData* table_instance = TableGetInstanceData(table, n); + BulletText("Instance %d: HoveredRow: %d, LastOuterHeight: %.2f", n, table_instance->HoveredRowLast, table_instance->LastOuterHeight); + } //BulletText("BgDrawChannels: %d/%d", 0, table->BgDrawChannelUnfrozen); float sum_weights = 0.0f; for (int n = 0; n < table->ColumnsCount; n++) @@ -3908,7 +4172,7 @@ void ImGui::BeginColumns(const char* str_id, int columns_count, ImGuiOldColumnFl // Set state for first column // We aim so that the right-most column will have the same clipping width as other after being clipped by parent ClipRect const float column_padding = g.Style.ItemSpacing.x; - const float half_clip_extend_x = ImFloor(ImMax(window->WindowPadding.x * 0.5f, window->WindowBorderSize)); + const float half_clip_extend_x = ImTrunc(ImMax(window->WindowPadding.x * 0.5f, window->WindowBorderSize)); const float max_1 = window->WorkRect.Max.x + column_padding - ImMax(column_padding - window->WindowPadding.x, 0.0f); const float max_2 = window->WorkRect.Max.x + half_clip_extend_x; columns->OffMinX = window->DC.Indent.x - column_padding + ImMax(column_padding - window->WindowPadding.x, 0.0f); @@ -3955,8 +4219,9 @@ void ImGui::BeginColumns(const char* str_id, int columns_count, ImGuiOldColumnFl float width = offset_1 - offset_0; PushItemWidth(width * 0.65f); window->DC.ColumnsOffset.x = ImMax(column_padding - window->WindowPadding.x, 0.0f); - window->DC.CursorPos.x = IM_FLOOR(window->Pos.x + window->DC.Indent.x + window->DC.ColumnsOffset.x); + window->DC.CursorPos.x = IM_TRUNC(window->Pos.x + window->DC.Indent.x + window->DC.ColumnsOffset.x); window->WorkRect.Max.x = window->Pos.x + offset_1 - column_padding; + window->WorkRect.Max.y = window->ContentRegionRect.Max.y; } void ImGui::NextColumn() @@ -3970,7 +4235,7 @@ void ImGui::NextColumn() if (columns->Count == 1) { - window->DC.CursorPos.x = IM_FLOOR(window->Pos.x + window->DC.Indent.x + window->DC.ColumnsOffset.x); + window->DC.CursorPos.x = IM_TRUNC(window->Pos.x + window->DC.Indent.x + window->DC.ColumnsOffset.x); IM_ASSERT(columns->Current == 0); return; } @@ -4002,7 +4267,7 @@ void ImGui::NextColumn() window->DC.IsSameLine = false; columns->LineMinY = columns->LineMaxY; } - window->DC.CursorPos.x = IM_FLOOR(window->Pos.x + window->DC.Indent.x + window->DC.ColumnsOffset.x); + window->DC.CursorPos.x = IM_TRUNC(window->Pos.x + window->DC.Indent.x + window->DC.ColumnsOffset.x); window->DC.CursorPos.y = columns->LineMinY; window->DC.CurrLineSize = ImVec2(0.0f, 0.0f); window->DC.CurrLineTextBaseOffset = 0.0f; @@ -4066,7 +4331,7 @@ void ImGui::EndColumns() // Draw column const ImU32 col = GetColorU32(held ? ImGuiCol_SeparatorActive : hovered ? ImGuiCol_SeparatorHovered : ImGuiCol_Separator); - const float xi = IM_FLOOR(x); + const float xi = IM_TRUNC(x); window->DrawList->AddLine(ImVec2(xi, y1 + 1.0f), ImVec2(xi, y2), col); } @@ -4087,7 +4352,7 @@ void ImGui::EndColumns() window->ParentWorkRect = columns->HostBackupParentWorkRect; window->DC.CurrentColumns = NULL; window->DC.ColumnsOffset.x = 0.0f; - window->DC.CursorPos.x = IM_FLOOR(window->Pos.x + window->DC.Indent.x + window->DC.ColumnsOffset.x); + window->DC.CursorPos.x = IM_TRUNC(window->Pos.x + window->DC.Indent.x + window->DC.ColumnsOffset.x); NavUpdateCurrentWindowIsScrollPushableX(); } diff --git a/libs/imgui/imgui_widgets.cpp b/libs/imgui/imgui_widgets.cpp index 83f2711..1ddf4c7 100644 --- a/libs/imgui/imgui_widgets.cpp +++ b/libs/imgui/imgui_widgets.cpp @@ -1,4 +1,4 @@ -// dear imgui, v1.89.6 +// dear imgui, v1.90.4 // (widgets code) /* @@ -18,6 +18,8 @@ Index of this file: // [SECTION] Widgets: ColorEdit, ColorPicker, ColorButton, etc. // [SECTION] Widgets: TreeNode, CollapsingHeader, etc. // [SECTION] Widgets: Selectable +// [SECTION] Widgets: Typing-Select support +// [SECTION] Widgets: Multi-Select support // [SECTION] Widgets: ListBox // [SECTION] Widgets: PlotLines, PlotHistogram // [SECTION] Widgets: Value helpers @@ -41,11 +43,7 @@ Index of this file: #include "imgui_internal.h" // System includes -#if defined(_MSC_VER) && _MSC_VER <= 1500 // MSVC 2008 or earlier -#include // intptr_t -#else #include // intptr_t -#endif //------------------------------------------------------------------------- // Warnings @@ -124,7 +122,7 @@ static const ImU64 IM_U64_MAX = (2ULL * 9223372036854775807LL + 1); //------------------------------------------------------------------------- // For InputTextEx() -static bool InputTextFilterCharacter(unsigned int* p_char, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data, ImGuiInputSource input_source); +static bool InputTextFilterCharacter(ImGuiContext* ctx, unsigned int* p_char, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data, ImGuiInputSource input_source); static int InputTextCalcTextLenAndLineCount(const char* text_begin, const char** out_text_end); static ImVec2 InputTextCalcTextSizeW(ImGuiContext* ctx, const ImWchar* text_begin, const ImWchar* text_end, const ImWchar** remaining = NULL, ImVec2* out_offset = NULL, bool stop_on_new_line = false); @@ -479,6 +477,9 @@ void ImGui::BulletTextV(const char* fmt, va_list args) // Frame N + RepeatDelay + RepeatRate*N true true - true //------------------------------------------------------------------------------------------------------------------------------------------------- +// FIXME: For refactor we could output flags, incl mouse hovered vs nav keyboard vs nav triggered etc. +// And better standardize how widgets use 'GetColor32((held && hovered) ? ... : hovered ? ...)' vs 'GetColor32(held ? ... : hovered ? ...);' +// For mouse feedback we typically prefer the 'held && hovered' test, but for nav feedback not always. Outputting hovered=true on Activation may be misleading. bool ImGui::ButtonBehavior(const ImRect& bb, ImGuiID id, bool* out_hovered, bool* out_held, ImGuiButtonFlags flags) { ImGuiContext& g = *GImGui; @@ -492,8 +493,16 @@ bool ImGui::ButtonBehavior(const ImRect& bb, ImGuiID id, bool* out_hovered, bool if ((flags & ImGuiButtonFlags_PressedOnMask_) == 0) flags |= ImGuiButtonFlags_PressedOnDefault_; + // Default behavior inherited from item flags + // Note that _both_ ButtonFlags and ItemFlags are valid sources, so copy one into the item_flags and only check that. + ImGuiItemFlags item_flags = (g.LastItemData.ID == id ? g.LastItemData.InFlags : g.CurrentItemFlags); + if (flags & ImGuiButtonFlags_AllowOverlap) + item_flags |= ImGuiItemFlags_AllowOverlap; + if (flags & ImGuiButtonFlags_Repeat) + item_flags |= ImGuiItemFlags_ButtonRepeat; + ImGuiWindow* backup_hovered_window = g.HoveredWindow; - const bool flatten_hovered_children = (flags & ImGuiButtonFlags_FlattenChildren) && g.HoveredWindow && g.HoveredWindow->RootWindow == window; + const bool flatten_hovered_children = (flags & ImGuiButtonFlags_FlattenChildren) && g.HoveredWindow && g.HoveredWindow->RootWindowDockTree == window->RootWindowDockTree; if (flatten_hovered_children) g.HoveredWindow = window; @@ -504,11 +513,7 @@ bool ImGui::ButtonBehavior(const ImRect& bb, ImGuiID id, bool* out_hovered, bool #endif bool pressed = false; - bool hovered = ItemHoverable(bb, id); - - // Drag source doesn't report as hovered - if (hovered && g.DragDropActive && g.DragDropPayload.SourceId == id && !(g.DragDropSourceFlags & ImGuiDragDropFlags_SourceNoDisableHover)) - hovered = false; + bool hovered = ItemHoverable(bb, id, item_flags); // Special mode for Drag and Drop where holding button pressed for a long time while dragging another item triggers the button if (g.DragDropActive && (flags & ImGuiButtonFlags_PressedOnDragDropHold) && !(g.DragDropSourceFlags & ImGuiDragDropFlags_SourceNoHoldToOpenOthers)) @@ -527,10 +532,6 @@ bool ImGui::ButtonBehavior(const ImRect& bb, ImGuiID id, bool* out_hovered, bool if (flatten_hovered_children) g.HoveredWindow = backup_hovered_window; - // AllowOverlap mode (rarely used) requires previous frame HoveredId to be null or to match. This allows using patterns where a later submitted widget overlaps a previous one. - if (hovered && (flags & ImGuiButtonFlags_AllowItemOverlap) && (g.HoveredIdPreviousFrame != id && g.HoveredIdPreviousFrame != 0)) - hovered = false; - // Mouse handling const ImGuiID test_owner_id = (flags & ImGuiButtonFlags_NoTestKeyOwner) ? ImGuiKeyOwner_Any : id; if (hovered) @@ -579,7 +580,7 @@ bool ImGui::ButtonBehavior(const ImRect& bb, ImGuiID id, bool* out_hovered, bool { if (mouse_button_released != -1) { - const bool has_repeated_at_least_once = (flags & ImGuiButtonFlags_Repeat) && g.IO.MouseDownDurationPrev[mouse_button_released] >= g.IO.KeyRepeatDelay; // Repeat mode trumps on release behavior + const bool has_repeated_at_least_once = (item_flags & ImGuiItemFlags_ButtonRepeat) && g.IO.MouseDownDurationPrev[mouse_button_released] >= g.IO.KeyRepeatDelay; // Repeat mode trumps on release behavior if (!has_repeated_at_least_once) pressed = true; if (!(flags & ImGuiButtonFlags_NoNavFocus)) @@ -590,7 +591,7 @@ bool ImGui::ButtonBehavior(const ImRect& bb, ImGuiID id, bool* out_hovered, bool // 'Repeat' mode acts when held regardless of _PressedOn flags (see table above). // Relies on repeat logic of IsMouseClicked() but we may as well do it ourselves if we end up exposing finer RepeatDelay/RepeatRate settings. - if (g.ActiveId == id && (flags & ImGuiButtonFlags_Repeat)) + if (g.ActiveId == id && (item_flags & ImGuiItemFlags_ButtonRepeat)) if (g.IO.MouseDownDuration[g.ActiveIdMouseButton] > 0.0f && IsMouseClicked(g.ActiveIdMouseButton, test_owner_id, ImGuiInputFlags_Repeat)) pressed = true; } @@ -599,16 +600,16 @@ bool ImGui::ButtonBehavior(const ImRect& bb, ImGuiID id, bool* out_hovered, bool g.NavDisableHighlight = true; } - // Gamepad/Keyboard navigation - // We report navigated item as hovered but we don't set g.HoveredId to not interfere with mouse. - if (g.NavId == id && !g.NavDisableHighlight && g.NavDisableMouseHover && (g.ActiveId == 0 || g.ActiveId == id || g.ActiveId == window->MoveId)) + // Gamepad/Keyboard handling + // We report navigated and navigation-activated items as hovered but we don't set g.HoveredId to not interfere with mouse. + if (g.NavId == id && !g.NavDisableHighlight && g.NavDisableMouseHover) if (!(flags & ImGuiButtonFlags_NoHoveredOnFocus)) hovered = true; if (g.NavActivateDownId == id) { bool nav_activated_by_code = (g.NavActivateId == id); bool nav_activated_by_inputs = (g.NavActivatePressedId == id); - if (!nav_activated_by_inputs && (flags & ImGuiButtonFlags_Repeat)) + if (!nav_activated_by_inputs && (item_flags & ImGuiItemFlags_ButtonRepeat)) { // Avoid pressing multiple keys from triggering excessive amount of repeat events const ImGuiKeyData* key1 = GetKeyData(ImGuiKey_Space); @@ -623,8 +624,10 @@ bool ImGui::ButtonBehavior(const ImRect& bb, ImGuiID id, bool* out_hovered, bool pressed = true; SetActiveID(id, window); g.ActiveIdSource = g.NavInputSource; - if (!(flags & ImGuiButtonFlags_NoNavFocus)) + if (!(flags & ImGuiButtonFlags_NoNavFocus) && !(g.NavActivateFlags & ImGuiActivateFlags_FromShortcut)) SetFocusID(id, window); + if (g.NavActivateFlags & ImGuiActivateFlags_FromShortcut) + g.ActiveIdFromShortcut = true; } } @@ -655,7 +658,7 @@ bool ImGui::ButtonBehavior(const ImRect& bb, ImGuiID id, bool* out_hovered, bool { // Report as pressed when releasing the mouse (this is the most common path) bool is_double_click_release = (flags & ImGuiButtonFlags_PressedOnDoubleClick) && g.IO.MouseReleased[mouse_button] && g.IO.MouseClickedLastCount[mouse_button] == 2; - bool is_repeating_already = (flags & ImGuiButtonFlags_Repeat) && g.IO.MouseDownDurationPrev[mouse_button] >= g.IO.KeyRepeatDelay; // Repeat mode trumps + bool is_repeating_already = (item_flags & ImGuiItemFlags_ButtonRepeat) && g.IO.MouseDownDurationPrev[mouse_button] >= g.IO.KeyRepeatDelay; // Repeat mode trumps bool is_button_avail_or_owned = TestKeyOwner(MouseButtonToKey(mouse_button), test_owner_id); if (!is_double_click_release && !is_repeating_already && is_button_avail_or_owned) pressed = true; @@ -668,13 +671,19 @@ bool ImGui::ButtonBehavior(const ImRect& bb, ImGuiID id, bool* out_hovered, bool else if (g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad) { // When activated using Nav, we hold on the ActiveID until activation button is released - if (g.NavActivateDownId != id) + if (g.NavActivateDownId == id) + held = true; // hovered == true not true as we are already likely hovered on direct activation. + else ClearActiveID(); } if (pressed) g.ActiveIdHasBeenPressedBefore = true; } + // Activation highlight (this may be a remote activation) + if (g.NavHighlightActivatedId == id) + hovered = true; + if (out_hovered) *out_hovered = hovered; if (out_held) *out_held = held; @@ -702,9 +711,6 @@ bool ImGui::ButtonEx(const char* label, const ImVec2& size_arg, ImGuiButtonFlags if (!ItemAdd(bb, id)) return false; - if (g.LastItemData.InFlags & ImGuiItemFlags_ButtonRepeat) - flags |= ImGuiButtonFlags_Repeat; - bool hovered, held; bool pressed = ButtonBehavior(bb, id, &hovered, &held, flags); @@ -781,9 +787,6 @@ bool ImGui::ArrowButtonEx(const char* str_id, ImGuiDir dir, ImVec2 size, ImGuiBu if (!ItemAdd(bb, id)) return false; - if (g.LastItemData.InFlags & ImGuiItemFlags_ButtonRepeat) - flags |= ImGuiButtonFlags_Repeat; - bool hovered, held; bool pressed = ButtonBehavior(bb, id, &hovered, &held, flags); @@ -812,14 +815,14 @@ bool ImGui::CloseButton(ImGuiID id, const ImVec2& pos) // Tweak 1: Shrink hit-testing area if button covers an abnormally large proportion of the visible region. That's in order to facilitate moving the window away. (#3825) // This may better be applied as a general hit-rect reduction mechanism for all widgets to ensure the area to move window is always accessible? - const ImRect bb(pos, pos + ImVec2(g.FontSize, g.FontSize) + g.Style.FramePadding * 2.0f); + const ImRect bb(pos, pos + ImVec2(g.FontSize, g.FontSize)); ImRect bb_interact = bb; const float area_to_visible_ratio = window->OuterRectClipped.GetArea() / bb.GetArea(); if (area_to_visible_ratio < 1.5f) - bb_interact.Expand(ImFloor(bb_interact.GetSize() * -0.25f)); + bb_interact.Expand(ImTrunc(bb_interact.GetSize() * -0.25f)); // Tweak 2: We intentionally allow interaction when clipped so that a mechanical Alt,Right,Activate sequence can always close a window. - // (this isn't the regular behavior of buttons, but it doesn't affect the user much because navigation tends to keep items visible). + // (this isn't the common behavior of buttons, but it doesn't affect the user because navigation tends to keep items visible in scrolling layer). bool is_clipped = !ItemAdd(bb_interact, id); bool hovered, held; @@ -843,26 +846,34 @@ bool ImGui::CloseButton(ImGuiID id, const ImVec2& pos) return pressed; } -bool ImGui::CollapseButton(ImGuiID id, const ImVec2& pos) +// The Collapse button also functions as a Dock Menu button. +bool ImGui::CollapseButton(ImGuiID id, const ImVec2& pos, ImGuiDockNode* dock_node) { ImGuiContext& g = *GImGui; ImGuiWindow* window = g.CurrentWindow; - ImRect bb(pos, pos + ImVec2(g.FontSize, g.FontSize) + g.Style.FramePadding * 2.0f); - ItemAdd(bb, id); + ImRect bb(pos, pos + ImVec2(g.FontSize, g.FontSize)); + bool is_clipped = !ItemAdd(bb, id); bool hovered, held; bool pressed = ButtonBehavior(bb, id, &hovered, &held, ImGuiButtonFlags_None); + if (is_clipped) + return pressed; // Render + //bool is_dock_menu = (window->DockNodeAsHost && !window->Collapsed); ImU32 bg_col = GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button); ImU32 text_col = GetColorU32(ImGuiCol_Text); if (hovered || held) - window->DrawList->AddCircleFilled(bb.GetCenter()/*+ ImVec2(0.0f, -0.5f)*/, g.FontSize * 0.5f + 1.0f, bg_col); - RenderArrow(window->DrawList, bb.Min + g.Style.FramePadding, text_col, window->Collapsed ? ImGuiDir_Right : ImGuiDir_Down, 1.0f); + window->DrawList->AddCircleFilled(bb.GetCenter() + ImVec2(0.0f, -0.5f), g.FontSize * 0.5f + 1.0f, bg_col); + + if (dock_node) + RenderArrowDockMenu(window->DrawList, bb.Min, g.FontSize, text_col); + else + RenderArrow(window->DrawList, bb.Min, text_col, window->Collapsed ? ImGuiDir_Right : ImGuiDir_Down, 1.0f); // Switch to moving the window after mouse is moved beyond the initial drag threshold if (IsItemActive() && IsMouseDragging(0)) - StartMouseMovingWindow(window); + StartMouseMovingWindowOrNode(window, dock_node, true); // Undock from window/collapse menu button return pressed; } @@ -881,9 +892,9 @@ ImRect ImGui::GetWindowScrollbarRect(ImGuiWindow* window, ImGuiAxis axis) const float scrollbar_size = window->ScrollbarSizes[axis ^ 1]; // (ScrollbarSizes.x = width of Y scrollbar; ScrollbarSizes.y = height of X scrollbar) IM_ASSERT(scrollbar_size > 0.0f); if (axis == ImGuiAxis_X) - return ImRect(inner_rect.Min.x, ImMax(outer_rect.Min.y, outer_rect.Max.y - border_size - scrollbar_size), inner_rect.Max.x, outer_rect.Max.y); + return ImRect(inner_rect.Min.x, ImMax(outer_rect.Min.y, outer_rect.Max.y - border_size - scrollbar_size), inner_rect.Max.x - border_size, outer_rect.Max.y - border_size); else - return ImRect(ImMax(outer_rect.Min.x, outer_rect.Max.x - border_size - scrollbar_size), inner_rect.Min.y, outer_rect.Max.x, inner_rect.Max.y); + return ImRect(ImMax(outer_rect.Min.x, outer_rect.Max.x - border_size - scrollbar_size), inner_rect.Min.y, outer_rect.Max.x - border_size, inner_rect.Max.y - border_size); } void ImGui::Scrollbar(ImGuiAxis axis) @@ -944,7 +955,7 @@ bool ImGui::ScrollbarEx(const ImRect& bb_frame, ImGuiID id, ImGuiAxis axis, ImS6 const bool allow_interaction = (alpha >= 1.0f); ImRect bb = bb_frame; - bb.Expand(ImVec2(-ImClamp(IM_FLOOR((bb_frame_width - 2.0f) * 0.5f), 0.0f, 3.0f), -ImClamp(IM_FLOOR((bb_frame_height - 2.0f) * 0.5f), 0.0f, 3.0f))); + bb.Expand(ImVec2(-ImClamp(IM_TRUNC((bb_frame_width - 2.0f) * 0.5f), 0.0f, 3.0f), -ImClamp(IM_TRUNC((bb_frame_height - 2.0f) * 0.5f), 0.0f, 3.0f))); // V denote the main, longer axis of the scrollbar (= height for a vertical scrollbar) const float scrollbar_size_v = (axis == ImGuiAxis_X) ? bb.GetWidth() : bb.GetHeight(); @@ -972,7 +983,6 @@ bool ImGui::ScrollbarEx(const ImRect& bb_frame, ImGuiID id, ImGuiAxis axis, ImS6 // Click position in scrollbar normalized space (0.0f->1.0f) const float clicked_v_norm = ImSaturate((mouse_pos_v - scrollbar_pos_v) / scrollbar_size_v); - SetHoveredID(id); bool seek_absolute = false; if (g.ActiveIdIsJustActivated) @@ -1013,33 +1023,30 @@ bool ImGui::ScrollbarEx(const ImRect& bb_frame, ImGuiID id, ImGuiAxis axis, ImS6 return held; } -void ImGui::Image(ImTextureID user_texture_id, const ImVec2& size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& tint_col, const ImVec4& border_col) +// - Read about ImTextureID here: https://github.com/ocornut/imgui/wiki/Image-Loading-and-Displaying-Examples +// - 'uv0' and 'uv1' are texture coordinates. Read about them from the same link above. +void ImGui::Image(ImTextureID user_texture_id, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& tint_col, const ImVec4& border_col) { ImGuiWindow* window = GetCurrentWindow(); if (window->SkipItems) return; - ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size); - if (border_col.w > 0.0f) - bb.Max += ImVec2(2, 2); + const float border_size = (border_col.w > 0.0f) ? 1.0f : 0.0f; + const ImVec2 padding(border_size, border_size); + const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + image_size + padding * 2.0f); ItemSize(bb); if (!ItemAdd(bb, 0)) return; - if (border_col.w > 0.0f) - { - window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(border_col), 0.0f); - window->DrawList->AddImage(user_texture_id, bb.Min + ImVec2(1, 1), bb.Max - ImVec2(1, 1), uv0, uv1, GetColorU32(tint_col)); - } - else - { - window->DrawList->AddImage(user_texture_id, bb.Min, bb.Max, uv0, uv1, GetColorU32(tint_col)); - } + // Render + if (border_size > 0.0f) + window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(border_col), 0.0f, ImDrawFlags_None, border_size); + window->DrawList->AddImage(user_texture_id, bb.Min + padding, bb.Max - padding, uv0, uv1, GetColorU32(tint_col)); } // ImageButton() is flawed as 'id' is always derived from 'texture_id' (see #2464 #1390) // We provide this internal helper to write your own variant while we figure out how to redesign the public ImageButton() API. -bool ImGui::ImageButtonEx(ImGuiID id, ImTextureID texture_id, const ImVec2& size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& bg_col, const ImVec4& tint_col, ImGuiButtonFlags flags) +bool ImGui::ImageButtonEx(ImGuiID id, ImTextureID texture_id, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& bg_col, const ImVec4& tint_col, ImGuiButtonFlags flags) { ImGuiContext& g = *GImGui; ImGuiWindow* window = GetCurrentWindow(); @@ -1047,7 +1054,7 @@ bool ImGui::ImageButtonEx(ImGuiID id, ImTextureID texture_id, const ImVec2& size return false; const ImVec2 padding = g.Style.FramePadding; - const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size + padding * 2.0f); + const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + image_size + padding * 2.0f); ItemSize(bb); if (!ItemAdd(bb, id)) return false; @@ -1066,14 +1073,15 @@ bool ImGui::ImageButtonEx(ImGuiID id, ImTextureID texture_id, const ImVec2& size return pressed; } -bool ImGui::ImageButton(const char* str_id, ImTextureID user_texture_id, const ImVec2& size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& bg_col, const ImVec4& tint_col) +// Note that ImageButton() adds style.FramePadding*2.0f to provided size. This is in order to facilitate fitting an image in a button. +bool ImGui::ImageButton(const char* str_id, ImTextureID user_texture_id, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& bg_col, const ImVec4& tint_col) { ImGuiContext& g = *GImGui; ImGuiWindow* window = g.CurrentWindow; if (window->SkipItems) return false; - return ImageButtonEx(window->GetID(str_id), user_texture_id, size, uv0, uv1, bg_col, tint_col); + return ImageButtonEx(window->GetID(str_id), user_texture_id, image_size, uv0, uv1, bg_col, tint_col); } #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS @@ -1140,12 +1148,12 @@ bool ImGui::Checkbox(const char* label, bool* v) { // Undocumented tristate/mixed/indeterminate checkbox (#2644) // This may seem awkwardly designed because the aim is to make ImGuiItemFlags_MixedValue supported by all widgets (not just checkbox) - ImVec2 pad(ImMax(1.0f, IM_FLOOR(square_sz / 3.6f)), ImMax(1.0f, IM_FLOOR(square_sz / 3.6f))); + ImVec2 pad(ImMax(1.0f, IM_TRUNC(square_sz / 3.6f)), ImMax(1.0f, IM_TRUNC(square_sz / 3.6f))); window->DrawList->AddRectFilled(check_bb.Min + pad, check_bb.Max - pad, check_col, style.FrameRounding); } else if (*v) { - const float pad = ImMax(1.0f, IM_FLOOR(square_sz / 6.0f)); + const float pad = ImMax(1.0f, IM_TRUNC(square_sz / 6.0f)); RenderCheckMark(window->DrawList, check_bb.Min + ImVec2(pad, pad), check_col, square_sz - pad * 2.0f); } @@ -1168,10 +1176,8 @@ bool ImGui::CheckboxFlagsT(const char* label, T* flags, T flags_value) if (!all_on && any_on) { ImGuiContext& g = *GImGui; - ImGuiItemFlags backup_item_flags = g.CurrentItemFlags; - g.CurrentItemFlags |= ImGuiItemFlags_MixedValue; + g.NextItemData.ItemFlags |= ImGuiItemFlags_MixedValue; pressed = Checkbox(label, &all_on); - g.CurrentItemFlags = backup_item_flags; } else { @@ -1242,7 +1248,7 @@ bool ImGui::RadioButton(const char* label, bool active) window->DrawList->AddCircleFilled(center, radius, GetColorU32((held && hovered) ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg), num_segment); if (active) { - const float pad = ImMax(1.0f, IM_FLOOR(square_sz / 6.0f)); + const float pad = ImMax(1.0f, IM_TRUNC(square_sz / 6.0f)); window->DrawList->AddCircleFilled(center, radius - pad, GetColorU32(ImGuiCol_CheckMark)); } @@ -1422,26 +1428,19 @@ void ImGui::SeparatorEx(ImGuiSeparatorFlags flags, float thickness) else if (flags & ImGuiSeparatorFlags_Horizontal) { // Horizontal Separator - float x1 = window->Pos.x; - float x2 = window->Pos.x + window->Size.x; - - // FIXME-WORKRECT: old hack (#205) until we decide of consistent behavior with WorkRect/Indent and Separator - if (g.GroupStack.Size > 0 && g.GroupStack.back().WindowID == window->ID) - x1 += window->DC.Indent.x; - - // FIXME-WORKRECT: In theory we should simply be using WorkRect.Min.x/Max.x everywhere but it isn't aesthetically what we want, - // need to introduce a variant of WorkRect for that purpose. (#4787) - if (ImGuiTable* table = g.CurrentTable) - { - x1 = table->Columns[table->CurrentColumn].MinX; - x2 = table->Columns[table->CurrentColumn].MaxX; - } + float x1 = window->DC.CursorPos.x; + float x2 = window->WorkRect.Max.x; + // Preserve legacy behavior inside Columns() // Before Tables API happened, we relied on Separator() to span all columns of a Columns() set. // We currently don't need to provide the same feature for tables because tables naturally have border features. ImGuiOldColumns* columns = (flags & ImGuiSeparatorFlags_SpanAllColumns) ? window->DC.CurrentColumns : NULL; if (columns) + { + x1 = window->Pos.x + window->DC.Indent.x; // Used to be Pos.x before 2023/10/03 + x2 = window->Pos.x + window->Size.x; PushColumnsBackground(); + } // We don't provide our width to the layout so that it doesn't get feed back into AutoFit // FIXME: This prevents ->CursorMaxPos based bounding box evaluation from working (e.g. TableEndCell) @@ -1475,7 +1474,11 @@ void ImGui::Separator() // Those flags should eventually be configurable by the user // FIXME: We cannot g.Style.SeparatorTextBorderSize for thickness as it relates to SeparatorText() which is a decorated separator, not defaulting to 1.0f. ImGuiSeparatorFlags flags = (window->DC.LayoutType == ImGuiLayoutType_Horizontal) ? ImGuiSeparatorFlags_Vertical : ImGuiSeparatorFlags_Horizontal; - flags |= ImGuiSeparatorFlags_SpanAllColumns; // NB: this only applies to legacy Columns() api as they relied on Separator() a lot. + + // Only applies to legacy Columns() api as they relied on Separator() a lot. + if (window->DC.CurrentColumns) + flags |= ImGuiSeparatorFlags_SpanAllColumns; + SeparatorEx(flags, 1.0f); } @@ -1492,14 +1495,14 @@ void ImGui::SeparatorTextEx(ImGuiID id, const char* label, const char* label_end const float separator_thickness = style.SeparatorTextBorderSize; const ImVec2 min_size(label_size.x + extra_w + padding.x * 2.0f, ImMax(label_size.y + padding.y * 2.0f, separator_thickness)); const ImRect bb(pos, ImVec2(window->WorkRect.Max.x, pos.y + min_size.y)); - const float text_baseline_y = ImFloor((bb.GetHeight() - label_size.y) * style.SeparatorTextAlign.y + 0.99999f); //ImMax(padding.y, ImFloor((style.SeparatorTextSize - label_size.y) * 0.5f)); + const float text_baseline_y = ImTrunc((bb.GetHeight() - label_size.y) * style.SeparatorTextAlign.y + 0.99999f); //ImMax(padding.y, ImTrunc((style.SeparatorTextSize - label_size.y) * 0.5f)); ItemSize(min_size, text_baseline_y); if (!ItemAdd(bb, id)) return; const float sep1_x1 = pos.x; const float sep2_x2 = bb.Max.x; - const float seps_y = ImFloor((bb.Min.y + bb.Max.y) * 0.5f + 0.99999f); + const float seps_y = ImTrunc((bb.Min.y + bb.Max.y) * 0.5f + 0.99999f); const float label_avail_w = ImMax(0.0f, sep2_x2 - sep1_x1 - padding.x * 2.0f); const ImVec2 label_pos(pos.x + padding.x + ImMax(0.0f, (label_avail_w - label_size.x - extra_w) * style.SeparatorTextAlign.x), pos.y + text_baseline_y); // FIXME-ALIGN @@ -1553,14 +1556,20 @@ bool ImGui::SplitterBehavior(const ImRect& bb, ImGuiID id, ImGuiAxis axis, float if (!ItemAdd(bb, id, NULL, ImGuiItemFlags_NoNav)) return false; + // FIXME: AFAIK the only leftover reason for passing ImGuiButtonFlags_AllowOverlap here is + // to allow caller of SplitterBehavior() to call SetItemAllowOverlap() after the item. + // Nowadays we would instead want to use SetNextItemAllowOverlap() before the item. + ImGuiButtonFlags button_flags = ImGuiButtonFlags_FlattenChildren; +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + button_flags |= ImGuiButtonFlags_AllowOverlap; +#endif + bool hovered, held; ImRect bb_interact = bb; bb_interact.Expand(axis == ImGuiAxis_Y ? ImVec2(0.0f, hover_extend) : ImVec2(hover_extend, 0.0f)); - ButtonBehavior(bb_interact, id, &hovered, &held, ImGuiButtonFlags_FlattenChildren | ImGuiButtonFlags_AllowItemOverlap); + ButtonBehavior(bb_interact, id, &hovered, &held, button_flags); if (hovered) g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredRect; // for IsItemHovered(), because bb_interact is larger than bb - if (g.ActiveId != id) - SetItemAllowOverlap(); if (held || (hovered && g.HoveredIdPreviousFrame == id && g.HoveredIdTimer >= hover_visibility_delay)) SetMouseCursor(axis == ImGuiAxis_Y ? ImGuiMouseCursor_ResizeNS : ImGuiMouseCursor_ResizeEW); @@ -1568,8 +1577,7 @@ bool ImGui::SplitterBehavior(const ImRect& bb, ImGuiID id, ImGuiAxis axis, float ImRect bb_render = bb; if (held) { - ImVec2 mouse_delta_2d = g.IO.MousePos - g.ActiveIdClickOffset - bb_interact.Min; - float mouse_delta = (axis == ImGuiAxis_Y) ? mouse_delta_2d.y : mouse_delta_2d.x; + float mouse_delta = (g.IO.MousePos - g.ActiveIdClickOffset - bb_interact.Min)[axis]; // Minimum pane size float size_1_maximum_delta = ImMax(0.0f, *size1 - min_size1); @@ -1582,12 +1590,8 @@ bool ImGui::SplitterBehavior(const ImRect& bb, ImGuiID id, ImGuiAxis axis, float // Apply resize if (mouse_delta != 0.0f) { - if (mouse_delta < 0.0f) - IM_ASSERT(*size1 + mouse_delta >= min_size1); - if (mouse_delta > 0.0f) - IM_ASSERT(*size2 - mouse_delta >= min_size2); - *size1 += mouse_delta; - *size2 -= mouse_delta; + *size1 = ImMax(*size1 + mouse_delta, min_size1); + *size2 = ImMax(*size2 - mouse_delta, min_size2); bb_render.Translate((axis == ImGuiAxis_X) ? ImVec2(mouse_delta, 0.0f) : ImVec2(0.0f, mouse_delta)); MarkItemEdited(id); } @@ -1641,7 +1645,7 @@ void ImGui::ShrinkWidths(ImGuiShrinkWidthItem* items, int count, float width_exc width_excess = 0.0f; for (int n = 0; n < count; n++) { - float width_rounded = ImFloor(items[n].Width); + float width_rounded = ImTrunc(items[n].Width); width_excess += items[n].Width - width_rounded; items[n].Width = width_rounded; } @@ -1687,10 +1691,13 @@ bool ImGui::BeginCombo(const char* label, const char* preview_value, ImGuiComboF const ImGuiStyle& style = g.Style; const ImGuiID id = window->GetID(label); IM_ASSERT((flags & (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)) != (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)); // Can't use both flags together + if (flags & ImGuiComboFlags_WidthFitPreview) + IM_ASSERT((flags & (ImGuiComboFlags_NoPreview | (ImGuiComboFlags)ImGuiComboFlags_CustomPreview)) == 0); const float arrow_size = (flags & ImGuiComboFlags_NoArrowButton) ? 0.0f : GetFrameHeight(); const ImVec2 label_size = CalcTextSize(label, NULL, true); - const float w = (flags & ImGuiComboFlags_NoPreview) ? arrow_size : CalcItemWidth(); + const float preview_width = ((flags & ImGuiComboFlags_WidthFitPreview) && (preview_value != NULL)) ? CalcTextSize(preview_value, NULL, true).x : 0.0f; + const float w = (flags & ImGuiComboFlags_NoPreview) ? arrow_size : ((flags & ImGuiComboFlags_WidthFitPreview) ? (arrow_size + preview_width + style.FramePadding.x * 2.0f) : CalcItemWidth()); const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f)); const ImRect total_bb(bb.Min, bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f)); ItemSize(total_bb, style.FramePadding.y); @@ -1783,7 +1790,7 @@ bool ImGui::BeginComboPopup(ImGuiID popup_id, const ImRect& bb, ImGuiComboFlags // This is essentially a specialized version of BeginPopupEx() char name[16]; - ImFormatString(name, IM_ARRAYSIZE(name), "##Combo_%02d", g.BeginPopupStack.Size); // Recycle windows based on depth + ImFormatString(name, IM_ARRAYSIZE(name), "##Combo_%02d", g.BeginComboDepth); // Recycle windows based on depth // Set position given a custom constraint (peak into expected window size so we can position it) // FIXME: This might be easier to express with an hypothetical SetNextWindowPosConstraints() function? @@ -1810,12 +1817,15 @@ bool ImGui::BeginComboPopup(ImGuiID popup_id, const ImRect& bb, ImGuiComboFlags IM_ASSERT(0); // This should never happen as we tested for IsPopupOpen() above return false; } + g.BeginComboDepth++; return true; } void ImGui::EndCombo() { + ImGuiContext& g = *GImGui; EndPopup(); + g.BeginComboDepth--; } // Call directly after the BeginCombo/EndCombo block. The preview is designed to only host non-interactive elements @@ -1829,7 +1839,7 @@ bool ImGui::BeginComboPreview() if (window->SkipItems || !(g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible)) return false; IM_ASSERT(g.LastItemData.Rect.Min.x == preview_data->PreviewRect.Min.x && g.LastItemData.Rect.Min.y == preview_data->PreviewRect.Min.y); // Didn't call after BeginCombo/EndCombo block or forgot to pass ImGuiComboFlags_CustomPreview flag? - if (!window->ClipRect.Contains(preview_data->PreviewRect)) // Narrower test (optional) + if (!window->ClipRect.Overlaps(preview_data->PreviewRect)) // Narrower test (optional) return false; // FIXME: This could be contained in a PushWorkRect() api @@ -1872,18 +1882,15 @@ void ImGui::EndComboPreview() } // Getter for the old Combo() API: const char*[] -static bool Items_ArrayGetter(void* data, int idx, const char** out_text) +static const char* Items_ArrayGetter(void* data, int idx) { const char* const* items = (const char* const*)data; - if (out_text) - *out_text = items[idx]; - return true; + return items[idx]; } // Getter for the old Combo() API: "item1\0item2\0item3\0" -static bool Items_SingleStringGetter(void* data, int idx, const char** out_text) +static const char* Items_SingleStringGetter(void* data, int idx) { - // FIXME-OPT: we could pre-compute the indices to fasten this. But only 1 active combo means the waste is limited. const char* items_separated_by_zeros = (const char*)data; int items_count = 0; const char* p = items_separated_by_zeros; @@ -1894,22 +1901,18 @@ static bool Items_SingleStringGetter(void* data, int idx, const char** out_text) p += strlen(p) + 1; items_count++; } - if (!*p) - return false; - if (out_text) - *out_text = p; - return true; + return *p ? p : NULL; } // Old API, prefer using BeginCombo() nowadays if you can. -bool ImGui::Combo(const char* label, int* current_item, bool (*items_getter)(void*, int, const char**), void* data, int items_count, int popup_max_height_in_items) +bool ImGui::Combo(const char* label, int* current_item, const char* (*getter)(void* user_data, int idx), void* user_data, int items_count, int popup_max_height_in_items) { ImGuiContext& g = *GImGui; // Call the getter to obtain the preview string which is a parameter to BeginCombo() const char* preview_value = NULL; if (*current_item >= 0 && *current_item < items_count) - items_getter(data, *current_item, &preview_value); + preview_value = getter(user_data, *current_item); // The old Combo() API exposed "popup_max_height_in_items". The new more general BeginCombo() API doesn't have/need it, but we emulate it here. if (popup_max_height_in_items != -1 && !(g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasSizeConstraint)) @@ -1923,12 +1926,13 @@ bool ImGui::Combo(const char* label, int* current_item, bool (*items_getter)(voi bool value_changed = false; for (int i = 0; i < items_count; i++) { + const char* item_text = getter(user_data, i); + if (item_text == NULL) + item_text = "*Unknown item*"; + PushID(i); const bool item_selected = (i == *current_item); - const char* item_text; - if (!items_getter(data, i, &item_text)) - item_text = "*Unknown item*"; - if (Selectable(item_text, item_selected)) + if (Selectable(item_text, item_selected) && *current_item != i) { value_changed = true; *current_item = i; @@ -1967,13 +1971,37 @@ bool ImGui::Combo(const char* label, int* current_item, const char* items_separa return value_changed; } +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + +struct ImGuiGetNameFromIndexOldToNewCallbackData { void* UserData; bool (*OldCallback)(void*, int, const char**); }; +static const char* ImGuiGetNameFromIndexOldToNewCallback(void* user_data, int idx) +{ + ImGuiGetNameFromIndexOldToNewCallbackData* data = (ImGuiGetNameFromIndexOldToNewCallbackData*)user_data; + const char* s = NULL; + data->OldCallback(data->UserData, idx, &s); + return s; +} + +bool ImGui::ListBox(const char* label, int* current_item, bool (*old_getter)(void*, int, const char**), void* user_data, int items_count, int height_in_items) +{ + ImGuiGetNameFromIndexOldToNewCallbackData old_to_new_data = { user_data, old_getter }; + return ListBox(label, current_item, ImGuiGetNameFromIndexOldToNewCallback, &old_to_new_data, items_count, height_in_items); +} +bool ImGui::Combo(const char* label, int* current_item, bool (*old_getter)(void*, int, const char**), void* user_data, int items_count, int popup_max_height_in_items) +{ + ImGuiGetNameFromIndexOldToNewCallbackData old_to_new_data = { user_data, old_getter }; + return Combo(label, current_item, ImGuiGetNameFromIndexOldToNewCallback, &old_to_new_data, items_count, popup_max_height_in_items); +} + +#endif + //------------------------------------------------------------------------- // [SECTION] Data Type and Data Formatting Helpers [Internal] //------------------------------------------------------------------------- // - DataTypeGetInfo() // - DataTypeFormatString() // - DataTypeApplyOp() -// - DataTypeApplyOpFromText() +// - DataTypeApplyFromText() // - DataTypeCompare() // - DataTypeClamp() // - GetMinimumStepAtDecimalPrecision @@ -2415,19 +2443,18 @@ bool ImGui::DragScalar(const char* label, ImGuiDataType data_type, void* p_data, if (format == NULL) format = DataTypeGetInfo(data_type)->PrintFmt; - const bool hovered = ItemHoverable(frame_bb, id); + const bool hovered = ItemHoverable(frame_bb, id, g.LastItemData.InFlags); bool temp_input_is_active = temp_input_allowed && TempInputIsActive(id); if (!temp_input_is_active) { // Tabbing or CTRL-clicking on Drag turns it into an InputText - const bool input_requested_by_tabbing = temp_input_allowed && (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_FocusedByTabbing) != 0; const bool clicked = hovered && IsMouseClicked(0, id); const bool double_clicked = (hovered && g.IO.MouseClickedCount[0] == 2 && TestKeyOwner(ImGuiKey_MouseLeft, id)); - const bool make_active = (input_requested_by_tabbing || clicked || double_clicked || g.NavActivateId == id); + const bool make_active = (clicked || double_clicked || g.NavActivateId == id); if (make_active && (clicked || double_clicked)) SetKeyOwner(ImGuiKey_MouseLeft, id); if (make_active && temp_input_allowed) - if (input_requested_by_tabbing || (clicked && g.IO.KeyCtrl) || double_clicked || (g.NavActivateId == id && (g.NavActivateFlags & ImGuiActivateFlags_PreferInput))) + if ((clicked && g.IO.KeyCtrl) || double_clicked || (g.NavActivateId == id && (g.NavActivateFlags & ImGuiActivateFlags_PreferInput))) temp_input_is_active = true; // (Optional) simple click (without moving) turns Drag into an InputText @@ -2778,14 +2805,14 @@ bool ImGui::SliderBehaviorT(const ImRect& bb, ImGuiID id, ImGuiDataType data_typ const ImGuiAxis axis = (flags & ImGuiSliderFlags_Vertical) ? ImGuiAxis_Y : ImGuiAxis_X; const bool is_logarithmic = (flags & ImGuiSliderFlags_Logarithmic) != 0; const bool is_floating_point = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double); - const SIGNEDTYPE v_range = (v_min < v_max ? v_max - v_min : v_min - v_max); + const float v_range_f = (float)(v_min < v_max ? v_max - v_min : v_min - v_max); // We don't need high precision for what we do with it. // Calculate bounds const float grab_padding = 2.0f; // FIXME: Should be part of style. const float slider_sz = (bb.Max[axis] - bb.Min[axis]) - grab_padding * 2.0f; float grab_sz = style.GrabMinSize; - if (!is_floating_point && v_range >= 0) // v_range < 0 may happen on integer overflows - grab_sz = ImMax((float)(slider_sz / (v_range + 1)), style.GrabMinSize); // For integer sliders: if possible have the grab size represent 1 unit + if (!is_floating_point && v_range_f >= 0.0f) // v_range_f < 0 may happen on integer overflows + grab_sz = ImMax(slider_sz / (v_range_f + 1), style.GrabMinSize); // For integer sliders: if possible have the grab size represent 1 unit grab_sz = ImMin(grab_sz, slider_sz); const float slider_usable_sz = slider_sz - grab_sz; const float slider_usable_pos_min = bb.Min[axis] + grab_padding + grab_sz * 0.5f; @@ -2854,8 +2881,8 @@ bool ImGui::SliderBehaviorT(const ImRect& bb, ImGuiID id, ImGuiDataType data_typ } else { - if ((v_range >= -100.0f && v_range <= 100.0f) || tweak_slow) - input_delta = ((input_delta < 0.0f) ? -1.0f : +1.0f) / (float)v_range; // Gamepad/keyboard tweak speeds in integer steps + if ((v_range_f >= -100.0f && v_range_f <= 100.0f && v_range_f != 0.0f) || tweak_slow) + input_delta = ((input_delta < 0.0f) ? -1.0f : +1.0f) / v_range_f; // Gamepad/keyboard tweak speeds in integer steps else input_delta /= 100.0f; } @@ -2902,6 +2929,10 @@ bool ImGui::SliderBehaviorT(const ImRect& bb, ImGuiID id, ImGuiDataType data_typ } } + if (set_new_value) + if ((g.LastItemData.InFlags & ImGuiItemFlags_ReadOnly) || (flags & ImGuiSliderFlags_ReadOnly)) + set_new_value = false; + if (set_new_value) { TYPE v_new = ScaleValueFromRatioT(data_type, clicked_t, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize); @@ -2947,11 +2978,6 @@ bool ImGui::SliderBehavior(const ImRect& bb, ImGuiID id, ImGuiDataType data_type // Read imgui.cpp "API BREAKING CHANGES" section for 1.78 if you hit this assert. IM_ASSERT((flags == 1 || (flags & ImGuiSliderFlags_InvalidMask_) == 0) && "Invalid ImGuiSliderFlags flag! Has the 'float power' argument been mistakenly cast to flags? Call function with ImGuiSliderFlags_Logarithmic flags instead."); - // Those are the things we can do easily outside the SliderBehaviorT<> template, saves code generation. - ImGuiContext& g = *GImGui; - if ((g.LastItemData.InFlags & ImGuiItemFlags_ReadOnly) || (flags & ImGuiSliderFlags_ReadOnly)) - return false; - switch (data_type) { case ImGuiDataType_S8: { ImS32 v32 = (ImS32)*(ImS8*)p_v; bool r = SliderBehaviorT(bb, id, ImGuiDataType_S32, &v32, *(const ImS8*)p_min, *(const ImS8*)p_max, format, flags, out_grab_bb); if (r) *(ImS8*)p_v = (ImS8)v32; return r; } @@ -3008,18 +3034,17 @@ bool ImGui::SliderScalar(const char* label, ImGuiDataType data_type, void* p_dat if (format == NULL) format = DataTypeGetInfo(data_type)->PrintFmt; - const bool hovered = ItemHoverable(frame_bb, id); + const bool hovered = ItemHoverable(frame_bb, id, g.LastItemData.InFlags); bool temp_input_is_active = temp_input_allowed && TempInputIsActive(id); if (!temp_input_is_active) { // Tabbing or CTRL-clicking on Slider turns it into an input box - const bool input_requested_by_tabbing = temp_input_allowed && (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_FocusedByTabbing) != 0; const bool clicked = hovered && IsMouseClicked(0, id); - const bool make_active = (input_requested_by_tabbing || clicked || g.NavActivateId == id); + const bool make_active = (clicked || g.NavActivateId == id); if (make_active && clicked) SetKeyOwner(ImGuiKey_MouseLeft, id); if (make_active && temp_input_allowed) - if (input_requested_by_tabbing || (clicked && g.IO.KeyCtrl) || (g.NavActivateId == id && (g.NavActivateFlags & ImGuiActivateFlags_PreferInput))) + if ((clicked && g.IO.KeyCtrl) || (g.NavActivateId == id && (g.NavActivateFlags & ImGuiActivateFlags_PreferInput))) temp_input_is_active = true; if (make_active && !temp_input_is_active) @@ -3175,7 +3200,7 @@ bool ImGui::VSliderScalar(const char* label, const ImVec2& size, ImGuiDataType d if (format == NULL) format = DataTypeGetInfo(data_type)->PrintFmt; - const bool hovered = ItemHoverable(frame_bb, id); + const bool hovered = ItemHoverable(frame_bb, id, g.LastItemData.InFlags); const bool clicked = hovered && IsMouseClicked(0, id); if (clicked || g.NavActivateId == id) { @@ -3391,14 +3416,6 @@ bool ImGui::TempInputText(const ImRect& bb, ImGuiID id, const char* label, char* return value_changed; } -static inline ImGuiInputTextFlags InputScalar_DefaultCharsFilter(ImGuiDataType data_type, const char* format) -{ - if (data_type == ImGuiDataType_Float || data_type == ImGuiDataType_Double) - return ImGuiInputTextFlags_CharsScientific; - const char format_last_char = format[0] ? format[strlen(format) - 1] : 0; - return (format_last_char == 'x' || format_last_char == 'X') ? ImGuiInputTextFlags_CharsHexadecimal : ImGuiInputTextFlags_CharsDecimal; -} - // Note that Drag/Slider functions are only forwarding the min/max values clamping values if the ImGuiSliderFlags_AlwaysClamp flag is set! // This is intended: this way we allow CTRL+Click manual input to set a value out of bounds, for maximum flexibility. // However this may not be ideal for all uses, as some user code may break on out of bound values. @@ -3415,8 +3432,7 @@ bool ImGui::TempInputScalar(const ImRect& bb, ImGuiID id, const char* label, ImG DataTypeFormatString(data_buf, IM_ARRAYSIZE(data_buf), data_type, p_data, format); ImStrTrimBlanks(data_buf); - ImGuiInputTextFlags flags = ImGuiInputTextFlags_AutoSelectAll | ImGuiInputTextFlags_NoMarkEdited; - flags |= InputScalar_DefaultCharsFilter(data_type, format); + ImGuiInputTextFlags flags = ImGuiInputTextFlags_AutoSelectAll | (ImGuiInputTextFlags)ImGuiInputTextFlags_NoMarkEdited; bool value_changed = false; if (TempInputText(bb, id, label, data_buf, IM_ARRAYSIZE(data_buf), flags)) @@ -3460,10 +3476,7 @@ bool ImGui::InputScalar(const char* label, ImGuiDataType data_type, void* p_data char buf[64]; DataTypeFormatString(buf, IM_ARRAYSIZE(buf), data_type, p_data, format); - // Testing ActiveId as a minor optimization as filtering is not needed until active - if (g.ActiveId == 0 && (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_CharsScientific)) == 0) - flags |= InputScalar_DefaultCharsFilter(data_type, format); - flags |= ImGuiInputTextFlags_AutoSelectAll | ImGuiInputTextFlags_NoMarkEdited; // We call MarkItemEdited() ourselves by comparing the actual data rather than the string. + flags |= ImGuiInputTextFlags_AutoSelectAll | (ImGuiInputTextFlags)ImGuiInputTextFlags_NoMarkEdited; // We call MarkItemEdited() ourselves by comparing the actual data rather than the string. bool value_changed = false; if (p_step == NULL) @@ -3557,7 +3570,6 @@ bool ImGui::InputScalarN(const char* label, ImGuiDataType data_type, void* p_dat bool ImGui::InputFloat(const char* label, float* v, float step, float step_fast, const char* format, ImGuiInputTextFlags flags) { - flags |= ImGuiInputTextFlags_CharsScientific; return InputScalar(label, ImGuiDataType_Float, (void*)v, (void*)(step > 0.0f ? &step : NULL), (void*)(step_fast > 0.0f ? &step_fast : NULL), format, flags); } @@ -3600,7 +3612,6 @@ bool ImGui::InputInt4(const char* label, int v[4], ImGuiInputTextFlags flags) bool ImGui::InputDouble(const char* label, double* v, double step, double step_fast, const char* format, ImGuiInputTextFlags flags) { - flags |= ImGuiInputTextFlags_CharsScientific; return InputScalar(label, ImGuiDataType_Double, (void*)v, (void*)(step > 0.0 ? &step : NULL), (void*)(step_fast > 0.0 ? &step_fast : NULL), format, flags); } @@ -3698,8 +3709,8 @@ namespace ImStb { static int STB_TEXTEDIT_STRINGLEN(const ImGuiInputTextState* obj) { return obj->CurLenW; } -static ImWchar STB_TEXTEDIT_GETCHAR(const ImGuiInputTextState* obj, int idx) { return obj->TextW[idx]; } -static float STB_TEXTEDIT_GETWIDTH(ImGuiInputTextState* obj, int line_start_idx, int char_idx) { ImWchar c = obj->TextW[line_start_idx + char_idx]; if (c == '\n') return STB_TEXTEDIT_GETWIDTH_NEWLINE; ImGuiContext& g = *obj->Ctx; return g.Font->GetCharAdvance(c) * (g.FontSize / g.Font->FontSize); } +static ImWchar STB_TEXTEDIT_GETCHAR(const ImGuiInputTextState* obj, int idx) { IM_ASSERT(idx <= obj->CurLenW); return obj->TextW[idx]; } +static float STB_TEXTEDIT_GETWIDTH(ImGuiInputTextState* obj, int line_start_idx, int char_idx) { ImWchar c = obj->TextW[line_start_idx + char_idx]; if (c == '\n') return IMSTB_TEXTEDIT_GETWIDTH_NEWLINE; ImGuiContext& g = *obj->Ctx; return g.Font->GetCharAdvance(c) * (g.FontSize / g.Font->FontSize); } static int STB_TEXTEDIT_KEYTOTEXT(int key) { return key >= 0x200000 ? 0 : key; } static ImWchar STB_TEXTEDIT_NEWLINE = '\n'; static void STB_TEXTEDIT_LAYOUTROW(StbTexteditRow* r, ImGuiInputTextState* obj, int line_start_idx) @@ -3817,12 +3828,13 @@ static bool STB_TEXTEDIT_INSERTCHARS(ImGuiInputTextState* obj, int pos, const Im #define STB_TEXTEDIT_K_PGDOWN 0x20000F // keyboard input to move cursor down a page #define STB_TEXTEDIT_K_SHIFT 0x400000 -#define STB_TEXTEDIT_IMPLEMENTATION +#define IMSTB_TEXTEDIT_IMPLEMENTATION +#define IMSTB_TEXTEDIT_memmove memmove #include "imstb_textedit.h" // stb_textedit internally allows for a single undo record to do addition and deletion, but somehow, calling // the stb_textedit_paste() function creates two separate records, so we perform it manually. (FIXME: Report to nothings/stb?) -static void stb_textedit_replace(ImGuiInputTextState* str, STB_TexteditState* state, const STB_TEXTEDIT_CHARTYPE* text, int text_len) +static void stb_textedit_replace(ImGuiInputTextState* str, STB_TexteditState* state, const IMSTB_TEXTEDIT_CHARTYPE* text, int text_len) { stb_text_makeundo_replace(str, state, 0, str->CurLenW, text_len); ImStb::STB_TEXTEDIT_DELETECHARS(str, 0, str->CurLenW); @@ -3875,6 +3887,10 @@ void ImGuiInputTextCallbackData::DeleteChars(int pos, int bytes_count) void ImGuiInputTextCallbackData::InsertChars(int pos, const char* new_text, const char* new_text_end) { + // Accept null ranges + if (new_text == new_text_end) + return; + const bool is_resizable = (Flags & ImGuiInputTextFlags_CallbackResize) != 0; const int new_text_len = new_text_end ? (int)(new_text_end - new_text) : (int)strlen(new_text); if (new_text_len + BufTextLen >= BufSize) @@ -3906,7 +3922,7 @@ void ImGuiInputTextCallbackData::InsertChars(int pos, const char* new_text, cons } // Return false to discard a character. -static bool InputTextFilterCharacter(unsigned int* p_char, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data, ImGuiInputSource input_source) +static bool InputTextFilterCharacter(ImGuiContext* ctx, unsigned int* p_char, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data, ImGuiInputSource input_source) { IM_ASSERT(input_source == ImGuiInputSource_Keyboard || input_source == ImGuiInputSource_Clipboard); unsigned int c = *p_char; @@ -3916,8 +3932,8 @@ static bool InputTextFilterCharacter(unsigned int* p_char, ImGuiInputTextFlags f if (c < 0x20) { bool pass = false; - pass |= (c == '\n' && (flags & ImGuiInputTextFlags_Multiline)); // Note that an Enter KEY will emit \r and be ignored (we poll for KEY in InputText() code) - pass |= (c == '\t' && (flags & ImGuiInputTextFlags_AllowTabInput)); + pass |= (c == '\n') && (flags & ImGuiInputTextFlags_Multiline) != 0; // Note that an Enter KEY will emit \r and be ignored (we poll for KEY in InputText() code) + pass |= (c == '\t') && (flags & ImGuiInputTextFlags_AllowTabInput) != 0; if (!pass) return false; apply_named_filters = false; // Override named filters below so newline and tabs can still be inserted. @@ -3945,10 +3961,13 @@ static bool InputTextFilterCharacter(unsigned int* p_char, ImGuiInputTextFlags f // The standard mandate that programs starts in the "C" locale where the decimal point is '.'. // We don't really intend to provide widespread support for it, but out of empathy for people stuck with using odd API, we support the bare minimum aka overriding the decimal point. // Change the default decimal_point with: - // ImGui::GetCurrentContext()->PlatformLocaleDecimalPoint = *localeconv()->decimal_point; + // ImGui::GetIO()->PlatformLocaleDecimalPoint = *localeconv()->decimal_point; // Users of non-default decimal point (in particular ',') may be affected by word-selection logic (is_word_boundary_from_right/is_word_boundary_from_left) functions. - ImGuiContext& g = *GImGui; - const unsigned c_decimal_point = (unsigned int)g.PlatformLocaleDecimalPoint; + ImGuiContext& g = *ctx; + const unsigned c_decimal_point = (unsigned int)g.IO.PlatformLocaleDecimalPoint; + if (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsScientific)) + if (c == '.' || c == ',') + c = c_decimal_point; // Full-width -> half-width conversion for numeric fields (https://en.wikipedia.org/wiki/Halfwidth_and_Fullwidth_Forms_(Unicode_block) // While this is mostly convenient, this has the side-effect for uninformed users accidentally inputting full-width characters that they may @@ -4034,7 +4053,7 @@ static void InputTextReconcileUndoStateAfterUserCallback(ImGuiInputTextState* st const int insert_len = new_last_diff - first_diff + 1; const int delete_len = old_last_diff - first_diff + 1; if (insert_len > 0 || delete_len > 0) - if (STB_TEXTEDIT_CHARTYPE* p = stb_text_createundo(&state->Stb.undostate, first_diff, delete_len, insert_len)) + if (IMSTB_TEXTEDIT_CHARTYPE* p = stb_text_createundo(&state->Stb.undostate, first_diff, delete_len, insert_len)) for (int i = 0; i < delete_len; i++) p[i] = ImStb::STB_TEXTEDIT_GETCHAR(state, first_diff + i); } @@ -4050,8 +4069,16 @@ void ImGui::InputTextDeactivateHook(ImGuiID id) if (id == 0 || state->ID != id) return; g.InputTextDeactivatedState.ID = state->ID; - g.InputTextDeactivatedState.TextA.resize(state->CurLenA + 1); - memcpy(g.InputTextDeactivatedState.TextA.Data, state->TextA.Data ? state->TextA.Data : "", state->CurLenA + 1); + if (state->Flags & ImGuiInputTextFlags_ReadOnly) + { + g.InputTextDeactivatedState.TextA.resize(0); // In theory this data won't be used, but clear to be neat. + } + else + { + IM_ASSERT(state->TextA.Data != 0); + g.InputTextDeactivatedState.TextA.resize(state->CurLenA + 1); + memcpy(g.InputTextDeactivatedState.TextA.Data, state->TextA.Data, state->CurLenA + 1); + } } // Edit a string of text @@ -4078,12 +4105,6 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ const bool RENDER_SELECTION_WHEN_INACTIVE = false; const bool is_multiline = (flags & ImGuiInputTextFlags_Multiline) != 0; - const bool is_readonly = (flags & ImGuiInputTextFlags_ReadOnly) != 0; - const bool is_password = (flags & ImGuiInputTextFlags_Password) != 0; - const bool is_undoable = (flags & ImGuiInputTextFlags_NoUndoRedo) == 0; - const bool is_resizable = (flags & ImGuiInputTextFlags_CallbackResize) != 0; - if (is_resizable) - IM_ASSERT(callback != NULL); // Must provide a callback if you set the ImGuiInputTextFlags_CallbackResize flag! if (is_multiline) // Open group before calling GetID() because groups tracks id created within their scope (including the scrollbar) BeginGroup(); @@ -4097,7 +4118,6 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ ImGuiWindow* draw_window = window; ImVec2 inner_size = frame_size; - ImGuiItemStatusFlags item_status_flags = 0; ImGuiLastItemData item_data_backup; if (is_multiline) { @@ -4108,17 +4128,25 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ EndGroup(); return false; } - item_status_flags = g.LastItemData.StatusFlags; item_data_backup = g.LastItemData; window->DC.CursorPos = backup_pos; + // Prevent NavActivation from Tabbing when our widget accepts Tab inputs: this allows cycling through widgets without stopping. + if (g.NavActivateId == id && (g.NavActivateFlags & ImGuiActivateFlags_FromTabbing) && (flags & ImGuiInputTextFlags_AllowTabInput)) + g.NavActivateId = 0; + + // Prevent NavActivate reactivating in BeginChild() when we are already active. + const ImGuiID backup_activate_id = g.NavActivateId; + if (g.ActiveId == id) // Prevent reactivation + g.NavActivateId = 0; + // We reproduce the contents of BeginChildFrame() in order to provide 'label' so our window internal data are easier to read/debug. - // FIXME-NAV: Pressing NavActivate will trigger general child activation right before triggering our own below. Harmless but bizarre. PushStyleColor(ImGuiCol_ChildBg, style.Colors[ImGuiCol_FrameBg]); PushStyleVar(ImGuiStyleVar_ChildRounding, style.FrameRounding); PushStyleVar(ImGuiStyleVar_ChildBorderSize, style.FrameBorderSize); PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); // Ensure no clip rect so mouse hover can reach FramePadding edges bool child_visible = BeginChildEx(label, id, frame_bb.GetSize(), true, ImGuiWindowFlags_NoMove); + g.NavActivateId = backup_activate_id; PopStyleVar(3); PopStyleColor(); if (!child_visible) @@ -4139,16 +4167,23 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ if (!(flags & ImGuiInputTextFlags_MergedItem)) if (!ItemAdd(total_bb, id, &frame_bb, ImGuiItemFlags_Inputable)) return false; - item_status_flags = g.LastItemData.StatusFlags; } - const bool hovered = ItemHoverable(frame_bb, id); + const bool hovered = ItemHoverable(frame_bb, id, g.LastItemData.InFlags); if (hovered) g.MouseCursor = ImGuiMouseCursor_TextInput; // We are only allowed to access the state if we are already the active widget. ImGuiInputTextState* state = GetInputTextState(id); - const bool input_requested_by_tabbing = (item_status_flags & ImGuiItemStatusFlags_FocusedByTabbing) != 0; + if (g.LastItemData.InFlags & ImGuiItemFlags_ReadOnly) + flags |= ImGuiInputTextFlags_ReadOnly; + const bool is_readonly = (flags & ImGuiInputTextFlags_ReadOnly) != 0; + const bool is_password = (flags & ImGuiInputTextFlags_Password) != 0; + const bool is_undoable = (flags & ImGuiInputTextFlags_NoUndoRedo) == 0; + const bool is_resizable = (flags & ImGuiInputTextFlags_CallbackResize) != 0; + if (is_resizable) + IM_ASSERT(callback != NULL); // Must provide a callback if you set the ImGuiInputTextFlags_CallbackResize flag! + const bool input_requested_by_nav = (g.ActiveId != id) && ((g.NavActivateId == id) && ((g.NavActivateFlags & ImGuiActivateFlags_PreferInput) || (g.NavInputSource == ImGuiInputSource_Keyboard))); const bool user_clicked = hovered && io.MouseClicked[0]; @@ -4159,27 +4194,32 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ float scroll_y = is_multiline ? draw_window->Scroll.y : FLT_MAX; + const bool init_reload_from_user_buf = (state != NULL && state->ReloadUserBuf); const bool init_changed_specs = (state != NULL && state->Stb.single_line != !is_multiline); // state != NULL means its our state. - const bool init_make_active = (user_clicked || user_scroll_finish || input_requested_by_nav || input_requested_by_tabbing); + const bool init_make_active = (user_clicked || user_scroll_finish || input_requested_by_nav); const bool init_state = (init_make_active || user_scroll_active); - if ((init_state && g.ActiveId != id) || init_changed_specs) + if ((init_state && g.ActiveId != id) || init_changed_specs || init_reload_from_user_buf) { // Access state even if we don't own it yet. state = &g.InputTextState; state->CursorAnimReset(); + state->ReloadUserBuf = false; // Backup state of deactivating item so they'll have a chance to do a write to output buffer on the same frame they report IsItemDeactivatedAfterEdit (#4714) InputTextDeactivateHook(state->ID); - // Take a copy of the initial buffer value (both in original UTF-8 format and converted to wchar) - // From the moment we focused we are ignoring the content of 'buf' (unless we are in read-only mode) + // From the moment we focused we are normally ignoring the content of 'buf' (unless we are in read-only mode) const int buf_len = (int)strlen(buf); - state->InitialTextA.resize(buf_len + 1); // UTF-8. we use +1 to make sure that .Data is always pointing to at least an empty string. - memcpy(state->InitialTextA.Data, buf, buf_len + 1); + if (!init_reload_from_user_buf) + { + // Take a copy of the initial buffer value. + state->InitialTextA.resize(buf_len + 1); // UTF-8. we use +1 to make sure that .Data is always pointing to at least an empty string. + memcpy(state->InitialTextA.Data, buf, buf_len + 1); + } // Preserve cursor position and undo/redo stack if we come back to same widget - // FIXME: Since we reworked this on 2022/06, may want to differenciate recycle_cursor vs recycle_undostate? - bool recycle_state = (state->ID == id && !init_changed_specs); + // FIXME: Since we reworked this on 2022/06, may want to differentiate recycle_cursor vs recycle_undostate? + bool recycle_state = (state->ID == id && !init_changed_specs && !init_reload_from_user_buf); if (recycle_state && (state->CurLenA != buf_len || (state->TextAIsValid && strncmp(state->TextA.Data, buf, buf_len) != 0))) recycle_state = false; @@ -4204,13 +4244,19 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ stb_textedit_initialize_state(&state->Stb, !is_multiline); } - if (!is_multiline) + if (init_reload_from_user_buf) + { + state->Stb.select_start = state->ReloadSelectionStart; + state->Stb.cursor = state->Stb.select_end = state->ReloadSelectionEnd; + state->CursorClamp(); + } + else if (!is_multiline) { if (flags & ImGuiInputTextFlags_AutoSelectAll) select_all = true; if (input_requested_by_nav && (!recycle_state || !(g.NavActivateFlags & ImGuiActivateFlags_TryToPreserveState))) select_all = true; - if (input_requested_by_tabbing || (user_clicked && io.KeyCtrl)) + if (user_clicked && io.KeyCtrl) select_all = true; } @@ -4234,6 +4280,8 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right); if (is_multiline || (flags & ImGuiInputTextFlags_CallbackHistory)) g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Up) | (1 << ImGuiDir_Down); + SetKeyOwner(ImGuiKey_Enter, id); + SetKeyOwner(ImGuiKey_KeypadEnter, id); SetKeyOwner(ImGuiKey_Home, id); SetKeyOwner(ImGuiKey_End, id); if (is_multiline) @@ -4243,8 +4291,6 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ } if (is_osx) SetKeyOwner(ImGuiMod_Alt, id); - if (flags & (ImGuiInputTextFlags_CallbackCompletion | ImGuiInputTextFlags_AllowTabInput)) // Disable keyboard tabbing out as we will use the \t character. - SetShortcutRouting(ImGuiKey_Tab, id); } // We have an edge case if ActiveId was set through another widget (e.g. widget being swapped), clear id immediately (don't wait until the end of the function) @@ -4373,11 +4419,20 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ // We expect backends to emit a Tab key but some also emit a Tab character which we ignore (#2467, #1336) // (For Tab and Enter: Win32/SFML/Allegro are sending both keys and chars, GLFW and SDL are only sending keys. For Space they all send all threes) - if ((flags & ImGuiInputTextFlags_AllowTabInput) && Shortcut(ImGuiKey_Tab, id) && !is_readonly) + if ((flags & ImGuiInputTextFlags_AllowTabInput) && !is_readonly) { - unsigned int c = '\t'; // Insert TAB - if (InputTextFilterCharacter(&c, flags, callback, callback_user_data, ImGuiInputSource_Keyboard)) - state->OnKeyPressed((int)c); + if (Shortcut(ImGuiKey_Tab, id, ImGuiInputFlags_Repeat)) + { + unsigned int c = '\t'; // Insert TAB + if (InputTextFilterCharacter(&g, &c, flags, callback, callback_user_data, ImGuiInputSource_Keyboard)) + state->OnKeyPressed((int)c); + } + // FIXME: Implement Shift+Tab + /* + if (Shortcut(ImGuiKey_Tab | ImGuiMod_Shift, id, ImGuiInputFlags_Repeat)) + { + } + */ } // Process regular text input (before we check for Return because using some IME will effectively send a Return?) @@ -4392,7 +4447,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ unsigned int c = (unsigned int)io.InputQueueCharacters[n]; if (c == '\t') // Skip Tab, see above. continue; - if (InputTextFilterCharacter(&c, flags, callback, callback_user_data, ImGuiInputSource_Keyboard)) + if (InputTextFilterCharacter(&g, &c, flags, callback, callback_user_data, ImGuiInputSource_Keyboard)) state->OnKeyPressed((int)c); } @@ -4475,7 +4530,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ else if (!is_readonly) { unsigned int c = '\n'; // Insert new line - if (InputTextFilterCharacter(&c, flags, callback, callback_user_data, ImGuiInputSource_Keyboard)) + if (InputTextFilterCharacter(&g, &c, flags, callback, callback_user_data, ImGuiInputSource_Keyboard)) state->OnKeyPressed((int)c); } } @@ -4483,7 +4538,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ { if (flags & ImGuiInputTextFlags_EscapeClearsAll) { - if (state->CurLenA > 0) + if (buf[0] != 0) { revert_edit = true; } @@ -4542,7 +4597,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ { unsigned int c; s += ImTextCharFromUtf8(&c, s, NULL); - if (!InputTextFilterCharacter(&c, flags, callback, callback_user_data, ImGuiInputSource_Clipboard)) + if (!InputTextFilterCharacter(&g, &c, flags, callback, callback_user_data, ImGuiInputSource_Clipboard)) continue; clipboard_filtered[clipboard_filtered_len++] = (ImWchar)c; } @@ -4571,9 +4626,11 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ if (flags & ImGuiInputTextFlags_EscapeClearsAll) { // Clear input + IM_ASSERT(buf[0] != 0); apply_new_text = ""; apply_new_text_length = 0; - STB_TEXTEDIT_CHARTYPE empty_string; + value_changed = true; + IMSTB_TEXTEDIT_CHARTYPE empty_string; stb_textedit_replace(state, &state->Stb, &empty_string, 0); } else if (strcmp(buf, state->InitialTextA.Data) != 0) @@ -4601,9 +4658,12 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ ImTextStrToUtf8(state->TextA.Data, state->TextA.Size, state->TextW.Data, NULL); } - // When using 'ImGuiInputTextFlags_EnterReturnsTrue' as a special case we reapply the live buffer back to the input buffer before clearing ActiveId, even though strictly speaking it wasn't modified on this frame. + // When using 'ImGuiInputTextFlags_EnterReturnsTrue' as a special case we reapply the live buffer back to the input buffer + // before clearing ActiveId, even though strictly speaking it wasn't modified on this frame. // If we didn't do that, code like InputInt() with ImGuiInputTextFlags_EnterReturnsTrue would fail. - // This also allows the user to use InputText() with ImGuiInputTextFlags_EnterReturnsTrue without maintaining any user-side storage (please note that if you use this property along ImGuiInputTextFlags_CallbackResize you can end up with your temporary string object unnecessarily allocating once a frame, either store your string data, either if you don't then don't use ImGuiInputTextFlags_CallbackResize). + // This also allows the user to use InputText() with ImGuiInputTextFlags_EnterReturnsTrue without maintaining any user-side storage + // (please note that if you use this property along ImGuiInputTextFlags_CallbackResize you can end up with your temporary string object + // unnecessarily allocating once a frame, either store your string data, either if you don't then don't use ImGuiInputTextFlags_CallbackResize). const bool apply_edit_back_to_user_buffer = !revert_edit || (validated && (flags & ImGuiInputTextFlags_EnterReturnsTrue) != 0); if (apply_edit_back_to_user_buffer) { @@ -4679,7 +4739,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ if (callback_data.SelectionEnd != utf8_selection_end || buf_dirty) { state->Stb.select_end = (callback_data.SelectionEnd == callback_data.SelectionStart) ? state->Stb.select_start : ImTextCountCharsFromUtf8(callback_data.Buf, callback_data.Buf + callback_data.SelectionEnd); } if (buf_dirty) { - IM_ASSERT((flags & ImGuiInputTextFlags_ReadOnly) == 0); + IM_ASSERT(!is_readonly); IM_ASSERT(callback_data.BufTextLen == (int)strlen(callback_data.Buf)); // You need to maintain BufTextLen if you change the text! InputTextReconcileUndoStateAfterUserCallback(state, callback_data.Buf, callback_data.BufTextLen); // FIXME: Move the rest of this block inside function and rename to InputTextReconcileStateAfterUserCallback() ? if (callback_data.BufTextLen > backup_current_text_length && is_resizable) @@ -4704,11 +4764,11 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ // Handle reapplying final data on deactivation (see InputTextDeactivateHook() for details) if (g.InputTextDeactivatedState.ID == id) { - if (g.ActiveId != id && IsItemDeactivatedAfterEdit() && !is_readonly) + if (g.ActiveId != id && IsItemDeactivatedAfterEdit() && !is_readonly && strcmp(g.InputTextDeactivatedState.TextA.Data, buf) != 0) { apply_new_text = g.InputTextDeactivatedState.TextA.Data; apply_new_text_length = g.InputTextDeactivatedState.TextA.Size - 1; - value_changed |= (strcmp(g.InputTextDeactivatedState.TextA.Data, buf) != 0); + value_changed = true; //IMGUI_DEBUG_LOG("InputText(): apply Deactivated data for 0x%08X: \"%.*s\".\n", id, apply_new_text_length, apply_new_text); } g.InputTextDeactivatedState.ID = 0; @@ -4850,9 +4910,9 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ const float scroll_increment_x = inner_size.x * 0.25f; const float visible_width = inner_size.x - style.FramePadding.x; if (cursor_offset.x < state->ScrollX) - state->ScrollX = IM_FLOOR(ImMax(0.0f, cursor_offset.x - scroll_increment_x)); + state->ScrollX = IM_TRUNC(ImMax(0.0f, cursor_offset.x - scroll_increment_x)); else if (cursor_offset.x - visible_width >= state->ScrollX) - state->ScrollX = IM_FLOOR(cursor_offset.x - visible_width + scroll_increment_x); + state->ScrollX = IM_TRUNC(cursor_offset.x - visible_width + scroll_increment_x); } else { @@ -4902,7 +4962,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ else { ImVec2 rect_size = InputTextCalcTextSizeW(&g, p, text_selected_end, &p, NULL, true); - if (rect_size.x <= 0.0f) rect_size.x = IM_FLOOR(g.Font->GetCharAdvance((ImWchar)' ') * 0.50f); // So we can see selected empty lines + if (rect_size.x <= 0.0f) rect_size.x = IM_TRUNC(g.Font->GetCharAdvance((ImWchar)' ') * 0.50f); // So we can see selected empty lines ImRect rect(rect_pos + ImVec2(0.0f, bg_offy_up - g.FontSize), rect_pos + ImVec2(rect_size.x, bg_offy_dn)); rect.ClipWith(clip_rect); if (rect.Overlaps(clip_rect)) @@ -4925,7 +4985,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ { state->CursorAnim += io.DeltaTime; bool cursor_is_visible = (!g.IO.ConfigInputTextCursorBlink) || (state->CursorAnim <= 0.0f) || ImFmod(state->CursorAnim, 1.20f) <= 0.80f; - ImVec2 cursor_screen_pos = ImFloor(draw_pos + cursor_offset - draw_scroll); + ImVec2 cursor_screen_pos = ImTrunc(draw_pos + cursor_offset - draw_scroll); ImRect cursor_screen_rect(cursor_screen_pos.x, cursor_screen_pos.y - g.FontSize + 0.5f, cursor_screen_pos.x + 1.0f, cursor_screen_pos.y - 1.5f); if (cursor_is_visible && cursor_screen_rect.Overlaps(clip_rect)) draw_window->DrawList->AddLine(cursor_screen_rect.Min, cursor_screen_rect.GetBL(), GetColorU32(ImGuiCol_Text)); @@ -4936,6 +4996,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ g.PlatformImeData.WantVisible = true; g.PlatformImeData.InputPos = ImVec2(cursor_screen_pos.x - 1.0f, cursor_screen_pos.y - g.FontSize); g.PlatformImeData.InputLineHeight = g.FontSize; + g.PlatformImeViewport = window->Viewport->ID; } } } @@ -4963,11 +5024,9 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ { // For focus requests to work on our multiline we need to ensure our child ItemAdd() call specifies the ImGuiItemFlags_Inputable (ref issue #4761)... Dummy(ImVec2(text_size.x, text_size.y + style.FramePadding.y)); - ImGuiItemFlags backup_item_flags = g.CurrentItemFlags; - g.CurrentItemFlags |= ImGuiItemFlags_Inputable | ImGuiItemFlags_NoTabStop; + g.NextItemData.ItemFlags |= ImGuiItemFlags_Inputable | ImGuiItemFlags_NoTabStop; EndChild(); item_data_backup.StatusFlags |= (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredWindow); - g.CurrentItemFlags = backup_item_flags; // ...and then we need to undo the group overriding last item data, which gets a bit messy as EndGroup() tries to forward scrollbar being active... // FIXME: This quite messy/tricky, should attempt to get rid of the child window. @@ -5011,10 +5070,10 @@ void ImGui::DebugNodeInputTextState(ImGuiInputTextState* state) Text("CurLenW: %d, CurLenA: %d, Cursor: %d, Selection: %d..%d", state->CurLenW, state->CurLenA, stb_state->cursor, stb_state->select_start, stb_state->select_end); Text("has_preferred_x: %d (%.2f)", stb_state->has_preferred_x, stb_state->preferred_x); Text("undo_point: %d, redo_point: %d, undo_char_point: %d, redo_char_point: %d", undo_state->undo_point, undo_state->redo_point, undo_state->undo_char_point, undo_state->redo_char_point); - if (BeginChild("undopoints", ImVec2(0.0f, GetTextLineHeight() * 15), true)) // Visualize undo state + if (BeginChild("undopoints", ImVec2(0.0f, GetTextLineHeight() * 10), ImGuiChildFlags_Border | ImGuiChildFlags_ResizeY)) // Visualize undo state { PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0)); - for (int n = 0; n < STB_TEXTEDIT_UNDOSTATECOUNT; n++) + for (int n = 0; n < IMSTB_TEXTEDIT_UNDOSTATECOUNT; n++) { ImStb::StbUndoRecord* undo_rec = &undo_state->undo_rec[n]; const char undo_rec_type = (n < undo_state->undo_point) ? 'u' : (n >= undo_state->redo_point) ? 'r' : ' '; @@ -5096,10 +5155,8 @@ bool ImGui::ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flag ImGuiContext& g = *GImGui; const ImGuiStyle& style = g.Style; const float square_sz = GetFrameHeight(); - const float w_full = CalcItemWidth(); - const float w_button = (flags & ImGuiColorEditFlags_NoSmallPreview) ? 0.0f : (square_sz + style.ItemInnerSpacing.x); - const float w_inputs = w_full - w_button; const char* label_display_end = FindRenderedTextEnd(label); + float w_full = CalcItemWidth(); g.NextItemData.ClearFlags(); BeginGroup(); @@ -5133,6 +5190,9 @@ bool ImGui::ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flag const bool alpha = (flags & ImGuiColorEditFlags_NoAlpha) == 0; const bool hdr = (flags & ImGuiColorEditFlags_HDR) != 0; const int components = alpha ? 4 : 3; + const float w_button = (flags & ImGuiColorEditFlags_NoSmallPreview) ? 0.0f : (square_sz + style.ItemInnerSpacing.x); + const float w_inputs = ImMax(w_full - w_button, 1.0f); + w_full = w_inputs + w_button; // Convert to the formats we need float f[4] = { col[0], col[1], col[2], alpha ? col[3] : 1.0f }; @@ -5156,10 +5216,9 @@ bool ImGui::ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flag if ((flags & (ImGuiColorEditFlags_DisplayRGB | ImGuiColorEditFlags_DisplayHSV)) != 0 && (flags & ImGuiColorEditFlags_NoInputs) == 0) { // RGB/HSV 0..255 Sliders - const float w_item_one = ImMax(1.0f, IM_FLOOR((w_inputs - (style.ItemInnerSpacing.x) * (components - 1)) / (float)components)); - const float w_item_last = ImMax(1.0f, IM_FLOOR(w_inputs - (w_item_one + style.ItemInnerSpacing.x) * (components - 1))); + const float w_items = w_inputs - style.ItemInnerSpacing.x * (components - 1); - const bool hide_prefix = (w_item_one <= CalcTextSize((flags & ImGuiColorEditFlags_Float) ? "M:0.000" : "M:000").x); + const bool hide_prefix = (IM_TRUNC(w_items / components) <= CalcTextSize((flags & ImGuiColorEditFlags_Float) ? "M:0.000" : "M:000").x); static const char* ids[4] = { "##X", "##Y", "##Z", "##W" }; static const char* fmt_table_int[3][4] = { @@ -5175,11 +5234,14 @@ bool ImGui::ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flag }; const int fmt_idx = hide_prefix ? 0 : (flags & ImGuiColorEditFlags_DisplayHSV) ? 2 : 1; + float prev_split = 0.0f; for (int n = 0; n < components; n++) { if (n > 0) SameLine(0, style.ItemInnerSpacing.x); - SetNextItemWidth((n + 1 < components) ? w_item_one : w_item_last); + float next_split = IM_TRUNC(w_items * (n + 1) / components); + SetNextItemWidth(ImMax(next_split - prev_split, 1.0f)); + prev_split = next_split; // FIXME: When ImGuiColorEditFlags_HDR flag is passed HS values snap in weird ways when SV values go below 0. if (flags & ImGuiColorEditFlags_Float) @@ -5204,7 +5266,7 @@ bool ImGui::ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flag else ImFormatString(buf, IM_ARRAYSIZE(buf), "#%02X%02X%02X", ImClamp(i[0], 0, 255), ImClamp(i[1], 0, 255), ImClamp(i[2], 0, 255)); SetNextItemWidth(w_inputs); - if (InputText("##Text", buf, IM_ARRAYSIZE(buf), ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_CharsUppercase)) + if (InputText("##Text", buf, IM_ARRAYSIZE(buf), ImGuiInputTextFlags_CharsUppercase)) { value_changed = true; char* p = buf; @@ -5302,7 +5364,7 @@ bool ImGui::ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flag // Drag and Drop Target // NB: The flag test is merely an optional micro-optimization, BeginDragDropTarget() does the same test. - if ((g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredRect) && !(flags & ImGuiColorEditFlags_NoDragDrop) && BeginDragDropTarget()) + if ((g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredRect) && !(g.LastItemData.InFlags & ImGuiItemFlags_ReadOnly) && !(flags & ImGuiColorEditFlags_NoDragDrop) && BeginDragDropTarget()) { bool accepted_drag_drop = false; if (const ImGuiPayload* payload = AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_3F)) @@ -5367,6 +5429,7 @@ bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags fl ImGuiIO& io = g.IO; const float width = CalcItemWidth(); + const bool is_readonly = ((g.NextItemData.ItemFlags | g.CurrentItemFlags) & ImGuiItemFlags_ReadOnly) != 0; g.NextItemData.ClearFlags(); PushID(label); @@ -5401,7 +5464,7 @@ bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags fl float sv_picker_size = ImMax(bars_width * 1, width - (alpha_bar ? 2 : 1) * (bars_width + style.ItemInnerSpacing.x)); // Saturation/Value picking box float bar0_pos_x = picker_pos.x + sv_picker_size + style.ItemInnerSpacing.x; float bar1_pos_x = bar0_pos_x + bars_width + style.ItemInnerSpacing.x; - float bars_triangles_half_sz = IM_FLOOR(bars_width * 0.20f); + float bars_triangles_half_sz = IM_TRUNC(bars_width * 0.20f); float backup_initial_col[4]; memcpy(backup_initial_col, col, components * sizeof(float)); @@ -5437,7 +5500,7 @@ bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags fl { // Hue wheel + SV triangle logic InvisibleButton("hsv", ImVec2(sv_picker_size + style.ItemInnerSpacing.x + bars_width, sv_picker_size)); - if (IsItemActive()) + if (IsItemActive() && !is_readonly) { ImVec2 initial_off = g.IO.MouseClickedPos[0] - wheel_center; ImVec2 current_off = g.IO.MousePos - wheel_center; @@ -5472,7 +5535,7 @@ bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags fl { // SV rectangle logic InvisibleButton("sv", ImVec2(sv_picker_size, sv_picker_size)); - if (IsItemActive()) + if (IsItemActive() && !is_readonly) { S = ImSaturate((io.MousePos.x - picker_pos.x) / (sv_picker_size - 1)); V = 1.0f - ImSaturate((io.MousePos.y - picker_pos.y) / (sv_picker_size - 1)); @@ -5485,7 +5548,7 @@ bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags fl // Hue bar logic SetCursorScreenPos(ImVec2(bar0_pos_x, picker_pos.y)); InvisibleButton("hue", ImVec2(bars_width, sv_picker_size)); - if (IsItemActive()) + if (IsItemActive() && !is_readonly) { H = ImSaturate((io.MousePos.y - picker_pos.y) / (sv_picker_size - 1)); value_changed = value_changed_h = true; @@ -5569,7 +5632,7 @@ bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags fl if ((flags & ImGuiColorEditFlags_NoInputs) == 0) { PushItemWidth((alpha_bar ? bar1_pos_x : bar0_pos_x) + bars_width - picker_pos.x); - ImGuiColorEditFlags sub_flags_to_forward = ImGuiColorEditFlags_DataTypeMask_ | ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_NoOptions | ImGuiColorEditFlags_NoSmallPreview | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf; + ImGuiColorEditFlags sub_flags_to_forward = ImGuiColorEditFlags_DataTypeMask_ | ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_NoOptions | ImGuiColorEditFlags_NoTooltip | ImGuiColorEditFlags_NoSmallPreview | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf; ImGuiColorEditFlags sub_flags = (flags & sub_flags_to_forward) | ImGuiColorEditFlags_NoPicker; if (flags & ImGuiColorEditFlags_DisplayRGB || (flags & ImGuiColorEditFlags_DisplayMask_) == 0) if (ColorEdit4("##rgb", col, sub_flags | ImGuiColorEditFlags_DisplayRGB)) @@ -5691,7 +5754,7 @@ bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags fl } // Render cursor/preview circle (clamp S/V within 0..1 range because floating points colors may lead HSV values to be out of range) - float sv_cursor_rad = value_changed_sv ? 10.0f : 6.0f; + float sv_cursor_rad = value_changed_sv ? wheel_thickness * 0.55f : wheel_thickness * 0.40f; int sv_cursor_segments = draw_list->_CalcCircleAutoSegmentCount(sv_cursor_rad); // Lock segment count so the +1 one matches others. draw_list->AddCircleFilled(sv_cursor_pos, sv_cursor_rad, user_col32_striped_of_alpha, sv_cursor_segments); draw_list->AddCircle(sv_cursor_pos, sv_cursor_rad + 1, col_midgrey, sv_cursor_segments); @@ -5801,7 +5864,7 @@ bool ImGui::ColorButton(const char* desc_id, const ImVec4& col, ImGuiColorEditFl } // Tooltip - if (!(flags & ImGuiColorEditFlags_NoTooltip) && hovered) + if (!(flags & ImGuiColorEditFlags_NoTooltip) && hovered && IsItemHovered(ImGuiHoveredFlags_ForTooltip)) ColorTooltip(desc_id, &col.x, flags & (ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf)); return pressed; @@ -5831,7 +5894,7 @@ void ImGui::ColorTooltip(const char* text, const float* col, ImGuiColorEditFlags { ImGuiContext& g = *GImGui; - if (!BeginTooltipEx(ImGuiTooltipFlags_OverridePreviousTooltip, ImGuiWindowFlags_None)) + if (!BeginTooltipEx(ImGuiTooltipFlags_OverridePrevious, ImGuiWindowFlags_None)) return; const char* text_end = text ? FindRenderedTextEnd(text, NULL) : text; if (text_end > text) @@ -5869,6 +5932,7 @@ void ImGui::ColorEditOptionsPopup(const float* col, ImGuiColorEditFlags flags) if ((!allow_opt_inputs && !allow_opt_datatype) || !BeginPopup("context")) return; ImGuiContext& g = *GImGui; + g.LockMarkEdited++; ImGuiColorEditFlags opts = g.ColorEditOptions; if (allow_opt_inputs) { @@ -5911,6 +5975,7 @@ void ImGui::ColorEditOptionsPopup(const float* col, ImGuiColorEditFlags flags) g.ColorEditOptions = opts; EndPopup(); + g.LockMarkEdited--; } void ImGui::ColorPickerOptionsPopup(const float* ref_col, ImGuiColorEditFlags flags) @@ -5920,6 +5985,7 @@ void ImGui::ColorPickerOptionsPopup(const float* ref_col, ImGuiColorEditFlags fl if ((!allow_opt_picker && !allow_opt_alpha_bar) || !BeginPopup("context")) return; ImGuiContext& g = *GImGui; + g.LockMarkEdited++; if (allow_opt_picker) { ImVec2 picker_size(g.FontSize * 8, ImMax(g.FontSize * 8 - (GetFrameHeight() + g.Style.ItemInnerSpacing.x), 1.0f)); // FIXME: Picker size copied from main picker function @@ -5949,6 +6015,7 @@ void ImGui::ColorPickerOptionsPopup(const float* ref_col, ImGuiColorEditFlags fl CheckboxFlags("Alpha Bar", &g.ColorEditOptions, ImGuiColorEditFlags_AlphaBar); } EndPopup(); + g.LockMarkEdited--; } //------------------------------------------------------------------------- @@ -6121,42 +6188,69 @@ bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* l // We vertically grow up to current line height up the typical widget height. const float frame_height = ImMax(ImMin(window->DC.CurrLineSize.y, g.FontSize + style.FramePadding.y * 2), label_size.y + padding.y * 2); + const bool span_all_columns = (flags & ImGuiTreeNodeFlags_SpanAllColumns) != 0 && (g.CurrentTable != NULL); ImRect frame_bb; - frame_bb.Min.x = (flags & ImGuiTreeNodeFlags_SpanFullWidth) ? window->WorkRect.Min.x : window->DC.CursorPos.x; + frame_bb.Min.x = span_all_columns ? window->ParentWorkRect.Min.x : (flags & ImGuiTreeNodeFlags_SpanFullWidth) ? window->WorkRect.Min.x : window->DC.CursorPos.x; frame_bb.Min.y = window->DC.CursorPos.y; - frame_bb.Max.x = window->WorkRect.Max.x; + frame_bb.Max.x = span_all_columns ? window->ParentWorkRect.Max.x : window->WorkRect.Max.x; frame_bb.Max.y = window->DC.CursorPos.y + frame_height; if (display_frame) { // Framed header expand a little outside the default padding, to the edge of InnerClipRect // (FIXME: May remove this at some point and make InnerClipRect align with WindowPadding.x instead of WindowPadding.x*0.5f) - frame_bb.Min.x -= IM_FLOOR(window->WindowPadding.x * 0.5f - 1.0f); - frame_bb.Max.x += IM_FLOOR(window->WindowPadding.x * 0.5f); + frame_bb.Min.x -= IM_TRUNC(window->WindowPadding.x * 0.5f - 1.0f); + frame_bb.Max.x += IM_TRUNC(window->WindowPadding.x * 0.5f); } - const float text_offset_x = g.FontSize + (display_frame ? padding.x * 3 : padding.x * 2); // Collapser arrow width + Spacing + const float text_offset_x = g.FontSize + (display_frame ? padding.x * 3 : padding.x * 2); // Collapsing arrow width + Spacing const float text_offset_y = ImMax(padding.y, window->DC.CurrLineTextBaseOffset); // Latch before ItemSize changes it - const float text_width = g.FontSize + (label_size.x > 0.0f ? label_size.x + padding.x * 2 : 0.0f); // Include collapser + const float text_width = g.FontSize + (label_size.x > 0.0f ? label_size.x + padding.x * 2 : 0.0f); // Include collapsing ImVec2 text_pos(window->DC.CursorPos.x + text_offset_x, window->DC.CursorPos.y + text_offset_y); ItemSize(ImVec2(text_width, frame_height), padding.y); // For regular tree nodes, we arbitrary allow to click past 2 worth of ItemSpacing ImRect interact_bb = frame_bb; - if (!display_frame && (flags & (ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_SpanFullWidth)) == 0) + if (!display_frame && (flags & (ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_SpanFullWidth | ImGuiTreeNodeFlags_SpanAllColumns)) == 0) interact_bb.Max.x = frame_bb.Min.x + text_width + style.ItemSpacing.x * 2.0f; - // Store a flag for the current depth to tell if we will allow closing this node when navigating one of its child. - // For this purpose we essentially compare if g.NavIdIsAlive went from 0 to 1 between TreeNode() and TreePop(). - // This is currently only support 32 level deep and we are fine with (1 << Depth) overflowing into a zero. - const bool is_leaf = (flags & ImGuiTreeNodeFlags_Leaf) != 0; - bool is_open = TreeNodeUpdateNextOpen(id, flags); - if (is_open && !g.NavIdIsAlive && (flags & ImGuiTreeNodeFlags_NavLeftJumpsBackHere) && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen)) - window->DC.TreeJumpToParentOnPopMask |= (1 << window->DC.TreeDepth); + // Modify ClipRect for the ItemAdd(), faster than doing a PushColumnsBackground/PushTableBackgroundChannel for every Selectable.. + const float backup_clip_rect_min_x = window->ClipRect.Min.x; + const float backup_clip_rect_max_x = window->ClipRect.Max.x; + if (span_all_columns) + { + window->ClipRect.Min.x = window->ParentWorkRect.Min.x; + window->ClipRect.Max.x = window->ParentWorkRect.Max.x; + } + // Compute open and multi-select states before ItemAdd() as it clear NextItem data. + bool is_open = TreeNodeUpdateNextOpen(id, flags); bool item_add = ItemAdd(interact_bb, id); g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasDisplayRect; g.LastItemData.DisplayRect = frame_bb; + if (span_all_columns) + { + window->ClipRect.Min.x = backup_clip_rect_min_x; + window->ClipRect.Max.x = backup_clip_rect_max_x; + } + + // If a NavLeft request is happening and ImGuiTreeNodeFlags_NavLeftJumpsBackHere enabled: + // Store data for the current depth to allow returning to this node from any child item. + // For this purpose we essentially compare if g.NavIdIsAlive went from 0 to 1 between TreeNode() and TreePop(). + // It will become tempting to enable ImGuiTreeNodeFlags_NavLeftJumpsBackHere by default or move it to ImGuiStyle. + // Currently only supports 32 level deep and we are fine with (1 << Depth) overflowing into a zero, easy to increase. + if (is_open && !g.NavIdIsAlive && (flags & ImGuiTreeNodeFlags_NavLeftJumpsBackHere) && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen)) + if (g.NavMoveDir == ImGuiDir_Left && g.NavWindow == window && NavMoveRequestButNoResultYet()) + { + g.NavTreeNodeStack.resize(g.NavTreeNodeStack.Size + 1); + ImGuiNavTreeNodeData* nav_tree_node_data = &g.NavTreeNodeStack.back(); + nav_tree_node_data->ID = id; + nav_tree_node_data->InFlags = g.LastItemData.InFlags; + nav_tree_node_data->NavRect = g.LastItemData.NavRect; + window->DC.TreeJumpToParentOnPopMask |= (1 << window->DC.TreeDepth); + } + + const bool is_leaf = (flags & ImGuiTreeNodeFlags_Leaf) != 0; if (!item_add) { if (is_open && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen)) @@ -6165,9 +6259,16 @@ bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* l return is_open; } + if (span_all_columns) + { + TablePushBackgroundChannel(); + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasClipRect; + g.LastItemData.ClipRect = window->ClipRect; + } + ImGuiButtonFlags button_flags = ImGuiTreeNodeFlags_None; - if (flags & ImGuiTreeNodeFlags_AllowItemOverlap) - button_flags |= ImGuiButtonFlags_AllowItemOverlap; + if ((flags & ImGuiTreeNodeFlags_AllowOverlap) || (g.LastItemData.InFlags & ImGuiItemFlags_AllowOverlap)) + button_flags |= ImGuiButtonFlags_AllowOverlap; if (!is_leaf) button_flags |= ImGuiButtonFlags_PressedOnDragDropHold; @@ -6240,8 +6341,6 @@ bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* l g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledOpen; } } - if (flags & ImGuiTreeNodeFlags_AllowItemOverlap) - SetItemAllowOverlap(); // In this branch, TreeNodeBehavior() cannot toggle the selection so this will never trigger. if (selected != was_selected) //-V547 @@ -6249,7 +6348,7 @@ bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* l // Render const ImU32 text_col = GetColorU32(ImGuiCol_Text); - ImGuiNavHighlightFlags nav_highlight_flags = ImGuiNavHighlightFlags_TypeThin; + ImGuiNavHighlightFlags nav_highlight_flags = ImGuiNavHighlightFlags_Compact; if (display_frame) { // Framed type @@ -6259,15 +6358,14 @@ bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* l if (flags & ImGuiTreeNodeFlags_Bullet) RenderBullet(window->DrawList, ImVec2(text_pos.x - text_offset_x * 0.60f, text_pos.y + g.FontSize * 0.5f), text_col); else if (!is_leaf) - RenderArrow(window->DrawList, ImVec2(text_pos.x - text_offset_x + padding.x, text_pos.y), text_col, is_open ? ImGuiDir_Down : ImGuiDir_Right, 1.0f); + RenderArrow(window->DrawList, ImVec2(text_pos.x - text_offset_x + padding.x, text_pos.y), text_col, is_open ? ((flags & ImGuiTreeNodeFlags_UpsideDownArrow) ? ImGuiDir_Up : ImGuiDir_Down) : ImGuiDir_Right, 1.0f); else // Leaf without bullet, left-adjusted text - text_pos.x -= text_offset_x; + text_pos.x -= text_offset_x -padding.x; if (flags & ImGuiTreeNodeFlags_ClipLabelForTrailingButton) frame_bb.Max.x -= g.FontSize + style.FramePadding.x; if (g.LogEnabled) LogSetNextTextDecoration("###", "###"); - RenderTextClipped(text_pos, frame_bb.Max, label, label_end, &label_size); } else { @@ -6281,12 +6379,20 @@ bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* l if (flags & ImGuiTreeNodeFlags_Bullet) RenderBullet(window->DrawList, ImVec2(text_pos.x - text_offset_x * 0.5f, text_pos.y + g.FontSize * 0.5f), text_col); else if (!is_leaf) - RenderArrow(window->DrawList, ImVec2(text_pos.x - text_offset_x + padding.x, text_pos.y + g.FontSize * 0.15f), text_col, is_open ? ImGuiDir_Down : ImGuiDir_Right, 0.70f); + RenderArrow(window->DrawList, ImVec2(text_pos.x - text_offset_x + padding.x, text_pos.y + g.FontSize * 0.15f), text_col, is_open ? ((flags & ImGuiTreeNodeFlags_UpsideDownArrow) ? ImGuiDir_Up : ImGuiDir_Down) : ImGuiDir_Right, 0.70f); if (g.LogEnabled) LogSetNextTextDecoration(">", NULL); - RenderText(text_pos, label, label_end, false); } + if (span_all_columns) + TablePopBackgroundChannel(); + + // Label + if (display_frame) + RenderTextClipped(text_pos, frame_bb.Max, label, label_end, &label_size); + else + RenderText(text_pos, label, label_end, false); + if (is_open && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen)) TreePushOverrideID(id); IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | (is_leaf ? 0 : ImGuiItemStatusFlags_Openable) | (is_open ? ImGuiItemStatusFlags_Opened : 0)); @@ -6328,12 +6434,14 @@ void ImGui::TreePop() ImU32 tree_depth_mask = (1 << window->DC.TreeDepth); // Handle Left arrow to move to parent tree node (when ImGuiTreeNodeFlags_NavLeftJumpsBackHere is enabled) - if (g.NavMoveDir == ImGuiDir_Left && g.NavWindow == window && NavMoveRequestButNoResultYet()) - if (g.NavIdIsAlive && (window->DC.TreeJumpToParentOnPopMask & tree_depth_mask)) - { - SetNavID(window->IDStack.back(), g.NavLayer, 0, ImRect()); - NavMoveRequestCancel(); - } + if (window->DC.TreeJumpToParentOnPopMask & tree_depth_mask) // Only set during request + { + ImGuiNavTreeNodeData* nav_tree_node_data = &g.NavTreeNodeStack.back(); + IM_ASSERT(nav_tree_node_data->ID == window->IDStack.back()); + if (g.NavIdIsAlive && g.NavMoveDir == ImGuiDir_Left && g.NavWindow == window && NavMoveRequestButNoResultYet()) + NavMoveRequestResolveWithPastTreeNode(&g.NavMoveResultLocal, nav_tree_node_data); + g.NavTreeNodeStack.pop_back(); + } window->DC.TreeJumpToParentOnPopMask &= tree_depth_mask - 1; IM_ASSERT(window->IDStack.Size > 1); // There should always be 1 element in the IDStack (pushed during window creation). If this triggers you called TreePop/PopID too much. @@ -6385,7 +6493,7 @@ bool ImGui::CollapsingHeader(const char* label, bool* p_visible, ImGuiTreeNodeFl ImGuiID id = window->GetID(label); flags |= ImGuiTreeNodeFlags_CollapsingHeader; if (p_visible) - flags |= ImGuiTreeNodeFlags_AllowItemOverlap | ImGuiTreeNodeFlags_ClipLabelForTrailingButton; + flags |= ImGuiTreeNodeFlags_AllowOverlap | (ImGuiTreeNodeFlags)ImGuiTreeNodeFlags_ClipLabelForTrailingButton; bool is_open = TreeNodeBehavior(id, flags, label); if (p_visible != NULL) { @@ -6395,8 +6503,8 @@ bool ImGui::CollapsingHeader(const char* label, bool* p_visible, ImGuiTreeNodeFl ImGuiContext& g = *GImGui; ImGuiLastItemData last_item_backup = g.LastItemData; float button_size = g.FontSize; - float button_x = ImMax(g.LastItemData.Rect.Min.x, g.LastItemData.Rect.Max.x - g.Style.FramePadding.x * 2.0f - button_size); - float button_y = g.LastItemData.Rect.Min.y; + float button_x = ImMax(g.LastItemData.Rect.Min.x, g.LastItemData.Rect.Max.x - g.Style.FramePadding.x - button_size); + float button_y = g.LastItemData.Rect.Min.y + g.Style.FramePadding.y; ImGuiID close_button_id = GetIDWithSeed("#CLOSE", NULL, id); if (CloseButton(close_button_id, ImVec2(button_x, button_y))) *p_visible = false; @@ -6414,7 +6522,7 @@ bool ImGui::CollapsingHeader(const char* label, bool* p_visible, ImGuiTreeNodeFl // Tip: pass a non-visible label (e.g. "##hello") then you can use the space to draw other text or image. // But you need to make sure the ID is unique, e.g. enclose calls in PushID/PopID or use ##unique_id. -// With this scheme, ImGuiSelectableFlags_SpanAllColumns and ImGuiSelectableFlags_AllowItemOverlap are also frequently used flags. +// With this scheme, ImGuiSelectableFlags_SpanAllColumns and ImGuiSelectableFlags_AllowOverlap are also frequently used flags. // FIXME: Selectable() with (size.x == 0.0f) and (SelectableTextAlign.x > 0.0f) followed by SameLine() is currently not supported. bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags flags, const ImVec2& size_arg) { @@ -6451,8 +6559,8 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl { const float spacing_x = span_all_columns ? 0.0f : style.ItemSpacing.x; const float spacing_y = style.ItemSpacing.y; - const float spacing_L = IM_FLOOR(spacing_x * 0.50f); - const float spacing_U = IM_FLOOR(spacing_y * 0.50f); + const float spacing_L = IM_TRUNC(spacing_x * 0.50f); + const float spacing_U = IM_TRUNC(spacing_y * 0.50f); bb.Min.x -= spacing_L; bb.Min.y -= spacing_U; bb.Max.x += (spacing_x - spacing_L); @@ -6460,7 +6568,7 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl } //if (g.IO.KeyCtrl) { GetForegroundDrawList()->AddRect(bb.Min, bb.Max, IM_COL32(0, 255, 0, 255)); } - // Modify ClipRect for the ItemAdd(), faster than doing a PushColumnsBackground/PushTableBackground for every Selectable.. + // Modify ClipRect for the ItemAdd(), faster than doing a PushColumnsBackground/PushTableBackgroundChannel for every Selectable.. const float backup_clip_rect_min_x = window->ClipRect.Min.x; const float backup_clip_rect_max_x = window->ClipRect.Max.x; if (span_all_columns) @@ -6486,10 +6594,15 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl // FIXME: We can standardize the behavior of those two, we could also keep the fast path of override ClipRect + full push on render only, // which would be advantageous since most selectable are not selected. - if (span_all_columns && window->DC.CurrentColumns) - PushColumnsBackground(); - else if (span_all_columns && g.CurrentTable) - TablePushBackgroundChannel(); + if (span_all_columns) + { + if (g.CurrentTable) + TablePushBackgroundChannel(); + else if (window->DC.CurrentColumns) + PushColumnsBackground(); + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasClipRect; + g.LastItemData.ClipRect = window->ClipRect; + } // We use NoHoldingActiveID on menus so user can click and _hold_ on a menu then drag to browse child entries ImGuiButtonFlags button_flags = 0; @@ -6498,7 +6611,7 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl if (flags & ImGuiSelectableFlags_SelectOnClick) { button_flags |= ImGuiButtonFlags_PressedOnClick; } if (flags & ImGuiSelectableFlags_SelectOnRelease) { button_flags |= ImGuiButtonFlags_PressedOnRelease; } if (flags & ImGuiSelectableFlags_AllowDoubleClick) { button_flags |= ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnDoubleClick; } - if (flags & ImGuiSelectableFlags_AllowItemOverlap) { button_flags |= ImGuiButtonFlags_AllowItemOverlap; } + if ((flags & ImGuiSelectableFlags_AllowOverlap) || (g.LastItemData.InFlags & ImGuiItemFlags_AllowOverlap)) { button_flags |= ImGuiButtonFlags_AllowOverlap; } const bool was_selected = selected; bool hovered, held; @@ -6527,9 +6640,6 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl if (pressed) MarkItemEdited(id); - if (flags & ImGuiSelectableFlags_AllowItemOverlap) - SetItemAllowOverlap(); - // In this branch, Selectable() cannot toggle the selection so this will never trigger. if (selected != was_selected) //-V547 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledSelection; @@ -6540,12 +6650,16 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl const ImU32 col = GetColorU32((held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header); RenderFrame(bb.Min, bb.Max, col, false, 0.0f); } - RenderNavHighlight(bb, id, ImGuiNavHighlightFlags_TypeThin | ImGuiNavHighlightFlags_NoRounding); + if (g.NavId == id) + RenderNavHighlight(bb, id, ImGuiNavHighlightFlags_Compact | ImGuiNavHighlightFlags_NoRounding); - if (span_all_columns && window->DC.CurrentColumns) - PopColumnsBackground(); - else if (span_all_columns && g.CurrentTable) - TablePopBackgroundChannel(); + if (span_all_columns) + { + if (g.CurrentTable) + TablePopBackgroundChannel(); + else if (window->DC.CurrentColumns) + PopColumnsBackground(); + } RenderTextClipped(text_min, text_max, label, NULL, &label_size, style.SelectableTextAlign, &bb); @@ -6570,6 +6684,212 @@ bool ImGui::Selectable(const char* label, bool* p_selected, ImGuiSelectableFlags return false; } + +//------------------------------------------------------------------------- +// [SECTION] Widgets: Typing-Select support +//------------------------------------------------------------------------- + +// [Experimental] Currently not exposed in public API. +// Consume character inputs and return search request, if any. +// This would typically only be called on the focused window or location you want to grab inputs for, e.g. +// if (ImGui::IsWindowFocused(...)) +// if (ImGuiTypingSelectRequest* req = ImGui::GetTypingSelectRequest()) +// focus_idx = ImGui::TypingSelectFindMatch(req, my_items.size(), [](void*, int n) { return my_items[n]->Name; }, &my_items, -1); +// However the code is written in a way where calling it from multiple locations is safe (e.g. to obtain buffer). +ImGuiTypingSelectRequest* ImGui::GetTypingSelectRequest(ImGuiTypingSelectFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiTypingSelectState* data = &g.TypingSelectState; + ImGuiTypingSelectRequest* out_request = &data->Request; + + // Clear buffer + const float TYPING_SELECT_RESET_TIMER = 1.80f; // FIXME: Potentially move to IO config. + const int TYPING_SELECT_SINGLE_CHAR_COUNT_FOR_LOCK = 4; // Lock single char matching when repeating same char 4 times + if (data->SearchBuffer[0] != 0) + { + bool clear_buffer = false; + clear_buffer |= (g.NavFocusScopeId != data->FocusScope); + clear_buffer |= (data->LastRequestTime + TYPING_SELECT_RESET_TIMER < g.Time); + clear_buffer |= g.NavAnyRequest; + clear_buffer |= g.ActiveId != 0 && g.NavActivateId == 0; // Allow temporary SPACE activation to not interfere + clear_buffer |= IsKeyPressed(ImGuiKey_Escape) || IsKeyPressed(ImGuiKey_Enter); + clear_buffer |= IsKeyPressed(ImGuiKey_Backspace) && (flags & ImGuiTypingSelectFlags_AllowBackspace) == 0; + //if (clear_buffer) { IMGUI_DEBUG_LOG("GetTypingSelectRequest(): Clear SearchBuffer.\n"); } + if (clear_buffer) + data->Clear(); + } + + // Append to buffer + const int buffer_max_len = IM_ARRAYSIZE(data->SearchBuffer) - 1; + int buffer_len = (int)strlen(data->SearchBuffer); + bool select_request = false; + for (ImWchar w : g.IO.InputQueueCharacters) + { + const int w_len = ImTextCountUtf8BytesFromStr(&w, &w + 1); + if (w < 32 || (buffer_len == 0 && ImCharIsBlankW(w)) || (buffer_len + w_len > buffer_max_len)) // Ignore leading blanks + continue; + char w_buf[5]; + ImTextCharToUtf8(w_buf, (unsigned int)w); + if (data->SingleCharModeLock && w_len == out_request->SingleCharSize && memcmp(w_buf, data->SearchBuffer, w_len) == 0) + { + select_request = true; // Same character: don't need to append to buffer. + continue; + } + if (data->SingleCharModeLock) + { + data->Clear(); // Different character: clear + buffer_len = 0; + } + memcpy(data->SearchBuffer + buffer_len, w_buf, w_len + 1); // Append + buffer_len += w_len; + select_request = true; + } + g.IO.InputQueueCharacters.resize(0); + + // Handle backspace + if ((flags & ImGuiTypingSelectFlags_AllowBackspace) && IsKeyPressed(ImGuiKey_Backspace, 0, ImGuiInputFlags_Repeat)) + { + char* p = (char*)(void*)ImTextFindPreviousUtf8Codepoint(data->SearchBuffer, data->SearchBuffer + buffer_len); + *p = 0; + buffer_len = (int)(p - data->SearchBuffer); + } + + // Return request if any + if (buffer_len == 0) + return NULL; + if (select_request) + { + data->FocusScope = g.NavFocusScopeId; + data->LastRequestFrame = g.FrameCount; + data->LastRequestTime = (float)g.Time; + } + out_request->Flags = flags; + out_request->SearchBufferLen = buffer_len; + out_request->SearchBuffer = data->SearchBuffer; + out_request->SelectRequest = (data->LastRequestFrame == g.FrameCount); + out_request->SingleCharMode = false; + out_request->SingleCharSize = 0; + + // Calculate if buffer contains the same character repeated. + // - This can be used to implement a special search mode on first character. + // - Performed on UTF-8 codepoint for correctness. + // - SingleCharMode is always set for first input character, because it usually leads to a "next". + if (flags & ImGuiTypingSelectFlags_AllowSingleCharMode) + { + const char* buf_begin = out_request->SearchBuffer; + const char* buf_end = out_request->SearchBuffer + out_request->SearchBufferLen; + const int c0_len = ImTextCountUtf8BytesFromChar(buf_begin, buf_end); + const char* p = buf_begin + c0_len; + for (; p < buf_end; p += c0_len) + if (memcmp(buf_begin, p, (size_t)c0_len) != 0) + break; + const int single_char_count = (p == buf_end) ? (out_request->SearchBufferLen / c0_len) : 0; + out_request->SingleCharMode = (single_char_count > 0 || data->SingleCharModeLock); + out_request->SingleCharSize = (ImS8)c0_len; + data->SingleCharModeLock |= (single_char_count >= TYPING_SELECT_SINGLE_CHAR_COUNT_FOR_LOCK); // From now on we stop search matching to lock to single char mode. + } + + return out_request; +} + +static int ImStrimatchlen(const char* s1, const char* s1_end, const char* s2) +{ + int match_len = 0; + while (s1 < s1_end && ImToUpper(*s1++) == ImToUpper(*s2++)) + match_len++; + return match_len; +} + +// Default handler for finding a result for typing-select. You may implement your own. +// You might want to display a tooltip to visualize the current request SearchBuffer +// When SingleCharMode is set: +// - it is better to NOT display a tooltip of other on-screen display indicator. +// - the index of the currently focused item is required. +// if your SetNextItemSelectionData() values are indices, you can obtain it from ImGuiMultiSelectIO::NavIdItem, otherwise from g.NavLastValidSelectionUserData. +int ImGui::TypingSelectFindMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data, int nav_item_idx) +{ + if (req == NULL || req->SelectRequest == false) // Support NULL parameter so both calls can be done from same spot. + return -1; + int idx = -1; + if (req->SingleCharMode && (req->Flags & ImGuiTypingSelectFlags_AllowSingleCharMode)) + idx = TypingSelectFindNextSingleCharMatch(req, items_count, get_item_name_func, user_data, nav_item_idx); + else + idx = TypingSelectFindBestLeadingMatch(req, items_count, get_item_name_func, user_data); + if (idx != -1) + NavRestoreHighlightAfterMove(); + return idx; +} + +// Special handling when a single character is repeated: perform search on a single letter and goes to next. +int ImGui::TypingSelectFindNextSingleCharMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data, int nav_item_idx) +{ + // FIXME: Assume selection user data is index. Would be extremely practical. + //if (nav_item_idx == -1) + // nav_item_idx = (int)g.NavLastValidSelectionUserData; + + int first_match_idx = -1; + bool return_next_match = false; + for (int idx = 0; idx < items_count; idx++) + { + const char* item_name = get_item_name_func(user_data, idx); + if (ImStrimatchlen(req->SearchBuffer, req->SearchBuffer + req->SingleCharSize, item_name) < req->SingleCharSize) + continue; + if (return_next_match) // Return next matching item after current item. + return idx; + if (first_match_idx == -1 && nav_item_idx == -1) // Return first match immediately if we don't have a nav_item_idx value. + return idx; + if (first_match_idx == -1) // Record first match for wrapping. + first_match_idx = idx; + if (nav_item_idx == idx) // Record that we encountering nav_item so we can return next match. + return_next_match = true; + } + return first_match_idx; // First result +} + +int ImGui::TypingSelectFindBestLeadingMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data) +{ + int longest_match_idx = -1; + int longest_match_len = 0; + for (int idx = 0; idx < items_count; idx++) + { + const char* item_name = get_item_name_func(user_data, idx); + const int match_len = ImStrimatchlen(req->SearchBuffer, req->SearchBuffer + req->SearchBufferLen, item_name); + if (match_len <= longest_match_len) + continue; + longest_match_idx = idx; + longest_match_len = match_len; + if (match_len == req->SearchBufferLen) + break; + } + return longest_match_idx; +} + +void ImGui::DebugNodeTypingSelectState(ImGuiTypingSelectState* data) +{ +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + Text("SearchBuffer = \"%s\"", data->SearchBuffer); + Text("SingleCharMode = %d, Size = %d, Lock = %d", data->Request.SingleCharMode, data->Request.SingleCharSize, data->SingleCharModeLock); + Text("LastRequest = time: %.2f, frame: %d", data->LastRequestTime, data->LastRequestFrame); +#else + IM_UNUSED(data); +#endif +} + + +//------------------------------------------------------------------------- +// [SECTION] Widgets: Multi-Select support +//------------------------------------------------------------------------- + +void ImGui::SetNextItemSelectionUserData(ImGuiSelectionUserData selection_user_data) +{ + // Note that flags will be cleared by ItemAdd(), so it's only useful for Navigation code! + // This designed so widgets can also cheaply set this before calling ItemAdd(), so we are not tied to MultiSelect api. + ImGuiContext& g = *GImGui; + g.NextItemData.ItemFlags |= ImGuiItemFlags_HasSelectionUserData; + g.NextItemData.SelectionUserData = selection_user_data; +} + + //------------------------------------------------------------------------- // [SECTION] Widgets: ListBox //------------------------------------------------------------------------- @@ -6578,6 +6898,7 @@ bool ImGui::Selectable(const char* label, bool* p_selected, ImGuiSelectableFlags // - ListBox() //------------------------------------------------------------------------- +// This is essentially a thin wrapper to using BeginChild/EndChild with the ImGuiChildFlags_FrameStyle flag for stylistic changes + displaying a label. // Tip: To have a list filling the entire window width, use size.x = -FLT_MIN and pass an non-visible label e.g. "##empty" // Tip: If your vertical size is calculated from an item count (e.g. 10 * item_height) consider adding a fractional part to facilitate seeing scrolling boundaries (e.g. 10.25 * item_height). bool ImGui::BeginListBox(const char* label, const ImVec2& size_arg) @@ -6593,7 +6914,7 @@ bool ImGui::BeginListBox(const char* label, const ImVec2& size_arg) // Size default to hold ~7.25 items. // Fractional number of items helps seeing that we can scroll down/up without looking at scrollbar. - ImVec2 size = ImFloor(CalcItemSize(size_arg, CalcItemWidth(), GetTextLineHeightWithSpacing() * 7.25f + style.FramePadding.y * 2.0f)); + ImVec2 size = ImTrunc(CalcItemSize(size_arg, CalcItemWidth(), GetTextLineHeightWithSpacing() * 7.25f + style.FramePadding.y * 2.0f)); ImVec2 frame_size = ImVec2(size.x, ImMax(size.y, label_size.y)); ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size); ImRect bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f)); @@ -6603,10 +6924,11 @@ bool ImGui::BeginListBox(const char* label, const ImVec2& size_arg) { ItemSize(bb.GetSize(), style.FramePadding.y); ItemAdd(bb, 0, &frame_bb); + g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values return false; } - // FIXME-OPT: We could omit the BeginGroup() if label_size.x but would need to omit the EndGroup() as well. + // FIXME-OPT: We could omit the BeginGroup() if label_size.x == 0.0f but would need to omit the EndGroup() as well. BeginGroup(); if (label_size.x > 0.0f) { @@ -6615,7 +6937,7 @@ bool ImGui::BeginListBox(const char* label, const ImVec2& size_arg) window->DC.CursorMaxPos = ImMax(window->DC.CursorMaxPos, label_pos + label_size); } - BeginChildFrame(id, frame_bb.GetSize()); + BeginChild(id, frame_bb.GetSize(), ImGuiChildFlags_FrameStyle); return true; } @@ -6626,7 +6948,7 @@ void ImGui::EndListBox() IM_ASSERT((window->Flags & ImGuiWindowFlags_ChildWindow) && "Mismatched BeginListBox/EndListBox calls. Did you test the return value of BeginListBox?"); IM_UNUSED(window); - EndChildFrame(); + EndChild(); EndGroup(); // This is only required to be able to do IsItemXXX query on the whole ListBox including label } @@ -6638,7 +6960,7 @@ bool ImGui::ListBox(const char* label, int* current_item, const char* const item // This is merely a helper around BeginListBox(), EndListBox(). // Considering using those directly to submit custom data or store selection differently. -bool ImGui::ListBox(const char* label, int* current_item, bool (*items_getter)(void*, int, const char**), void* data, int items_count, int height_in_items) +bool ImGui::ListBox(const char* label, int* current_item, const char* (*getter)(void* user_data, int idx), void* user_data, int items_count, int height_in_items) { ImGuiContext& g = *GImGui; @@ -6646,7 +6968,7 @@ bool ImGui::ListBox(const char* label, int* current_item, bool (*items_getter)(v if (height_in_items < 0) height_in_items = ImMin(items_count, 7); float height_in_items_f = height_in_items + 0.25f; - ImVec2 size(0.0f, ImFloor(GetTextLineHeightWithSpacing() * height_in_items_f + g.Style.FramePadding.y * 2.0f)); + ImVec2 size(0.0f, ImTrunc(GetTextLineHeightWithSpacing() * height_in_items_f + g.Style.FramePadding.y * 2.0f)); if (!BeginListBox(label, size)) return false; @@ -6659,8 +6981,8 @@ bool ImGui::ListBox(const char* label, int* current_item, bool (*items_getter)(v while (clipper.Step()) for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) { - const char* item_text; - if (!items_getter(data, i, &item_text)) + const char* item_text = getter(user_data, i); + if (item_text == NULL) item_text = "*Unknown item*"; PushID(i); @@ -6714,7 +7036,7 @@ int ImGui::PlotEx(ImGuiPlotType plot_type, const char* label, float (*values_get ItemSize(total_bb, style.FramePadding.y); if (!ItemAdd(total_bb, 0, &frame_bb)) return -1; - const bool hovered = ItemHoverable(frame_bb, id); + const bool hovered = ItemHoverable(frame_bb, id, g.LastItemData.InFlags); // Determine scale from values if not specified if (scale_min == FLT_MAX || scale_max == FLT_MAX) @@ -7007,12 +7329,18 @@ void ImGui::EndMenuBar() PopClipRect(); PopID(); window->DC.MenuBarOffset.x = window->DC.CursorPos.x - window->Pos.x; // Save horizontal position so next append can reuse it. This is kinda equivalent to a per-layer CursorPos. - g.GroupStack.back().EmitItem = false; - EndGroup(); // Restore position on layer 0 + + // FIXME: Extremely confusing, cleanup by (a) working on WorkRect stack system (b) not using a Group confusingly here. + ImGuiGroupData& group_data = g.GroupStack.back(); + group_data.EmitItem = false; + ImVec2 restore_cursor_max_pos = group_data.BackupCursorMaxPos; + window->DC.IdealMaxPos.x = ImMax(window->DC.IdealMaxPos.x, window->DC.CursorMaxPos.x - window->Scroll.x); // Convert ideal extents for scrolling layer equivalent. + EndGroup(); // Restore position on layer 0 // FIXME: Misleading to use a group for that backup/restore window->DC.LayoutType = ImGuiLayoutType_Vertical; window->DC.IsSameLine = false; window->DC.NavLayerCurrent = ImGuiNavLayer_Main; window->DC.MenuBarAppending = false; + window->DC.CursorMaxPos = restore_cursor_max_pos; } // Important: calling order matters! @@ -7023,10 +7351,10 @@ bool ImGui::BeginViewportSideBar(const char* name, ImGuiViewport* viewport_p, Im IM_ASSERT(dir != ImGuiDir_None); ImGuiWindow* bar_window = FindWindowByName(name); + ImGuiViewportP* viewport = (ImGuiViewportP*)(void*)(viewport_p ? viewport_p : GetMainViewport()); if (bar_window == NULL || bar_window->BeginCount == 0) { // Calculate and set window size/position - ImGuiViewportP* viewport = (ImGuiViewportP*)(void*)(viewport_p ? viewport_p : GetMainViewport()); ImRect avail_rect = viewport->GetBuildWorkRect(); ImGuiAxis axis = (dir == ImGuiDir_Up || dir == ImGuiDir_Down) ? ImGuiAxis_Y : ImGuiAxis_X; ImVec2 pos = avail_rect.Min; @@ -7044,7 +7372,8 @@ bool ImGui::BeginViewportSideBar(const char* name, ImGuiViewport* viewport_p, Im viewport->BuildWorkOffsetMax[axis] -= axis_size; } - window_flags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove; + window_flags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDocking; + SetNextWindowViewport(viewport->ID); // Enforce viewport so we don't create our own viewport when ImGuiConfigFlags_ViewportsNoMerge is set. PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); PushStyleVar(ImGuiStyleVar_WindowMinSize, ImVec2(0, 0)); // Lift normal size constraint bool is_open = Begin(name, NULL, window_flags); @@ -7058,6 +7387,9 @@ bool ImGui::BeginMainMenuBar() ImGuiContext& g = *GImGui; ImGuiViewportP* viewport = (ImGuiViewportP*)(void*)GetMainViewport(); + // Notify of viewport change so GetFrameHeight() can be accurate in case of DPI change + SetCurrentViewport(NULL, viewport); + // For the main menu bar, which cannot be moved, we honor g.Style.DisplaySafeAreaPadding to ensure text can be visible on a TV set. // FIXME: This could be generalized as an opt-in way to clamp window->DC.CursorStartPos to avoid SafeArea? // FIXME: Consider removing support for safe area down the line... it's messy. Nowadays consoles have support for TV calibration in OS settings. @@ -7108,7 +7440,7 @@ static bool IsRootOfOpenMenuSet() const ImGuiPopupData* upper_popup = &g.OpenPopupStack[g.BeginPopupStack.Size]; if (window->DC.NavLayerCurrent != upper_popup->ParentNavLayer) return false; - return upper_popup->Window && (upper_popup->Window->Flags & ImGuiWindowFlags_ChildMenu) && ImGui::IsWindowChildOf(upper_popup->Window, window, true); + return upper_popup->Window && (upper_popup->Window->Flags & ImGuiWindowFlags_ChildMenu) && ImGui::IsWindowChildOf(upper_popup->Window, window, true, false); } bool ImGui::BeginMenuEx(const char* label, const char* icon, bool enabled) @@ -7168,15 +7500,15 @@ bool ImGui::BeginMenuEx(const char* label, const char* icon, bool enabled) // Menu inside an horizontal menu bar // Selectable extend their highlight by half ItemSpacing in each direction. // For ChildMenu, the popup position will be overwritten by the call to FindBestWindowPosForPopup() in Begin() - popup_pos = ImVec2(pos.x - 1.0f - IM_FLOOR(style.ItemSpacing.x * 0.5f), pos.y - style.FramePadding.y + window->MenuBarHeight()); - window->DC.CursorPos.x += IM_FLOOR(style.ItemSpacing.x * 0.5f); + popup_pos = ImVec2(pos.x - 1.0f - IM_TRUNC(style.ItemSpacing.x * 0.5f), pos.y - style.FramePadding.y + window->MenuBarHeight()); + window->DC.CursorPos.x += IM_TRUNC(style.ItemSpacing.x * 0.5f); PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(style.ItemSpacing.x * 2.0f, style.ItemSpacing.y)); float w = label_size.x; ImVec2 text_pos(window->DC.CursorPos.x + offsets->OffsetLabel, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset); pressed = Selectable("", menu_is_open, selectable_flags, ImVec2(w, label_size.y)); RenderText(text_pos, label); PopStyleVar(); - window->DC.CursorPos.x += IM_FLOOR(style.ItemSpacing.x * (-1.0f + 0.5f)); // -1 spacing to compensate the spacing added when Selectable() did a SameLine(). It would also work to call SameLine() ourselves after the PopStyleVar(). + window->DC.CursorPos.x += IM_TRUNC(style.ItemSpacing.x * (-1.0f + 0.5f)); // -1 spacing to compensate the spacing added when Selectable() did a SameLine(). It would also work to call SameLine() ourselves after the PopStyleVar(). } else { @@ -7185,7 +7517,7 @@ bool ImGui::BeginMenuEx(const char* label, const char* icon, bool enabled) // Only when they are other items sticking out we're going to add spacing, yet only register minimum width into the layout system. popup_pos = ImVec2(pos.x, pos.y - style.WindowPadding.y); float icon_w = (icon && icon[0]) ? CalcTextSize(icon, NULL).x : 0.0f; - float checkmark_w = IM_FLOOR(g.FontSize * 1.20f); + float checkmark_w = IM_TRUNC(g.FontSize * 1.20f); float min_w = window->DC.MenuColumns.DeclColumns(icon_w, label_size.x, 0.0f, checkmark_w); // Feedback to next frame float extra_w = ImMax(0.0f, GetContentRegionAvail().x - min_w); ImVec2 text_pos(window->DC.CursorPos.x + offsets->OffsetLabel, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset); @@ -7203,6 +7535,7 @@ bool ImGui::BeginMenuEx(const char* label, const char* icon, bool enabled) PopItemFlag(); bool want_open = false; + bool want_open_nav_init = false; bool want_close = false; if (window->DC.LayoutType == ImGuiLayoutType_Vertical) // (window->Flags & (ImGuiWindowFlags_Popup|ImGuiWindowFlags_ChildMenu)) { @@ -7213,18 +7546,18 @@ bool ImGui::BeginMenuEx(const char* label, const char* icon, bool enabled) ImGuiWindow* child_menu_window = (child_popup && child_popup->Window && child_popup->Window->ParentWindow == window) ? child_popup->Window : NULL; if (g.HoveredWindow == window && child_menu_window != NULL) { - float ref_unit = g.FontSize; // FIXME-DPI - float child_dir = (window->Pos.x < child_menu_window->Pos.x) ? 1.0f : -1.0f; - ImRect next_window_rect = child_menu_window->Rect(); + const float ref_unit = g.FontSize; // FIXME-DPI + const float child_dir = (window->Pos.x < child_menu_window->Pos.x) ? 1.0f : -1.0f; + const ImRect next_window_rect = child_menu_window->Rect(); ImVec2 ta = (g.IO.MousePos - g.IO.MouseDelta); ImVec2 tb = (child_dir > 0.0f) ? next_window_rect.GetTL() : next_window_rect.GetTR(); ImVec2 tc = (child_dir > 0.0f) ? next_window_rect.GetBL() : next_window_rect.GetBR(); - float extra = ImClamp(ImFabs(ta.x - tb.x) * 0.30f, ref_unit * 0.5f, ref_unit * 2.5f); // add a bit of extra slack. + const float pad_farmost_h = ImClamp(ImFabs(ta.x - tb.x) * 0.30f, ref_unit * 0.5f, ref_unit * 2.5f); // Add a bit of extra slack. ta.x += child_dir * -0.5f; tb.x += child_dir * ref_unit; tc.x += child_dir * ref_unit; - tb.y = ta.y + ImMax((tb.y - extra) - ta.y, -ref_unit * 8.0f); // triangle has maximum height to limit the slope and the bias toward large sub-menus - tc.y = ta.y + ImMin((tc.y + extra) - ta.y, +ref_unit * 8.0f); + tb.y = ta.y + ImMax((tb.y - pad_farmost_h) - ta.y, -ref_unit * 8.0f); // Triangle has maximum height to limit the slope and the bias toward large sub-menus + tc.y = ta.y + ImMin((tc.y + pad_farmost_h) - ta.y, +ref_unit * 8.0f); moving_toward_child_menu = ImTriangleContainsPoint(ta, tb, tc, g.IO.MousePos); //GetForegroundDrawList()->AddTriangleFilled(ta, tb, tc, moving_toward_child_menu ? IM_COL32(0,128,0,128) : IM_COL32(128,0,0,128)); // [DEBUG] } @@ -7232,18 +7565,22 @@ bool ImGui::BeginMenuEx(const char* label, const char* icon, bool enabled) // The 'HovereWindow == window' check creates an inconsistency (e.g. moving away from menu slowly tends to hit same window, whereas moving away fast does not) // But we also need to not close the top-menu menu when moving over void. Perhaps we should extend the triangle check to a larger polygon. // (Remember to test this on BeginPopup("A")->BeginMenu("B") sequence which behaves slightly differently as B isn't a Child of A and hovering isn't shared.) - if (menu_is_open && !hovered && g.HoveredWindow == window && !moving_toward_child_menu && !g.NavDisableMouseHover) + if (menu_is_open && !hovered && g.HoveredWindow == window && !moving_toward_child_menu && !g.NavDisableMouseHover && g.ActiveId == 0) want_close = true; // Open + // (note: at this point 'hovered' actually includes the NavDisableMouseHover == false test) if (!menu_is_open && pressed) // Click/activate to open want_open = true; else if (!menu_is_open && hovered && !moving_toward_child_menu) // Hover to open want_open = true; + else if (!menu_is_open && hovered && g.HoveredIdTimer >= 0.30f && g.MouseStationaryTimer >= 0.30f) // Hover to open (timer fallback) + want_open = true; if (g.NavId == id && g.NavMoveDir == ImGuiDir_Right) // Nav-Right to open { - want_open = true; + want_open = want_open_nav_init = true; NavMoveRequestCancel(); + NavRestoreHighlightAfterMove(); } } else @@ -7275,13 +7612,13 @@ bool ImGui::BeginMenuEx(const char* label, const char* icon, bool enabled) if (want_open && !menu_is_open && g.OpenPopupStack.Size > g.BeginPopupStack.Size) { - // Don't reopen/recycle same menu level in the same frame, first close the other menu and yield for a frame. + // Don't reopen/recycle same menu level in the same frame if it is a different menu ID, first close the other menu and yield for a frame. OpenPopup(label); } else if (want_open) { menu_is_open = true; - OpenPopup(label); + OpenPopup(label, ImGuiPopupFlags_NoReopen);// | (want_open_nav_init ? ImGuiPopupFlags_NoReopenAlwaysNavInit : 0)); } if (menu_is_open) @@ -7293,6 +7630,14 @@ bool ImGui::BeginMenuEx(const char* label, const char* icon, bool enabled) PopStyleVar(); if (menu_is_open) { + // Implement what ImGuiPopupFlags_NoReopenAlwaysNavInit would do: + // Perform an init request in the case the popup was already open (via a previous mouse hover) + if (want_open && want_open_nav_init && !g.NavInitRequest) + { + FocusWindow(g.CurrentWindow, ImGuiFocusRequestFlags_UnlessBelowModal); + NavInitWindow(g.CurrentWindow, false); + } + // Restore LastItemData so IsItemXXXX functions can work after BeginMenu()/EndMenu() // (This fixes using IsItemClicked() and IsItemHovered(), but IsItemHovered() also relies on its support for ImGuiItemFlags_NoWindowHoverableCheck) g.LastItemData = last_item_in_parent; @@ -7362,14 +7707,14 @@ bool ImGui::MenuItemEx(const char* label, const char* icon, const char* shortcut // Mimic the exact layout spacing of BeginMenu() to allow MenuItem() inside a menu bar, which is a little misleading but may be useful // Note that in this situation: we don't render the shortcut, we render a highlight instead of the selected tick mark. float w = label_size.x; - window->DC.CursorPos.x += IM_FLOOR(style.ItemSpacing.x * 0.5f); + window->DC.CursorPos.x += IM_TRUNC(style.ItemSpacing.x * 0.5f); ImVec2 text_pos(window->DC.CursorPos.x + offsets->OffsetLabel, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset); PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(style.ItemSpacing.x * 2.0f, style.ItemSpacing.y)); pressed = Selectable("", selected, selectable_flags, ImVec2(w, 0.0f)); PopStyleVar(); if (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible) RenderText(text_pos, label); - window->DC.CursorPos.x += IM_FLOOR(style.ItemSpacing.x * (-1.0f + 0.5f)); // -1 spacing to compensate the spacing added when Selectable() did a SameLine(). It would also work to call SameLine() ourselves after the PopStyleVar(). + window->DC.CursorPos.x += IM_TRUNC(style.ItemSpacing.x * (-1.0f + 0.5f)); // -1 spacing to compensate the spacing added when Selectable() did a SameLine(). It would also work to call SameLine() ourselves after the PopStyleVar(). } else { @@ -7378,7 +7723,7 @@ bool ImGui::MenuItemEx(const char* label, const char* icon, const char* shortcut // Only when they are other items sticking out we're going to add spacing, yet only register minimum width into the layout system. float icon_w = (icon && icon[0]) ? CalcTextSize(icon, NULL).x : 0.0f; float shortcut_w = (shortcut && shortcut[0]) ? CalcTextSize(shortcut, NULL).x : 0.0f; - float checkmark_w = IM_FLOOR(g.FontSize * 1.20f); + float checkmark_w = IM_TRUNC(g.FontSize * 1.20f); float min_w = window->DC.MenuColumns.DeclColumns(icon_w, label_size.x, shortcut_w, checkmark_w); // Feedback for next frame float stretch_w = ImMax(0.0f, GetContentRegionAvail().x - min_w); pressed = Selectable("", false, selectable_flags | ImGuiSelectableFlags_SpanAvailWidth, ImVec2(min_w, label_size.y)); @@ -7434,8 +7779,10 @@ bool ImGui::MenuItem(const char* label, const char* shortcut, bool* p_selected, // - TabBarCalcMaxTabWidth() [Internal] // - TabBarFindTabById() [Internal] // - TabBarFindTabByOrder() [Internal] +// - TabBarFindMostRecentlySelectedTabForActiveWindow() [Internal] // - TabBarGetCurrentTab() [Internal] // - TabBarGetTabName() [Internal] +// - TabBarAddTab() [Internal] // - TabBarRemoveTab() [Internal] // - TabBarCloseTab() [Internal] // - TabBarScrollClamp() [Internal] @@ -7523,6 +7870,8 @@ bool ImGui::BeginTabBar(const char* str_id, ImGuiTabBarFlags flags) ImGuiTabBar* tab_bar = g.TabBars.GetOrAddByKey(id); ImRect tab_bar_bb = ImRect(window->DC.CursorPos.x, window->DC.CursorPos.y, window->WorkRect.Max.x, window->DC.CursorPos.y + g.FontSize + g.Style.FramePadding.y * 2); tab_bar->ID = id; + tab_bar->SeparatorMinX = tab_bar->BarRect.Min.x - IM_TRUNC(window->WindowPadding.x * 0.5f); + tab_bar->SeparatorMaxX = tab_bar->BarRect.Max.x + IM_TRUNC(window->WindowPadding.x * 0.5f); return BeginTabBarEx(tab_bar, tab_bar_bb, flags | ImGuiTabBarFlags_IsFocused); } @@ -7533,6 +7882,7 @@ bool ImGui::BeginTabBarEx(ImGuiTabBar* tab_bar, const ImRect& tab_bar_bb, ImG if (window->SkipItems) return false; + IM_ASSERT(tab_bar->ID != 0); if ((flags & ImGuiTabBarFlags_DockNode) == 0) PushOverrideID(tab_bar->ID); @@ -7551,7 +7901,8 @@ bool ImGui::BeginTabBarEx(ImGuiTabBar* tab_bar, const ImRect& tab_bar_bb, ImG // Ensure correct ordering when toggling ImGuiTabBarFlags_Reorderable flag, or when a new tab was added while being not reorderable if ((flags & ImGuiTabBarFlags_Reorderable) != (tab_bar->Flags & ImGuiTabBarFlags_Reorderable) || (tab_bar->TabsAddedNew && !(flags & ImGuiTabBarFlags_Reorderable))) - ImQsort(tab_bar->Tabs.Data, tab_bar->Tabs.Size, sizeof(ImGuiTabItem), TabItemComparerByBeginOrder); + if ((flags & ImGuiTabBarFlags_DockNode) == 0) // FIXME: TabBar with DockNode can now be hybrid + ImQsort(tab_bar->Tabs.Data, tab_bar->Tabs.Size, sizeof(ImGuiTabItem), TabItemComparerByBeginOrder); tab_bar->TabsAddedNew = false; // Flags @@ -7575,12 +7926,12 @@ bool ImGui::BeginTabBarEx(ImGuiTabBar* tab_bar, const ImRect& tab_bar_bb, ImG window->DC.CursorPos = ImVec2(tab_bar->BarRect.Min.x, tab_bar->BarRect.Max.y + tab_bar->ItemSpacingY); // Draw separator + // (it would be misleading to draw this in EndTabBar() suggesting that it may be drawn over tabs, as tab bar are appendable) const ImU32 col = GetColorU32((flags & ImGuiTabBarFlags_IsFocused) ? ImGuiCol_TabActive : ImGuiCol_TabUnfocusedActive); - const float y = tab_bar->BarRect.Max.y - 1.0f; + if (g.Style.TabBarBorderSize > 0.0f) { - const float separator_min_x = tab_bar->BarRect.Min.x - IM_FLOOR(window->WindowPadding.x * 0.5f); - const float separator_max_x = tab_bar->BarRect.Max.x + IM_FLOOR(window->WindowPadding.x * 0.5f); - window->DrawList->AddLine(ImVec2(separator_min_x, y), ImVec2(separator_max_x, y), col, 1.0f); + const float y = tab_bar->BarRect.Max.y; + window->DrawList->AddRectFilled(ImVec2(tab_bar->SeparatorMinX, y - g.Style.TabBarBorderSize), ImVec2(tab_bar->SeparatorMaxX, y), col); } return true; } @@ -7787,7 +8138,7 @@ static void ImGui::TabBarLayout(ImGuiTabBar* tab_bar) for (int tab_n = shrink_data_offset; tab_n < shrink_data_offset + shrink_data_count; tab_n++) { ImGuiTabItem* tab = &tab_bar->Tabs[g.ShrinkWidthBuffer[tab_n].Index]; - float shrinked_width = IM_FLOOR(g.ShrinkWidthBuffer[tab_n].Width); + float shrinked_width = IM_TRUNC(g.ShrinkWidthBuffer[tab_n].Width); if (shrinked_width < 0.0f) continue; @@ -7833,6 +8184,10 @@ static void ImGui::TabBarLayout(ImGuiTabBar* tab_bar) tab_bar->VisibleTabId = tab_bar->SelectedTabId; tab_bar->VisibleTabWasSubmitted = false; + // CTRL+TAB can override visible tab temporarily + if (g.NavWindowingTarget != NULL && g.NavWindowingTarget->DockNode && g.NavWindowingTarget->DockNode->TabBar == tab_bar) + tab_bar->VisibleTabId = scroll_to_tab_id = g.NavWindowingTarget->TabId; + // Apply request requests if (scroll_to_tab_id != 0) TabBarScrollToTab(tab_bar, scroll_to_tab_id, sections); @@ -7878,11 +8233,11 @@ static void ImGui::TabBarLayout(ImGuiTabBar* tab_bar) // Dockable windows uses Name/ID in the global namespace. Non-dockable items use the ID stack. static ImU32 ImGui::TabBarCalcTabID(ImGuiTabBar* tab_bar, const char* label, ImGuiWindow* docked_window) { - IM_ASSERT(docked_window == NULL); // master branch only - IM_UNUSED(docked_window); - if (tab_bar->Flags & ImGuiTabBarFlags_DockNode) + if (docked_window != NULL) { - ImGuiID id = ImHashStr(label); + IM_UNUSED(tab_bar); + IM_ASSERT(tab_bar->Flags & ImGuiTabBarFlags_DockNode); + ImGuiID id = docked_window->TabId; KeepAliveID(id); return id; } @@ -7916,6 +8271,20 @@ ImGuiTabItem* ImGui::TabBarFindTabByOrder(ImGuiTabBar* tab_bar, int order) return &tab_bar->Tabs[order]; } +// FIXME: See references to #2304 in TODO.txt +ImGuiTabItem* ImGui::TabBarFindMostRecentlySelectedTabForActiveWindow(ImGuiTabBar* tab_bar) +{ + ImGuiTabItem* most_recently_selected_tab = NULL; + for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++) + { + ImGuiTabItem* tab = &tab_bar->Tabs[tab_n]; + if (most_recently_selected_tab == NULL || most_recently_selected_tab->LastFrameSelected < tab->LastFrameSelected) + if (tab->Window && tab->Window->WasActive) + most_recently_selected_tab = tab; + } + return most_recently_selected_tab; +} + ImGuiTabItem* ImGui::TabBarGetCurrentTab(ImGuiTabBar* tab_bar) { if (tab_bar->LastTabItemIdx <= 0 || tab_bar->LastTabItemIdx >= tab_bar->Tabs.Size) @@ -7925,12 +8294,35 @@ ImGuiTabItem* ImGui::TabBarGetCurrentTab(ImGuiTabBar* tab_bar) const char* ImGui::TabBarGetTabName(ImGuiTabBar* tab_bar, ImGuiTabItem* tab) { + if (tab->Window) + return tab->Window->Name; if (tab->NameOffset == -1) return "N/A"; IM_ASSERT(tab->NameOffset < tab_bar->TabsNames.Buf.Size); return tab_bar->TabsNames.Buf.Data + tab->NameOffset; } +// The purpose of this call is to register tab in advance so we can control their order at the time they appear. +// Otherwise calling this is unnecessary as tabs are appending as needed by the BeginTabItem() function. +void ImGui::TabBarAddTab(ImGuiTabBar* tab_bar, ImGuiTabItemFlags tab_flags, ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(TabBarFindTabByID(tab_bar, window->TabId) == NULL); + IM_ASSERT(g.CurrentTabBar != tab_bar); // Can't work while the tab bar is active as our tab doesn't have an X offset yet, in theory we could/should test something like (tab_bar->CurrFrameVisible < g.FrameCount) but we'd need to solve why triggers the commented early-out assert in BeginTabBarEx() (probably dock node going from implicit to explicit in same frame) + + if (!window->HasCloseButton) + tab_flags |= ImGuiTabItemFlags_NoCloseButton; // Set _NoCloseButton immediately because it will be used for first-frame width calculation. + + ImGuiTabItem new_tab; + new_tab.ID = window->TabId; + new_tab.Flags = tab_flags; + new_tab.LastFrameVisible = tab_bar->CurrFrameVisible; // Required so BeginTabBar() doesn't ditch the tab + if (new_tab.LastFrameVisible == -1) + new_tab.LastFrameVisible = g.FrameCount - 1; + new_tab.Window = window; // Required so tab bar layout can compute the tab width before tab submission + tab_bar->Tabs.push_back(new_tab); +} + // The *TabId fields are already set by the docking system _before_ the actual TabItem was created, so we clear them regardless. void ImGui::TabBarRemoveTab(ImGuiTabBar* tab_bar, ImGuiID tab_id) { @@ -7947,7 +8339,7 @@ void ImGui::TabBarCloseTab(ImGuiTabBar* tab_bar, ImGuiTabItem* tab) if (tab->Flags & ImGuiTabItemFlags_Button) return; // A button appended with TabItemButton(). - if (!(tab->Flags & ImGuiTabItemFlags_UnsavedDocument)) + if ((tab->Flags & (ImGuiTabItemFlags_UnsavedDocument | ImGuiTabItemFlags_NoAssumedClosure)) == 0) { // This will remove a frame of lag for selecting another tab on closure. // However we don't run it in the case where the 'Unsaved' flag is set, so user gets a chance to fully undo the closure @@ -8211,7 +8603,7 @@ bool ImGui::BeginTabItem(const char* label, bool* p_open, ImGuiTabItemFlags f IM_ASSERT_USER_ERROR(tab_bar, "Needs to be called between BeginTabBar() and EndTabBar()!"); return false; } - IM_ASSERT(!(flags & ImGuiTabItemFlags_Button)); // BeginTabItem() Can't be used with button flags, use TabItemButton() instead! + IM_ASSERT((flags & ImGuiTabItemFlags_Button) == 0); // BeginTabItem() Can't be used with button flags, use TabItemButton() instead! bool ret = TabItemEx(tab_bar, label, p_open, flags, NULL); if (ret && !(flags & ImGuiTabItemFlags_NoPushId)) @@ -8321,11 +8713,14 @@ bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open, const bool is_tab_button = (flags & ImGuiTabItemFlags_Button) != 0; tab->LastFrameVisible = g.FrameCount; tab->Flags = flags; + tab->Window = docked_window; // Append name _WITH_ the zero-terminator + // (regular tabs are permitted in a DockNode tab bar, but window tabs not permitted in a non-DockNode tab bar) if (docked_window != NULL) { - IM_ASSERT(docked_window == NULL); // master branch only + IM_ASSERT(tab_bar->Flags & ImGuiTabBarFlags_DockNode); + tab->NameOffset = -1; } else { @@ -8350,7 +8745,7 @@ bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open, tab_bar->VisibleTabWasSubmitted = true; // On the very first frame of a tab bar we let first tab contents be visible to minimize appearing glitches - if (!tab_contents_visible && tab_bar->SelectedTabId == 0 && tab_bar_appearing) + if (!tab_contents_visible && tab_bar->SelectedTabId == 0 && tab_bar_appearing && docked_window == NULL) if (tab_bar->Tabs.Size == 1 && !(tab_bar->Flags & ImGuiTabBarFlags_AutoSelectNewTabs)) tab_contents_visible = true; @@ -8374,7 +8769,7 @@ bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open, const bool is_central_section = (tab->Flags & ImGuiTabItemFlags_SectionMask_) == 0; size.x = tab->Width; if (is_central_section) - window->DC.CursorPos = tab_bar->BarRect.Min + ImVec2(IM_FLOOR(tab->Offset - tab_bar->ScrollingAnim), 0.0f); + window->DC.CursorPos = tab_bar->BarRect.Min + ImVec2(IM_TRUNC(tab->Offset - tab_bar->ScrollingAnim), 0.0f); else window->DC.CursorPos = tab_bar->BarRect.Min + ImVec2(tab->Offset, 0.0f); ImVec2 pos = window->DC.CursorPos; @@ -8398,40 +8793,89 @@ bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open, } // Click to Select a tab - ImGuiButtonFlags button_flags = ((is_tab_button ? ImGuiButtonFlags_PressedOnClickRelease : ImGuiButtonFlags_PressedOnClick) | ImGuiButtonFlags_AllowItemOverlap); - if (g.DragDropActive) + ImGuiButtonFlags button_flags = ((is_tab_button ? ImGuiButtonFlags_PressedOnClickRelease : ImGuiButtonFlags_PressedOnClick) | ImGuiButtonFlags_AllowOverlap); + if (g.DragDropActive && !g.DragDropPayload.IsDataType(IMGUI_PAYLOAD_TYPE_WINDOW)) // FIXME: May be an opt-in property of the payload to disable this button_flags |= ImGuiButtonFlags_PressedOnDragDropHold; bool hovered, held; bool pressed = ButtonBehavior(bb, id, &hovered, &held, button_flags); if (pressed && !is_tab_button) TabBarQueueFocus(tab_bar, tab); - // Allow the close button to overlap unless we are dragging (in which case we don't want any overlapping tabs to be hovered) - if (g.ActiveId != id) - SetItemAllowOverlap(); + // Transfer active id window so the active id is not owned by the dock host (as StartMouseMovingWindow() + // will only do it on the drag). This allows FocusWindow() to be more conservative in how it clears active id. + if (held && docked_window && g.ActiveId == id && g.ActiveIdIsJustActivated) + g.ActiveIdWindow = docked_window; - // Drag and drop: re-order tabs - if (held && !tab_appearing && IsMouseDragging(0)) + // Drag and drop a single floating window node moves it + ImGuiDockNode* node = docked_window ? docked_window->DockNode : NULL; + const bool single_floating_window_node = node && node->IsFloatingNode() && (node->Windows.Size == 1); + if (held && single_floating_window_node && IsMouseDragging(0, 0.0f)) { - if (!g.DragDropActive && (tab_bar->Flags & ImGuiTabBarFlags_Reorderable)) + // Move + StartMouseMovingWindow(docked_window); + } + else if (held && !tab_appearing && IsMouseDragging(0)) + { + // Drag and drop: re-order tabs + int drag_dir = 0; + float drag_distance_from_edge_x = 0.0f; + if (!g.DragDropActive && ((tab_bar->Flags & ImGuiTabBarFlags_Reorderable) || (docked_window != NULL))) { // While moving a tab it will jump on the other side of the mouse, so we also test for MouseDelta.x if (g.IO.MouseDelta.x < 0.0f && g.IO.MousePos.x < bb.Min.x) { + drag_dir = -1; + drag_distance_from_edge_x = bb.Min.x - g.IO.MousePos.x; TabBarQueueReorderFromMousePos(tab_bar, tab, g.IO.MousePos); } else if (g.IO.MouseDelta.x > 0.0f && g.IO.MousePos.x > bb.Max.x) { + drag_dir = +1; + drag_distance_from_edge_x = g.IO.MousePos.x - bb.Max.x; TabBarQueueReorderFromMousePos(tab_bar, tab, g.IO.MousePos); } } + + // Extract a Dockable window out of it's tab bar + const bool can_undock = docked_window != NULL && !(docked_window->Flags & ImGuiWindowFlags_NoMove) && !(node->MergedFlags & ImGuiDockNodeFlags_NoUndocking); + if (can_undock) + { + // We use a variable threshold to distinguish dragging tabs within a tab bar and extracting them out of the tab bar + bool undocking_tab = (g.DragDropActive && g.DragDropPayload.SourceId == id); + if (!undocking_tab) //&& (!g.IO.ConfigDockingWithShift || g.IO.KeyShift) + { + float threshold_base = g.FontSize; + float threshold_x = (threshold_base * 2.2f); + float threshold_y = (threshold_base * 1.5f) + ImClamp((ImFabs(g.IO.MouseDragMaxDistanceAbs[0].x) - threshold_base * 2.0f) * 0.20f, 0.0f, threshold_base * 4.0f); + //GetForegroundDrawList()->AddRect(ImVec2(bb.Min.x - threshold_x, bb.Min.y - threshold_y), ImVec2(bb.Max.x + threshold_x, bb.Max.y + threshold_y), IM_COL32_WHITE); // [DEBUG] + + float distance_from_edge_y = ImMax(bb.Min.y - g.IO.MousePos.y, g.IO.MousePos.y - bb.Max.y); + if (distance_from_edge_y >= threshold_y) + undocking_tab = true; + if (drag_distance_from_edge_x > threshold_x) + if ((drag_dir < 0 && TabBarGetTabOrder(tab_bar, tab) == 0) || (drag_dir > 0 && TabBarGetTabOrder(tab_bar, tab) == tab_bar->Tabs.Size - 1)) + undocking_tab = true; + } + + if (undocking_tab) + { + // Undock + // FIXME: refactor to share more code with e.g. StartMouseMovingWindow + DockContextQueueUndockWindow(&g, docked_window); + g.MovingWindow = docked_window; + SetActiveID(g.MovingWindow->MoveId, g.MovingWindow); + g.ActiveIdClickOffset -= g.MovingWindow->Pos - bb.Min; + g.ActiveIdNoClearOnFocusLoss = true; + SetActiveIdUsingAllKeyboardKeys(); + } + } } #if 0 if (hovered && g.HoveredIdNotActiveTimer > TOOLTIP_DELAY && bb.GetWidth() < tab->ContentWidth) { // Enlarge tab display when hovering - bb.Max.x = bb.Min.x + IM_FLOOR(ImLerp(bb.GetWidth(), tab->ContentWidth, ImSaturate((g.HoveredIdNotActiveTimer - 0.40f) * 6.0f))); + bb.Max.x = bb.Min.x + IM_TRUNC(ImLerp(bb.GetWidth(), tab->ContentWidth, ImSaturate((g.HoveredIdNotActiveTimer - 0.40f) * 6.0f))); display_draw_list = GetForegroundDrawList(window); TabItemBackground(display_draw_list, bb, flags, GetColorU32(ImGuiCol_TitleBgActive)); } @@ -8452,7 +8896,7 @@ bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open, flags |= ImGuiTabItemFlags_NoCloseWithMiddleMouseButton; // Render tab label, process close button - const ImGuiID close_button_id = p_open ? GetIDWithSeed("#CLOSE", NULL, id) : 0; + const ImGuiID close_button_id = p_open ? GetIDWithSeed("#CLOSE", NULL, docked_window ? docked_window->ID : id) : 0; bool just_closed; bool text_clipped; TabItemLabelAndCloseButton(display_draw_list, bb, tab_just_unsaved ? (flags & ~ImGuiTabItemFlags_UnsavedDocument) : flags, tab_bar->FramePadding, label, id, close_button_id, tab_contents_visible, &just_closed, &text_clipped); @@ -8462,6 +8906,11 @@ bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open, TabBarCloseTab(tab_bar, tab); } + // Forward Hovered state so IsItemHovered() after Begin() can work (even though we are technically hovering our parent) + // That state is copied to window->DockTabItemStatusFlags by our caller. + if (docked_window && (hovered || g.HoveredId == close_button_id)) + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredWindow; + // Restore main window position so user can draw there if (want_clip_rect) PopClipRect(); @@ -8474,8 +8923,7 @@ bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open, // FIXME: We may want disabled tab to still display the tooltip? if (text_clipped && g.HoveredId == id && !held) if (!(tab_bar->Flags & ImGuiTabBarFlags_NoTooltip) && !(tab->Flags & ImGuiTabItemFlags_NoTooltip)) - if (IsItemHovered(ImGuiHoveredFlags_DelayNormal)) - SetTooltip("%.*s", (int)(FindRenderedTextEnd(label) - label), label); + SetItemTooltip("%.*s", (int)(FindRenderedTextEnd(label) - label), label); IM_ASSERT(!is_tab_button || !(tab_bar->SelectedTabId == tab->ID && is_tab_button)); // TabItemButton should not be selected if (is_tab_button) @@ -8497,6 +8945,16 @@ void ImGui::SetTabItemClosed(const char* label) if (ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_id)) tab->WantClose = true; // Will be processed by next call to TabBarLayout() } + else if (ImGuiWindow* window = FindWindowByName(label)) + { + if (window->DockIsActive) + if (ImGuiDockNode* node = window->DockNode) + { + ImGuiID tab_id = TabBarCalcTabID(node->TabBar, label, window); + TabBarRemoveTab(node->TabBar, tab_id); + window->DockTabWantClose = true; + } + } } ImVec2 ImGui::TabItemCalcSize(const char* label, bool has_close_button_or_unsaved_marker) @@ -8511,10 +8969,9 @@ ImVec2 ImGui::TabItemCalcSize(const char* label, bool has_close_button_or_unsave return ImVec2(ImMin(size.x, TabBarCalcMaxTabWidth()), size.y); } -ImVec2 ImGui::TabItemCalcSize(ImGuiWindow*) +ImVec2 ImGui::TabItemCalcSize(ImGuiWindow* window) { - IM_ASSERT(0); // This function exists to facilitate merge with 'docking' branch. - return ImVec2(0.0f, 0.0f); + return TabItemCalcSize(window->Name, window->HasCloseButton || (window->Flags & ImGuiWindowFlags_UnsavedDocument)); } void ImGui::TabItemBackground(ImDrawList* draw_list, const ImRect& bb, ImGuiTabItemFlags flags, ImU32 col) @@ -8526,7 +8983,7 @@ void ImGui::TabItemBackground(ImDrawList* draw_list, const ImRect& bb, ImGuiTabI IM_ASSERT(width > 0.0f); const float rounding = ImMax(0.0f, ImMin((flags & ImGuiTabItemFlags_Button) ? g.Style.FrameRounding : g.Style.TabRounding, width * 0.5f - 1.0f)); const float y1 = bb.Min.y + 1.0f; - const float y2 = bb.Max.y - 1.0f; + const float y2 = bb.Max.y - g.Style.TabBarBorderSize; draw_list->PathLineTo(ImVec2(bb.Min.x, y2)); draw_list->PathArcToFast(ImVec2(bb.Min.x + rounding, y1 + rounding), rounding, 6, 9); draw_list->PathArcToFast(ImVec2(bb.Max.x - rounding, y1 + rounding), rounding, 9, 12); @@ -8577,7 +9034,7 @@ void ImGui::TabItemLabelAndCloseButton(ImDrawList* draw_list, const ImRect& bb, } const float button_sz = g.FontSize; - const ImVec2 button_pos(ImMax(bb.Min.x, bb.Max.x - frame_padding.x * 2.0f - button_sz), bb.Min.y); + const ImVec2 button_pos(ImMax(bb.Min.x, bb.Max.x - frame_padding.x - button_sz), bb.Min.y + frame_padding.y); // Close Button & Unsaved Marker // We are relying on a subtle and confusing distinction between 'hovered' and 'g.HoveredId' which happens because we are using ImGuiButtonFlags_AllowOverlapMode + SetItemAllowOverlap() @@ -8595,10 +9052,8 @@ void ImGui::TabItemLabelAndCloseButton(ImDrawList* draw_list, const ImRect& bb, if (close_button_visible) { ImGuiLastItemData last_item_backup = g.LastItemData; - PushStyleVar(ImGuiStyleVar_FramePadding, frame_padding); if (CloseButton(close_button_id, button_pos)) close_button_pressed = true; - PopStyleVar(); g.LastItemData = last_item_backup; // Close with middle mouse button @@ -8607,7 +9062,7 @@ void ImGui::TabItemLabelAndCloseButton(ImDrawList* draw_list, const ImRect& bb, } else if (unsaved_marker_visible) { - const ImRect bullet_bb(button_pos, button_pos + ImVec2(button_sz, button_sz) + g.Style.FramePadding * 2.0f); + const ImRect bullet_bb(button_pos, button_pos + ImVec2(button_sz, button_sz)); RenderBullet(draw_list, bullet_bb.GetCenter(), GetColorU32(ImGuiCol_Text)); } diff --git a/libs/imgui/implot.cpp b/libs/imgui/implot.cpp index f94ebce..597b043 100644 --- a/libs/imgui/implot.cpp +++ b/libs/imgui/implot.cpp @@ -1,6 +1,6 @@ // MIT License -// Copyright (c) 2022 Evan Pezent +// Copyright (c) 2023 Evan Pezent // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -20,7 +20,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -// ImPlot v0.14 +// ImPlot v0.17 /* @@ -31,6 +31,9 @@ Below is a change-log of API breaking changes only. If you are using one of the When you are not sure about a old symbol or function name, try using the Search/Find function of your IDE to look for comments or references in all implot files. You can read releases logs https://github.com/epezent/implot/releases for more details. +- 2023/08/20 (0.17) - ImPlotFlags_NoChild was removed as child windows are no longer needed to capture scroll. You can safely remove this flag if you were using it. +- 2023/06/26 (0.15) - Various build fixes related to updates in Dear ImGui internals. +- 2022/11/25 (0.15) - Make PlotText honor ImPlotItemFlags_NoFit. - 2022/06/19 (0.14) - The signature of ColormapScale has changed to accommodate a new ImPlotColormapScaleFlags parameter - 2022/06/17 (0.14) - **IMPORTANT** All PlotX functions now take an ImPlotX_Flags `flags` parameter. Where applicable, it is located before the existing `offset` and `stride` parameters. If you were providing offset and stride values, you will need to update your function call to include a `flags` value. If you fail to do this, you will likely see @@ -133,6 +136,11 @@ You can read releases logs https://github.com/epezent/implot/releases for more d #define ImDrawFlags_RoundCornersAll ImDrawCornerFlags_All #endif +// Support for pre-1.89.7 versions. +#if (IMGUI_VERSION_NUM < 18966) +#define ImGuiButtonFlags_AllowOverlap ImGuiButtonFlags_AllowItemOverlap +#endif + // Visual Studio warnings #ifdef _MSC_VER #pragma warning (disable: 4996) // 'This function or variable may be unsafe': strcpy, strdup, sprintf, vsnprintf, sscanf, fopen @@ -285,35 +293,35 @@ struct ImPlotStyleVarInfo { static const ImPlotStyleVarInfo GPlotStyleVarInfo[] = { - { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImPlotStyle, LineWeight) }, // ImPlotStyleVar_LineWeight - { ImGuiDataType_S32, 1, (ImU32)IM_OFFSETOF(ImPlotStyle, Marker) }, // ImPlotStyleVar_Marker - { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImPlotStyle, MarkerSize) }, // ImPlotStyleVar_MarkerSize - { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImPlotStyle, MarkerWeight) }, // ImPlotStyleVar_MarkerWeight - { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImPlotStyle, FillAlpha) }, // ImPlotStyleVar_FillAlpha - { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImPlotStyle, ErrorBarSize) }, // ImPlotStyleVar_ErrorBarSize - { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImPlotStyle, ErrorBarWeight) }, // ImPlotStyleVar_ErrorBarWeight - { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImPlotStyle, DigitalBitHeight) }, // ImPlotStyleVar_DigitalBitHeight - { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImPlotStyle, DigitalBitGap) }, // ImPlotStyleVar_DigitalBitGap + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImPlotStyle, LineWeight) }, // ImPlotStyleVar_LineWeight + { ImGuiDataType_S32, 1, (ImU32)offsetof(ImPlotStyle, Marker) }, // ImPlotStyleVar_Marker + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImPlotStyle, MarkerSize) }, // ImPlotStyleVar_MarkerSize + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImPlotStyle, MarkerWeight) }, // ImPlotStyleVar_MarkerWeight + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImPlotStyle, FillAlpha) }, // ImPlotStyleVar_FillAlpha + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImPlotStyle, ErrorBarSize) }, // ImPlotStyleVar_ErrorBarSize + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImPlotStyle, ErrorBarWeight) }, // ImPlotStyleVar_ErrorBarWeight + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImPlotStyle, DigitalBitHeight) }, // ImPlotStyleVar_DigitalBitHeight + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImPlotStyle, DigitalBitGap) }, // ImPlotStyleVar_DigitalBitGap - { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImPlotStyle, PlotBorderSize) }, // ImPlotStyleVar_PlotBorderSize - { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImPlotStyle, MinorAlpha) }, // ImPlotStyleVar_MinorAlpha - { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImPlotStyle, MajorTickLen) }, // ImPlotStyleVar_MajorTickLen - { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImPlotStyle, MinorTickLen) }, // ImPlotStyleVar_MinorTickLen - { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImPlotStyle, MajorTickSize) }, // ImPlotStyleVar_MajorTickSize - { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImPlotStyle, MinorTickSize) }, // ImPlotStyleVar_MinorTickSize - { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImPlotStyle, MajorGridSize) }, // ImPlotStyleVar_MajorGridSize - { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImPlotStyle, MinorGridSize) }, // ImPlotStyleVar_MinorGridSize - { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImPlotStyle, PlotPadding) }, // ImPlotStyleVar_PlotPadding - { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImPlotStyle, LabelPadding) }, // ImPlotStyleVar_LabelPaddine - { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImPlotStyle, LegendPadding) }, // ImPlotStyleVar_LegendPadding - { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImPlotStyle, LegendInnerPadding) }, // ImPlotStyleVar_LegendInnerPadding - { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImPlotStyle, LegendSpacing) }, // ImPlotStyleVar_LegendSpacing + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImPlotStyle, PlotBorderSize) }, // ImPlotStyleVar_PlotBorderSize + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImPlotStyle, MinorAlpha) }, // ImPlotStyleVar_MinorAlpha + { ImGuiDataType_Float, 2, (ImU32)offsetof(ImPlotStyle, MajorTickLen) }, // ImPlotStyleVar_MajorTickLen + { ImGuiDataType_Float, 2, (ImU32)offsetof(ImPlotStyle, MinorTickLen) }, // ImPlotStyleVar_MinorTickLen + { ImGuiDataType_Float, 2, (ImU32)offsetof(ImPlotStyle, MajorTickSize) }, // ImPlotStyleVar_MajorTickSize + { ImGuiDataType_Float, 2, (ImU32)offsetof(ImPlotStyle, MinorTickSize) }, // ImPlotStyleVar_MinorTickSize + { ImGuiDataType_Float, 2, (ImU32)offsetof(ImPlotStyle, MajorGridSize) }, // ImPlotStyleVar_MajorGridSize + { ImGuiDataType_Float, 2, (ImU32)offsetof(ImPlotStyle, MinorGridSize) }, // ImPlotStyleVar_MinorGridSize + { ImGuiDataType_Float, 2, (ImU32)offsetof(ImPlotStyle, PlotPadding) }, // ImPlotStyleVar_PlotPadding + { ImGuiDataType_Float, 2, (ImU32)offsetof(ImPlotStyle, LabelPadding) }, // ImPlotStyleVar_LabelPaddine + { ImGuiDataType_Float, 2, (ImU32)offsetof(ImPlotStyle, LegendPadding) }, // ImPlotStyleVar_LegendPadding + { ImGuiDataType_Float, 2, (ImU32)offsetof(ImPlotStyle, LegendInnerPadding) }, // ImPlotStyleVar_LegendInnerPadding + { ImGuiDataType_Float, 2, (ImU32)offsetof(ImPlotStyle, LegendSpacing) }, // ImPlotStyleVar_LegendSpacing - { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImPlotStyle, MousePosPadding) }, // ImPlotStyleVar_MousePosPadding - { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImPlotStyle, AnnotationPadding) }, // ImPlotStyleVar_AnnotationPadding - { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImPlotStyle, FitPadding) }, // ImPlotStyleVar_FitPadding - { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImPlotStyle, PlotDefaultSize) }, // ImPlotStyleVar_PlotDefaultSize - { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImPlotStyle, PlotMinSize) } // ImPlotStyleVar_PlotMinSize + { ImGuiDataType_Float, 2, (ImU32)offsetof(ImPlotStyle, MousePosPadding) }, // ImPlotStyleVar_MousePosPadding + { ImGuiDataType_Float, 2, (ImU32)offsetof(ImPlotStyle, AnnotationPadding) }, // ImPlotStyleVar_AnnotationPadding + { ImGuiDataType_Float, 2, (ImU32)offsetof(ImPlotStyle, FitPadding) }, // ImPlotStyleVar_FitPadding + { ImGuiDataType_Float, 2, (ImU32)offsetof(ImPlotStyle, PlotDefaultSize) }, // ImPlotStyleVar_PlotDefaultSize + { ImGuiDataType_Float, 2, (ImU32)offsetof(ImPlotStyle, PlotMinSize) } // ImPlotStyleVar_PlotMinSize }; static const ImPlotStyleVarInfo* GetPlotStyleVarInfo(ImPlotStyleVar idx) { @@ -333,8 +341,8 @@ void AddTextVertical(ImDrawList *DrawList, ImVec2 pos, ImU32 col, const char *te ImGuiContext& g = *GImGui; ImFont* font = g.Font; // Align to be pixel perfect - pos.x = IM_FLOOR(pos.x); - pos.y = IM_FLOOR(pos.y); + pos.x = ImFloor(pos.x); + pos.y = ImFloor(pos.y); const float scale = g.FontSize / font->FontSize; const char* s = text_begin; int chars_exp = (int)(text_end - s); @@ -485,10 +493,6 @@ void Initialize(ImPlotContext* ctx) { } void ResetCtxForNextPlot(ImPlotContext* ctx) { - // end child window if it was made - if (ctx->ChildWindowMade) - ImGui::EndChild(); - ctx->ChildWindowMade = false; // reset the next plot/item data ctx->NextPlotData.Reset(); ctx->NextItemData.Reset(); @@ -582,6 +586,28 @@ ImVec2 CalcLegendSize(ImPlotItemGroup& items, const ImVec2& pad, const ImVec2& s return legend_size; } +bool ClampLegendRect(ImRect& legend_rect, const ImRect& outer_rect, const ImVec2& pad) { + bool clamped = false; + ImRect outer_rect_pad(outer_rect.Min + pad, outer_rect.Max - pad); + if (legend_rect.Min.x < outer_rect_pad.Min.x) { + legend_rect.Min.x = outer_rect_pad.Min.x; + clamped = true; + } + if (legend_rect.Min.y < outer_rect_pad.Min.y) { + legend_rect.Min.y = outer_rect_pad.Min.y; + clamped = true; + } + if (legend_rect.Max.x > outer_rect_pad.Max.x) { + legend_rect.Max.x = outer_rect_pad.Max.x; + clamped = true; + } + if (legend_rect.Max.y > outer_rect_pad.Max.y) { + legend_rect.Max.y = outer_rect_pad.Max.y; + clamped = true; + } + return clamped; +} + int LegendSortingComp(const void* _a, const void* _b) { ImPlotItemGroup* items = GImPlot->SortItems; const int a = *(const int*)_a; @@ -1296,12 +1322,12 @@ bool DragFloat(const char*, F*, float, F, F) { template <> bool DragFloat(const char* label, double* v, float v_speed, double v_min, double v_max) { - return ImGui::DragScalar(label, ImGuiDataType_Double, v, v_speed, &v_min, &v_max, "%.3f", 1); + return ImGui::DragScalar(label, ImGuiDataType_Double, v, v_speed, &v_min, &v_max, "%.3g", 1); } template <> bool DragFloat(const char* label, float* v, float v_speed, float v_min, float v_max) { - return ImGui::DragScalar(label, ImGuiDataType_Float, v, v_speed, &v_min, &v_max, "%.3f", 1); + return ImGui::DragScalar(label, ImGuiDataType_Float, v, v_speed, &v_min, &v_max, "%.3g", 1); } inline void BeginDisabledControls(bool cond) { @@ -1548,7 +1574,7 @@ void ShowPlotContextMenu(ImPlotPlot& plot) { ImFlipFlag(plot.Flags, ImPlotFlags_Crosshairs); ImGui::EndMenu(); } - if (gp.CurrentSubplot != nullptr && !ImHasFlag(gp.CurrentPlot->Flags, ImPlotSubplotFlags_NoMenus)) { + if (gp.CurrentSubplot != nullptr && !ImHasFlag(gp.CurrentSubplot->Flags, ImPlotSubplotFlags_NoMenus)) { ImGui::Separator(); if ((ImGui::BeginMenu("Subplots"))) { ShowSubplotsContextMenu(*gp.CurrentSubplot); @@ -1808,7 +1834,7 @@ bool UpdateInput(ImPlotPlot& plot) { // BUTTON STATE ----------------------------------------------------------- - const ImGuiButtonFlags plot_button_flags = ImGuiButtonFlags_AllowItemOverlap + const ImGuiButtonFlags plot_button_flags = ImGuiButtonFlags_AllowOverlap | ImGuiButtonFlags_PressedOnClick | ImGuiButtonFlags_PressedOnDoubleClick | ImGuiButtonFlags_MouseButtonLeft @@ -1818,7 +1844,9 @@ bool UpdateInput(ImPlotPlot& plot) { | plot_button_flags; const bool plot_clicked = ImGui::ButtonBehavior(plot.PlotRect,plot.ID,&plot.Hovered,&plot.Held,plot_button_flags); - ImGui::SetItemAllowOverlap(); +#if (IMGUI_VERSION_NUM < 18966) + ImGui::SetItemAllowOverlap(); // Handled by ButtonBehavior() +#endif if (plot_clicked) { if (!ImHasFlag(plot.Flags, ImPlotFlags_NoBoxSelect) && IO.MouseClicked[gp.InputMap.Select] && ImHasFlag(IO.KeyMods, gp.InputMap.SelectMod)) { @@ -1951,10 +1979,12 @@ bool UpdateInput(ImPlotPlot& plot) { // SCROLL INPUT ----------------------------------------------------------- - if (any_hov && IO.MouseWheel != 0 && ImHasFlag(IO.KeyMods, gp.InputMap.ZoomMod)) { + if (any_hov && ImHasFlag(IO.KeyMods, gp.InputMap.ZoomMod)) { float zoom_rate = gp.InputMap.ZoomRate; - if (IO.MouseWheel > 0) + if (IO.MouseWheel == 0.0f) + zoom_rate = 0; + else if (IO.MouseWheel > 0) zoom_rate = (-zoom_rate) / (1.0f + (2.0f * zoom_rate)); ImVec2 rect_size = plot.PlotRect.GetSize(); float tx = ImRemap(IO.MousePos.x, plot.PlotRect.Min.x, plot.PlotRect.Max.x, 0.0f, 1.0f); @@ -1965,14 +1995,17 @@ bool UpdateInput(ImPlotPlot& plot) { const bool equal_zoom = axis_equal && x_axis.OrthoAxis != nullptr; const bool equal_locked = (equal_zoom != false) && x_axis.OrthoAxis->IsInputLocked(); if (x_hov[i] && !x_axis.IsInputLocked() && !equal_locked) { - float correction = (plot.Hovered && equal_zoom) ? 0.5f : 1.0f; - const double plot_l = x_axis.PixelsToPlot(plot.PlotRect.Min.x - rect_size.x * tx * zoom_rate * correction); - const double plot_r = x_axis.PixelsToPlot(plot.PlotRect.Max.x + rect_size.x * (1 - tx) * zoom_rate * correction); - x_axis.SetMin(x_axis.IsInverted() ? plot_r : plot_l); - x_axis.SetMax(x_axis.IsInverted() ? plot_l : plot_r); - if (axis_equal && x_axis.OrthoAxis != nullptr) - x_axis.OrthoAxis->SetAspect(x_axis.GetAspect()); - changed = true; + ImGui::SetKeyOwner(ImGuiKey_MouseWheelY, plot.ID); + if (zoom_rate != 0.0f) { + float correction = (plot.Hovered && equal_zoom) ? 0.5f : 1.0f; + const double plot_l = x_axis.PixelsToPlot(plot.PlotRect.Min.x - rect_size.x * tx * zoom_rate * correction); + const double plot_r = x_axis.PixelsToPlot(plot.PlotRect.Max.x + rect_size.x * (1 - tx) * zoom_rate * correction); + x_axis.SetMin(x_axis.IsInverted() ? plot_r : plot_l); + x_axis.SetMax(x_axis.IsInverted() ? plot_l : plot_r); + if (axis_equal && x_axis.OrthoAxis != nullptr) + x_axis.OrthoAxis->SetAspect(x_axis.GetAspect()); + changed = true; + } } } for (int i = 0; i < IMPLOT_NUM_Y_AXES; i++) { @@ -1980,14 +2013,17 @@ bool UpdateInput(ImPlotPlot& plot) { const bool equal_zoom = axis_equal && y_axis.OrthoAxis != nullptr; const bool equal_locked = equal_zoom && y_axis.OrthoAxis->IsInputLocked(); if (y_hov[i] && !y_axis.IsInputLocked() && !equal_locked) { - float correction = (plot.Hovered && equal_zoom) ? 0.5f : 1.0f; - const double plot_t = y_axis.PixelsToPlot(plot.PlotRect.Min.y - rect_size.y * ty * zoom_rate * correction); - const double plot_b = y_axis.PixelsToPlot(plot.PlotRect.Max.y + rect_size.y * (1 - ty) * zoom_rate * correction); - y_axis.SetMin(y_axis.IsInverted() ? plot_t : plot_b); - y_axis.SetMax(y_axis.IsInverted() ? plot_b : plot_t); - if (axis_equal && y_axis.OrthoAxis != nullptr) - y_axis.OrthoAxis->SetAspect(y_axis.GetAspect()); - changed = true; + ImGui::SetKeyOwner(ImGuiKey_MouseWheelY, plot.ID); + if (zoom_rate != 0.0f) { + float correction = (plot.Hovered && equal_zoom) ? 0.5f : 1.0f; + const double plot_t = y_axis.PixelsToPlot(plot.PlotRect.Min.y - rect_size.y * ty * zoom_rate * correction); + const double plot_b = y_axis.PixelsToPlot(plot.PlotRect.Max.y + rect_size.y * (1 - ty) * zoom_rate * correction); + y_axis.SetMin(y_axis.IsInverted() ? plot_t : plot_b); + y_axis.SetMax(y_axis.IsInverted() ? plot_b : plot_t); + if (axis_equal && y_axis.OrthoAxis != nullptr) + y_axis.OrthoAxis->SetAspect(y_axis.GetAspect()); + changed = true; + } } } } @@ -2269,19 +2305,19 @@ void SetupAxesLimits(double x_min, double x_max, double y_min, double y_max, ImP void SetupLegend(ImPlotLocation location, ImPlotLegendFlags flags) { ImPlotContext& gp = *GImPlot; - IM_ASSERT_USER_ERROR(gp.CurrentPlot != nullptr && !gp.CurrentPlot->SetupLocked, - "Setup needs to be called after BeginPlot and before any setup locking functions (e.g. PlotX)!"); - IM_ASSERT_USER_ERROR(gp.CurrentItems != nullptr, - "SetupLegend() needs to be called within an itemized context!"); - ImPlotLegend& legend = gp.CurrentItems->Legend; - // check and set location - if (location != legend.PreviousLocation) - legend.Location = location; - legend.PreviousLocation = location; - // check and set flags - if (flags != legend.PreviousFlags) - legend.Flags = flags; - legend.PreviousFlags = flags; + IM_ASSERT_USER_ERROR((gp.CurrentPlot != nullptr && !gp.CurrentPlot->SetupLocked) || (gp.CurrentSubplot != nullptr && gp.CurrentPlot == nullptr), + "Setup needs to be called after BeginPlot or BeginSubplots and before any setup locking functions (e.g. PlotX)!"); + if (gp.CurrentItems) { + ImPlotLegend& legend = gp.CurrentItems->Legend; + // check and set location + if (location != legend.PreviousLocation) + legend.Location = location; + legend.PreviousLocation = location; + // check and set flags + if (flags != legend.PreviousFlags) + legend.Flags = flags; + legend.PreviousFlags = flags; + } } void SetupMouseText(ImPlotLocation location, ImPlotMouseTextFlags flags) { @@ -2393,22 +2429,6 @@ bool BeginPlot(const char* title_id, const ImVec2& size, ImPlotFlags flags) { for (int i = 0; i < ImAxis_COUNT; ++i) ApplyNextPlotData(i); - // capture scroll with a child region - if (!ImHasFlag(plot.Flags, ImPlotFlags_NoChild)) { - ImVec2 child_size; - if (gp.CurrentSubplot != nullptr) - child_size = gp.CurrentSubplot->CellSize; - else - child_size = ImVec2(size.x == 0 ? gp.Style.PlotDefaultSize.x : size.x, size.y == 0 ? gp.Style.PlotDefaultSize.y : size.y); - ImGui::BeginChild(title_id, child_size, false, ImGuiWindowFlags_NoScrollbar); - Window = ImGui::GetCurrentWindow(); - Window->ScrollMax.y = 1.0f; - gp.ChildWindowMade = true; - } - else { - gp.ChildWindowMade = false; - } - // clear text buffers plot.ClearTextBuffer(); plot.SetTitle(title_id); @@ -2550,7 +2570,7 @@ void SetupFinish() { // (2) get y tick labels (needed for left/right pad) for (int i = 0; i < IMPLOT_NUM_Y_AXES; i++) { ImPlotAxis& axis = plot.YAxis(i); - if (axis.WillRender() && axis.ShowDefaultTicks) { + if (axis.WillRender() && axis.ShowDefaultTicks && plot_height > 0) { axis.Locator(axis.Ticker, axis.Range, plot_height, true, axis.Formatter, axis.FormatterData); } } @@ -2563,7 +2583,7 @@ void SetupFinish() { // (4) get x ticks for (int i = 0; i < IMPLOT_NUM_X_AXES; i++) { ImPlotAxis& axis = plot.XAxis(i); - if (axis.WillRender() && axis.ShowDefaultTicks) { + if (axis.WillRender() && axis.ShowDefaultTicks && plot_width > 0) { axis.Locator(axis.Ticker, axis.Range, plot_width, false, axis.Formatter, axis.FormatterData); } } @@ -2758,7 +2778,7 @@ void EndPlot() { // FINAL RENDER ----------------------------------------------------------- - const bool render_border = gp.Style.PlotBorderSize > 0 && gp.Style.Colors[ImPlotCol_PlotBorder].w > 0; + const bool render_border = gp.Style.PlotBorderSize > 0 && GetStyleColorVec4(ImPlotCol_PlotBorder).w > 0; const bool any_x_held = plot.Held || AnyAxesHeld(&plot.Axes[ImAxis_X1], IMPLOT_NUM_X_AXES); const bool any_y_held = plot.Held || AnyAxesHeld(&plot.Axes[ImAxis_Y1], IMPLOT_NUM_Y_AXES); @@ -3023,24 +3043,57 @@ void EndPlot() { legend.Location, legend_out ? gp.Style.PlotPadding : gp.Style.LegendPadding); legend.Rect = ImRect(legend_pos, legend_pos + legend_size); - // test hover - legend.Hovered = ImGui::IsWindowHovered() && legend.Rect.Contains(IO.MousePos); + legend.RectClamped = legend.Rect; + const bool legend_scrollable = ClampLegendRect(legend.RectClamped, + legend_out ? plot.FrameRect : plot.PlotRect, + legend_out ? gp.Style.PlotPadding : gp.Style.LegendPadding + ); + const ImGuiButtonFlags legend_button_flags = ImGuiButtonFlags_AllowOverlap + | ImGuiButtonFlags_PressedOnClick + | ImGuiButtonFlags_PressedOnDoubleClick + | ImGuiButtonFlags_MouseButtonLeft + | ImGuiButtonFlags_MouseButtonRight + | ImGuiButtonFlags_MouseButtonMiddle + | ImGuiButtonFlags_FlattenChildren; + ImGui::KeepAliveID(plot.Items.ID); + ImGui::ButtonBehavior(legend.RectClamped, plot.Items.ID, &legend.Hovered, &legend.Held, legend_button_flags); + legend.Hovered = legend.Hovered || (ImGui::IsWindowHovered() && legend.RectClamped.Contains(IO.MousePos)); - if (legend_out) - ImGui::PushClipRect(plot.FrameRect.Min, plot.FrameRect.Max, true); - else - PushPlotClipRect(); - ImU32 col_bg = GetStyleColorU32(ImPlotCol_LegendBg); - ImU32 col_bd = GetStyleColorU32(ImPlotCol_LegendBorder); - DrawList.AddRectFilled(legend.Rect.Min, legend.Rect.Max, col_bg); - DrawList.AddRect(legend.Rect.Min, legend.Rect.Max, col_bd); + if (legend_scrollable) { + if (legend.Hovered) { + ImGui::SetKeyOwner(ImGuiKey_MouseWheelY, plot.Items.ID); + if (IO.MouseWheel != 0.0f) { + ImVec2 max_step = legend.Rect.GetSize() * 0.67f; + float font_size = ImGui::GetCurrentWindow()->CalcFontSize(); + float scroll_step = ImFloor(ImMin(2 * font_size, max_step.x)); + legend.Scroll.x += scroll_step * IO.MouseWheel; + legend.Scroll.y += scroll_step * IO.MouseWheel; + } + } + const ImVec2 min_scroll_offset = legend.RectClamped.GetSize() - legend.Rect.GetSize(); + legend.Scroll.x = ImClamp(legend.Scroll.x, min_scroll_offset.x, 0.0f); + legend.Scroll.y = ImClamp(legend.Scroll.y, min_scroll_offset.y, 0.0f); + const ImVec2 scroll_offset = legend_horz ? ImVec2(legend.Scroll.x, 0) : ImVec2(0, legend.Scroll.y); + ImVec2 legend_offset = legend.RectClamped.Min - legend.Rect.Min + scroll_offset; + legend.Rect.Min += legend_offset; + legend.Rect.Max += legend_offset; + } else { + legend.Scroll = ImVec2(0,0); + } + + const ImU32 col_bg = GetStyleColorU32(ImPlotCol_LegendBg); + const ImU32 col_bd = GetStyleColorU32(ImPlotCol_LegendBorder); + ImGui::PushClipRect(legend.RectClamped.Min, legend.RectClamped.Max, true); + DrawList.AddRectFilled(legend.RectClamped.Min, legend.RectClamped.Max, col_bg); bool legend_contextable = ShowLegendEntries(plot.Items, legend.Rect, legend.Hovered, gp.Style.LegendInnerPadding, gp.Style.LegendSpacing, !legend_horz, DrawList) && !ImHasFlag(legend.Flags, ImPlotLegendFlags_NoMenus); + DrawList.AddRect(legend.RectClamped.Min, legend.RectClamped.Max, col_bd); + ImGui::PopClipRect(); // main ctx menu if (gp.OpenContextThisFrame && legend_contextable && !ImHasFlag(plot.Flags, ImPlotFlags_NoMenus)) ImGui::OpenPopup("##LegendContext"); - ImGui::PopClipRect(); + if (ImGui::BeginPopup("##LegendContext")) { ImGui::Text("Legend"); ImGui::Separator(); if (ShowLegendContextMenu(legend, !ImHasFlag(plot.Flags, ImPlotFlags_NoLegend))) @@ -3350,7 +3403,7 @@ bool BeginSubplots(const char* title, int rows, int cols, const ImVec2& size, Im subplot.FrameRect = ImRect(Window->DC.CursorPos, Window->DC.CursorPos + frame_size); subplot.GridRect.Min = subplot.FrameRect.Min + half_pad + ImVec2(0,pad_top); subplot.GridRect.Max = subplot.FrameRect.Max - half_pad; - subplot.FrameHovered = subplot.FrameRect.Contains(ImGui::GetMousePos()) && ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows); + subplot.FrameHovered = subplot.FrameRect.Contains(ImGui::GetMousePos()) && ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows|ImGuiHoveredFlags_AllowWhenBlockedByActiveItem); // outside legend adjustments (TODO: make function) const bool share_items = ImHasFlag(subplot.Flags, ImPlotSubplotFlags_ShareItems); @@ -3397,7 +3450,7 @@ bool BeginSubplots(const char* title, int rows, int cols, const ImVec2& size, Im ImGui::KeepAliveID(sep_id); const ImRect sep_bb = ImRect(subplot.GridRect.Min.x, ypos-SUBPLOT_SPLITTER_HALF_THICKNESS, subplot.GridRect.Max.x, ypos+SUBPLOT_SPLITTER_HALF_THICKNESS); bool sep_hov = false, sep_hld = false; - const bool sep_clk = ImGui::ButtonBehavior(sep_bb, sep_id, &sep_hov, &sep_hld, ImGuiButtonFlags_FlattenChildren | ImGuiButtonFlags_AllowItemOverlap | ImGuiButtonFlags_PressedOnClick | ImGuiButtonFlags_PressedOnDoubleClick); + const bool sep_clk = ImGui::ButtonBehavior(sep_bb, sep_id, &sep_hov, &sep_hld, ImGuiButtonFlags_FlattenChildren | ImGuiButtonFlags_PressedOnClick | ImGuiButtonFlags_PressedOnDoubleClick); if ((sep_hov && G.HoveredIdTimer > SUBPLOT_SPLITTER_FEEDBACK_TIMER) || sep_hld) { if (sep_clk && ImGui::IsMouseDoubleClicked(0)) { float p = (subplot.RowRatios[r] + subplot.RowRatios[r+1])/2; @@ -3427,7 +3480,7 @@ bool BeginSubplots(const char* title, int rows, int cols, const ImVec2& size, Im ImGui::KeepAliveID(sep_id); const ImRect sep_bb = ImRect(xpos-SUBPLOT_SPLITTER_HALF_THICKNESS, subplot.GridRect.Min.y, xpos+SUBPLOT_SPLITTER_HALF_THICKNESS, subplot.GridRect.Max.y); bool sep_hov = false, sep_hld = false; - const bool sep_clk = ImGui::ButtonBehavior(sep_bb, sep_id, &sep_hov, &sep_hld, ImGuiButtonFlags_FlattenChildren | ImGuiButtonFlags_AllowItemOverlap | ImGuiButtonFlags_PressedOnClick | ImGuiButtonFlags_PressedOnDoubleClick); + const bool sep_clk = ImGui::ButtonBehavior(sep_bb, sep_id, &sep_hov, &sep_hld, ImGuiButtonFlags_FlattenChildren | ImGuiButtonFlags_PressedOnClick | ImGuiButtonFlags_PressedOnDoubleClick); if ((sep_hov && G.HoveredIdTimer > SUBPLOT_SPLITTER_FEEDBACK_TIMER) || sep_hld) { if (sep_clk && ImGui::IsMouseDoubleClicked(0)) { float p = (subplot.ColRatios[c] + subplot.ColRatios[c+1])/2; @@ -3488,6 +3541,7 @@ void EndSubplots() { ImPlotContext& gp = *GImPlot; IM_ASSERT_USER_ERROR(gp.CurrentSubplot != nullptr, "Mismatched BeginSubplots()/EndSubplots()!"); ImPlotSubplot& subplot = *gp.CurrentSubplot; + const ImGuiIO& IO = ImGui::GetIO(); // set alignments for (int r = 0; r < subplot.Rows; ++r) subplot.RowAlignmentData[r].End(); @@ -3506,24 +3560,60 @@ void EndSubplots() { const bool share_items = ImHasFlag(subplot.Flags, ImPlotSubplotFlags_ShareItems); ImDrawList& DrawList = *ImGui::GetWindowDrawList(); if (share_items && !ImHasFlag(subplot.Flags, ImPlotSubplotFlags_NoLegend) && subplot.Items.GetLegendCount() > 0) { - const bool legend_horz = ImHasFlag(subplot.Items.Legend.Flags, ImPlotLegendFlags_Horizontal); + ImPlotLegend& legend = subplot.Items.Legend; + const bool legend_horz = ImHasFlag(legend.Flags, ImPlotLegendFlags_Horizontal); const ImVec2 legend_size = CalcLegendSize(subplot.Items, gp.Style.LegendInnerPadding, gp.Style.LegendSpacing, !legend_horz); - const ImVec2 legend_pos = GetLocationPos(subplot.FrameRect, legend_size, subplot.Items.Legend.Location, gp.Style.PlotPadding); - subplot.Items.Legend.Rect = ImRect(legend_pos, legend_pos + legend_size); - subplot.Items.Legend.Hovered = subplot.FrameHovered && subplot.Items.Legend.Rect.Contains(ImGui::GetIO().MousePos); - ImGui::PushClipRect(subplot.FrameRect.Min, subplot.FrameRect.Max, true); - ImU32 col_bg = GetStyleColorU32(ImPlotCol_LegendBg); - ImU32 col_bd = GetStyleColorU32(ImPlotCol_LegendBorder); - DrawList.AddRectFilled(subplot.Items.Legend.Rect.Min, subplot.Items.Legend.Rect.Max, col_bg); - DrawList.AddRect(subplot.Items.Legend.Rect.Min, subplot.Items.Legend.Rect.Max, col_bd); - bool legend_contextable = ShowLegendEntries(subplot.Items, subplot.Items.Legend.Rect, subplot.Items.Legend.Hovered, gp.Style.LegendInnerPadding, gp.Style.LegendSpacing, !legend_horz, DrawList) - && !ImHasFlag(subplot.Items.Legend.Flags, ImPlotLegendFlags_NoMenus); + const ImVec2 legend_pos = GetLocationPos(subplot.FrameRect, legend_size, legend.Location, gp.Style.PlotPadding); + legend.Rect = ImRect(legend_pos, legend_pos + legend_size); + legend.RectClamped = legend.Rect; + const bool legend_scrollable = ClampLegendRect(legend.RectClamped,subplot.FrameRect, gp.Style.PlotPadding); + const ImGuiButtonFlags legend_button_flags = ImGuiButtonFlags_AllowOverlap + | ImGuiButtonFlags_PressedOnClick + | ImGuiButtonFlags_PressedOnDoubleClick + | ImGuiButtonFlags_MouseButtonLeft + | ImGuiButtonFlags_MouseButtonRight + | ImGuiButtonFlags_MouseButtonMiddle + | ImGuiButtonFlags_FlattenChildren; + ImGui::KeepAliveID(subplot.Items.ID); + ImGui::ButtonBehavior(legend.RectClamped, subplot.Items.ID, &legend.Hovered, &legend.Held, legend_button_flags); + legend.Hovered = legend.Hovered || (subplot.FrameHovered && legend.RectClamped.Contains(ImGui::GetIO().MousePos)); + + if (legend_scrollable) { + if (legend.Hovered) { + ImGui::SetKeyOwner(ImGuiKey_MouseWheelY, subplot.Items.ID); + if (IO.MouseWheel != 0.0f) { + ImVec2 max_step = legend.Rect.GetSize() * 0.67f; + float font_size = ImGui::GetCurrentWindow()->CalcFontSize(); + float scroll_step = ImFloor(ImMin(2 * font_size, max_step.x)); + legend.Scroll.x += scroll_step * IO.MouseWheel; + legend.Scroll.y += scroll_step * IO.MouseWheel; + } + } + const ImVec2 min_scroll_offset = legend.RectClamped.GetSize() - legend.Rect.GetSize(); + legend.Scroll.x = ImClamp(legend.Scroll.x, min_scroll_offset.x, 0.0f); + legend.Scroll.y = ImClamp(legend.Scroll.y, min_scroll_offset.y, 0.0f); + const ImVec2 scroll_offset = legend_horz ? ImVec2(legend.Scroll.x, 0) : ImVec2(0, legend.Scroll.y); + ImVec2 legend_offset = legend.RectClamped.Min - legend.Rect.Min + scroll_offset; + legend.Rect.Min += legend_offset; + legend.Rect.Max += legend_offset; + } else { + legend.Scroll = ImVec2(0,0); + } + + const ImU32 col_bg = GetStyleColorU32(ImPlotCol_LegendBg); + const ImU32 col_bd = GetStyleColorU32(ImPlotCol_LegendBorder); + ImGui::PushClipRect(legend.RectClamped.Min, legend.RectClamped.Max, true); + DrawList.AddRectFilled(legend.RectClamped.Min, legend.RectClamped.Max, col_bg); + bool legend_contextable = ShowLegendEntries(subplot.Items, legend.Rect, legend.Hovered, gp.Style.LegendInnerPadding, gp.Style.LegendSpacing, !legend_horz, DrawList) + && !ImHasFlag(legend.Flags, ImPlotLegendFlags_NoMenus); + DrawList.AddRect(legend.RectClamped.Min, legend.RectClamped.Max, col_bd); + ImGui::PopClipRect(); + if (legend_contextable && !ImHasFlag(subplot.Flags, ImPlotSubplotFlags_NoMenus) && ImGui::GetIO().MouseReleased[gp.InputMap.Menu]) ImGui::OpenPopup("##LegendContext"); - ImGui::PopClipRect(); if (ImGui::BeginPopup("##LegendContext")) { ImGui::Text("Legend"); ImGui::Separator(); - if (ShowLegendContextMenu(subplot.Items.Legend, !ImHasFlag(subplot.Flags, ImPlotFlags_NoLegend))) + if (ShowLegendContextMenu(legend, !ImHasFlag(subplot.Flags, ImPlotFlags_NoLegend))) ImFlipFlag(subplot.Flags, ImPlotFlags_NoLegend); ImGui::EndPopup(); } @@ -3804,7 +3894,7 @@ IMPLOT_API void TagYV(double y, const ImVec4& color, const char* fmt, va_list ar static const float DRAG_GRAB_HALF_SIZE = 4.0f; -bool DragPoint(int n_id, double* x, double* y, const ImVec4& col, float radius, ImPlotDragToolFlags flags) { +bool DragPoint(int n_id, double* x, double* y, const ImVec4& col, float radius, ImPlotDragToolFlags flags, bool* out_clicked, bool* out_hovered, bool* out_held) { ImGui::PushID("#IMPLOT_DRAG_POINT"); IM_ASSERT_USER_ERROR(GImPlot->CurrentPlot != nullptr, "DragPoint() needs to be called between BeginPlot() and EndPlot()!"); SetupLock(); @@ -3826,30 +3916,34 @@ bool DragPoint(int n_id, double* x, double* y, const ImVec4& col, float radius, bool hovered = false, held = false; ImGui::KeepAliveID(id); - if (input) - ImGui::ButtonBehavior(rect,id,&hovered,&held); + if (input) { + bool clicked = ImGui::ButtonBehavior(rect,id,&hovered,&held); + if (out_clicked) *out_clicked = clicked; + if (out_hovered) *out_hovered = hovered; + if (out_held) *out_held = held; + } - bool dragging = false; + bool modified = false; if (held && ImGui::IsMouseDragging(0)) { *x = ImPlot::GetPlotMousePos(IMPLOT_AUTO,IMPLOT_AUTO).x; *y = ImPlot::GetPlotMousePos(IMPLOT_AUTO,IMPLOT_AUTO).y; - dragging = true; + modified = true; } PushPlotClipRect(); ImDrawList& DrawList = *GetPlotDrawList(); if ((hovered || held) && show_curs) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); - if (dragging && no_delay) + if (modified && no_delay) pos = PlotToPixels(*x,*y,IMPLOT_AUTO,IMPLOT_AUTO); DrawList.AddCircleFilled(pos, radius, col32); PopPlotClipRect(); ImGui::PopID(); - return dragging; + return modified; } -bool DragLineX(int n_id, double* value, const ImVec4& col, float thickness, ImPlotDragToolFlags flags) { +bool DragLineX(int n_id, double* value, const ImVec4& col, float thickness, ImPlotDragToolFlags flags, bool* out_clicked, bool* out_hovered, bool* out_held) { // ImGui::PushID("#IMPLOT_DRAG_LINE_X"); ImPlotContext& gp = *GImPlot; IM_ASSERT_USER_ERROR(gp.CurrentPlot != nullptr, "DragLineX() needs to be called between BeginPlot() and EndPlot()!"); @@ -3871,8 +3965,12 @@ bool DragLineX(int n_id, double* value, const ImVec4& col, float thickness, ImPl bool hovered = false, held = false; ImGui::KeepAliveID(id); - if (input) - ImGui::ButtonBehavior(rect,id,&hovered,&held); + if (input) { + bool clicked = ImGui::ButtonBehavior(rect,id,&hovered,&held); + if (out_clicked) *out_clicked = clicked; + if (out_hovered) *out_hovered = hovered; + if (out_held) *out_held = held; + } if ((hovered || held) && show_curs) ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeEW); @@ -3881,15 +3979,15 @@ bool DragLineX(int n_id, double* value, const ImVec4& col, float thickness, ImPl ImVec4 color = IsColorAuto(col) ? ImGui::GetStyleColorVec4(ImGuiCol_Text) : col; ImU32 col32 = ImGui::ColorConvertFloat4ToU32(color); - bool dragging = false; + bool modified = false; if (held && ImGui::IsMouseDragging(0)) { *value = ImPlot::GetPlotMousePos(IMPLOT_AUTO,IMPLOT_AUTO).x; - dragging = true; + modified = true; } PushPlotClipRect(); ImDrawList& DrawList = *GetPlotDrawList(); - if (dragging && no_delay) + if (modified && no_delay) x = IM_ROUND(PlotToPixels(*value,0,IMPLOT_AUTO,IMPLOT_AUTO).x); DrawList.AddLine(ImVec2(x,yt), ImVec2(x,yb), col32, thickness); DrawList.AddLine(ImVec2(x,yt), ImVec2(x,yt+len), col32, 3*thickness); @@ -3897,10 +3995,10 @@ bool DragLineX(int n_id, double* value, const ImVec4& col, float thickness, ImPl PopPlotClipRect(); // ImGui::PopID(); - return dragging; + return modified; } -bool DragLineY(int n_id, double* value, const ImVec4& col, float thickness, ImPlotDragToolFlags flags) { +bool DragLineY(int n_id, double* value, const ImVec4& col, float thickness, ImPlotDragToolFlags flags, bool* out_clicked, bool* out_hovered, bool* out_held) { ImGui::PushID("#IMPLOT_DRAG_LINE_Y"); ImPlotContext& gp = *GImPlot; IM_ASSERT_USER_ERROR(gp.CurrentPlot != nullptr, "DragLineY() needs to be called between BeginPlot() and EndPlot()!"); @@ -3923,8 +4021,12 @@ bool DragLineY(int n_id, double* value, const ImVec4& col, float thickness, ImPl bool hovered = false, held = false; ImGui::KeepAliveID(id); - if (input) - ImGui::ButtonBehavior(rect,id,&hovered,&held); + if (input) { + bool clicked = ImGui::ButtonBehavior(rect,id,&hovered,&held); + if (out_clicked) *out_clicked = clicked; + if (out_hovered) *out_hovered = hovered; + if (out_held) *out_held = held; + } if ((hovered || held) && show_curs) ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeNS); @@ -3933,15 +4035,15 @@ bool DragLineY(int n_id, double* value, const ImVec4& col, float thickness, ImPl ImVec4 color = IsColorAuto(col) ? ImGui::GetStyleColorVec4(ImGuiCol_Text) : col; ImU32 col32 = ImGui::ColorConvertFloat4ToU32(color); - bool dragging = false; + bool modified = false; if (held && ImGui::IsMouseDragging(0)) { *value = ImPlot::GetPlotMousePos(IMPLOT_AUTO,IMPLOT_AUTO).y; - dragging = true; + modified = true; } PushPlotClipRect(); ImDrawList& DrawList = *GetPlotDrawList(); - if (dragging && no_delay) + if (modified && no_delay) y = IM_ROUND(PlotToPixels(0, *value,IMPLOT_AUTO,IMPLOT_AUTO).y); DrawList.AddLine(ImVec2(xl,y), ImVec2(xr,y), col32, thickness); DrawList.AddLine(ImVec2(xl,y), ImVec2(xl+len,y), col32, 3*thickness); @@ -3949,10 +4051,10 @@ bool DragLineY(int n_id, double* value, const ImVec4& col, float thickness, ImPl PopPlotClipRect(); ImGui::PopID(); - return dragging; + return modified; } -bool DragRect(int n_id, double* x_min, double* y_min, double* x_max, double* y_max, const ImVec4& col, ImPlotDragToolFlags flags) { +bool DragRect(int n_id, double* x_min, double* y_min, double* x_max, double* y_max, const ImVec4& col, ImPlotDragToolFlags flags, bool* out_clicked, bool* out_hovered, bool* out_held) { ImGui::PushID("#IMPLOT_DRAG_RECT"); IM_ASSERT_USER_ERROR(GImPlot->CurrentPlot != nullptr, "DragRect() needs to be called between BeginPlot() and EndPlot()!"); SetupLock(); @@ -3989,13 +4091,18 @@ bool DragRect(int n_id, double* x_min, double* y_min, double* x_max, double* y_m ImU32 col32_a = ImGui::ColorConvertFloat4ToU32(color); const ImGuiID id = ImGui::GetCurrentWindow()->GetID(n_id); - bool dragging = false; - bool hovered = false, held = false; + bool modified = false; + bool clicked = false, hovered = false, held = false; ImRect b_rect(pc.x-DRAG_GRAB_HALF_SIZE,pc.y-DRAG_GRAB_HALF_SIZE,pc.x+DRAG_GRAB_HALF_SIZE,pc.y+DRAG_GRAB_HALF_SIZE); ImGui::KeepAliveID(id); - if (input) - ImGui::ButtonBehavior(b_rect,id,&hovered,&held); + if (input) { + // middle point + clicked = ImGui::ButtonBehavior(b_rect,id,&hovered,&held); + if (out_clicked) *out_clicked = clicked; + if (out_hovered) *out_hovered = hovered; + if (out_held) *out_held = held; + } if ((hovered || held) && show_curs) ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeAll); @@ -4005,7 +4112,7 @@ bool DragRect(int n_id, double* x_min, double* y_min, double* x_max, double* y_m *y[i] = pp.y; *x[i] = pp.x; } - dragging = true; + modified = true; } for (int i = 0; i < 4; ++i) { @@ -4013,15 +4120,19 @@ bool DragRect(int n_id, double* x_min, double* y_min, double* x_max, double* y_m b_rect = ImRect(p[i].x-DRAG_GRAB_HALF_SIZE,p[i].y-DRAG_GRAB_HALF_SIZE,p[i].x+DRAG_GRAB_HALF_SIZE,p[i].y+DRAG_GRAB_HALF_SIZE); ImGuiID p_id = id + i + 1; ImGui::KeepAliveID(p_id); - if (input) - ImGui::ButtonBehavior(b_rect,p_id,&hovered,&held); + if (input) { + clicked = ImGui::ButtonBehavior(b_rect,p_id,&hovered,&held); + if (out_clicked) *out_clicked = *out_clicked || clicked; + if (out_hovered) *out_hovered = *out_hovered || hovered; + if (out_held) *out_held = *out_held || held; + } if ((hovered || held) && show_curs) ImGui::SetMouseCursor(cur[i]); if (held && ImGui::IsMouseDragging(0)) { *x[i] = ImPlot::GetPlotMousePos(IMPLOT_AUTO,IMPLOT_AUTO).x; *y[i] = ImPlot::GetPlotMousePos(IMPLOT_AUTO,IMPLOT_AUTO).y; - dragging = true; + modified = true; } // edges @@ -4031,8 +4142,12 @@ bool DragRect(int n_id, double* x_min, double* y_min, double* x_max, double* y_m : ImRect(e_min.x - DRAG_GRAB_HALF_SIZE, e_min.y + DRAG_GRAB_HALF_SIZE, e_max.x + DRAG_GRAB_HALF_SIZE, e_max.y - DRAG_GRAB_HALF_SIZE); ImGuiID e_id = id + i + 5; ImGui::KeepAliveID(e_id); - if (input) - ImGui::ButtonBehavior(b_rect,e_id,&hovered,&held); + if (input) { + clicked = ImGui::ButtonBehavior(b_rect,e_id,&hovered,&held); + if (out_clicked) *out_clicked = *out_clicked || clicked; + if (out_hovered) *out_hovered = *out_hovered || hovered; + if (out_held) *out_held = *out_held || held; + } if ((hovered || held) && show_curs) h[i] ? ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeNS) : ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeEW); if (held && ImGui::IsMouseDragging(0)) { @@ -4040,7 +4155,7 @@ bool DragRect(int n_id, double* x_min, double* y_min, double* x_max, double* y_m *y[i] = ImPlot::GetPlotMousePos(IMPLOT_AUTO,IMPLOT_AUTO).y; else *x[i] = ImPlot::GetPlotMousePos(IMPLOT_AUTO,IMPLOT_AUTO).x; - dragging = true; + modified = true; } if (hovered && ImGui::IsMouseDoubleClicked(0)) { @@ -4049,14 +4164,22 @@ bool DragRect(int n_id, double* x_min, double* y_min, double* x_max, double* y_m *y[i] = ((y[i] == y_min && *y_min < *y_max) || (y[i] == y_max && *y_max < *y_min)) ? b.Y.Min : b.Y.Max; else *x[i] = ((x[i] == x_min && *x_min < *x_max) || (x[i] == x_max && *x_max < *x_min)) ? b.X.Min : b.X.Max; - dragging = true; + modified = true; } } + const bool mouse_inside = rect_grab.Contains(ImGui::GetMousePos()); + const bool mouse_clicked = ImGui::IsMouseClicked(0); + const bool mouse_down = ImGui::IsMouseDown(0); + if (input && mouse_inside) { + if (out_clicked) *out_clicked = *out_clicked || mouse_clicked; + if (out_hovered) *out_hovered = true; + if (out_held) *out_held = *out_held || mouse_down; + } PushPlotClipRect(); ImDrawList& DrawList = *GetPlotDrawList(); - if (dragging && no_delay) { + if (modified && no_delay) { for (int i = 0; i < 4; ++i) p[i] = PlotToPixels(*x[i],*y[i],IMPLOT_AUTO,IMPLOT_AUTO); pc = PlotToPixels((*x_min+*x_max)/2,(*y_min+*y_max)/2,IMPLOT_AUTO,IMPLOT_AUTO); @@ -4064,18 +4187,18 @@ bool DragRect(int n_id, double* x_min, double* y_min, double* x_max, double* y_m } DrawList.AddRectFilled(rect.Min, rect.Max, col32_a); DrawList.AddRect(rect.Min, rect.Max, col32); - if (input && (dragging || rect_grab.Contains(ImGui::GetMousePos()))) { + if (input && (modified || mouse_inside)) { DrawList.AddCircleFilled(pc,DRAG_GRAB_HALF_SIZE,col32); for (int i = 0; i < 4; ++i) DrawList.AddCircleFilled(p[i],DRAG_GRAB_HALF_SIZE,col32); } PopPlotClipRect(); ImGui::PopID(); - return dragging; + return modified; } -bool DragRect(int id, ImPlotRect* bounds, const ImVec4& col, ImPlotDragToolFlags flags) { - return DragRect(id, &bounds->X.Min, &bounds->Y.Min,&bounds->X.Max, &bounds->Y.Max, col, flags); +bool DragRect(int id, ImPlotRect* bounds, const ImVec4& col, ImPlotDragToolFlags flags, bool* out_clicked, bool* out_hovered, bool* out_held) { + return DragRect(id, &bounds->X.Min, &bounds->Y.Min,&bounds->X.Max, &bounds->Y.Max, col, flags, out_clicked, out_hovered, out_held); } //----------------------------------------------------------------------------- @@ -4171,7 +4294,7 @@ bool BeginDragDropTargetAxis(ImAxis axis) { bool BeginDragDropTargetLegend() { SetupLock(); ImPlotItemGroup& items = *GImPlot->CurrentItems; - ImRect rect = items.Legend.Rect; + ImRect rect = items.Legend.RectClamped; return ImGui::BeginDragDropTargetCustom(rect, items.ID); } @@ -5110,6 +5233,7 @@ void ShowMetricsWindow(bool* p_popen) { static bool show_frame_rects = false; static bool show_subplot_frame_rects = false; static bool show_subplot_grid_rects = false; + static bool show_legend_rects = false; ImDrawList& fg = *ImGui::GetForegroundDrawList(); @@ -5134,6 +5258,7 @@ void ShowMetricsWindow(bool* p_popen) { ImGui::Checkbox("Show Axis Rects", &show_axis_rects); ImGui::Checkbox("Show Subplot Frame Rects", &show_subplot_frame_rects); ImGui::Checkbox("Show Subplot Grid Rects", &show_subplot_grid_rects); + ImGui::Checkbox("Show Legend Rects", &show_legend_rects); ImGui::TreePop(); } const int n_plots = gp.Plots.GetBufSize(); @@ -5155,6 +5280,10 @@ void ShowMetricsWindow(bool* p_popen) { fg.AddRect(plot->Axes[i].HoverRect.Min, plot->Axes[i].HoverRect.Max, IM_COL32(0,255,0,255)); } } + if (show_legend_rects && plot->Items.GetLegendCount() > 0) { + fg.AddRect(plot->Items.Legend.Rect.Min, plot->Items.Legend.Rect.Max, IM_COL32(255,192,0,255)); + fg.AddRect(plot->Items.Legend.RectClamped.Min, plot->Items.Legend.RectClamped.Max, IM_COL32(255,128,0,255)); + } } for (int p = 0; p < n_subplots; ++p) { ImPlotSubplot* subplot = gp.Subplots.GetByIndex(p); @@ -5162,6 +5291,10 @@ void ShowMetricsWindow(bool* p_popen) { fg.AddRect(subplot->FrameRect.Min, subplot->FrameRect.Max, IM_COL32(255,0,0,255)); if (show_subplot_grid_rects) fg.AddRect(subplot->GridRect.Min, subplot->GridRect.Max, IM_COL32(0,0,255,255)); + if (show_legend_rects && subplot->Items.GetLegendCount() > 0) { + fg.AddRect(subplot->Items.Legend.Rect.Min, subplot->Items.Legend.Rect.Max, IM_COL32(255,192,0,255)); + fg.AddRect(subplot->Items.Legend.RectClamped.Min, subplot->Items.Legend.RectClamped.Max, IM_COL32(255,128,0,255)); + } } if (ImGui::TreeNode("Plots","Plots (%d)", n_plots)) { for (int p = 0; p < n_plots; ++p) { diff --git a/libs/imgui/implot.h b/libs/imgui/implot.h index b0d1696..3054331 100644 --- a/libs/imgui/implot.h +++ b/libs/imgui/implot.h @@ -1,6 +1,6 @@ // MIT License -// Copyright (c) 2022 Evan Pezent +// Copyright (c) 2023 Evan Pezent // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -20,7 +20,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -// ImPlot v0.14 +// ImPlot v0.17 // Table of Contents: // @@ -60,7 +60,7 @@ #endif // ImPlot version string. -#define IMPLOT_VERSION "0.14" +#define IMPLOT_VERSION "0.17" // Indicates variable should deduced automatically. #define IMPLOT_AUTO -1 // Special color used to indicate that a color should be deduced automatically. @@ -135,10 +135,9 @@ enum ImPlotFlags_ { ImPlotFlags_NoInputs = 1 << 3, // the user will not be able to interact with the plot ImPlotFlags_NoMenus = 1 << 4, // the user will not be able to open context menus ImPlotFlags_NoBoxSelect = 1 << 5, // the user will not be able to box-select - ImPlotFlags_NoChild = 1 << 6, // a child window region will not be used to capture mouse scroll (can boost performance for single ImGui window applications) - ImPlotFlags_NoFrame = 1 << 7, // the ImGui frame will not be rendered - ImPlotFlags_Equal = 1 << 8, // x and y axes pairs will be constrained to have the same units/pixel - ImPlotFlags_Crosshairs = 1 << 9, // the default mouse cursor will be replaced with a crosshair when hovered + ImPlotFlags_NoFrame = 1 << 6, // the ImGui frame will not be rendered + ImPlotFlags_Equal = 1 << 7, // x and y axes pairs will be constrained to have the same units/pixel + ImPlotFlags_Crosshairs = 1 << 8, // the default mouse cursor will be replaced with a crosshair when hovered ImPlotFlags_CanvasOnly = ImPlotFlags_NoTitle | ImPlotFlags_NoLegend | ImPlotFlags_NoMenus | ImPlotFlags_NoBoxSelect | ImPlotFlags_NoMouseText }; @@ -287,8 +286,9 @@ enum ImPlotInfLinesFlags_ { // Flags for PlotPieChart enum ImPlotPieChartFlags_ { - ImPlotPieChartFlags_None = 0, // default - ImPlotPieChartFlags_Normalize = 1 << 10 // force normalization of pie chart values (i.e. always make a full circle if sum < 0) + ImPlotPieChartFlags_None = 0, // default + ImPlotPieChartFlags_Normalize = 1 << 10, // force normalization of pie chart values (i.e. always make a full circle if sum < 0) + ImPlotPieChartFlags_IgnoreHidden = 1 << 11 // ignore hidden slices when drawing the pie chart (as if they were not there) }; // Flags for PlotHeatmap @@ -464,41 +464,43 @@ enum ImPlotBin_ { }; // Double precision version of ImVec2 used by ImPlot. Extensible by end users. +IM_MSVC_RUNTIME_CHECKS_OFF struct ImPlotPoint { double x, y; - ImPlotPoint() { x = y = 0.0; } - ImPlotPoint(double _x, double _y) { x = _x; y = _y; } - ImPlotPoint(const ImVec2& p) { x = p.x; y = p.y; } - double operator[] (size_t idx) const { return (&x)[idx]; } - double& operator[] (size_t idx) { return (&x)[idx]; } + constexpr ImPlotPoint() : x(0.0), y(0.0) { } + constexpr ImPlotPoint(double _x, double _y) : x(_x), y(_y) { } + constexpr ImPlotPoint(const ImVec2& p) : x((double)p.x), y((double)p.y) { } + double& operator[] (size_t idx) { IM_ASSERT(idx == 0 || idx == 1); return ((double*)(void*)(char*)this)[idx]; } + double operator[] (size_t idx) const { IM_ASSERT(idx == 0 || idx == 1); return ((const double*)(const void*)(const char*)this)[idx]; } #ifdef IMPLOT_POINT_CLASS_EXTRA IMPLOT_POINT_CLASS_EXTRA // Define additional constructors and implicit cast operators in imconfig.h // to convert back and forth between your math types and ImPlotPoint. #endif }; +IM_MSVC_RUNTIME_CHECKS_RESTORE // Range defined by a min/max value. struct ImPlotRange { double Min, Max; - ImPlotRange() { Min = 0; Max = 0; } - ImPlotRange(double _min, double _max) { Min = _min; Max = _max; } - bool Contains(double value) const { return value >= Min && value <= Max; } - double Size() const { return Max - Min; } - double Clamp(double value) const { return (value < Min) ? Min : (value > Max) ? Max : value; } + constexpr ImPlotRange() : Min(0.0), Max(0.0) { } + constexpr ImPlotRange(double _min, double _max) : Min(_min), Max(_max) { } + bool Contains(double value) const { return value >= Min && value <= Max; } + double Size() const { return Max - Min; } + double Clamp(double value) const { return (value < Min) ? Min : (value > Max) ? Max : value; } }; // Combination of two range limits for X and Y axes. Also an AABB defined by Min()/Max(). struct ImPlotRect { ImPlotRange X, Y; - ImPlotRect() { } - ImPlotRect(double x_min, double x_max, double y_min, double y_max) { X.Min = x_min; X.Max = x_max; Y.Min = y_min; Y.Max = y_max; } - bool Contains(const ImPlotPoint& p) const { return Contains(p.x, p.y); } - bool Contains(double x, double y) const { return X.Contains(x) && Y.Contains(y); } - ImPlotPoint Size() const { return ImPlotPoint(X.Size(), Y.Size()); } - ImPlotPoint Clamp(const ImPlotPoint& p) { return Clamp(p.x, p.y); } - ImPlotPoint Clamp(double x, double y) { return ImPlotPoint(X.Clamp(x),Y.Clamp(y)); } - ImPlotPoint Min() const { return ImPlotPoint(X.Min, Y.Min); } - ImPlotPoint Max() const { return ImPlotPoint(X.Max, Y.Max); } + constexpr ImPlotRect() : X(0.0,0.0), Y(0.0,0.0) { } + constexpr ImPlotRect(double x_min, double x_max, double y_min, double y_max) : X(x_min, x_max), Y(y_min, y_max) { } + bool Contains(const ImPlotPoint& p) const { return Contains(p.x, p.y); } + bool Contains(double x, double y) const { return X.Contains(x) && Y.Contains(y); } + ImPlotPoint Size() const { return ImPlotPoint(X.Size(), Y.Size()); } + ImPlotPoint Clamp(const ImPlotPoint& p) { return Clamp(p.x, p.y); } + ImPlotPoint Clamp(double x, double y) { return ImPlotPoint(X.Clamp(x),Y.Clamp(y)); } + ImPlotPoint Min() const { return ImPlotPoint(X.Min, Y.Min); } + ImPlotPoint Max() const { return ImPlotPoint(X.Max, Y.Max); } }; // Plot style structure @@ -728,7 +730,7 @@ IMPLOT_API void EndSubplots(); // Enables an axis or sets the label and/or flags for an existing axis. Leave #label = nullptr for no label. IMPLOT_API void SetupAxis(ImAxis axis, const char* label=nullptr, ImPlotAxisFlags flags=0); -// Sets an axis range limits. If ImPlotCond_Always is used, the axes limits will be locked. +// Sets an axis range limits. If ImPlotCond_Always is used, the axes limits will be locked. Inversion with v_min > v_max is not supported; use SetupAxisLimits instead. IMPLOT_API void SetupAxisLimits(ImAxis axis, double v_min, double v_max, ImPlotCond cond = ImPlotCond_Once); // Links an axis range limits to external values. Set to nullptr for no linkage. The pointer data must remain valid until EndPlot. IMPLOT_API void SetupAxisLinks(ImAxis axis, double* link_min, double* link_max); @@ -754,7 +756,7 @@ IMPLOT_API void SetupAxes(const char* x_label, const char* y_label, ImPlotAxisFl // Sets the primary X and Y axes range limits. If ImPlotCond_Always is used, the axes limits will be locked (shorthand for two calls to SetupAxisLimits). IMPLOT_API void SetupAxesLimits(double x_min, double x_max, double y_min, double y_max, ImPlotCond cond = ImPlotCond_Once); -// Sets up the plot legend. +// Sets up the plot legend. This can also be called immediately after BeginSubplots when using ImPlotSubplotFlags_ShareItems. IMPLOT_API void SetupLegend(ImPlotLocation location, ImPlotLegendFlags flags=0); // Set the location of the current plot's mouse position text (default = South|East). IMPLOT_API void SetupMouseText(ImPlotLocation location, ImPlotMouseTextFlags flags=0); @@ -891,6 +893,7 @@ IMPLOT_TMP void PlotStems(const char* label_id, const T* xs, const T* ys, int co IMPLOT_TMP void PlotInfLines(const char* label_id, const T* values, int count, ImPlotInfLinesFlags flags=0, int offset=0, int stride=sizeof(T)); // Plots a pie chart. Center and radius are in plot units. #label_fmt can be set to nullptr for no labels. +IMPLOT_TMP void PlotPieChart(const char* const label_ids[], const T* values, int count, double x, double y, double radius, ImPlotFormatter fmt, void* fmt_data=nullptr, double angle0=90, ImPlotPieChartFlags flags=0); IMPLOT_TMP void PlotPieChart(const char* const label_ids[], const T* values, int count, double x, double y, double radius, const char* label_fmt="%.1f", double angle0=90, ImPlotPieChartFlags flags=0); // Plots a 2D heatmap chart. Values are expected to be in row-major order by default. Leave #scale_min and scale_max both at 0 for automatic color scaling, or set them to a predefined range. #label_fmt can be set to nullptr for no labels. @@ -923,16 +926,18 @@ IMPLOT_API void PlotDummy(const char* label_id, ImPlotDummyFlags flags=0); // The following can be used to render interactive elements and/or annotations. // Like the item plotting functions above, they apply to the current x and y -// axes, which can be changed with `SetAxis/SetAxes`. +// axes, which can be changed with `SetAxis/SetAxes`. These functions return true +// when user interaction causes the provided coordinates to change. Additional +// user interactions can be retrieved through the optional output parameters. // Shows a draggable point at x,y. #col defaults to ImGuiCol_Text. -IMPLOT_API bool DragPoint(int id, double* x, double* y, const ImVec4& col, float size = 4, ImPlotDragToolFlags flags=0); +IMPLOT_API bool DragPoint(int id, double* x, double* y, const ImVec4& col, float size = 4, ImPlotDragToolFlags flags = 0, bool* out_clicked = nullptr, bool* out_hovered = nullptr, bool* held = nullptr); // Shows a draggable vertical guide line at an x-value. #col defaults to ImGuiCol_Text. -IMPLOT_API bool DragLineX(int id, double* x, const ImVec4& col, float thickness = 1, ImPlotDragToolFlags flags=0); +IMPLOT_API bool DragLineX(int id, double* x, const ImVec4& col, float thickness = 1, ImPlotDragToolFlags flags = 0, bool* out_clicked = nullptr, bool* out_hovered = nullptr, bool* held = nullptr); // Shows a draggable horizontal guide line at a y-value. #col defaults to ImGuiCol_Text. -IMPLOT_API bool DragLineY(int id, double* y, const ImVec4& col, float thickness = 1, ImPlotDragToolFlags flags=0); +IMPLOT_API bool DragLineY(int id, double* y, const ImVec4& col, float thickness = 1, ImPlotDragToolFlags flags = 0, bool* out_clicked = nullptr, bool* out_hovered = nullptr, bool* held = nullptr); // Shows a draggable and resizeable rectangle. -IMPLOT_API bool DragRect(int id, double* x1, double* y1, double* x2, double* y2, const ImVec4& col, ImPlotDragToolFlags flags=0); +IMPLOT_API bool DragRect(int id, double* x1, double* y1, double* x2, double* y2, const ImVec4& col, ImPlotDragToolFlags flags = 0, bool* out_clicked = nullptr, bool* out_hovered = nullptr, bool* held = nullptr); // Shows an annotation callout at a chosen point. Clamping keeps annotations in the plot area. Annotations are always rendered on top. IMPLOT_API void Annotation(double x, double y, const ImVec4& col, const ImVec2& pix_offset, bool clamp, bool round = false); diff --git a/libs/imgui/implot_demo.cpp b/libs/imgui/implot_demo.cpp index 00f853d..c72ffd0 100644 --- a/libs/imgui/implot_demo.cpp +++ b/libs/imgui/implot_demo.cpp @@ -1,6 +1,6 @@ // MIT License -// Copyright (c) 2022 Evan Pezent +// Copyright (c) 2023 Evan Pezent // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -20,7 +20,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -// ImPlot v0.14 +// ImPlot v0.17 // We define this so that the demo does not accidentally use deprecated API #ifndef IMPLOT_DISABLE_OBSOLETE_FUNCTIONS @@ -610,10 +610,8 @@ void Demo_PieCharts() { static ImPlotPieChartFlags flags = 0; ImGui::SetNextItemWidth(250); ImGui::DragFloat4("Values", data1, 0.01f, 0, 1); - if ((data1[0] + data1[1] + data1[2] + data1[3]) < 1) { - ImGui::SameLine(); - CHECKBOX_FLAG(flags,ImPlotPieChartFlags_Normalize); - } + CHECKBOX_FLAG(flags, ImPlotPieChartFlags_Normalize); + CHECKBOX_FLAG(flags, ImPlotPieChartFlags_IgnoreHidden); if (ImPlot::BeginPlot("##Pie1", ImVec2(250,250), ImPlotFlags_Equal | ImPlotFlags_NoMouseText)) { ImPlot::SetupAxes(nullptr, nullptr, ImPlotAxisFlags_NoDecorations, ImPlotAxisFlags_NoDecorations); @@ -1147,7 +1145,7 @@ void Demo_MultipleAxes() { ImPlot::SetAxes(ImAxis_X1, ImAxis_Y2); ImPlot::PlotLine("f(x) = cos(x)*.2+.5", xs, ys2, 1001); } - if (y3_axis) { + if (x2_axis && y3_axis) { ImPlot::SetAxes(ImAxis_X2, ImAxis_Y3); ImPlot::PlotLine("f(x) = sin(x+.5)*100+200 ", xs2, ys3, 1001); } @@ -1264,7 +1262,7 @@ ImPlotPoint SinewaveGetter(int i, void* data) { void Demo_SubplotsSizing() { - static ImPlotSubplotFlags flags = ImPlotSubplotFlags_None; + static ImPlotSubplotFlags flags = ImPlotSubplotFlags_ShareItems|ImPlotSubplotFlags_NoLegend; ImGui::CheckboxFlags("ImPlotSubplotFlags_NoResize", (unsigned int*)&flags, ImPlotSubplotFlags_NoResize); ImGui::CheckboxFlags("ImPlotSubplotFlags_NoTitle", (unsigned int*)&flags, ImPlotSubplotFlags_NoTitle); @@ -1272,17 +1270,26 @@ void Demo_SubplotsSizing() { static int cols = 3; ImGui::SliderInt("Rows",&rows,1,5); ImGui::SliderInt("Cols",&cols,1,5); + if (rows < 1 || cols < 1) { + ImGui::TextColored(ImVec4(1,0,0,1), "Nice try, but the number of rows and columns must be greater than 0!"); + return; + } static float rratios[] = {5,1,1,1,1,1}; static float cratios[] = {5,1,1,1,1,1}; ImGui::DragScalarN("Row Ratios",ImGuiDataType_Float,rratios,rows,0.01f,nullptr); ImGui::DragScalarN("Col Ratios",ImGuiDataType_Float,cratios,cols,0.01f,nullptr); if (ImPlot::BeginSubplots("My Subplots", rows, cols, ImVec2(-1,400), flags, rratios, cratios)) { + int id = 0; for (int i = 0; i < rows*cols; ++i) { if (ImPlot::BeginPlot("",ImVec2(),ImPlotFlags_NoLegend)) { ImPlot::SetupAxes(nullptr,nullptr,ImPlotAxisFlags_NoDecorations,ImPlotAxisFlags_NoDecorations); float fi = 0.01f * (i+1); - ImPlot::SetNextLineStyle(SampleColormap((float)i/(float)(rows*cols-1),ImPlotColormap_Jet)); - ImPlot::PlotLineG("data",SinewaveGetter,&fi,1000); + if (rows*cols > 1) { + ImPlot::SetNextLineStyle(SampleColormap((float)i/(float)(rows*cols-1),ImPlotColormap_Jet)); + } + char label[16]; + snprintf(label, sizeof(label), "data%d", id++); + ImPlot::PlotLineG(label,SinewaveGetter,&fi,1000); ImPlot::EndPlot(); } } @@ -1302,6 +1309,7 @@ void Demo_SubplotItemSharing() { static int id[] = {0,1,2,3,4,5}; static int curj = -1; if (ImPlot::BeginSubplots("##ItemSharing", rows, cols, ImVec2(-1,400), flags)) { + ImPlot::SetupLegend(ImPlotLocation_South, ImPlotLegendFlags_Sort|ImPlotLegendFlags_Horizontal); for (int i = 0; i < rows*cols; ++i) { if (ImPlot::BeginPlot("")) { float fc = 0.01f; @@ -1376,6 +1384,9 @@ void Demo_LegendOptions() { ImGui::SliderFloat2("LegendInnerPadding", (float*)&GetStyle().LegendInnerPadding, 0.0f, 10.0f, "%.0f"); ImGui::SliderFloat2("LegendSpacing", (float*)&GetStyle().LegendSpacing, 0.0f, 5.0f, "%.0f"); + static int num_dummy_items = 25; + ImGui::SliderInt("Num Dummy Items (Demo Scrolling)", &num_dummy_items, 0, 100); + if (ImPlot::BeginPlot("##Legend",ImVec2(-1,0))) { ImPlot::SetupLegend(loc, flags); static MyImPlot::WaveData data1(0.001, 0.2, 4, 0.2); @@ -1384,12 +1395,17 @@ void Demo_LegendOptions() { static MyImPlot::WaveData data4(0.001, 0.2, 4, 0.8); static MyImPlot::WaveData data5(0.001, 0.2, 4, 1.0); - ImPlot::PlotLineG("Item B", MyImPlot::SawWave, &data1, 1000); // "Item B" added to legend - ImPlot::PlotLineG("Item A##IDText", MyImPlot::SawWave, &data2, 1000); // "Item A" added to legend, text after ## used for ID only + ImPlot::PlotLineG("Item 002", MyImPlot::SawWave, &data1, 1000); // "Item B" added to legend + ImPlot::PlotLineG("Item 001##IDText", MyImPlot::SawWave, &data2, 1000); // "Item A" added to legend, text after ## used for ID only ImPlot::PlotLineG("##NotListed", MyImPlot::SawWave, &data3, 1000); // plotted, but not added to legend - ImPlot::PlotLineG("Item C", MyImPlot::SawWave, &data4, 1000); // "Item C" added to legend - ImPlot::PlotLineG("Item C", MyImPlot::SawWave, &data5, 1000); // combined with previous "Item C" + ImPlot::PlotLineG("Item 003", MyImPlot::SawWave, &data4, 1000); // "Item C" added to legend + ImPlot::PlotLineG("Item 003", MyImPlot::SawWave, &data5, 1000); // combined with previous "Item C" + for (int i = 0; i < num_dummy_items; ++i) { + char label[16]; + snprintf(label, sizeof(label), "Item %03d", i+4); + ImPlot::PlotDummy(label); + } ImPlot::EndPlot(); } } @@ -1403,15 +1419,18 @@ void Demo_DragPoints() { ImGui::CheckboxFlags("NoFit", (unsigned int*)&flags, ImPlotDragToolFlags_NoFit); ImGui::SameLine(); ImGui::CheckboxFlags("NoInput", (unsigned int*)&flags, ImPlotDragToolFlags_NoInputs); ImPlotAxisFlags ax_flags = ImPlotAxisFlags_NoTickLabels | ImPlotAxisFlags_NoTickMarks; + bool clicked[4] = {false, false, false, false}; + bool hovered[4] = {false, false, false, false}; + bool held[4] = {false, false, false, false}; if (ImPlot::BeginPlot("##Bezier",ImVec2(-1,0),ImPlotFlags_CanvasOnly)) { ImPlot::SetupAxes(nullptr,nullptr,ax_flags,ax_flags); ImPlot::SetupAxesLimits(0,1,0,1); static ImPlotPoint P[] = {ImPlotPoint(.05f,.05f), ImPlotPoint(0.2,0.4), ImPlotPoint(0.8,0.6), ImPlotPoint(.95f,.95f)}; - ImPlot::DragPoint(0,&P[0].x,&P[0].y, ImVec4(0,0.9f,0,1),4,flags); - ImPlot::DragPoint(1,&P[1].x,&P[1].y, ImVec4(1,0.5f,1,1),4,flags); - ImPlot::DragPoint(2,&P[2].x,&P[2].y, ImVec4(0,0.5f,1,1),4,flags); - ImPlot::DragPoint(3,&P[3].x,&P[3].y, ImVec4(0,0.9f,0,1),4,flags); + ImPlot::DragPoint(0,&P[0].x,&P[0].y, ImVec4(0,0.9f,0,1),4,flags, &clicked[0], &hovered[0], &held[0]); + ImPlot::DragPoint(1,&P[1].x,&P[1].y, ImVec4(1,0.5f,1,1),4,flags, &clicked[1], &hovered[1], &held[1]); + ImPlot::DragPoint(2,&P[2].x,&P[2].y, ImVec4(0,0.5f,1,1),4,flags, &clicked[2], &hovered[2], &held[2]); + ImPlot::DragPoint(3,&P[3].x,&P[3].y, ImVec4(0,0.9f,0,1),4,flags, &clicked[3], &hovered[3], &held[3]); static ImPlotPoint B[100]; for (int i = 0; i < 100; ++i) { @@ -1424,14 +1443,12 @@ void Demo_DragPoints() { B[i] = ImPlotPoint(w1*P[0].x + w2*P[1].x + w3*P[2].x + w4*P[3].x, w1*P[0].y + w2*P[1].y + w3*P[2].y + w4*P[3].y); } - - ImPlot::SetNextLineStyle(ImVec4(1,0.5f,1,1)); + ImPlot::SetNextLineStyle(ImVec4(1,0.5f,1,1),hovered[1]||held[1] ? 2.0f : 1.0f); ImPlot::PlotLine("##h1",&P[0].x, &P[0].y, 2, 0, 0, sizeof(ImPlotPoint)); - ImPlot::SetNextLineStyle(ImVec4(0,0.5f,1,1)); + ImPlot::SetNextLineStyle(ImVec4(0,0.5f,1,1), hovered[2]||held[2] ? 2.0f : 1.0f); ImPlot::PlotLine("##h2",&P[2].x, &P[2].y, 2, 0, 0, sizeof(ImPlotPoint)); - ImPlot::SetNextLineStyle(ImVec4(0,0.9f,0,1), 2); + ImPlot::SetNextLineStyle(ImVec4(0,0.9f,0,1), hovered[0]||held[0]||hovered[3]||held[3] ? 3.0f : 2.0f); ImPlot::PlotLine("##bez",&B[0].x, &B[0].y, 100, 0, 0, sizeof(ImPlotPoint)); - ImPlot::EndPlot(); } } @@ -1445,6 +1462,9 @@ void Demo_DragLines() { static double y1 = 0.25; static double y2 = 0.75; static double f = 0.1; + bool clicked = false; + bool hovered = false; + bool held = false; static ImPlotDragToolFlags flags = ImPlotDragToolFlags_None; ImGui::CheckboxFlags("NoCursors", (unsigned int*)&flags, ImPlotDragToolFlags_NoCursors); ImGui::SameLine(); ImGui::CheckboxFlags("NoFit", (unsigned int*)&flags, ImPlotDragToolFlags_NoFit); ImGui::SameLine(); @@ -1460,8 +1480,9 @@ void Demo_DragLines() { xs[i] = (x2+x1)/2+fabs(x2-x1)*(i/1000.0f - 0.5f); ys[i] = (y1+y2)/2+fabs(y2-y1)/2*sin(f*i/10); } + ImPlot::DragLineY(120482,&f,ImVec4(1,0.5f,1,1),1,flags, &clicked, &hovered, &held); + ImPlot::SetNextLineStyle(IMPLOT_AUTO_COL, hovered||held ? 2.0f : 1.0f); ImPlot::PlotLine("Interactive Data", xs, ys, 1000); - ImPlot::DragLineY(120482,&f,ImVec4(1,0.5f,1,1),1,flags); ImPlot::EndPlot(); } } @@ -1476,6 +1497,9 @@ void Demo_DragRects() { static float y_data3[512]; static float sampling_freq = 44100; static float freq = 500; + bool clicked = false; + bool hovered = false; + bool held = false; for (size_t i = 0; i < 512; ++i) { const float t = i / sampling_freq; x_data[i] = t; @@ -1485,6 +1509,7 @@ void Demo_DragRects() { y_data3[i] = y_data2[i] * -0.6f + sinf(3 * arg) * 0.4f; } ImGui::BulletText("Click and drag the edges, corners, and center of the rect."); + ImGui::BulletText("Double click edges to expand rect to plot extents."); static ImPlotRect rect(0.0025,0.0045,0,0.5); static ImPlotDragToolFlags flags = ImPlotDragToolFlags_None; ImGui::CheckboxFlags("NoCursors", (unsigned int*)&flags, ImPlotDragToolFlags_NoCursors); ImGui::SameLine(); @@ -1497,9 +1522,11 @@ void Demo_DragRects() { ImPlot::PlotLine("Signal 1", x_data, y_data1, 512); ImPlot::PlotLine("Signal 2", x_data, y_data2, 512); ImPlot::PlotLine("Signal 3", x_data, y_data3, 512); - ImPlot::DragRect(0,&rect.X.Min,&rect.Y.Min,&rect.X.Max,&rect.Y.Max,ImVec4(1,0,1,1),flags); + ImPlot::DragRect(0,&rect.X.Min,&rect.Y.Min,&rect.X.Max,&rect.Y.Max,ImVec4(1,0,1,1),flags, &clicked, &hovered, &held); ImPlot::EndPlot(); } + ImVec4 bg_col = held ? ImVec4(0.5f,0,0.5f,1) : (hovered ? ImVec4(0.25f,0,0.25f,1) : ImPlot::GetStyle().Colors[ImPlotCol_PlotBg]); + ImPlot::PushStyleColor(ImPlotCol_PlotBg, bg_col); if (ImPlot::BeginPlot("##rect",ImVec2(-1,150), ImPlotFlags_CanvasOnly)) { ImPlot::SetupAxes(nullptr,nullptr,ImPlotAxisFlags_NoDecorations,ImPlotAxisFlags_NoDecorations); ImPlot::SetupAxesLimits(rect.X.Min, rect.X.Max, rect.Y.Min, rect.Y.Max, ImGuiCond_Always); @@ -1508,6 +1535,8 @@ void Demo_DragRects() { ImPlot::PlotLine("Signal 3", x_data, y_data3, 512); ImPlot::EndPlot(); } + ImPlot::PopStyleColor(); + ImGui::Text("Rect is %sclicked, %shovered, %sheld", clicked ? "" : "not ", hovered ? "" : "not ", held ? "" : "not "); } //----------------------------------------------------------------------------- @@ -2299,7 +2328,7 @@ ImPlotPoint Spiral(int idx, void*) { void Sparkline(const char* id, const float* values, int count, float min_v, float max_v, int offset, const ImVec4& col, const ImVec2& size) { ImPlot::PushStyleVar(ImPlotStyleVar_PlotPadding, ImVec2(0,0)); - if (ImPlot::BeginPlot(id,size,ImPlotFlags_CanvasOnly|ImPlotFlags_NoChild)) { + if (ImPlot::BeginPlot(id,size,ImPlotFlags_CanvasOnly)) { ImPlot::SetupAxes(nullptr,nullptr,ImPlotAxisFlags_NoDecorations,ImPlotAxisFlags_NoDecorations); ImPlot::SetupAxesLimits(0, count - 1, min_v, max_v, ImGuiCond_Always); ImPlot::SetNextLineStyle(col); diff --git a/libs/imgui/implot_internal.h b/libs/imgui/implot_internal.h index 51f0b42..cd05478 100644 --- a/libs/imgui/implot_internal.h +++ b/libs/imgui/implot_internal.h @@ -1,6 +1,6 @@ // MIT License -// Copyright (c) 2022 Evan Pezent +// Copyright (c) 2023 Evan Pezent // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -20,7 +20,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -// ImPlot v0.14 +// ImPlot v0.17 // You may use this file to debug, understand or extend ImPlot features but we // don't provide any guarantee of forward compatibility! @@ -907,8 +907,9 @@ struct ImPlotAxis } void PullLinks() { - if (LinkedMin) { SetMin(*LinkedMin,true); } - if (LinkedMax) { SetMax(*LinkedMax,true); } + if (LinkedMin && LinkedMax) { SetRange(*LinkedMin, *LinkedMax); } + else if (LinkedMin) { SetMin(*LinkedMin,true); } + else if (LinkedMax) { SetMax(*LinkedMax,true); } } }; @@ -965,9 +966,11 @@ struct ImPlotLegend ImPlotLegendFlags PreviousFlags; ImPlotLocation Location; ImPlotLocation PreviousLocation; + ImVec2 Scroll; ImVector Indices; ImGuiTextBuffer Labels; ImRect Rect; + ImRect RectClamped; bool Hovered; bool Held; bool CanGoInside; @@ -977,6 +980,7 @@ struct ImPlotLegend CanGoInside = true; Hovered = Held = false; Location = PreviousLocation = ImPlotLocation_NorthWest; + Scroll = ImVec2(0,0); } void Reset() { Indices.shrink(0); Labels.Buf.shrink(0); } @@ -1136,7 +1140,6 @@ struct ImPlotSubplot { ID = 0; Flags = PreviousFlags = ImPlotSubplotFlags_None; Rows = Cols = CurrentIdx = 0; - FrameHovered = false; Items.Legend.Location = ImPlotLocation_North; Items.Legend.Flags = ImPlotLegendFlags_Horizontal|ImPlotLegendFlags_Outside; Items.Legend.CanGoInside = false; @@ -1215,9 +1218,6 @@ struct ImPlotContext { ImPlotAnnotationCollection Annotations; ImPlotTagCollection Tags; - // Flags - bool ChildWindowMade; - // Style and Colormaps ImPlotStyle Style; ImVector ColorModifiers; @@ -1414,13 +1414,15 @@ IMPLOT_API void ShowAxisContextMenu(ImPlotAxis& axis, ImPlotAxis* equal_axis, bo // Gets the position of an inner rect that is located inside of an outer rect according to an ImPlotLocation and padding amount. IMPLOT_API ImVec2 GetLocationPos(const ImRect& outer_rect, const ImVec2& inner_size, ImPlotLocation location, const ImVec2& pad = ImVec2(0,0)); -// Calculates the bounding box size of a legend +// Calculates the bounding box size of a legend _before_ clipping. IMPLOT_API ImVec2 CalcLegendSize(ImPlotItemGroup& items, const ImVec2& pad, const ImVec2& spacing, bool vertical); +// Clips calculated legend size +IMPLOT_API bool ClampLegendRect(ImRect& legend_rect, const ImRect& outer_rect, const ImVec2& pad); // Renders legend entries into a bounding box IMPLOT_API bool ShowLegendEntries(ImPlotItemGroup& items, const ImRect& legend_bb, bool interactable, const ImVec2& pad, const ImVec2& spacing, bool vertical, ImDrawList& DrawList); -// Shows an alternate legend for the plot identified by #title_id, outside of the plot frame (can be called before or after of Begin/EndPlot but must occur in the same ImGui window!). +// Shows an alternate legend for the plot identified by #title_id, outside of the plot frame (can be called before or after of Begin/EndPlot but must occur in the same ImGui window! This is not thoroughly tested nor scrollable!). IMPLOT_API void ShowAltLegend(const char* title_id, bool vertical = true, const ImVec2 size = ImVec2(0,0), bool interactable = true); -// Shows an legends's context menu. +// Shows a legend's context menu. IMPLOT_API bool ShowLegendContextMenu(ImPlotLegend& legend, bool visible); //----------------------------------------------------------------------------- diff --git a/libs/imgui/implot_items.cpp b/libs/imgui/implot_items.cpp index 345f703..1871d15 100644 --- a/libs/imgui/implot_items.cpp +++ b/libs/imgui/implot_items.cpp @@ -1,6 +1,6 @@ // MIT License -// Copyright (c) 2020 Evan Pezent +// Copyright (c) 2023 Evan Pezent // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -20,7 +20,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -// ImPlot v0.14 +// ImPlot v0.17 #define IMGUI_DEFINE_MATH_OPERATORS #include "implot.h" @@ -98,11 +98,11 @@ static IMPLOT_INLINE float ImInvSqrt(float x) { return 1.0f / sqrtf(x); } #define _CAT(x, y) _CAT_(x, y) #define _CAT_(x,y) x ## y #define _INSTANTIATE_FOR_NUMERIC_TYPES(chain) _CAT(_INSTANTIATE_FOR_NUMERIC_TYPES_1 chain, _END) -#define _INSTANTIATE_FOR_NUMERIC_TYPES_1(T) INSTANTIATE_MACRO(T); _INSTANTIATE_FOR_NUMERIC_TYPES_2 -#define _INSTANTIATE_FOR_NUMERIC_TYPES_2(T) INSTANTIATE_MACRO(T); _INSTANTIATE_FOR_NUMERIC_TYPES_1 +#define _INSTANTIATE_FOR_NUMERIC_TYPES_1(T) INSTANTIATE_MACRO(T) _INSTANTIATE_FOR_NUMERIC_TYPES_2 +#define _INSTANTIATE_FOR_NUMERIC_TYPES_2(T) INSTANTIATE_MACRO(T) _INSTANTIATE_FOR_NUMERIC_TYPES_1 #define _INSTANTIATE_FOR_NUMERIC_TYPES_1_END #define _INSTANTIATE_FOR_NUMERIC_TYPES_2_END -#define CALL_INSTANTIATE_FOR_NUMERIC_TYPES() _INSTANTIATE_FOR_NUMERIC_TYPES(IMPLOT_NUMERIC_TYPES); +#define CALL_INSTANTIATE_FOR_NUMERIC_TYPES() _INSTANTIATE_FOR_NUMERIC_TYPES(IMPLOT_NUMERIC_TYPES) namespace ImPlot { @@ -1287,7 +1287,7 @@ struct RendererShaded : RendererBase { return false; } const int intersect = (P11.y > P12.y && P22.y > P21.y) || (P12.y > P11.y && P21.y > P22.y); - ImVec2 intersection = Intersection(P11,P21,P12,P22); + const ImVec2 intersection = intersect == 0 ? ImVec2(0,0) : Intersection(P11,P21,P12,P22); draw_list._VtxWritePtr[0].pos = P11; draw_list._VtxWritePtr[0].uv = UV; draw_list._VtxWritePtr[0].col = Col; @@ -1569,6 +1569,10 @@ void RenderMarkers(const _Getter& getter, ImPlotMarker marker, float size, bool template void PlotLineEx(const char* label_id, const _Getter& getter, ImPlotLineFlags flags) { if (BeginItemEx(label_id, Fitter1<_Getter>(getter), flags, ImPlotCol_Line)) { + if (getter.Count <= 0) { + EndItem(); + return; + } const ImPlotNextItemData& s = GetItemData(); if (getter.Count > 1) { if (ImHasFlag(flags, ImPlotLineFlags_Shaded) && s.RenderFill) { @@ -1640,6 +1644,10 @@ void PlotLineG(const char* label_id, ImPlotGetter getter_func, void* data, int c template void PlotScatterEx(const char* label_id, const Getter& getter, ImPlotScatterFlags flags) { if (BeginItemEx(label_id, Fitter1(getter), flags, ImPlotCol_MarkerOutline)) { + if (getter.Count <= 0) { + EndItem(); + return; + } const ImPlotNextItemData& s = GetItemData(); ImPlotMarker marker = s.Marker == ImPlotMarker_None ? ImPlotMarker_Circle: s.Marker; if (marker != ImPlotMarker_None) { @@ -1686,8 +1694,12 @@ void PlotScatterG(const char* label_id, ImPlotGetter getter_func, void* data, in template void PlotStairsEx(const char* label_id, const Getter& getter, ImPlotStairsFlags flags) { if (BeginItemEx(label_id, Fitter1(getter), flags, ImPlotCol_Line)) { + if (getter.Count <= 0) { + EndItem(); + return; + } const ImPlotNextItemData& s = GetItemData(); - if (getter.Count > 1 ) { + if (getter.Count > 1) { if (s.RenderFill && ImHasFlag(flags,ImPlotStairsFlags_Shaded)) { const ImU32 col_fill = ImGui::GetColorU32(s.Colors[ImPlotCol_Fill]); if (ImHasFlag(flags, ImPlotStairsFlags_PreStep)) @@ -1746,6 +1758,10 @@ void PlotStairsG(const char* label_id, ImPlotGetter getter_func, void* data, int template void PlotShadedEx(const char* label_id, const Getter1& getter1, const Getter2& getter2, ImPlotShadedFlags flags) { if (BeginItemEx(label_id, Fitter2(getter1,getter2), flags, ImPlotCol_Fill)) { + if (getter1.Count <= 0 || getter2.Count <= 0) { + EndItem(); + return; + } const ImPlotNextItemData& s = GetItemData(); if (s.RenderFill) { const ImU32 col = ImGui::GetColorU32(s.Colors[ImPlotCol_Fill]); @@ -1806,6 +1822,10 @@ void PlotShadedG(const char* label_id, ImPlotGetter getter_func1, void* data1, I template void PlotBarsVEx(const char* label_id, const Getter1& getter1, const Getter2 getter2, double width, ImPlotBarsFlags flags) { if (BeginItemEx(label_id, FitterBarV(getter1,getter2,width), flags, ImPlotCol_Fill)) { + if (getter1.Count <= 0 || getter2.Count <= 0) { + EndItem(); + return; + } const ImPlotNextItemData& s = GetItemData(); const ImU32 col_fill = ImGui::GetColorU32(s.Colors[ImPlotCol_Fill]); const ImU32 col_line = ImGui::GetColorU32(s.Colors[ImPlotCol_Line]); @@ -1826,6 +1846,10 @@ void PlotBarsVEx(const char* label_id, const Getter1& getter1, const Getter2 get template void PlotBarsHEx(const char* label_id, const Getter1& getter1, const Getter2& getter2, double height, ImPlotBarsFlags flags) { if (BeginItemEx(label_id, FitterBarH(getter1,getter2,height), flags, ImPlotCol_Fill)) { + if (getter1.Count <= 0 || getter2.Count <= 0) { + EndItem(); + return; + } const ImPlotNextItemData& s = GetItemData(); const ImU32 col_fill = ImGui::GetColorU32(s.Colors[ImPlotCol_Fill]); const ImU32 col_line = ImGui::GetColorU32(s.Colors[ImPlotCol_Line]); @@ -1982,6 +2006,10 @@ CALL_INSTANTIATE_FOR_NUMERIC_TYPES() template void PlotErrorBarsVEx(const char* label_id, const _GetterPos& getter_pos, const _GetterNeg& getter_neg, ImPlotErrorBarsFlags flags) { if (BeginItemEx(label_id, Fitter2<_GetterPos,_GetterNeg>(getter_pos, getter_neg), flags, IMPLOT_AUTO)) { + if (getter_pos.Count <= 0 || getter_neg.Count <= 0) { + EndItem(); + return; + } const ImPlotNextItemData& s = GetItemData(); ImDrawList& draw_list = *GetPlotDrawList(); const ImU32 col = ImGui::GetColorU32(s.Colors[ImPlotCol_ErrorBar]); @@ -2003,6 +2031,10 @@ void PlotErrorBarsVEx(const char* label_id, const _GetterPos& getter_pos, const template void PlotErrorBarsHEx(const char* label_id, const _GetterPos& getter_pos, const _GetterNeg& getter_neg, ImPlotErrorBarsFlags flags) { if (BeginItemEx(label_id, Fitter2<_GetterPos,_GetterNeg>(getter_pos, getter_neg), flags, IMPLOT_AUTO)) { + if (getter_pos.Count <= 0 || getter_neg.Count <= 0) { + EndItem(); + return; + } const ImPlotNextItemData& s = GetItemData(); ImDrawList& draw_list = *GetPlotDrawList(); const ImU32 col = ImGui::GetColorU32(s.Colors[ImPlotCol_ErrorBar]); @@ -2060,13 +2092,17 @@ CALL_INSTANTIATE_FOR_NUMERIC_TYPES() //----------------------------------------------------------------------------- template -void PlotStemsEx(const char* label_id, const _GetterM& get_mark, const _GetterB& get_base, ImPlotStemsFlags flags) { - if (BeginItemEx(label_id, Fitter2<_GetterM,_GetterB>(get_mark,get_base), flags, ImPlotCol_Line)) { +void PlotStemsEx(const char* label_id, const _GetterM& getter_mark, const _GetterB& getter_base, ImPlotStemsFlags flags) { + if (BeginItemEx(label_id, Fitter2<_GetterM,_GetterB>(getter_mark,getter_base), flags, ImPlotCol_Line)) { + if (getter_mark.Count <= 0 || getter_base.Count <= 0) { + EndItem(); + return; + } const ImPlotNextItemData& s = GetItemData(); // render stems if (s.RenderLine) { const ImU32 col_line = ImGui::GetColorU32(s.Colors[ImPlotCol_Line]); - RenderPrimitives2(get_mark, get_base, col_line, s.LineWeight); + RenderPrimitives2(getter_mark, getter_base, col_line, s.LineWeight); } // render markers if (s.Marker != ImPlotMarker_None) { @@ -2074,7 +2110,7 @@ void PlotStemsEx(const char* label_id, const _GetterM& get_mark, const _GetterB& PushPlotClipRect(s.MarkerSize); const ImU32 col_line = ImGui::GetColorU32(s.Colors[ImPlotCol_MarkerOutline]); const ImU32 col_fill = ImGui::GetColorU32(s.Colors[ImPlotCol_MarkerFill]); - RenderMarkers<_GetterM>(get_mark, s.Marker, s.MarkerSize, s.RenderMarkerFill, col_fill, s.RenderMarkerLine, col_line, s.MarkerWeight); + RenderMarkers<_GetterM>(getter_mark, s.Marker, s.MarkerSize, s.RenderMarkerFill, col_fill, s.RenderMarkerLine, col_line, s.MarkerWeight); } EndItem(); } @@ -2123,13 +2159,17 @@ template void PlotInfLines(const char* label_id, const T* values, int count, ImPlotInfLinesFlags flags, int offset, int stride) { const ImPlotRect lims = GetPlotLimits(IMPLOT_AUTO,IMPLOT_AUTO); if (ImHasFlag(flags, ImPlotInfLinesFlags_Horizontal)) { - GetterXY> get_min(IndexerConst(lims.X.Min),IndexerIdx(values,count,offset,stride),count); - GetterXY> get_max(IndexerConst(lims.X.Max),IndexerIdx(values,count,offset,stride),count); - if (BeginItemEx(label_id, FitterY>>(get_min), flags, ImPlotCol_Line)) { + GetterXY> getter_min(IndexerConst(lims.X.Min),IndexerIdx(values,count,offset,stride),count); + GetterXY> getter_max(IndexerConst(lims.X.Max),IndexerIdx(values,count,offset,stride),count); + if (BeginItemEx(label_id, FitterY>>(getter_min), flags, ImPlotCol_Line)) { + if (count <= 0) { + EndItem(); + return; + } const ImPlotNextItemData& s = GetItemData(); const ImU32 col_line = ImGui::GetColorU32(s.Colors[ImPlotCol_Line]); if (s.RenderLine) - RenderPrimitives2(get_min, get_max, col_line, s.LineWeight); + RenderPrimitives2(getter_min, getter_max, col_line, s.LineWeight); EndItem(); } } @@ -2137,6 +2177,10 @@ void PlotInfLines(const char* label_id, const T* values, int count, ImPlotInfLin GetterXY,IndexerConst> get_min(IndexerIdx(values,count,offset,stride),IndexerConst(lims.Y.Min),count); GetterXY,IndexerConst> get_max(IndexerIdx(values,count,offset,stride),IndexerConst(lims.Y.Max),count); if (BeginItemEx(label_id, FitterX,IndexerConst>>(get_min), flags, ImPlotCol_Line)) { + if (count <= 0) { + EndItem(); + return; + } const ImPlotNextItemData& s = GetItemData(); const ImU32 col_line = ImGui::GetColorU32(s.Colors[ImPlotCol_Line]); if (s.RenderLine) @@ -2172,57 +2216,121 @@ IMPLOT_INLINE void RenderPieSlice(ImDrawList& draw_list, const ImPlotPoint& cent } template -void PlotPieChart(const char* const label_ids[], const T* values, int count, double x, double y, double radius, const char* fmt, double angle0, ImPlotPieChartFlags flags) { - IM_ASSERT_USER_ERROR(GImPlot->CurrentPlot != nullptr, "PlotPieChart() needs to be called between BeginPlot() and EndPlot()!"); - ImDrawList & draw_list = *GetPlotDrawList(); +double PieChartSum(const T* values, int count, bool ignore_hidden) { double sum = 0; - for (int i = 0; i < count; ++i) - sum += (double)values[i]; - const bool normalize = ImHasFlag(flags,ImPlotPieChartFlags_Normalize) || sum > 1.0; - ImPlotPoint center(x,y); - PushPlotClipRect(); + if (ignore_hidden) { + ImPlotContext& gp = *GImPlot; + ImPlotItemGroup& Items = *gp.CurrentItems; + for (int i = 0; i < count; ++i) { + if (i >= Items.GetItemCount()) + break; + + ImPlotItem* item = Items.GetItemByIndex(i); + IM_ASSERT(item != nullptr); + if (item->Show) { + sum += (double)values[i]; + } + } + } + else { + for (int i = 0; i < count; ++i) { + sum += (double)values[i]; + } + } + return sum; +} + +template +void PlotPieChartEx(const char* const label_ids[], const T* values, int count, ImPlotPoint center, double radius, double angle0, ImPlotPieChartFlags flags) { + ImDrawList& draw_list = *GetPlotDrawList(); + + const bool ignore_hidden = ImHasFlag(flags, ImPlotPieChartFlags_IgnoreHidden); + const double sum = PieChartSum(values, count, ignore_hidden); + const bool normalize = ImHasFlag(flags, ImPlotPieChartFlags_Normalize) || sum > 1.0; + double a0 = angle0 * 2 * IM_PI / 360.0; double a1 = angle0 * 2 * IM_PI / 360.0; - ImPlotPoint Pmin = ImPlotPoint(x-radius,y-radius); - ImPlotPoint Pmax = ImPlotPoint(x+radius,y+radius); + ImPlotPoint Pmin = ImPlotPoint(center.x - radius, center.y - radius); + ImPlotPoint Pmax = ImPlotPoint(center.x + radius, center.y + radius); for (int i = 0; i < count; ++i) { - double percent = normalize ? (double)values[i] / sum : (double)values[i]; - a1 = a0 + 2 * IM_PI * percent; - if (BeginItemEx(label_ids[i], FitterRect(Pmin,Pmax))) { - ImU32 col = GetCurrentItem()->Color; - if (percent < 0.5) { - RenderPieSlice(draw_list, center, radius, a0, a1, col); - } - else { - RenderPieSlice(draw_list, center, radius, a0, a0 + (a1 - a0) * 0.5, col); - RenderPieSlice(draw_list, center, radius, a0 + (a1 - a0) * 0.5, a1, col); + ImPlotItem* item = GetItem(label_ids[i]); + + const double percent = normalize ? (double)values[i] / sum : (double)values[i]; + const bool skip = sum <= 0.0 || (ignore_hidden && item != nullptr && !item->Show); + if (!skip) + a1 = a0 + 2 * IM_PI * percent; + + if (BeginItemEx(label_ids[i], FitterRect(Pmin, Pmax))) { + if (sum > 0.0) { + ImU32 col = GetCurrentItem()->Color; + if (percent < 0.5) { + RenderPieSlice(draw_list, center, radius, a0, a1, col); + } + else { + RenderPieSlice(draw_list, center, radius, a0, a0 + (a1 - a0) * 0.5, col); + RenderPieSlice(draw_list, center, radius, a0 + (a1 - a0) * 0.5, a1, col); + } } EndItem(); } - a0 = a1; + if (!skip) + a0 = a1; } +} + +int PieChartFormatter(double value, char* buff, int size, void* data) { + const char* fmt = (const char*)data; + return snprintf(buff, size, fmt, value); +}; + +template +void PlotPieChart(const char* const label_ids[], const T* values, int count, double x, double y, double radius, const char* fmt, double angle0, ImPlotPieChartFlags flags) { + PlotPieChart(label_ids, values, count, x, y, radius, PieChartFormatter, (void*)fmt, angle0, flags); +} +#define INSTANTIATE_MACRO(T) template IMPLOT_API void PlotPieChart(const char* const label_ids[], const T* values, int count, double x, double y, double radius, const char* fmt, double angle0, ImPlotPieChartFlags flags); +CALL_INSTANTIATE_FOR_NUMERIC_TYPES() +#undef INSTANTIATE_MACRO + +template +void PlotPieChart(const char* const label_ids[], const T* values, int count, double x, double y, double radius, ImPlotFormatter fmt, void* fmt_data, double angle0, ImPlotPieChartFlags flags) { + IM_ASSERT_USER_ERROR(GImPlot->CurrentPlot != nullptr, "PlotPieChart() needs to be called between BeginPlot() and EndPlot()!"); + ImDrawList& draw_list = *GetPlotDrawList(); + + const bool ignore_hidden = ImHasFlag(flags, ImPlotPieChartFlags_IgnoreHidden); + const double sum = PieChartSum(values, count, ignore_hidden); + const bool normalize = ImHasFlag(flags, ImPlotPieChartFlags_Normalize) || sum > 1.0; + ImPlotPoint center(x, y); + + PushPlotClipRect(); + PlotPieChartEx(label_ids, values, count, center, radius, angle0, flags); if (fmt != nullptr) { - a0 = angle0 * 2 * IM_PI / 360.0; - a1 = angle0 * 2 * IM_PI / 360.0; + double a0 = angle0 * 2 * IM_PI / 360.0; + double a1 = angle0 * 2 * IM_PI / 360.0; char buffer[32]; for (int i = 0; i < count; ++i) { ImPlotItem* item = GetItem(label_ids[i]); - double percent = normalize ? (double)values[i] / sum : (double)values[i]; - a1 = a0 + 2 * IM_PI * percent; - if (item->Show) { - ImFormatString(buffer, 32, fmt, (double)values[i]); - ImVec2 size = ImGui::CalcTextSize(buffer); - double angle = a0 + (a1 - a0) * 0.5; - ImVec2 pos = PlotToPixels(center.x + 0.5 * radius * cos(angle), center.y + 0.5 * radius * sin(angle),IMPLOT_AUTO,IMPLOT_AUTO); - ImU32 col = CalcTextColor(ImGui::ColorConvertU32ToFloat4(item->Color)); - draw_list.AddText(pos - size * 0.5f, col, buffer); + IM_ASSERT(item != nullptr); + + const double percent = normalize ? (double)values[i] / sum : (double)values[i]; + const bool skip = ignore_hidden && item != nullptr && !item->Show; + + if (!skip) { + a1 = a0 + 2 * IM_PI * percent; + if (item->Show) { + fmt((double)values[i], buffer, 32, fmt_data); + ImVec2 size = ImGui::CalcTextSize(buffer); + double angle = a0 + (a1 - a0) * 0.5; + ImVec2 pos = PlotToPixels(center.x + 0.5 * radius * cos(angle), center.y + 0.5 * radius * sin(angle), IMPLOT_AUTO, IMPLOT_AUTO); + ImU32 col = CalcTextColor(ImGui::ColorConvertU32ToFloat4(item->Color)); + draw_list.AddText(pos - size * 0.5f, col, buffer); + } + a0 = a1; } - a0 = a1; } } PopPlotClipRect(); } -#define INSTANTIATE_MACRO(T) template IMPLOT_API void PlotPieChart(const char* const label_ids[], const T* values, int count, double x, double y, double radius, const char* fmt, double angle0, ImPlotPieChartFlags flags); +#define INSTANTIATE_MACRO(T) template IMPLOT_API void PlotPieChart(const char* const label_ids[], const T* values, int count, double x, double y, double radius, ImPlotFormatter fmt, void* fmt_data, double angle0, ImPlotPieChartFlags flags); CALL_INSTANTIATE_FOR_NUMERIC_TYPES() #undef INSTANTIATE_MACRO @@ -2283,8 +2391,8 @@ struct GetterHeatmapColMaj { { } template IMPLOT_INLINE RectC operator()(I idx) const { double val = (double)Values[idx]; - const int r = idx % Cols; - const int c = idx / Cols; + const int r = idx % Rows; + const int c = idx / Rows; const ImPlotPoint p(XRef + HalfSize.x + c*Width, YRef + YDir * (HalfSize.y + r*Height)); RectC rect; rect.Pos = p; @@ -2375,6 +2483,10 @@ void RenderHeatmap(ImDrawList& draw_list, const T* values, int rows, int cols, d template void PlotHeatmap(const char* label_id, const T* values, int rows, int cols, double scale_min, double scale_max, const char* fmt, const ImPlotPoint& bounds_min, const ImPlotPoint& bounds_max, ImPlotHeatmapFlags flags) { if (BeginItemEx(label_id, FitterRect(bounds_min, bounds_max))) { + if (rows <= 0 || cols <= 0) { + EndItem(); + return; + } ImDrawList& draw_list = *GetPlotDrawList(); const bool col_maj = ImHasFlag(flags, ImPlotHeatmapFlags_ColMajor); RenderHeatmap(draw_list, values, rows, cols, scale_min, scale_max, fmt, bounds_min, bounds_max, true, col_maj); @@ -2539,6 +2651,10 @@ double PlotHistogram2D(const char* label_id, const T* xs, const T* ys, int count } if (BeginItemEx(label_id, FitterRect(range))) { + if (y_bins <= 0 || x_bins <= 0) { + EndItem(); + return max_count; + } ImDrawList& draw_list = *GetPlotDrawList(); RenderHeatmap(draw_list, &bin_counts.Data[0], y_bins, x_bins, 0, max_count, nullptr, range.Min(), range.Max(), false, col_maj); EndItem(); @@ -2597,8 +2713,8 @@ void PlotDigitalEx(const char* label_id, Getter getter, ImPlotDigitalFlags flags //do not extend plot outside plot range if (pMin.x < x_axis.PixelMin) pMin.x = x_axis.PixelMin; if (pMax.x < x_axis.PixelMin) pMax.x = x_axis.PixelMin; - if (pMin.x > x_axis.PixelMax) pMin.x = x_axis.PixelMax; - if (pMax.x > x_axis.PixelMax) pMax.x = x_axis.PixelMax; + if (pMin.x > x_axis.PixelMax) pMin.x = x_axis.PixelMax - 1; //fix issue related to https://github.com/ocornut/imgui/issues/3976 + if (pMax.x > x_axis.PixelMax) pMax.x = x_axis.PixelMax - 1; //fix issue related to https://github.com/ocornut/imgui/issues/3976 //plot a rectangle that extends up to x2 with y1 height if ((pMax.x > pMin.x) && (gp.CurrentPlot->PlotRect.Contains(pMin) || gp.CurrentPlot->PlotRect.Contains(pMax))) { // ImVec4 colAlpha = item->Color; diff --git a/libs/imgui/imstb_textedit.h b/libs/imgui/imstb_textedit.h index a8a8231..c30f1a1 100644 --- a/libs/imgui/imstb_textedit.h +++ b/libs/imgui/imstb_textedit.h @@ -2,8 +2,9 @@ // This is a slightly modified version of stb_textedit.h 1.14. // Those changes would need to be pushed into nothings/stb: // - Fix in stb_textedit_discard_redo (see https://github.com/nothings/stb/issues/321) -// - Fix in stb_textedit_find_charpos to handle last line (see https://github.com/ocornut/imgui/issues/6000) +// - Fix in stb_textedit_find_charpos to handle last line (see https://github.com/ocornut/imgui/issues/6000 + #6783) // Grep for [DEAR IMGUI] to find the changes. +// - Also renamed macros used or defined outside of IMSTB_TEXTEDIT_IMPLEMENTATION block from STB_TEXTEDIT_* to IMSTB_TEXTEDIT_* // stb_textedit.h - v1.14 - public domain - Sean Barrett // Development of this library was sponsored by RAD Game Tools @@ -30,7 +31,7 @@ // DEPENDENCIES // // Uses the C runtime function 'memmove', which you can override -// by defining STB_TEXTEDIT_memmove before the implementation. +// by defining IMSTB_TEXTEDIT_memmove before the implementation. // Uses no other functions. Performs no runtime allocations. // // @@ -274,8 +275,8 @@ //// //// -#ifndef INCLUDE_STB_TEXTEDIT_H -#define INCLUDE_STB_TEXTEDIT_H +#ifndef INCLUDE_IMSTB_TEXTEDIT_H +#define INCLUDE_IMSTB_TEXTEDIT_H //////////////////////////////////////////////////////////////////////// // @@ -286,33 +287,33 @@ // and undo state. // -#ifndef STB_TEXTEDIT_UNDOSTATECOUNT -#define STB_TEXTEDIT_UNDOSTATECOUNT 99 +#ifndef IMSTB_TEXTEDIT_UNDOSTATECOUNT +#define IMSTB_TEXTEDIT_UNDOSTATECOUNT 99 #endif -#ifndef STB_TEXTEDIT_UNDOCHARCOUNT -#define STB_TEXTEDIT_UNDOCHARCOUNT 999 +#ifndef IMSTB_TEXTEDIT_UNDOCHARCOUNT +#define IMSTB_TEXTEDIT_UNDOCHARCOUNT 999 #endif -#ifndef STB_TEXTEDIT_CHARTYPE -#define STB_TEXTEDIT_CHARTYPE int +#ifndef IMSTB_TEXTEDIT_CHARTYPE +#define IMSTB_TEXTEDIT_CHARTYPE int #endif -#ifndef STB_TEXTEDIT_POSITIONTYPE -#define STB_TEXTEDIT_POSITIONTYPE int +#ifndef IMSTB_TEXTEDIT_POSITIONTYPE +#define IMSTB_TEXTEDIT_POSITIONTYPE int #endif typedef struct { // private data - STB_TEXTEDIT_POSITIONTYPE where; - STB_TEXTEDIT_POSITIONTYPE insert_length; - STB_TEXTEDIT_POSITIONTYPE delete_length; + IMSTB_TEXTEDIT_POSITIONTYPE where; + IMSTB_TEXTEDIT_POSITIONTYPE insert_length; + IMSTB_TEXTEDIT_POSITIONTYPE delete_length; int char_storage; } StbUndoRecord; typedef struct { // private data - StbUndoRecord undo_rec [STB_TEXTEDIT_UNDOSTATECOUNT]; - STB_TEXTEDIT_CHARTYPE undo_char[STB_TEXTEDIT_UNDOCHARCOUNT]; + StbUndoRecord undo_rec [IMSTB_TEXTEDIT_UNDOSTATECOUNT]; + IMSTB_TEXTEDIT_CHARTYPE undo_char[IMSTB_TEXTEDIT_UNDOCHARCOUNT]; short undo_point, redo_point; int undo_char_point, redo_char_point; } StbUndoState; @@ -371,7 +372,7 @@ typedef struct float ymin,ymax; // height of row above and below baseline int num_chars; } StbTexteditRow; -#endif //INCLUDE_STB_TEXTEDIT_H +#endif //INCLUDE_IMSTB_TEXTEDIT_H //////////////////////////////////////////////////////////////////////////// @@ -384,11 +385,11 @@ typedef struct // implementation isn't include-guarded, since it might have indirectly // included just the "header" portion -#ifdef STB_TEXTEDIT_IMPLEMENTATION +#ifdef IMSTB_TEXTEDIT_IMPLEMENTATION -#ifndef STB_TEXTEDIT_memmove +#ifndef IMSTB_TEXTEDIT_memmove #include -#define STB_TEXTEDIT_memmove memmove +#define IMSTB_TEXTEDIT_memmove memmove #endif @@ -398,7 +399,7 @@ typedef struct // // traverse the layout to locate the nearest character to a display position -static int stb_text_locate_coord(STB_TEXTEDIT_STRING *str, float x, float y) +static int stb_text_locate_coord(IMSTB_TEXTEDIT_STRING *str, float x, float y) { StbTexteditRow r; int n = STB_TEXTEDIT_STRINGLEN(str); @@ -458,7 +459,7 @@ static int stb_text_locate_coord(STB_TEXTEDIT_STRING *str, float x, float y) } // API click: on mouse down, move the cursor to the clicked location, and reset the selection -static void stb_textedit_click(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, float x, float y) +static void stb_textedit_click(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state, float x, float y) { // In single-line mode, just always make y = 0. This lets the drag keep working if the mouse // goes off the top or bottom of the text @@ -476,7 +477,7 @@ static void stb_textedit_click(STB_TEXTEDIT_STRING *str, STB_TexteditState *stat } // API drag: on mouse drag, move the cursor and selection endpoint to the clicked location -static void stb_textedit_drag(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, float x, float y) +static void stb_textedit_drag(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state, float x, float y) { int p = 0; @@ -502,11 +503,11 @@ static void stb_textedit_drag(STB_TEXTEDIT_STRING *str, STB_TexteditState *state // // forward declarations -static void stb_text_undo(STB_TEXTEDIT_STRING *str, STB_TexteditState *state); -static void stb_text_redo(STB_TEXTEDIT_STRING *str, STB_TexteditState *state); -static void stb_text_makeundo_delete(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, int where, int length); +static void stb_text_undo(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state); +static void stb_text_redo(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state); +static void stb_text_makeundo_delete(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state, int where, int length); static void stb_text_makeundo_insert(STB_TexteditState *state, int where, int length); -static void stb_text_makeundo_replace(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, int where, int old_length, int new_length); +static void stb_text_makeundo_replace(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state, int where, int old_length, int new_length); typedef struct { @@ -518,7 +519,7 @@ typedef struct // find the x/y location of a character, and remember info about the previous row in // case we get a move-up event (for page up, we'll have to rescan) -static void stb_textedit_find_charpos(StbFindState *find, STB_TEXTEDIT_STRING *str, int n, int single_line) +static void stb_textedit_find_charpos(StbFindState *find, IMSTB_TEXTEDIT_STRING *str, int n, int single_line) { StbTexteditRow r; int prev_start = 0; @@ -549,7 +550,10 @@ static void stb_textedit_find_charpos(StbFindState *find, STB_TEXTEDIT_STRING *s i += r.num_chars; find->y += r.baseline_y_delta; if (i == z) // [DEAR IMGUI] + { + r.num_chars = 0; // [DEAR IMGUI] break; // [DEAR IMGUI] + } } find->first_char = first = i; @@ -566,7 +570,7 @@ static void stb_textedit_find_charpos(StbFindState *find, STB_TEXTEDIT_STRING *s #define STB_TEXT_HAS_SELECTION(s) ((s)->select_start != (s)->select_end) // make the selection/cursor state valid if client altered the string -static void stb_textedit_clamp(STB_TEXTEDIT_STRING *str, STB_TexteditState *state) +static void stb_textedit_clamp(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state) { int n = STB_TEXTEDIT_STRINGLEN(str); if (STB_TEXT_HAS_SELECTION(state)) { @@ -580,7 +584,7 @@ static void stb_textedit_clamp(STB_TEXTEDIT_STRING *str, STB_TexteditState *stat } // delete characters while updating undo -static void stb_textedit_delete(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, int where, int len) +static void stb_textedit_delete(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state, int where, int len) { stb_text_makeundo_delete(str, state, where, len); STB_TEXTEDIT_DELETECHARS(str, where, len); @@ -588,7 +592,7 @@ static void stb_textedit_delete(STB_TEXTEDIT_STRING *str, STB_TexteditState *sta } // delete the section -static void stb_textedit_delete_selection(STB_TEXTEDIT_STRING *str, STB_TexteditState *state) +static void stb_textedit_delete_selection(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state) { stb_textedit_clamp(str, state); if (STB_TEXT_HAS_SELECTION(state)) { @@ -625,7 +629,7 @@ static void stb_textedit_move_to_first(STB_TexteditState *state) } // move cursor to last character of selection -static void stb_textedit_move_to_last(STB_TEXTEDIT_STRING *str, STB_TexteditState *state) +static void stb_textedit_move_to_last(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state) { if (STB_TEXT_HAS_SELECTION(state)) { stb_textedit_sortselection(state); @@ -637,13 +641,13 @@ static void stb_textedit_move_to_last(STB_TEXTEDIT_STRING *str, STB_TexteditStat } #ifdef STB_TEXTEDIT_IS_SPACE -static int is_word_boundary( STB_TEXTEDIT_STRING *str, int idx ) +static int is_word_boundary( IMSTB_TEXTEDIT_STRING *str, int idx ) { return idx > 0 ? (STB_TEXTEDIT_IS_SPACE( STB_TEXTEDIT_GETCHAR(str,idx-1) ) && !STB_TEXTEDIT_IS_SPACE( STB_TEXTEDIT_GETCHAR(str, idx) ) ) : 1; } #ifndef STB_TEXTEDIT_MOVEWORDLEFT -static int stb_textedit_move_to_word_previous( STB_TEXTEDIT_STRING *str, int c ) +static int stb_textedit_move_to_word_previous( IMSTB_TEXTEDIT_STRING *str, int c ) { --c; // always move at least one character while( c >= 0 && !is_word_boundary( str, c ) ) @@ -658,7 +662,7 @@ static int stb_textedit_move_to_word_previous( STB_TEXTEDIT_STRING *str, int c ) #endif #ifndef STB_TEXTEDIT_MOVEWORDRIGHT -static int stb_textedit_move_to_word_next( STB_TEXTEDIT_STRING *str, int c ) +static int stb_textedit_move_to_word_next( IMSTB_TEXTEDIT_STRING *str, int c ) { const int len = STB_TEXTEDIT_STRINGLEN(str); ++c; // always move at least one character @@ -685,7 +689,7 @@ static void stb_textedit_prep_selection_at_cursor(STB_TexteditState *state) } // API cut: delete selection -static int stb_textedit_cut(STB_TEXTEDIT_STRING *str, STB_TexteditState *state) +static int stb_textedit_cut(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state) { if (STB_TEXT_HAS_SELECTION(state)) { stb_textedit_delete_selection(str,state); // implicitly clamps @@ -696,7 +700,7 @@ static int stb_textedit_cut(STB_TEXTEDIT_STRING *str, STB_TexteditState *state) } // API paste: replace existing selection with passed-in text -static int stb_textedit_paste_internal(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, STB_TEXTEDIT_CHARTYPE *text, int len) +static int stb_textedit_paste_internal(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state, IMSTB_TEXTEDIT_CHARTYPE *text, int len) { // if there's a selection, the paste should delete it stb_textedit_clamp(str, state); @@ -717,14 +721,14 @@ static int stb_textedit_paste_internal(STB_TEXTEDIT_STRING *str, STB_TexteditSta #endif // API key: process a keyboard input -static void stb_textedit_key(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, STB_TEXTEDIT_KEYTYPE key) +static void stb_textedit_key(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state, STB_TEXTEDIT_KEYTYPE key) { retry: switch (key) { default: { int c = STB_TEXTEDIT_KEYTOTEXT(key); if (c > 0) { - STB_TEXTEDIT_CHARTYPE ch = (STB_TEXTEDIT_CHARTYPE) c; + IMSTB_TEXTEDIT_CHARTYPE ch = (IMSTB_TEXTEDIT_CHARTYPE) c; // can't add newline in single-line mode if (c == '\n' && state->single_line) @@ -889,8 +893,8 @@ retry: x = row.x0; for (i=0; i < row.num_chars; ++i) { float dx = STB_TEXTEDIT_GETWIDTH(str, start, i); - #ifdef STB_TEXTEDIT_GETWIDTH_NEWLINE - if (dx == STB_TEXTEDIT_GETWIDTH_NEWLINE) + #ifdef IMSTB_TEXTEDIT_GETWIDTH_NEWLINE + if (dx == IMSTB_TEXTEDIT_GETWIDTH_NEWLINE) break; #endif x += dx; @@ -951,8 +955,8 @@ retry: x = row.x0; for (i=0; i < row.num_chars; ++i) { float dx = STB_TEXTEDIT_GETWIDTH(str, find.prev_first, i); - #ifdef STB_TEXTEDIT_GETWIDTH_NEWLINE - if (dx == STB_TEXTEDIT_GETWIDTH_NEWLINE) + #ifdef IMSTB_TEXTEDIT_GETWIDTH_NEWLINE + if (dx == IMSTB_TEXTEDIT_GETWIDTH_NEWLINE) break; #endif x += dx; @@ -1109,8 +1113,8 @@ retry: static void stb_textedit_flush_redo(StbUndoState *state) { - state->redo_point = STB_TEXTEDIT_UNDOSTATECOUNT; - state->redo_char_point = STB_TEXTEDIT_UNDOCHARCOUNT; + state->redo_point = IMSTB_TEXTEDIT_UNDOSTATECOUNT; + state->redo_char_point = IMSTB_TEXTEDIT_UNDOCHARCOUNT; } // discard the oldest entry in the undo list @@ -1122,13 +1126,13 @@ static void stb_textedit_discard_undo(StbUndoState *state) int n = state->undo_rec[0].insert_length, i; // delete n characters from all other records state->undo_char_point -= n; - STB_TEXTEDIT_memmove(state->undo_char, state->undo_char + n, (size_t) (state->undo_char_point*sizeof(STB_TEXTEDIT_CHARTYPE))); + IMSTB_TEXTEDIT_memmove(state->undo_char, state->undo_char + n, (size_t) (state->undo_char_point*sizeof(IMSTB_TEXTEDIT_CHARTYPE))); for (i=0; i < state->undo_point; ++i) if (state->undo_rec[i].char_storage >= 0) state->undo_rec[i].char_storage -= n; // @OPTIMIZE: get rid of char_storage and infer it } --state->undo_point; - STB_TEXTEDIT_memmove(state->undo_rec, state->undo_rec+1, (size_t) (state->undo_point*sizeof(state->undo_rec[0]))); + IMSTB_TEXTEDIT_memmove(state->undo_rec, state->undo_rec+1, (size_t) (state->undo_point*sizeof(state->undo_rec[0]))); } } @@ -1138,7 +1142,7 @@ static void stb_textedit_discard_undo(StbUndoState *state) // fill up even though the undo buffer didn't static void stb_textedit_discard_redo(StbUndoState *state) { - int k = STB_TEXTEDIT_UNDOSTATECOUNT-1; + int k = IMSTB_TEXTEDIT_UNDOSTATECOUNT-1; if (state->redo_point <= k) { // if the k'th undo state has characters, clean those up @@ -1146,7 +1150,7 @@ static void stb_textedit_discard_redo(StbUndoState *state) int n = state->undo_rec[k].insert_length, i; // move the remaining redo character data to the end of the buffer state->redo_char_point += n; - STB_TEXTEDIT_memmove(state->undo_char + state->redo_char_point, state->undo_char + state->redo_char_point-n, (size_t) ((STB_TEXTEDIT_UNDOCHARCOUNT - state->redo_char_point)*sizeof(STB_TEXTEDIT_CHARTYPE))); + IMSTB_TEXTEDIT_memmove(state->undo_char + state->redo_char_point, state->undo_char + state->redo_char_point-n, (size_t) ((IMSTB_TEXTEDIT_UNDOCHARCOUNT - state->redo_char_point)*sizeof(IMSTB_TEXTEDIT_CHARTYPE))); // adjust the position of all the other records to account for above memmove for (i=state->redo_point; i < k; ++i) if (state->undo_rec[i].char_storage >= 0) @@ -1154,12 +1158,12 @@ static void stb_textedit_discard_redo(StbUndoState *state) } // now move all the redo records towards the end of the buffer; the first one is at 'redo_point' // [DEAR IMGUI] - size_t move_size = (size_t)((STB_TEXTEDIT_UNDOSTATECOUNT - state->redo_point - 1) * sizeof(state->undo_rec[0])); + size_t move_size = (size_t)((IMSTB_TEXTEDIT_UNDOSTATECOUNT - state->redo_point - 1) * sizeof(state->undo_rec[0])); const char* buf_begin = (char*)state->undo_rec; (void)buf_begin; const char* buf_end = (char*)state->undo_rec + sizeof(state->undo_rec); (void)buf_end; IM_ASSERT(((char*)(state->undo_rec + state->redo_point)) >= buf_begin); IM_ASSERT(((char*)(state->undo_rec + state->redo_point + 1) + move_size) <= buf_end); - STB_TEXTEDIT_memmove(state->undo_rec + state->redo_point+1, state->undo_rec + state->redo_point, move_size); + IMSTB_TEXTEDIT_memmove(state->undo_rec + state->redo_point+1, state->undo_rec + state->redo_point, move_size); // now move redo_point to point to the new one ++state->redo_point; @@ -1173,32 +1177,32 @@ static StbUndoRecord *stb_text_create_undo_record(StbUndoState *state, int numch // if we have no free records, we have to make room, by sliding the // existing records down - if (state->undo_point == STB_TEXTEDIT_UNDOSTATECOUNT) + if (state->undo_point == IMSTB_TEXTEDIT_UNDOSTATECOUNT) stb_textedit_discard_undo(state); // if the characters to store won't possibly fit in the buffer, we can't undo - if (numchars > STB_TEXTEDIT_UNDOCHARCOUNT) { + if (numchars > IMSTB_TEXTEDIT_UNDOCHARCOUNT) { state->undo_point = 0; state->undo_char_point = 0; return NULL; } // if we don't have enough free characters in the buffer, we have to make room - while (state->undo_char_point + numchars > STB_TEXTEDIT_UNDOCHARCOUNT) + while (state->undo_char_point + numchars > IMSTB_TEXTEDIT_UNDOCHARCOUNT) stb_textedit_discard_undo(state); return &state->undo_rec[state->undo_point++]; } -static STB_TEXTEDIT_CHARTYPE *stb_text_createundo(StbUndoState *state, int pos, int insert_len, int delete_len) +static IMSTB_TEXTEDIT_CHARTYPE *stb_text_createundo(StbUndoState *state, int pos, int insert_len, int delete_len) { StbUndoRecord *r = stb_text_create_undo_record(state, insert_len); if (r == NULL) return NULL; r->where = pos; - r->insert_length = (STB_TEXTEDIT_POSITIONTYPE) insert_len; - r->delete_length = (STB_TEXTEDIT_POSITIONTYPE) delete_len; + r->insert_length = (IMSTB_TEXTEDIT_POSITIONTYPE) insert_len; + r->delete_length = (IMSTB_TEXTEDIT_POSITIONTYPE) delete_len; if (insert_len == 0) { r->char_storage = -1; @@ -1210,7 +1214,7 @@ static STB_TEXTEDIT_CHARTYPE *stb_text_createundo(StbUndoState *state, int pos, } } -static void stb_text_undo(STB_TEXTEDIT_STRING *str, STB_TexteditState *state) +static void stb_text_undo(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state) { StbUndoState *s = &state->undostate; StbUndoRecord u, *r; @@ -1237,7 +1241,7 @@ static void stb_text_undo(STB_TEXTEDIT_STRING *str, STB_TexteditState *state) // characters stored for *undoing* don't leave room for redo // if the last is true, we have to bail - if (s->undo_char_point + u.delete_length >= STB_TEXTEDIT_UNDOCHARCOUNT) { + if (s->undo_char_point + u.delete_length >= IMSTB_TEXTEDIT_UNDOCHARCOUNT) { // the undo records take up too much character space; there's no space to store the redo characters r->insert_length = 0; } else { @@ -1246,7 +1250,7 @@ static void stb_text_undo(STB_TEXTEDIT_STRING *str, STB_TexteditState *state) // there's definitely room to store the characters eventually while (s->undo_char_point + u.delete_length > s->redo_char_point) { // should never happen: - if (s->redo_point == STB_TEXTEDIT_UNDOSTATECOUNT) + if (s->redo_point == IMSTB_TEXTEDIT_UNDOSTATECOUNT) return; // there's currently not enough room, so discard a redo record stb_textedit_discard_redo(s); @@ -1278,11 +1282,11 @@ static void stb_text_undo(STB_TEXTEDIT_STRING *str, STB_TexteditState *state) s->redo_point--; } -static void stb_text_redo(STB_TEXTEDIT_STRING *str, STB_TexteditState *state) +static void stb_text_redo(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state) { StbUndoState *s = &state->undostate; StbUndoRecord *u, r; - if (s->redo_point == STB_TEXTEDIT_UNDOSTATECOUNT) + if (s->redo_point == IMSTB_TEXTEDIT_UNDOSTATECOUNT) return; // we need to do two things: apply the redo record, and create an undo record @@ -1334,20 +1338,20 @@ static void stb_text_makeundo_insert(STB_TexteditState *state, int where, int le stb_text_createundo(&state->undostate, where, 0, length); } -static void stb_text_makeundo_delete(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, int where, int length) +static void stb_text_makeundo_delete(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state, int where, int length) { int i; - STB_TEXTEDIT_CHARTYPE *p = stb_text_createundo(&state->undostate, where, length, 0); + IMSTB_TEXTEDIT_CHARTYPE *p = stb_text_createundo(&state->undostate, where, length, 0); if (p) { for (i=0; i < length; ++i) p[i] = STB_TEXTEDIT_GETCHAR(str, where+i); } } -static void stb_text_makeundo_replace(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, int where, int old_length, int new_length) +static void stb_text_makeundo_replace(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state, int where, int old_length, int new_length) { int i; - STB_TEXTEDIT_CHARTYPE *p = stb_text_createundo(&state->undostate, where, old_length, new_length); + IMSTB_TEXTEDIT_CHARTYPE *p = stb_text_createundo(&state->undostate, where, old_length, new_length); if (p) { for (i=0; i < old_length; ++i) p[i] = STB_TEXTEDIT_GETCHAR(str, where+i); @@ -1359,8 +1363,8 @@ static void stb_textedit_clear_state(STB_TexteditState *state, int is_single_lin { state->undostate.undo_point = 0; state->undostate.undo_char_point = 0; - state->undostate.redo_point = STB_TEXTEDIT_UNDOSTATECOUNT; - state->undostate.redo_char_point = STB_TEXTEDIT_UNDOCHARCOUNT; + state->undostate.redo_point = IMSTB_TEXTEDIT_UNDOSTATECOUNT; + state->undostate.redo_char_point = IMSTB_TEXTEDIT_UNDOCHARCOUNT; state->select_end = state->select_start = 0; state->cursor = 0; state->has_preferred_x = 0; @@ -1383,16 +1387,16 @@ static void stb_textedit_initialize_state(STB_TexteditState *state, int is_singl #pragma GCC diagnostic ignored "-Wcast-qual" #endif -static int stb_textedit_paste(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, STB_TEXTEDIT_CHARTYPE const *ctext, int len) +static int stb_textedit_paste(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state, IMSTB_TEXTEDIT_CHARTYPE const *ctext, int len) { - return stb_textedit_paste_internal(str, state, (STB_TEXTEDIT_CHARTYPE *) ctext, len); + return stb_textedit_paste_internal(str, state, (IMSTB_TEXTEDIT_CHARTYPE *) ctext, len); } #if defined(__GNUC__) || defined(__clang__) #pragma GCC diagnostic pop #endif -#endif//STB_TEXTEDIT_IMPLEMENTATION +#endif//IMSTB_TEXTEDIT_IMPLEMENTATION /* ------------------------------------------------------------------------------ diff --git a/libs/imgui_test_engine/LICENSE.txt b/libs/imgui_test_engine/LICENSE.txt new file mode 100644 index 0000000..12cacfe --- /dev/null +++ b/libs/imgui_test_engine/LICENSE.txt @@ -0,0 +1,57 @@ +Dear ImGui Test Engine License (v1.03) +Copyright (c) 2018-2023 Omar Cornut + +This document is a legal agreement ("License") between you ("Licensee") and +DISCO HELLO ("Licensor") that governs your use of Dear ImGui Test Engine ("Software"). + +1. LICENSE MODELS + +1.1. Free license + +The Licensor grants you a free license ("Free License") if you meet ANY of the following +criterion: + +- You are a natural person; +- You are not a legal entity, or you are a not-for-profit legal entity; +- You are using this Software for educational purposes; +- You are using this Software to create Derivative Software released publicly and under + an Open Source license, as defined by the Open Source Initiative; +- You are a legal entity with a turnover inferior to 2 million USD (or equivalent) during + your last fiscal year. + +1.2. Paid license + +If you do not meet any criterion of Article 1.1, Licensor grants you a trial period of a +maximum of 45 days, at no charge. Following this trial period, you must subscribe to a paid +license ("Paid License") with the Licensor to continue using the Software. +Paid Licenses are exclusively sold by DISCO HELLO. Paid Licenses and the associated +information are available at the following URL: http://www.dearimgui.com/licenses + +2. GRANT OF LICENSE + +2.1. License scope + +A limited and non-exclusive license is hereby granted, to the Licensee, to reproduce, +execute, publicly perform, and display, use, copy, modify, merge, distribute, or create +derivative works based on and/or derived from the Software ("Derivative Software"). + +2.2. Right of distribution + +License holders may also publish and/or distribute the Software or any Derivative +Software. The above copyright notice and this license shall be included in all copies +or substantial portions of the Software and/or Derivative Software. + +License holders may add their own attribution notices within the Derivative Software +that they distribute. Such attribution notices must not directly or indirectly imply a +modification of the License. License holders may provide to their modifications their +own copyright and/or additional or different terms and conditions, providing such +conditions complies with this License. + +3. DISCLAIMER + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES +OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/libs/imgui_test_engine/imgui_capture_tool.cpp b/libs/imgui_test_engine/imgui_capture_tool.cpp new file mode 100644 index 0000000..eb56e0e --- /dev/null +++ b/libs/imgui_test_engine/imgui_capture_tool.cpp @@ -0,0 +1,1114 @@ +// dear imgui test engine +// (screen/video capture tool) +// This is usable as a standalone applet or controlled by the test engine. + +// Two mode of operation: +// - Interactive: call ImGuiCaptureToolUI::ShowCaptureToolWindow() +// - Programmatic: generally via ImGuiTestContext::CaptureXXX functions + +// FIXME: This probably needs a rewrite, it's a bit too complicated. + +/* + +Index of this file: + +// [SECTION] Includes +// [SECTION] ImGuiCaptureImageBuf +// [SECTION] ImGuiCaptureContext +// [SECTION] ImGuiCaptureToolUI + +*/ + +//----------------------------------------------------------------------------- +// [SECTION] Includes +//----------------------------------------------------------------------------- + +#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS) +#define _CRT_SECURE_NO_WARNINGS +#endif + +#define IMGUI_DEFINE_MATH_OPERATORS +#include "imgui.h" +#include "imgui_internal.h" +#include "imgui_capture_tool.h" +#include "imgui_te_utils.h" // ImPathFindFilename, ImPathFindExtension, ImPathFixSeparatorsForCurrentOS, ImFileCreateDirectoryChain, ImOsOpenInShell +#include "thirdparty/Str/Str.h" + +//----------------------------------------------------------------------------- +// [SECTION] Link stb_image_write.h +//----------------------------------------------------------------------------- + +#if IMGUI_TEST_ENGINE_ENABLE_CAPTURE + +// Compile time options: +//#define IMGUI_STB_NAMESPACE ImStb +//#define IMGUI_STB_IMAGE_WRITE_FILENAME "my_folder/stb_image_write.h" +//#define IMGUI_DISABLE_STB_IMAGE_WRITE_IMPLEMENTATION + +// stb_image_write +#ifdef _MSC_VER +#pragma warning (push) +#pragma warning (disable: 4456) // declaration of 'xx' hides previous local declaration +#pragma warning (disable: 4457) // declaration of 'xx' hides function parameter +#else +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wsign-conversion" +#endif + +#ifdef IMGUI_STB_NAMESPACE +namespace IMGUI_STB_NAMESPACE +{ +#endif +#ifndef STB_IMAGE_WRITE_IMPLEMENTATION // in case the user already have an implementation in the _same_ compilation unit (e.g. unity builds) +#ifndef IMGUI_DISABLE_STB_IMAGE_WRITE_IMPLEMENTATION // in case the user already have an implementation in another compilation unit +#define STB_IMAGE_WRITE_IMPLEMENTATION +#endif +#ifdef IMGUI_STB_IMAGE_WRITE_FILENAME +#include IMGUI_STB_IMAGE_WRITE_FILENAME +#else +#include "thirdparty/stb/imstb_image_write.h" +#endif // #ifdef IMGUI_STB_IMAGE_WRITE_FILENAME +#endif // #ifndef STB_IMAGE_WRITE_IMPLEMENTATION +#ifdef IMGUI_STB_NAMESPACE +} // namespace ImStb +using namespace IMGUI_STB_NAMESPACE; +#endif + +#ifdef _MSC_VER +#pragma warning (pop) +#else +#pragma GCC diagnostic pop +#endif + +#endif // #if IMGUI_TEST_ENGINE_ENABLE_CAPTURE + +//----------------------------------------------------------------------------- +// [SECTION] ImGuiCaptureImageBuf +// Helper class for simple bitmap manipulation (not particularly efficient!) +//----------------------------------------------------------------------------- + +void ImGuiCaptureImageBuf::Clear() +{ + if (Data) + IM_FREE(Data); + Data = NULL; +} + +void ImGuiCaptureImageBuf::CreateEmpty(int w, int h) +{ + Clear(); + Width = w; + Height = h; + Data = (unsigned int*)IM_ALLOC((size_t)(Width * Height * 4)); + memset(Data, 0, (size_t)(Width * Height * 4)); +} + +bool ImGuiCaptureImageBuf::SaveFile(const char* filename) +{ +#if IMGUI_TEST_ENGINE_ENABLE_CAPTURE + IM_ASSERT(Data != NULL); + ImFileCreateDirectoryChain(filename, ImPathFindFilename(filename)); + int ret = stbi_write_png(filename, Width, Height, 4, Data, Width * 4); + return ret != 0; +#else + IM_UNUSED(filename); + return false; +#endif +} + +void ImGuiCaptureImageBuf::RemoveAlpha() +{ + unsigned int* p = Data; + int n = Width * Height; + while (n-- > 0) + { + *p |= IM_COL32_A_MASK; + p++; + } +} + +//----------------------------------------------------------------------------- +// [SECTION] ImGuiCaptureContext +//----------------------------------------------------------------------------- + +#if IMGUI_TEST_ENGINE_ENABLE_CAPTURE +static void HideOtherWindows(const ImGuiCaptureArgs* args) +{ + ImGuiContext& g = *GImGui; + for (ImGuiWindow* window : g.Windows) + { + if (window->Flags & ImGuiWindowFlags_ChildWindow) + continue; + if ((window->Flags & (ImGuiWindowFlags_Popup | ImGuiWindowFlags_Tooltip)) != 0 && (args->InFlags & ImGuiCaptureFlags_IncludeTooltipsAndPopups) != 0) + continue; + if (args->InCaptureWindows.contains(window)) + continue; + +#ifdef IMGUI_HAS_DOCK + bool should_hide_window = true; + for (ImGuiWindow* capture_window : args->InCaptureWindows) + { + if (capture_window->DockNode != NULL && capture_window->DockNode->HostWindow->RootWindow == window) + { + should_hide_window = false; + break; + } + } + if (!should_hide_window) + continue; +#endif // IMGUI_HAS_DOCK + + // Not overwriting HiddenFramesCanSkipItems or HiddenFramesCannotSkipItems since they have side-effects (e.g. preserving ContentsSize) + if (window->WasActive || window->Active) + window->HiddenFramesForRenderOnly = 2; + } +} +#endif // IMGUI_TEST_ENGINE_ENABLE_CAPTURE + +static ImRect GetMainViewportRect() +{ + ImGuiViewport* viewport = ImGui::GetMainViewport(); + return ImRect(viewport->Pos, viewport->Pos + viewport->Size); +} + +void ImGuiCaptureContext::PreNewFrame() +{ + const ImGuiCaptureArgs* args = _CaptureArgs; + if (args == NULL) + return; + + ImGuiContext& g = *GImGui; + + // Force mouse position. Hovered window is reset in ImGui::NewFrame() based on mouse real mouse position. + if (_FrameNo > 2 && (args->InFlags & ImGuiCaptureFlags_StitchAll) != 0) + { + IM_ASSERT(args->InCaptureWindows.Size == 1); + g.IO.MousePos = args->InCaptureWindows[0]->Pos + _MouseRelativeToWindowPos; + g.HoveredWindow = _HoveredWindow; + } +} + +void ImGuiCaptureContext::PreRender() +{ + ImGuiContext& g = *GImGui; + _BackupMouseDrawCursor = g.IO.MouseDrawCursor; + if (IsCapturing()) + { + const ImGuiCaptureArgs* args = _CaptureArgs; + g.IO.MouseDrawCursor = !(args->InFlags & ImGuiCaptureFlags_HideMouseCursor); + } +} + +void ImGuiCaptureContext::PostRender() +{ + ImGuiContext& g = *GImGui; + g.IO.MouseDrawCursor = _BackupMouseDrawCursor; +} + +void ImGuiCaptureContext::RestoreBackedUpData() +{ + // Restore window positions unconditionally. We may have moved them ourselves during capture. + ImGuiContext& g = *GImGui; + for (int n = 0; n < _WindowsData.Size; n++) + { + ImGuiWindow* window = _WindowsData[n].Window; + if (window->Hidden) + continue; + ImGui::SetWindowPos(window, _WindowsData[n].BackupRect.Min, ImGuiCond_Always); + ImGui::SetWindowSize(window, _WindowsData[n].BackupRect.GetSize(), ImGuiCond_Always); + } + g.Style.DisplayWindowPadding = _BackupDisplayWindowPadding; + g.Style.DisplaySafeAreaPadding = _BackupDisplaySafeAreaPadding; +} + +void ImGuiCaptureContext::ClearState() +{ + _FrameNo = _ChunkNo = 0; + _VideoLastFrameTime = 0; + _MouseRelativeToWindowPos = ImVec2(-FLT_MAX, -FLT_MAX); + _HoveredWindow = NULL; + _CaptureArgs = NULL; +} + +// Returns true when capture is in progress. +ImGuiCaptureStatus ImGuiCaptureContext::CaptureUpdate(ImGuiCaptureArgs* args) +{ +#if IMGUI_TEST_ENGINE_ENABLE_CAPTURE + ImGuiContext& g = *GImGui; + ImGuiIO& io = g.IO; + + // Sanity checks + IM_ASSERT(args != NULL); + IM_ASSERT(ScreenCaptureFunc != NULL); + IM_ASSERT(args->InOutputImageBuf != NULL || args->InOutputFile[0]); + IM_ASSERT(args->InRecordFPSTarget != 0); + if (_VideoRecording) + { + IM_ASSERT(args->InOutputFile[0] && "Output filename must be specified when recording videos."); + IM_ASSERT(args->InOutputImageBuf == NULL && "Output buffer cannot be specified when recording videos."); + IM_ASSERT((args->InFlags & ImGuiCaptureFlags_StitchAll) == 0 && "Image stitching is not supported when recording videos."); + if (!ImFileExist(VideoCaptureEncoderPath)) + { + fprintf(stderr, "Video encoder not found at \"%s\", video capturing failed.\n", VideoCaptureEncoderPath); + return ImGuiCaptureStatus_Error; + } + } + + ImGuiCaptureImageBuf* output = args->InOutputImageBuf ? args->InOutputImageBuf : &_CaptureBuf; + const ImRect viewport_rect = GetMainViewportRect(); + + // Hide other windows so they can't be seen visible behind captured window + if ((args->InFlags & ImGuiCaptureFlags_IncludeOtherWindows) == 0 && !args->InCaptureWindows.empty()) + HideOtherWindows(args); + + // Recording will be set to false when we are stopping video capture. + const bool is_recording_video = IsCapturingVideo(); + const double current_time_sec = ImGui::GetTime(); + if (is_recording_video && _VideoLastFrameTime > 0) + { + double delta_sec = current_time_sec - _VideoLastFrameTime; + if (delta_sec < 1.0 / args->InRecordFPSTarget) + return ImGuiCaptureStatus_InProgress; + } + + // Capture can be performed in single frame if we are capturing a rect. + const bool instant_capture = (args->InFlags & ImGuiCaptureFlags_Instant) != 0; + const bool is_capturing_explicit_rect = args->InCaptureRect.GetWidth() > 0 && args->InCaptureRect.GetHeight() > 0; + if (instant_capture) + { + IM_ASSERT(args->InCaptureWindows.empty()); + IM_ASSERT(is_capturing_explicit_rect); + IM_ASSERT(is_recording_video == false); + IM_ASSERT((args->InFlags & ImGuiCaptureFlags_StitchAll) == 0); + } + + // Do not start a capture process while mouse button is pressed. In case mouse cursor is hovering a captured window, + // pressed button may cause window to be repositioned unexpectedly. This is only important in stitched mode, because + // this is the only time we move mouse cursor. + if ((args->InFlags & ImGuiCaptureFlags_StitchAll) != 0) + if (g.IO.MouseDown[0] && _FrameNo == 0) + return ImGuiCaptureStatus_InProgress; + + //----------------------------------------------------------------- + // Frame 0: Initialize capture state + //----------------------------------------------------------------- + if (_FrameNo == 0) + { + if (is_recording_video) + { + // Determinate size alignment + const char* extension = (char*)ImPathFindExtension(args->InOutputFile); + if (args->InSizeAlign == 0) + { + if (strcmp(extension, ".gif") == 0) + args->InSizeAlign = 1; + else + args->InSizeAlign = 2; // mp4 wants >= 2 + } + IM_ASSERT(args->InSizeAlign > 0); + } + + // When recording, same args should have been passed to BeginVideoCapture(). + IM_ASSERT(!_VideoRecording || _CaptureArgs == args); + + _CaptureArgs = args; + _ChunkNo = 0; + _CaptureRect = _CapturedWindowRect = ImRect(FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX); + _WindowsData.clear(); + _BackupDisplayWindowPadding = g.Style.DisplayWindowPadding; + _BackupDisplaySafeAreaPadding = g.Style.DisplaySafeAreaPadding; + g.Style.DisplayWindowPadding = ImVec2(0, 0); // Allow windows to be positioned fully outside of visible viewport. + g.Style.DisplaySafeAreaPadding = ImVec2(0, 0); + + if (is_capturing_explicit_rect) + { + // Capture arbitrary rectangle. If any windows are specified in this mode only they will appear in captured region. + _CaptureRect = args->InCaptureRect; + if (args->InCaptureWindows.empty() && !instant_capture) + { + // Gather all top level windows. We will need to move them in order to capture regions larger than viewport. + for (ImGuiWindow* window : g.Windows) + { + // Child windows will be included by their parents. + if (window->ParentWindow != NULL) + continue; + if ((window->Flags & ImGuiWindowFlags_Popup || window->Flags & ImGuiWindowFlags_Tooltip) && !(args->InFlags & ImGuiCaptureFlags_IncludeTooltipsAndPopups)) + continue; + args->InCaptureWindows.push_back(window); + } + } + } + + // Save rectangle covering all windows and find top-left corner of combined rect which will be used to + // translate this group of windows to top-left corner of the screen. + for (ImGuiWindow* window : args->InCaptureWindows) + { + _CapturedWindowRect.Add(window->Rect()); + ImGuiCaptureWindowData window_data; + window_data.BackupRect = window->Rect(); + window_data.Window = window; + _WindowsData.push_back(window_data); + } + + if (args->InFlags & ImGuiCaptureFlags_StitchAll) + { + IM_ASSERT(is_capturing_explicit_rect == false && "ImGuiCaptureContext: capture of full window contents is not possible when capturing specified rect."); + IM_ASSERT(args->InCaptureWindows.Size == 1 && "ImGuiCaptureContext: capture of full window contents is not possible when capturing more than one window."); + + // Resize window to it's contents and capture it's entire width/height. However if window is bigger than it's contents - keep original size. + ImGuiWindow* window = args->InCaptureWindows[0]; + ImVec2 full_size = window->SizeFull; + + // Mouse cursor is relative to captured window even if it is not hovered, in which case cursor is kept off the window to prevent appearing in screenshot multiple times by accident. + _MouseRelativeToWindowPos = io.MousePos - window->Pos + window->Scroll; + + // FIXME-CAPTURE: Window width change may affect vertical content size if window contains text that wraps. To accurately position mouse cursor for capture we avoid horizontal resize. + // Instead window width should be set manually before capture, as it is simple to do and most of the time we already have a window of desired width. + //full_size.x = ImMax(window->SizeFull.x, window->ContentSize.x + (window->WindowPadding.x + window->WindowBorderSize) * 2); + full_size.y = ImMax(window->SizeFull.y, window->ContentSize.y + (window->WindowPadding.y + window->WindowBorderSize) * 2 + window->TitleBarHeight() + window->MenuBarHeight()); + ImGui::SetWindowSize(window, full_size); + _HoveredWindow = g.HoveredWindow; + } + else + { + _MouseRelativeToWindowPos = ImVec2(-FLT_MAX, -FLT_MAX); + _HoveredWindow = NULL; + } + } + else + { + IM_ASSERT(args == _CaptureArgs); // Capture args can not change mid-capture. + } + + //----------------------------------------------------------------- + // Frame 1: Skipped to allow window size to update fully + //----------------------------------------------------------------- + + //----------------------------------------------------------------- + // Frame 2: Position windows, lock rectangle, create capture buffer + //----------------------------------------------------------------- + if (_FrameNo == 2 || instant_capture) + { + // Move group of windows so combined rectangle position is at the top-left corner + padding and create combined + // capture rect of entire area that will be saved to screenshot. Doing this on the second frame because when + // ImGuiCaptureFlags_StitchAll flag is used we need to allow window to reposition. + // Repositioning of a window may take multiple frames, depending on whether window was already rendered or not. + if (args->InFlags & ImGuiCaptureFlags_StitchAll) + { + ImVec2 move_offset = ImVec2(args->InPadding, args->InPadding) - _CapturedWindowRect.Min + viewport_rect.Min; + IM_ASSERT(args->InCaptureWindows.Size == _WindowsData.Size); + for (int n = 0; n < _WindowsData.Size; n++) + { + ImGuiWindow* window = _WindowsData[n].Window; + _WindowsData[n].PosDuringCapture = window->Pos + move_offset; + ImGui::SetWindowPos(window, _WindowsData[n].PosDuringCapture); + } + } + + // Determine capture rectangle if not provided by user + if (!is_capturing_explicit_rect) + { + if (args->InCaptureWindows.Size > 0) + { + for (ImGuiWindow* window : args->InCaptureWindows) + _CaptureRect.Add(window->Rect()); + _CaptureRect.Expand(args->InPadding); + } + else + { + _CaptureRect = viewport_rect; + } + } + if ((args->InFlags & ImGuiCaptureFlags_StitchAll) == 0) + { + // Can not capture area outside of screen. Clip capture rect, since we are capturing only visible rect anyway. + _CaptureRect.ClipWith(viewport_rect); + + // Align size + // FIXME: ffmpeg + codec can possibly handle that better on their side. + ImVec2 capture_size_aligned = _CaptureRect.GetSize(); + if (args->InSizeAlign > 1) + { + // Round up + IM_ASSERT(ImIsPowerOfTwo(args->InSizeAlign)); + capture_size_aligned.x = (float)IM_MEMALIGN((int)capture_size_aligned.x, args->InSizeAlign); + capture_size_aligned.y = (float)IM_MEMALIGN((int)capture_size_aligned.y, args->InSizeAlign); + + // Unless will stray off viewport, then round down + if (_CaptureRect.Min.x + capture_size_aligned.x >= viewport_rect.Max.x) + capture_size_aligned.x -= args->InSizeAlign; + if (_CaptureRect.Min.y + capture_size_aligned.y >= viewport_rect.Max.y) + capture_size_aligned.y -= args->InSizeAlign; + + IM_ASSERT(capture_size_aligned.x > 0); + IM_ASSERT(capture_size_aligned.y > 0); + _CaptureRect.Max = _CaptureRect.Min + capture_size_aligned; + } + } + + // Initialize capture buffer. + IM_ASSERT(!_CaptureRect.IsInverted()); + args->OutImageSize = _CaptureRect.GetSize(); + output->CreateEmpty((int)_CaptureRect.GetWidth(), (int)_CaptureRect.GetHeight()); + } + + //----------------------------------------------------------------- + // Frame 4+N*4: Capture a frame + //----------------------------------------------------------------- + + const ImRect clip_rect = viewport_rect; + ImRect capture_rect = _CaptureRect; + capture_rect.ClipWith(clip_rect); + const int capture_height = ImMin((int)io.DisplaySize.y, (int)_CaptureRect.GetHeight()); + const int x1 = (int)(capture_rect.Min.x - clip_rect.Min.x); + const int y1 = (int)(capture_rect.Min.y - clip_rect.Min.y); + const int w = (int)capture_rect.GetWidth(); + const int h = (int)ImMin(output->Height - _ChunkNo * capture_height, capture_height); + + // Position windows + if ((_FrameNo > 2) && (args->InFlags & ImGuiCaptureFlags_StitchAll)) + { + // Unlike SetNextWindowPos(), SetWindowPos() will still perform viewport clamping, affecting support for io.ConfigWindowsMoveFromTitleBarOnly. + IM_ASSERT(args->InCaptureWindows.Size == _WindowsData.Size); + for (int n = 0; n < _WindowsData.Size; n++) + ImGui::SetWindowPos(_WindowsData[n].Window, _WindowsData[n].PosDuringCapture - ImVec2(0, (float)capture_height * _ChunkNo)); + } + + if (((_FrameNo > 2) && (_FrameNo % 4) == 0) || (is_recording_video && _FrameNo > 2) || instant_capture) + { + // FIXME: Implement capture of regions wider than viewport. + // Capture a portion of image. Capturing of windows wider than viewport is not implemented yet. + if (h > 0) + { + IM_ASSERT(w == output->Width); + if (args->InFlags & ImGuiCaptureFlags_StitchAll) + IM_ASSERT(h <= output->Height); // When stitching, image can be taller than captured viewport. + else + IM_ASSERT(h == output->Height); + + ImGuiID viewport_id = 0; +#ifdef IMGUI_HAS_VIEWPORT + if (args->InFlags & ImGuiCaptureFlags_StitchAll) + viewport_id = _WindowsData[0].Window->ViewportId; + else + viewport_id = ImGui::GetMainViewport()->ID; +#endif + + //printf("ScreenCaptureFunc x1: %d, y1: %d, w: %d, h: %d\n", x1, y1, w, h); + if (!ScreenCaptureFunc(viewport_id, x1, y1, w, h, &output->Data[_ChunkNo * w * capture_height], ScreenCaptureUserData)) + { + fprintf(stderr, "Screen capture function failed.\n"); + RestoreBackedUpData(); + ClearState(); + return ImGuiCaptureStatus_Error; + } + + if (args->InFlags & ImGuiCaptureFlags_StitchAll) + { + // Window moves up in order to expose it's lower part. + _ChunkNo++; + _CaptureRect.TranslateY(-(float)h); + } + + if (is_recording_video && (args->InFlags & ImGuiCaptureFlags_NoSave) == 0) + { + // _VideoEncoderPipe is NULL when recording just started. Initialize recording state. + if (_VideoEncoderPipe == NULL) + { + // First video frame, initialize now that dimensions are known. + const unsigned int width = (unsigned int)capture_rect.GetWidth(); + const unsigned int height = (unsigned int)capture_rect.GetHeight(); + IM_ASSERT(VideoCaptureEncoderPath != NULL && VideoCaptureEncoderPath[0]); + Str256f encoder_exe(VideoCaptureEncoderPath), cmd(""); + ImPathFixSeparatorsForCurrentOS(encoder_exe.c_str()); + ImFileCreateDirectoryChain(args->InOutputFile, ImPathFindFilename(args->InOutputFile)); +#if _WIN32 + cmd.append("\""); // On windows, entire command wrapped in quotes allows use of quotes for parameters. +#endif + const char* extension = (char*)ImPathFindExtension(args->InOutputFile); + if (strcmp(extension, ".gif") == 0) + { + IM_ASSERT(GifCaptureEncoderParams != NULL && GifCaptureEncoderParams[0]); + cmd.appendf("\"%s\" %s", encoder_exe.c_str(), GifCaptureEncoderParams); + } + else + { + IM_ASSERT(VideoCaptureEncoderParams != NULL && VideoCaptureEncoderParams[0]); + cmd.appendf("\"%s\" %s", encoder_exe.c_str(), VideoCaptureEncoderParams); + } +#if _WIN32 + cmd.append("\""); +#endif + ImStrReplace(&cmd, "$FPS", Str16f("%d", args->InRecordFPSTarget).c_str()); + ImStrReplace(&cmd, "$WIDTH", Str16f("%d", width).c_str()); + ImStrReplace(&cmd, "$HEIGHT", Str16f("%d", height).c_str()); + ImStrReplace(&cmd, "$OUTPUT", args->InOutputFile); + fprintf(stdout, "# %s\n", cmd.c_str()); + _VideoEncoderPipe = ImOsPOpen(cmd.c_str(), "w"); + IM_ASSERT(_VideoEncoderPipe != NULL); + } + + // Save new video frame + fwrite(output->Data, 1, output->Width * output->Height * 4, _VideoEncoderPipe); + } + if (is_recording_video) + _VideoLastFrameTime = current_time_sec; + } + + // Image is finalized immediately when we are not stitching. Otherwise, image is finalized when we have captured and stitched all frames. + if (!_VideoRecording && (!(args->InFlags & ImGuiCaptureFlags_StitchAll) || h <= 0)) + { + output->RemoveAlpha(); + + if (_VideoEncoderPipe != NULL) + { + // At this point _Recording is false, but we know we were recording because _VideoEncoderPipe is not NULL. Finalize video here. + ImOsPClose(_VideoEncoderPipe); + _VideoEncoderPipe = NULL; + } + else if (args->InOutputImageBuf == NULL) + { + // Save single frame. + if ((args->InFlags & ImGuiCaptureFlags_NoSave) == 0) + output->SaveFile(args->InOutputFile); + output->Clear(); + } + + RestoreBackedUpData(); + ClearState(); + return ImGuiCaptureStatus_Done; + } + } + + // Keep going + _FrameNo++; + return ImGuiCaptureStatus_InProgress; +#else + IM_UNUSED(args); + return ImGuiCaptureStatus_Done; +#endif +} + +void ImGuiCaptureContext::BeginVideoCapture(ImGuiCaptureArgs* args) +{ + IM_ASSERT(args != NULL); + IM_ASSERT(_VideoRecording == false); + IM_ASSERT(_VideoEncoderPipe == NULL); + IM_ASSERT(args->InRecordFPSTarget >= 1 && args->InRecordFPSTarget <= 100); + + ImFileCreateDirectoryChain(args->InOutputFile, ImPathFindFilename(args->InOutputFile)); + _VideoRecording = true; + _CaptureArgs = args; +} + +void ImGuiCaptureContext::EndVideoCapture() +{ + IM_ASSERT(_CaptureArgs != NULL); + IM_ASSERT(_VideoRecording == true); + + _VideoRecording = false; +} + +bool ImGuiCaptureContext::IsCapturingVideo() +{ + return _VideoRecording; +} + +bool ImGuiCaptureContext::IsCapturing() +{ + return _CaptureArgs != NULL; +} + +//----------------------------------------------------------------------------- +// ImGuiCaptureToolUI +//----------------------------------------------------------------------------- + +ImGuiCaptureToolUI::ImGuiCaptureToolUI() +{ + // Filename template for where screenshots will be saved. May contain directories or variation of %d format. + ImStrncpy(_OutputFileTemplate, "output/captures/imgui_capture_%04d.png", IM_ARRAYSIZE(_OutputFileTemplate)); +} + +// Interactively pick a single window +void ImGuiCaptureToolUI::_CaptureWindowPicker(ImGuiCaptureArgs* args) +{ + ImGuiContext& g = *GImGui; + ImGuiIO& io = g.IO; + + const float TEXT_BASE_WIDTH = ImGui::CalcTextSize("A").x; + const ImVec2 button_sz = ImVec2(TEXT_BASE_WIDTH * 30, 0.0f); + const ImGuiID picking_id = ImGui::GetID("##picking"); + + if (ImGui::Button("Capture Single Window..", button_sz)) + _StateIsPickingWindow = true; + + if (_StateIsPickingWindow) + { + // Picking a window + ImGuiWindow* capture_window = g.HoveredWindow ? g.HoveredWindow->RootWindow : NULL; + ImDrawList* fg_draw_list = ImGui::GetForegroundDrawList(); + ImGui::SetActiveID(picking_id, g.CurrentWindow); // Steal active ID so our click won't interact with something else. + ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + ImGui::SetTooltip("Capture window: '%s'\nPress ESC to cancel.", capture_window ? capture_window->Name : ""); + + // FIXME: Would be nice to have a way to enforce front-most windows. Perhaps make this Render() feature more generic. + //if (capture_window) + // g.NavWindowingTarget = capture_window; + + // Draw rect that is about to be captured + const ImRect viewport_rect = GetMainViewportRect(); + const ImU32 col_dim_overlay = IM_COL32(0, 0, 0, 40); + if (capture_window) + { + ImRect r = capture_window->Rect(); + r.Expand(args->InPadding); + r.ClipWith(ImRect(ImVec2(0, 0), io.DisplaySize)); + r.Expand(1.0f); + fg_draw_list->AddRect(r.Min, r.Max, IM_COL32_WHITE, 0.0f, 0, 2.0f); + ImGui::RenderRectFilledWithHole(fg_draw_list, viewport_rect, r, col_dim_overlay, 0.0f); + } + else + { + fg_draw_list->AddRectFilled(viewport_rect.Min, viewport_rect.Max, col_dim_overlay); + } + + if (ImGui::IsMouseClicked(0) && capture_window && _InitializeOutputFile()) + { + ImGui::FocusWindow(capture_window); + _SelectedWindows.resize(0); + _StateIsPickingWindow = false; + _StateIsCapturing = true; + args->InCaptureWindows.clear(); + args->InCaptureWindows.push_back(capture_window); + } + if (ImGui::IsKeyPressed(ImGuiKey_Escape)) + _StateIsPickingWindow = _StateIsCapturing = false; + } + else + { + if (ImGui::GetActiveID() == picking_id) + ImGui::ClearActiveID(); + } +} + +void ImGuiCaptureToolUI::_CaptureWindowsSelector(ImGuiCaptureContext* context, ImGuiCaptureArgs* args) +{ + ImGuiContext& g = *GImGui; + ImGuiIO& io = g.IO; + + // Gather selected windows + ImRect capture_rect(FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX); + for (ImGuiWindow* window : g.Windows) + { + if (window->WasActive == false) + continue; + if (window->Flags & ImGuiWindowFlags_ChildWindow) + continue; + const bool is_popup = (window->Flags & ImGuiWindowFlags_Popup) || (window->Flags & ImGuiWindowFlags_Tooltip); + if ((args->InFlags & ImGuiCaptureFlags_IncludeTooltipsAndPopups) && is_popup) + { + capture_rect.Add(window->Rect()); + args->InCaptureWindows.push_back(window); + continue; + } + if (is_popup) + continue; + if (_SelectedWindows.contains(window->RootWindow->ID)) + { + capture_rect.Add(window->Rect()); + args->InCaptureWindows.push_back(window); + } + } + const bool allow_capture = !capture_rect.IsInverted() && args->InCaptureWindows.Size > 0 && _OutputFileTemplate[0]; + + const float TEXT_BASE_WIDTH = ImGui::CalcTextSize("A").x; + const ImVec2 button_sz = ImVec2(TEXT_BASE_WIDTH * 30, 0.0f); + + // Capture Multiple Button + { + char label[64]; + ImFormatString(label, 64, "Capture Multiple (%d)###CaptureMultiple", args->InCaptureWindows.Size); + + if (!allow_capture) + ImGui::BeginDisabled(); + bool do_capture = ImGui::Button(label, button_sz); + do_capture |= io.KeyAlt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_C)); + if (!allow_capture) + ImGui::EndDisabled(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Alternatively press Alt+C to capture selection."); + if (do_capture && _InitializeOutputFile()) + _StateIsCapturing = true; + } + + // Record video button + // (Prefer 100/FPS to be an integer) + { + const bool is_capturing_video = context->IsCapturingVideo(); + if (is_capturing_video) + { + if (ImGui::Button("Stop capturing video###CaptureVideo", button_sz)) + context->EndVideoCapture(); + } + else + { + char label[64]; + ImFormatString(label, 64, "Capture video (%d)###CaptureVideo", args->InCaptureWindows.Size); + if (!allow_capture) + ImGui::BeginDisabled(); + if (ImGui::Button(label, button_sz) && _InitializeOutputFile()) + { + // File template will most likely end with .png, but we need a different extension for videos. + IM_ASSERT(VideoCaptureExtension != NULL && VideoCaptureExtension[0]); + char* ext = (char*)ImPathFindExtension(args->InOutputFile); + ImStrncpy(ext, VideoCaptureExtension, (size_t)(ext - args->InOutputFile)); + _StateIsCapturing = true; + context->BeginVideoCapture(args); + } + if (!allow_capture) + ImGui::EndDisabled(); + } + } + + // Draw capture rectangle + ImDrawList* draw_list = ImGui::GetForegroundDrawList(); + if (allow_capture && !_StateIsPickingWindow && !_StateIsCapturing) + { + IM_ASSERT(capture_rect.GetWidth() > 0); + IM_ASSERT(capture_rect.GetHeight() > 0); + const ImRect viewport_rect = GetMainViewportRect(); + capture_rect.Expand(args->InPadding); + capture_rect.ClipWith(viewport_rect); + draw_list->AddRect(capture_rect.Min - ImVec2(1.0f, 1.0f), capture_rect.Max + ImVec2(1.0f, 1.0f), IM_COL32_WHITE); + } + + ImGui::Separator(); + + // Show window list and update rectangles + ImGui::Text("Windows:"); + if (ImGui::BeginTable("split", 2)) + { + ImGui::TableSetupColumn(NULL, ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupColumn(NULL, ImGuiTableColumnFlags_WidthStretch); + for (ImGuiWindow* window : g.Windows) + { + if (!window->WasActive) + continue; + + const bool is_popup = (window->Flags & ImGuiWindowFlags_Popup) || (window->Flags & ImGuiWindowFlags_Tooltip); + if (is_popup) + continue; + + if (window->Flags & ImGuiWindowFlags_ChildWindow) + continue; + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::PushID(window); + + // Ensure that text after the ## is actually displayed to the user (FIXME: won't be able to check/uncheck from that portion of the text) + bool is_selected = _SelectedWindows.contains(window->RootWindow->ID); + if (ImGui::Checkbox(window->Name, &is_selected)) + { + if (is_selected) + _SelectedWindows.push_back(window->RootWindow->ID); + else + _SelectedWindows.find_erase_unsorted(window->RootWindow->ID); + } + + if (const char* remaining_text = ImGui::FindRenderedTextEnd(window->Name)) + if (remaining_text[0] != 0) + { + if (remaining_text > window->Name) + ImGui::SameLine(0, 1); + else + ImGui::SameLine(0, ImGui::GetStyle().ItemInnerSpacing.x); + ImGui::TextUnformatted(remaining_text); + } + + ImGui::TableSetColumnIndex(1); + ImGui::SetNextItemWidth(TEXT_BASE_WIDTH * 9.0f); + ImGui::DragFloat2("Pos", &window->Pos.x, 0.05f, 0.0f, 0.0f, "%.0f"); + ImGui::SameLine(); + ImGui::SetNextItemWidth(TEXT_BASE_WIDTH * 9.0f); + ImGui::DragFloat2("Size", &window->SizeFull.x, 0.05f, 0.0f, 0.0f, "%.0f"); + ImGui::PopID(); + } + ImGui::EndTable(); + } +} + +void ImGuiCaptureToolUI::ShowCaptureToolWindow(ImGuiCaptureContext* context, bool* p_open) +{ + // Update capturing + if (_StateIsCapturing) + { + ImGuiCaptureArgs* args = &_CaptureArgs; + if (context->IsCapturingVideo() || args->InCaptureWindows.Size > 1) + args->InFlags &= ~ImGuiCaptureFlags_StitchAll; + + if (context->_VideoRecording && ImGui::IsKeyPressed(ImGuiKey_Escape)) + context->EndVideoCapture(); + + ImGuiCaptureStatus status = context->CaptureUpdate(args); + if (status != ImGuiCaptureStatus_InProgress) + { + if (status == ImGuiCaptureStatus_Done) + ImStrncpy(OutputLastFilename, args->InOutputFile, IM_ARRAYSIZE(OutputLastFilename)); + _StateIsCapturing = false; + _FileCounter++; + } + } + + // Update UI + if (!ImGui::Begin("Dear ImGui Capture Tool", p_open)) + { + ImGui::End(); + return; + } + if (context->ScreenCaptureFunc == NULL) + { + ImGui::TextColored(ImVec4(1, 0, 0, 1), "Backend is missing ScreenCaptureFunc!"); + ImGui::End(); + return; + } + + ImGuiIO& io = ImGui::GetIO(); + ImGuiStyle& style = ImGui::GetStyle(); + + // Options + ImGui::SetNextItemOpen(true, ImGuiCond_Once); + if (ImGui::TreeNode("Options")) + { + // Open Last + { + const bool has_last_file_name = (OutputLastFilename[0] != 0); + if (!has_last_file_name) + ImGui::BeginDisabled(); + if (ImGui::Button("Open Last")) + ImOsOpenInShell(OutputLastFilename); + if (!has_last_file_name) + ImGui::EndDisabled(); + if (has_last_file_name && ImGui::IsItemHovered()) + ImGui::SetTooltip("Open %s", OutputLastFilename); + ImGui::SameLine(0.0f, style.ItemInnerSpacing.x); + } + + // Open Directory + { + char output_dir[256]; + strcpy(output_dir, _OutputFileTemplate); + char* output_filename = (char*)ImPathFindFilename(output_dir); + if (output_filename > output_dir) + output_filename[-1] = 0; + else + strcpy(output_dir, "."); + if (ImGui::Button("Open Directory")) + { + ImPathFixSeparatorsForCurrentOS(output_dir); + ImOsOpenInShell(output_dir); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Open %s/", output_dir); + } + + const float TEXT_BASE_WIDTH = ImGui::CalcTextSize("A").x; + const float BUTTON_WIDTH = (float)(int)-(TEXT_BASE_WIDTH * 26); + + ImGui::PushItemWidth(BUTTON_WIDTH); + ImGui::InputText("Output template", _OutputFileTemplate, IM_ARRAYSIZE(_OutputFileTemplate)); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Output template should contain one %%d (or variation of it) format variable. " + "Multiple captures will be saved with an increasing number to avoid overwriting same file."); + + _ShowEncoderConfigFields(context); + + ImGui::DragFloat("Padding", &_CaptureArgs.InPadding, 0.1f, 0, 32, "%.0f"); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Extra padding around captured area."); + ImGui::DragInt("Video FPS", &_CaptureArgs.InRecordFPSTarget, 0.1f, 10, 100, "%d fps"); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Target FPS for video captures."); + + if (ImGui::Button("Snap Windows To Grid", ImVec2(BUTTON_WIDTH, 0))) + _SnapWindowsToGrid(SnapGridSize); + ImGui::SameLine(0.0f, style.ItemInnerSpacing.x); + ImGui::SetNextItemWidth((float)(int)-(TEXT_BASE_WIDTH * 5)); + ImGui::DragFloat("##SnapGridSize", &SnapGridSize, 1.0f, 1.0f, 128.0f, "%.0f"); + + ImGui::Checkbox("Software Mouse Cursor", &io.MouseDrawCursor); + + bool content_stitching_available = _CaptureArgs.InCaptureWindows.Size <= 1; +#ifdef IMGUI_HAS_VIEWPORT + content_stitching_available &= !(io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable); +#endif + ImGui::BeginDisabled(!content_stitching_available); + ImGui::CheckboxFlags("Stitch full contents height", &_CaptureArgs.InFlags, ImGuiCaptureFlags_StitchAll); + ImGui::EndDisabled(); + if (!content_stitching_available && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) + ImGui::SetTooltip("Content stitching is not possible when using viewports."); + + ImGui::CheckboxFlags("Include other windows", &_CaptureArgs.InFlags, ImGuiCaptureFlags_IncludeOtherWindows); + ImGui::CheckboxFlags("Include tooltips & popups", &_CaptureArgs.InFlags, ImGuiCaptureFlags_IncludeTooltipsAndPopups); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Capture area will be expanded to include visible tooltips."); + + ImGui::PopItemWidth(); + ImGui::TreePop(); + } + + ImGui::Separator(); + + if (!_StateIsCapturing) + _CaptureArgs.InCaptureWindows.clear(); + _CaptureWindowPicker(&_CaptureArgs); + _CaptureWindowsSelector(context, &_CaptureArgs); + + ImGui::Separator(); + + ImGui::End(); +} + +// Move/resize all windows so they are neatly aligned on a grid +// This is an easy way of ensuring some form of alignment without specifying detailed constraints. +void ImGuiCaptureToolUI::_SnapWindowsToGrid(float cell_size) +{ + ImGuiContext& g = *GImGui; + for (ImGuiWindow* window : g.Windows) + { + if (!window->WasActive) + continue; + + if (window->Flags & ImGuiWindowFlags_ChildWindow) + continue; + + if ((window->Flags & ImGuiWindowFlags_Popup) || (window->Flags & ImGuiWindowFlags_Tooltip)) + continue; + + ImRect rect = window->Rect(); + rect.Min.x = ImFloor(rect.Min.x / cell_size) * cell_size; + rect.Min.y = ImFloor(rect.Min.y / cell_size) * cell_size; + rect.Max.x = ImFloor(rect.Max.x / cell_size) * cell_size; + rect.Max.y = ImFloor(rect.Max.y / cell_size) * cell_size; + ImGui::SetWindowPos(window, rect.Min); + ImGui::SetWindowSize(window, rect.GetSize()); + } +} + +bool ImGuiCaptureToolUI::_InitializeOutputFile() +{ + // Create output folder and decide of output filename + ImFormatString(_CaptureArgs.InOutputFile, IM_ARRAYSIZE(_CaptureArgs.InOutputFile), _OutputFileTemplate, + _FileCounter + 1); + ImPathFixSeparatorsForCurrentOS(_CaptureArgs.InOutputFile); + if (!ImFileCreateDirectoryChain(_CaptureArgs.InOutputFile, ImPathFindFilename(_CaptureArgs.InOutputFile))) + { + fprintf(stderr, "ImGuiCaptureContext: unable to create directory for file '%s'.\n", + _CaptureArgs.InOutputFile); + return false; + } + return true; +} + +bool ImGuiCaptureToolUI::_ShowEncoderConfigFields(ImGuiCaptureContext* context) +{ + ImGuiContext& g = *GImGui; + const float TEXT_BASE_WIDTH = ImGui::CalcTextSize("A").x; + const float BUTTON_WIDTH = (float)(int)-(TEXT_BASE_WIDTH * 26); + + bool modified = false; + if (context->VideoCaptureEncoderPathSize) + { + ImGui::PushItemWidth(BUTTON_WIDTH); + modified |= ImGui::InputText("Video Encoder Path", context->VideoCaptureEncoderPath, context->VideoCaptureEncoderPathSize); + const bool encoder_exe_missing = !ImFileExist(context->VideoCaptureEncoderPath); + if (encoder_exe_missing) + ImGui::ItemErrorFrame(IM_COL32(255, 0, 0, 255)); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Absolute or relative path to video encoder executable (e.g. \"path/to/ffmpeg.exe\"). Required for video recording.%s", encoder_exe_missing ? "\nFile does not exist!" : ""); + } + + struct CmdLineParamsInfo + { + const char* Title = NULL; + char* Params = NULL; + int ParamsSize = 0; + const char* DefaultCmdLineParams = NULL; + const char* VideoFileExt = NULL; + CmdLineParamsInfo(const char* title, char* params, int params_size, const char* default_cmd, const char* ext) { Title = title; Params = params; ParamsSize = params_size; DefaultCmdLineParams = default_cmd; VideoFileExt = ext; } + }; + CmdLineParamsInfo params_info[] = + { + { "Video Encoder params", context->VideoCaptureEncoderParams, context->VideoCaptureEncoderParamsSize, IMGUI_CAPTURE_DEFAULT_VIDEO_PARAMS_FOR_FFMPEG, ".mp4" }, + { "Gif Encoder params", context->GifCaptureEncoderParams, context->GifCaptureEncoderParamsSize, IMGUI_CAPTURE_DEFAULT_GIF_PARAMS_FOR_FFMPEG, ".gif" }, + }; + for (CmdLineParamsInfo& info : params_info) + { + if (info.ParamsSize == 0) + continue; // Can not be edited. + IM_ASSERT(info.Params != NULL); + ImGui::PushID(&info); + float small_button_width = ImGui::CalcTextSize("..").x + ImGui::GetStyle().FramePadding.x * 2.0f; + ImGui::PushItemWidth(BUTTON_WIDTH - small_button_width); + modified |= ImGui::InputText("###Params", info.Params, info.ParamsSize); + ImGui::SameLine(0.0f, 0.0f); + ImRect input_rect = g.LastItemData.Rect; + if (ImGui::Button("..")) + ImGui::OpenPopup("CmdParamsPopup"); + input_rect.Add(g.LastItemData.Rect); + ImGui::SetNextWindowSize(ImVec2(input_rect.GetWidth(), 0.0f)); + ImGui::SetNextWindowPos(input_rect.GetBL()); + if (ImGui::BeginPopup("CmdParamsPopup")) + { + ImGui::Text("Reset to default params for FFMPEG and %s file format:", info.VideoFileExt); + ImGui::Indent(); + float wrap_width = ImGui::GetContentRegionAvail().x - g.Style.FramePadding.x * 2; + ImVec2 text_size = ImGui::CalcTextSize(info.DefaultCmdLineParams, NULL, false, wrap_width); + if (ImGui::Selectable("###Reset", false, 0, text_size + g.Style.FramePadding * 2)) + { + ImStrncpy(info.Params, info.DefaultCmdLineParams, info.ParamsSize); + ImGui::CloseCurrentPopup(); + } + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + draw_list->AddText(NULL, 0, g.LastItemData.Rect.GetTL() + g.Style.FramePadding, ImGui::GetColorU32(ImGuiCol_Text), info.DefaultCmdLineParams, NULL, wrap_width); + ImGui::Unindent(); + + ImGui::Separator(); + ImGui::TextUnformatted( + "Command line parameters passed to video encoder executable.\n" + "Following variables may be used:\n" + "$FPS - target FPS\n" + "$WIDTH - width of captured frame\n" + "$HEIGHT - height of captured frame\n" + "$OUTPUT - video output file"); + ImGui::EndPopup(); + } + ImGui::SameLine(0, g.Style.ItemInnerSpacing.x); + ImGui::TextUnformatted(info.Title); + if (!info.Params[0]) + ImGui::ItemErrorFrame(IM_COL32(255, 0, 0, 255)); + ImGui::PopID(); + } + + if (VideoCaptureExtensionSize) + { + IM_ASSERT(VideoCaptureExtension != NULL); + ImGui::PushItemWidth(BUTTON_WIDTH); + if (ImGui::BeginCombo("Video format (default)", VideoCaptureExtension)) + { + const char* supported_exts[] = { ".gif", ".mp4" }; + for (auto& ext: supported_exts) + if (ImGui::Selectable(ext, strcmp(VideoCaptureExtension, ext) == 0)) + { + ImStrncpy(VideoCaptureExtension, ext, VideoCaptureExtensionSize); + modified = true; + } + ImGui::EndCombo(); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("File extension for captured video file."); + } + return modified; +} + +//----------------------------------------------------------------------------- diff --git a/libs/imgui_test_engine/imgui_capture_tool.h b/libs/imgui_test_engine/imgui_capture_tool.h new file mode 100644 index 0000000..e5489c5 --- /dev/null +++ b/libs/imgui_test_engine/imgui_capture_tool.h @@ -0,0 +1,180 @@ +// dear imgui test engine +// (screen/video capture tool) +// This is usable as a standalone applet or controlled by the test engine. + +#pragma once + +#include "imgui_te_utils.h" // ImFuncPtr + +//----------------------------------------------------------------------------- +// Forward declarations +//----------------------------------------------------------------------------- + +// Our types +struct ImGuiCaptureArgs; // Parameters for Capture +struct ImGuiCaptureContext; // State of an active capture tool +struct ImGuiCaptureImageBuf; // Simple helper to store an RGBA image in memory +struct ImGuiCaptureToolUI; // Capture tool instance + UI window + +typedef unsigned int ImGuiCaptureFlags; // See enum: ImGuiCaptureFlags_ + +// Capture function which needs to be provided by user application +typedef bool (ImGuiScreenCaptureFunc)(ImGuiID viewport_id, int x, int y, int w, int h, unsigned int* pixels, void* user_data); + +// External types +struct ImGuiWindow; // imgui.h + +//----------------------------------------------------------------------------- + +// [Internal] +// Helper class for simple bitmap manipulation (not particularly efficient!) +struct IMGUI_API ImGuiCaptureImageBuf +{ + int Width; + int Height; + unsigned int* Data; // RGBA8 + + ImGuiCaptureImageBuf() { Width = Height = 0; Data = NULL; } + ~ImGuiCaptureImageBuf() { Clear(); } + + void Clear(); // Free allocated memory buffer if such exists. + void CreateEmpty(int w, int h); // Reallocate buffer for pixel data and zero it. + bool SaveFile(const char* filename); // Save pixel data to specified image file. + void RemoveAlpha(); // Clear alpha channel from all pixels. +}; + +enum ImGuiCaptureFlags_ : unsigned int +{ + ImGuiCaptureFlags_None = 0, + ImGuiCaptureFlags_StitchAll = 1 << 0, // Capture entire window scroll area (by scrolling and taking multiple screenshot). Only works for a single window. + ImGuiCaptureFlags_IncludeOtherWindows = 1 << 1, // Disable hiding other windows (when CaptureAddWindow has been called by default other windows are hidden) + ImGuiCaptureFlags_IncludeTooltipsAndPopups = 1 << 2, // Expand capture area to automatically include visible popups and tooltips (use with ImGuiCaptureflags_HideOtherWindows) + ImGuiCaptureFlags_HideMouseCursor = 1 << 3, // Hide render software mouse cursor during capture. + ImGuiCaptureFlags_Instant = 1 << 4, // Perform capture on very same frame. Only works when capturing a rectangular region. Unsupported features: content stitching, window hiding, window relocation. + ImGuiCaptureFlags_NoSave = 1 << 5 // Do not save output image. +}; + +// Defines input and output arguments for capture process. +// When capturing from tests you can usually use the ImGuiTestContext::CaptureXXX() helpers functions. +struct ImGuiCaptureArgs +{ + // [Input] + ImGuiCaptureFlags InFlags = 0; // Flags for customizing behavior of screenshot tool. + ImVector InCaptureWindows; // Windows to capture. All other windows will be hidden. May be used with InCaptureRect to capture only some windows in specified rect. + ImRect InCaptureRect; // Screen rect to capture. Does not include padding. + float InPadding = 16.0f; // Extra padding at the edges of the screenshot. Ensure that there is available space around capture rect horizontally, also vertically if ImGuiCaptureFlags_StitchFullContents is not used. + char InOutputFile[256] = ""; // Output will be saved to a file if InOutputImageBuf is NULL. + ImGuiCaptureImageBuf* InOutputImageBuf = NULL; // _OR_ Output will be saved to image buffer if specified. + int InRecordFPSTarget = 30; // FPS target for recording videos. + int InSizeAlign = 0; // Resolution alignment (0 = auto, 1 = no alignment, >= 2 = align width/height to be multiple of given value) + + // [Output] + ImVec2 OutImageSize; // Produced image size. +}; + +enum ImGuiCaptureStatus +{ + ImGuiCaptureStatus_InProgress, + ImGuiCaptureStatus_Done, + ImGuiCaptureStatus_Error +}; + +struct ImGuiCaptureWindowData +{ + ImGuiWindow* Window; + ImRect BackupRect; + ImVec2 PosDuringCapture; +}; + +// Implements functionality for capturing images +struct IMGUI_API ImGuiCaptureContext +{ + // IO + ImFuncPtr(ImGuiScreenCaptureFunc) ScreenCaptureFunc = NULL; // Graphics backend specific function that captures specified portion of framebuffer and writes RGBA data to `pixels` buffer. + void* ScreenCaptureUserData = NULL; // Custom user pointer which is passed to ScreenCaptureFunc. (Optional) + char* VideoCaptureEncoderPath = NULL; // Video encoder path (not owned, stored externally). + int VideoCaptureEncoderPathSize = 0; // Optional. Set in order to edit this parameter from UI. + char* VideoCaptureEncoderParams = NULL; // Video encoder params (not owned, stored externally). + int VideoCaptureEncoderParamsSize = 0; // Optional. Set in order to edit this parameter from UI. + char* GifCaptureEncoderParams = NULL; // Video encoder params for GIF output (not owned, stored externally). + int GifCaptureEncoderParamsSize = 0; // Optional. Set in order to edit this parameter from UI. + + // [Internal] + ImRect _CaptureRect; // Viewport rect that is being captured. + ImRect _CapturedWindowRect; // Top-left corner of region that covers all windows included in capture. This is not same as _CaptureRect.Min when capturing explicitly specified rect. + int _ChunkNo = 0; // Number of chunk that is being captured when capture spans multiple frames. + int _FrameNo = 0; // Frame number during capture process that spans multiple frames. + ImVec2 _MouseRelativeToWindowPos; // Mouse cursor position relative to captured window (when _StitchAll is in use). + ImGuiWindow* _HoveredWindow = NULL; // Window which was hovered at capture start. + ImGuiCaptureImageBuf _CaptureBuf; // Output image buffer. + const ImGuiCaptureArgs* _CaptureArgs = NULL; // Current capture args. Set only if capture is in progress. + ImVector _WindowsData; // Backup windows that will have their rect modified and restored. args->InCaptureWindows can not be used because popups may get closed during capture and no longer appear in that list. + + // [Internal] Video recording + bool _VideoRecording = false; // Flag indicating that video recording is in progress. + double _VideoLastFrameTime = 0; // Time when last video frame was recorded. + FILE* _VideoEncoderPipe = NULL; // File writing to stdin of video encoder process. + + // [Internal] Backups + bool _BackupMouseDrawCursor = false; // Initial value of g.IO.MouseDrawCursor + ImVec2 _BackupDisplayWindowPadding; // Backup padding. We set it to {0, 0} during capture. + ImVec2 _BackupDisplaySafeAreaPadding; // Backup padding. We set it to {0, 0} during capture. + + //------------------------------------------------------------------------- + // Functions + //------------------------------------------------------------------------- + + ImGuiCaptureContext(ImGuiScreenCaptureFunc capture_func = NULL) { ScreenCaptureFunc = capture_func; _MouseRelativeToWindowPos = ImVec2(-FLT_MAX, -FLT_MAX); } + + // These functions should be called from appropriate context hooks. See ImGui::AddContextHook() for more info. + // (ImGuiTestEngine automatically calls that for you, so this only apply to independently created instance) + void PreNewFrame(); + void PreRender(); + void PostRender(); + + // Update capturing. If this function returns true then it should be called again with same arguments on the next frame. + ImGuiCaptureStatus CaptureUpdate(ImGuiCaptureArgs* args); + void RestoreBackedUpData(); + void ClearState(); + + // Begin video capture. Call CaptureUpdate() every frame afterwards until it returns false. + void BeginVideoCapture(ImGuiCaptureArgs* args); + void EndVideoCapture(); + bool IsCapturingVideo(); + bool IsCapturing(); +}; + +//----------------------------------------------------------------------------- +// ImGuiCaptureToolUI +//----------------------------------------------------------------------------- + +// Implements UI for capturing images +// (when using ImGuiTestEngine scripting API you may not need to use this at all) +struct IMGUI_API ImGuiCaptureToolUI +{ + float SnapGridSize = 32.0f; // Size of the grid cell for "snap to grid" functionality. + char OutputLastFilename[256] = ""; // File name of last captured file. + char* VideoCaptureExtension = NULL; // Video file extension (e.g. ".gif" or ".mp4") + int VideoCaptureExtensionSize = 0; // Optional. Set in order to edit this parameter from UI. + + ImGuiCaptureArgs _CaptureArgs; // Capture args + bool _StateIsPickingWindow = false; + bool _StateIsCapturing = false; + ImVector _SelectedWindows; + char _OutputFileTemplate[256] = ""; // + int _FileCounter = 0; // Counter which may be appended to file name when saving. By default, counting starts from 1. When done this field holds number of saved files. + + // Public + ImGuiCaptureToolUI(); + void ShowCaptureToolWindow(ImGuiCaptureContext* context, bool* p_open = NULL); // Render a capture tool window with various options and utilities. + + // [Internal] + void _CaptureWindowPicker(ImGuiCaptureArgs* args); // Render a window picker that captures picked window to file specified in file_name. + void _CaptureWindowsSelector(ImGuiCaptureContext* context, ImGuiCaptureArgs* args); // Render a selector for selecting multiple windows for capture. + void _SnapWindowsToGrid(float cell_size); // Snap edges of all visible windows to a virtual grid. + bool _InitializeOutputFile(); // Format output file template into capture args struct and ensure target directory exists. + bool _ShowEncoderConfigFields(ImGuiCaptureContext* context); +}; + +#define IMGUI_CAPTURE_DEFAULT_VIDEO_PARAMS_FOR_FFMPEG "-hide_banner -loglevel error -r $FPS -f rawvideo -pix_fmt rgba -s $WIDTHx$HEIGHT -i - -threads 0 -y -preset ultrafast -pix_fmt yuv420p -crf 20 $OUTPUT" +#define IMGUI_CAPTURE_DEFAULT_GIF_PARAMS_FOR_FFMPEG "-hide_banner -loglevel error -r $FPS -f rawvideo -pix_fmt rgba -s $WIDTHx$HEIGHT -i - -threads 0 -y -filter_complex \"split=2 [a] [b]; [a] palettegen [pal]; [b] [pal] paletteuse\" $OUTPUT" diff --git a/libs/imgui_test_engine/imgui_te_context.cpp b/libs/imgui_test_engine/imgui_te_context.cpp new file mode 100644 index 0000000..7493321 --- /dev/null +++ b/libs/imgui_test_engine/imgui_te_context.cpp @@ -0,0 +1,3960 @@ +// dear imgui +// (context when a running test + end user automation API) +// This is the main (if not only) interface that your Tests will be using. + +#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS) +#define _CRT_SECURE_NO_WARNINGS +#endif + +#define IMGUI_DEFINE_MATH_OPERATORS +#include "imgui_te_context.h" +#include "imgui.h" +#include "imgui_internal.h" +#include "imgui_te_engine.h" +#include "imgui_te_internal.h" +#include "imgui_te_perftool.h" +#include "imgui_te_utils.h" +#include "thirdparty/Str/Str.h" + +//------------------------------------------------------------------------- +// [SECTION] ImGuiTestRefDesc +//------------------------------------------------------------------------- + +ImGuiTestRefDesc::ImGuiTestRefDesc(const ImGuiTestRef& ref, const ImGuiTestItemInfo* item) +{ + if (ref.Path) + ImFormatString(Buf, IM_ARRAYSIZE(Buf), "'%s' > %08X", ref.Path, ref.ID); + else + ImFormatString(Buf, IM_ARRAYSIZE(Buf), "%08X > '%s'", ref.ID, item ? item->DebugLabel : "NULL"); +} + +//------------------------------------------------------------------------- +// [SECTION] ImGuiTestContextDepthScope +//------------------------------------------------------------------------- + +// Helper to increment/decrement the function depth (so our log entry can be padded accordingly) +#define IM_TOKENCONCAT_INTERNAL(x, y) x ## y +#define IM_TOKENCONCAT(x, y) IM_TOKENCONCAT_INTERNAL(x, y) +#define IMGUI_TEST_CONTEXT_REGISTER_DEPTH(_THIS) ImGuiTestContextDepthScope IM_TOKENCONCAT(depth_register, __LINE__)(_THIS) + +struct ImGuiTestContextDepthScope +{ + ImGuiTestContext* TestContext; + ImGuiTestContextDepthScope(ImGuiTestContext* ctx) { TestContext = ctx; TestContext->ActionDepth++; } + ~ImGuiTestContextDepthScope() { TestContext->ActionDepth--; } +}; + +//------------------------------------------------------------------------- +// [SECTION] Enum names helpers +//------------------------------------------------------------------------- + +inline const char* GetActionName(ImGuiTestAction action) +{ + switch (action) + { + case ImGuiTestAction_Unknown: return "Unknown"; + case ImGuiTestAction_Hover: return "Hover"; + case ImGuiTestAction_Click: return "Click"; + case ImGuiTestAction_DoubleClick: return "DoubleClick"; + case ImGuiTestAction_Check: return "Check"; + case ImGuiTestAction_Uncheck: return "Uncheck"; + case ImGuiTestAction_Open: return "Open"; + case ImGuiTestAction_Close: return "Close"; + case ImGuiTestAction_Input: return "Input"; + case ImGuiTestAction_NavActivate: return "NavActivate"; + case ImGuiTestAction_COUNT: + default: return "N/A"; + } +} + +inline const char* GetActionVerb(ImGuiTestAction action) +{ + switch (action) + { + case ImGuiTestAction_Unknown: return "Unknown"; + case ImGuiTestAction_Hover: return "Hovered"; + case ImGuiTestAction_Click: return "Clicked"; + case ImGuiTestAction_DoubleClick: return "DoubleClicked"; + case ImGuiTestAction_Check: return "Checked"; + case ImGuiTestAction_Uncheck: return "Unchecked"; + case ImGuiTestAction_Open: return "Opened"; + case ImGuiTestAction_Close: return "Closed"; + case ImGuiTestAction_Input: return "Input"; + case ImGuiTestAction_NavActivate: return "NavActivated"; + case ImGuiTestAction_COUNT: + default: return "N/A"; + } +} + + +//------------------------------------------------------------------------- +// [SECTION] ImGuiTestContext +// This is the interface that most tests will interact with. +//------------------------------------------------------------------------- + +void ImGuiTestContext::LogEx(ImGuiTestVerboseLevel level, ImGuiTestLogFlags flags, const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + LogExV(level, flags, fmt, args); + va_end(args); +} + +void ImGuiTestContext::LogExV(ImGuiTestVerboseLevel level, ImGuiTestLogFlags flags, const char* fmt, va_list args) +{ + ImGuiTestContext* ctx = this; + //ImGuiTest* test = ctx->Test; + + IM_ASSERT(level > ImGuiTestVerboseLevel_Silent && level < ImGuiTestVerboseLevel_COUNT); + + if (level == ImGuiTestVerboseLevel_Debug && ctx->ActionDepth > 1) + level = ImGuiTestVerboseLevel_Trace; + + // Log all messages that we may want to print in future. + if (EngineIO->ConfigVerboseLevelOnError < level) + return; + + ImGuiTestLog* log = &ctx->TestOutput->Log; + const int prev_size = log->Buffer.size(); + + //const char verbose_level_char = ImGuiTestEngine_GetVerboseLevelName(level)[0]; + //if (flags & ImGuiTestLogFlags_NoHeader) + // log->Buffer.appendf("[%c] ", verbose_level_char); + //else + // log->Buffer.appendf("[%c] [%04d] ", verbose_level_char, ctx->FrameCount); + if ((flags & ImGuiTestLogFlags_NoHeader) == 0) + log->Buffer.appendf("[%04d] ", ctx->FrameCount); + + if (level >= ImGuiTestVerboseLevel_Debug) + log->Buffer.appendf("-- %*s", ImMax(0, (ctx->ActionDepth - 1) * 2), ""); + log->Buffer.appendfv(fmt, args); + log->Buffer.append("\n"); + + log->UpdateLineOffsets(EngineIO, level, log->Buffer.begin() + prev_size); + LogToTTY(level, log->Buffer.c_str() + prev_size); + LogToDebugger(level, log->Buffer.c_str() + prev_size); +} + +void ImGuiTestContext::LogDebug(const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + LogExV(ImGuiTestVerboseLevel_Debug, ImGuiTestLogFlags_None, fmt, args); + va_end(args); +} + +void ImGuiTestContext::LogInfo(const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + LogExV(ImGuiTestVerboseLevel_Info, ImGuiTestLogFlags_None, fmt, args); + va_end(args); +} + +void ImGuiTestContext::LogWarning(const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + LogExV(ImGuiTestVerboseLevel_Warning, ImGuiTestLogFlags_None, fmt, args); + va_end(args); +} + +void ImGuiTestContext::LogError(const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + LogExV(ImGuiTestVerboseLevel_Error, ImGuiTestLogFlags_None, fmt, args); + va_end(args); +} + +void ImGuiTestContext::LogToTTY(ImGuiTestVerboseLevel level, const char* message, const char* message_end) +{ + IM_ASSERT(level > ImGuiTestVerboseLevel_Silent && level < ImGuiTestVerboseLevel_COUNT); + + if (!EngineIO->ConfigLogToTTY) + return; + + ImGuiTestContext* ctx = this; + ImGuiTestOutput* test_output = ctx->TestOutput; + ImGuiTestLog* log = &test_output->Log; + + if (test_output->Status == ImGuiTestStatus_Error) + { + // Current test failed. + if (!CachedLinesPrintedToTTY) + { + // Print all previous logged messages first + // FIXME: Can't use ExtractLinesAboveVerboseLevel() because we want to keep error level... + CachedLinesPrintedToTTY = true; + for (int i = 0; i < log->LineInfo.Size; i++) + { + ImGuiTestLogLineInfo& line_info = log->LineInfo[i]; + if (line_info.Level > EngineIO->ConfigVerboseLevelOnError) + continue; + char* line_begin = log->Buffer.Buf.Data + line_info.LineOffset; + char* line_end = strchr(line_begin, '\n'); + LogToTTY(line_info.Level, line_begin, line_end + 1); + } + // We already printed current line as well, so return now. + return; + } + // Otherwise print only current message. If we are executing here log level already is within range of + // ConfigVerboseLevelOnError setting. + } + else if (EngineIO->ConfigVerboseLevel < level) + { + // Skip printing messages of lower level than configured. + return; + } + + switch (level) + { + case ImGuiTestVerboseLevel_Warning: + ImOsConsoleSetTextColor(ImOsConsoleStream_StandardOutput, ImOsConsoleTextColor_BrightYellow); + break; + case ImGuiTestVerboseLevel_Error: + ImOsConsoleSetTextColor(ImOsConsoleStream_StandardOutput, ImOsConsoleTextColor_BrightRed); + break; + default: + ImOsConsoleSetTextColor(ImOsConsoleStream_StandardOutput, ImOsConsoleTextColor_White); + break; + } + if (message_end) + fprintf(stdout, "%.*s", (int)(message_end - message), message); + else + fprintf(stdout, "%s", message); + ImOsConsoleSetTextColor(ImOsConsoleStream_StandardOutput, ImOsConsoleTextColor_White); + fflush(stdout); +} + +void ImGuiTestContext::LogToDebugger(ImGuiTestVerboseLevel level, const char* message) +{ + IM_ASSERT(level > ImGuiTestVerboseLevel_Silent && level < ImGuiTestVerboseLevel_COUNT); + + if (!EngineIO->ConfigLogToDebugger) + return; + + if (EngineIO->ConfigVerboseLevel < level) + return; + + switch (level) + { + default: + break; + case ImGuiTestVerboseLevel_Error: + ImOsOutputDebugString("[error] "); + break; + case ImGuiTestVerboseLevel_Warning: + ImOsOutputDebugString("[warn.] "); + break; + case ImGuiTestVerboseLevel_Info: + ImOsOutputDebugString("[info ] "); + break; + case ImGuiTestVerboseLevel_Debug: + ImOsOutputDebugString("[debug] "); + break; + case ImGuiTestVerboseLevel_Trace: + ImOsOutputDebugString("[trace] "); + break; + } + + ImOsOutputDebugString(message); +} + +void ImGuiTestContext::LogBasicUiState() +{ + ImGuiID item_hovered_id = UiContext->HoveredIdPreviousFrame; + ImGuiID item_active_id = UiContext->ActiveId; + ImGuiTestItemInfo* item_hovered_info = item_hovered_id ? ImGuiTestEngine_FindItemInfo(Engine, item_hovered_id, "") : NULL; + ImGuiTestItemInfo* item_active_info = item_active_id ? ImGuiTestEngine_FindItemInfo(Engine, item_active_id, "") : NULL; + LogDebug("Hovered: 0x%08X (\"%s\"), Active: 0x%08X(\"%s\")", + item_hovered_id, item_hovered_info->ID != 0 ? item_hovered_info->DebugLabel : "", + item_active_id, item_active_info->ID != 0 ? item_active_info->DebugLabel : ""); +} + +void ImGuiTestContext::LogItemList(ImGuiTestItemList* items) +{ + for (const ImGuiTestItemInfo& info : *items) + LogDebug("- 0x%08X: depth %d: '%s' in window '%s'\n", info.ID, info.Depth, info.DebugLabel, info.Window->Name); +} + +void ImGuiTestContext::Finish(ImGuiTestStatus status) +{ + if (ActiveFunc == ImGuiTestActiveFunc_GuiFunc) + { + IM_ASSERT(status == ImGuiTestStatus_Success || status == ImGuiTestStatus_Unknown); + if (RunFlags & ImGuiTestRunFlags_GuiFuncOnly) + return; + if (TestOutput->Status == ImGuiTestStatus_Running) + TestOutput->Status = status; + } + else if (ActiveFunc == ImGuiTestActiveFunc_TestFunc) + { + IM_ASSERT(status == ImGuiTestStatus_Unknown); // To set Success from a TestFunc() you can 'return' from it. + if (TestOutput->Status == ImGuiTestStatus_Running) + TestOutput->Status = status; + } +} + +static void LogWarningFunc(void* user_data, const char* fmt, ...) +{ + ImGuiTestContext* ctx = (ImGuiTestContext*)user_data; + va_list args; + va_start(args, fmt); + ctx->LogExV(ImGuiTestVerboseLevel_Warning, ImGuiTestLogFlags_None, fmt, args); + va_end(args); +} + +static void LogNotAsWarningFunc(void* user_data, const char* fmt, ...) +{ + ImGuiTestContext* ctx = (ImGuiTestContext*)user_data; + va_list args; + va_start(args, fmt); + ctx->LogExV(ImGuiTestVerboseLevel_Debug, ImGuiTestLogFlags_None, fmt, args); + va_end(args); +} + +void ImGuiTestContext::RecoverFromUiContextErrors() +{ + IM_ASSERT(Test != NULL); + + // If we are _already_ in a test error state, recovering is normal so we'll hide the log. + const bool verbose = (TestOutput->Status != ImGuiTestStatus_Error) || (EngineIO->ConfigVerboseLevel >= ImGuiTestVerboseLevel_Debug); + if (verbose && (Test->Flags & ImGuiTestFlags_NoRecoveryWarnings) == 0) + ImGui::ErrorCheckEndFrameRecover(LogWarningFunc, this); + else + ImGui::ErrorCheckEndFrameRecover(LogNotAsWarningFunc, this); +} + +void ImGuiTestContext::Yield(int count) +{ + IM_ASSERT(count > 0); + while (count > 0) + { + ImGuiTestEngine_Yield(Engine); + count--; + } +} + +void ImGuiTestContext::YieldUntil(int frame_count) +{ + while (FrameCount < frame_count) + ImGuiTestEngine_Yield(Engine); +} + +// Supported values for ImGuiTestRunFlags: +// - ImGuiTestRunFlags_NoError: if child test fails, return false and do not mark parent test as failed. +// - ImGuiTestRunFlags_ShareVars: share generic vars and custom vars between child and parent tests. +// - ImGuiTestRunFlags_ShareTestContext +ImGuiTestStatus ImGuiTestContext::RunChildTest(const char* child_test_name, ImGuiTestRunFlags run_flags) +{ + if (IsError()) + return ImGuiTestStatus_Error; + + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + LogDebug("RunChildTest %s", child_test_name); + + ImGuiTest* child_test = ImGuiTestEngine_FindTestByName(Engine, NULL, child_test_name); + IM_CHECK_SILENT_RETV(child_test != NULL, ImGuiTestStatus_Error); + IM_CHECK_SILENT_RETV(child_test != Test, ImGuiTestStatus_Error); // Can't recursively run same test. + + ImGuiTestStatus parent_status = TestOutput->Status; + TestOutput->Status = ImGuiTestStatus_Running; + ImGuiTestEngine_RunTest(Engine, this, child_test, run_flags); + ImGuiTestStatus child_status = TestOutput->Status; + + // Restore parent status + TestOutput->Status = parent_status; + if (child_status == ImGuiTestStatus_Error && (run_flags & ImGuiTestRunFlags_NoError) == 0) + TestOutput->Status = ImGuiTestStatus_Error; + + // Return child status + LogWarning("(returning to parent test)"); + return child_status; +} + +// Return true to request aborting TestFunc +// Called via IM_SUSPEND_TESTFUNC() +bool ImGuiTestContext::SuspendTestFunc(const char* file, int line) +{ + if (IsError()) + return false; + + file = ImPathFindFilename(file); + if (file != NULL) + LogError("SuspendTestFunc() at %s:%d", file, line); + else + LogError("SuspendTestFunc()"); + + // Save relevant state. + // FIXME-TESTS: Saving/restoring window z-order could be desirable. + ImVec2 mouse_pos = Inputs->MousePosValue; + ImGuiTestRunFlags run_flags = RunFlags; +#if IMGUI_VERSION_NUM >= 18992 + ImGui::TeleportMousePos(mouse_pos); +#endif + + RunFlags |= ImGuiTestRunFlags_GuiFuncOnly; + TestOutput->Status = ImGuiTestStatus_Suspended; + while (TestOutput->Status == ImGuiTestStatus_Suspended && !Abort) + Yield(); + TestOutput->Status = ImGuiTestStatus_Running; + + // Restore relevant state. + RunFlags = run_flags; + Inputs->MousePosValue = mouse_pos; + + // Terminate TestFunc on abort, continue otherwise. + return Abort; +} + +// Sleep a given amount of time (unless running in Fast mode: there it will Yield once) +void ImGuiTestContext::Sleep(float time) +{ + if (IsError()) + return; + + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + if (EngineIO->ConfigRunSpeed == ImGuiTestRunSpeed_Fast) + { + LogEx(ImGuiTestVerboseLevel_Trace, ImGuiTestLogFlags_None, "Sleep(%.2f) -> Yield() in fast mode", time); + //ImGuiTestEngine_AddExtraTime(Engine, time); // We could add time, for now we have no use for it... + ImGuiTestEngine_Yield(Engine); + } + else + { + LogEx(ImGuiTestVerboseLevel_Trace, ImGuiTestLogFlags_None, "Sleep(%.2f)", time); + while (time > 0.0f && !Abort) + { + ImGuiTestEngine_Yield(Engine); + time -= UiContext->IO.DeltaTime; + } + } +} + +// This is useful when you need to wait a certain amount of time (even in Fast mode) +// Sleep for a given clock time from the point of view of the Dear ImGui context, without affecting wall clock time of the running application. +// FIXME: This makes sense for apps only relying on io.DeltaTime. +void ImGuiTestContext::SleepNoSkip(float time, float framestep_in_second) +{ + if (IsError()) + return; + + while (time > 0.0f && !Abort) + { + ImGuiTestEngine_SetDeltaTime(Engine, framestep_in_second); + ImGuiTestEngine_Yield(Engine); + time -= UiContext->IO.DeltaTime; + } +} + +void ImGuiTestContext::SleepShort() +{ + if (EngineIO->ConfigRunSpeed != ImGuiTestRunSpeed_Fast) + Sleep(EngineIO->ActionDelayShort); +} + +void ImGuiTestContext::SleepStandard() +{ + if (EngineIO->ConfigRunSpeed != ImGuiTestRunSpeed_Fast) + Sleep(EngineIO->ActionDelayStandard); +} + +void ImGuiTestContext::SetInputMode(ImGuiInputSource input_mode) +{ + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + LogDebug("SetInputMode %d", input_mode); + + IM_ASSERT(input_mode == ImGuiInputSource_Mouse || input_mode == ImGuiInputSource_Keyboard || input_mode == ImGuiInputSource_Gamepad); + InputMode = input_mode; + + if (InputMode == ImGuiInputSource_Keyboard || InputMode == ImGuiInputSource_Gamepad) + { + UiContext->NavDisableHighlight = false; + UiContext->NavDisableMouseHover = true; + } + else + { + UiContext->NavDisableHighlight = true; + UiContext->NavDisableMouseHover = false; + } +} + +void ImGuiTestContext::SetRef(ImGuiWindow* window) +{ + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + IM_CHECK_SILENT(window != NULL); + LogDebug("SetRef '%s' %08X", window->Name, window->ID); + + // We grab the ID directly and avoid ImHashDecoratedPath so "/" in window names are not ignored. + size_t len = strlen(window->Name); + IM_ASSERT(len < IM_ARRAYSIZE(RefStr) - 1); + strcpy(RefStr, window->Name); + RefID = RefWindowID = window->ID; + + MouseSetViewport(window); + + // Automatically uncollapse by default + if (!(OpFlags & ImGuiTestOpFlags_NoAutoUncollapse)) + WindowCollapse(window->ID, false); +} + +// SetRef() ok in GUI Func ONLY if pointer to a pointer. +// FIXME-TESTS: May be good to focus window when docked? Otherwise locate request won't even see an item? +void ImGuiTestContext::SetRef(ImGuiTestRef ref) +{ + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + if (ActiveFunc == ImGuiTestActiveFunc_TestFunc) + LogDebug("SetRef '%s' %08X", ref.Path ? ref.Path : "NULL", ref.ID); + + if (ref.Path) + { + size_t len = strlen(ref.Path); + IM_ASSERT(len < IM_ARRAYSIZE(RefStr) - 1); + + strcpy(RefStr, ref.Path); + RefID = GetID(ref.Path, ImGuiTestRef()); + } + else + { + RefStr[0] = 0; + RefID = ref.ID; + } + RefWindowID = 0; + + // Try to infer window + // (1) Try first element of ref path, it is most likely a window name and item lookup won't be necessary. + ImGuiWindow* window = GetWindowByRef(""); + if (window == NULL && ref.Path != NULL) + { + const char* name_begin = ref.Path; + while (*name_begin == '/') name_begin++; + const char* name_end = name_begin - 1; + do + { + name_end = strchr(name_end + 1, '/'); + } while (name_end != NULL && name_end > name_begin && name_end[-1] == '\\'); + window = GetWindowByRef(ImHashDecoratedPath(name_begin, name_end)); + } + + if (ActiveFunc == ImGuiTestActiveFunc_GuiFunc) + return; + + // (2) Ref was specified as an ID and points to an item therefore item lookup is unavoidable. + // FIXME: Maybe display something in log when that happens? + if (window == NULL) + if (ImGuiTestItemInfo* item_info = ItemInfo(RefID, ImGuiTestOpFlags_NoError)) + if (item_info->ID != 0) + window = item_info->Window; + + if (window) + { + RefWindowID = window->ID; + MouseSetViewport(window); + } + + // Automatically uncollapse by default + if (window && !(OpFlags & ImGuiTestOpFlags_NoAutoUncollapse)) + WindowCollapse(window->ID, false); +} + +ImGuiTestRef ImGuiTestContext::GetRef() +{ + return RefID; +} + +// Turn ref into a root ref unless ref is empty +// FIXME: This seems inconsistent? Clarify? +ImGuiWindow* ImGuiTestContext::GetWindowByRef(ImGuiTestRef ref) +{ + ImGuiID window_id = ref.IsEmpty() ? GetID(ref) : GetID(ref, "//"); + ImGuiWindow* window = ImGui::FindWindowByID(window_id); + return window; +} + +ImGuiID ImGuiTestContext::GetID(ImGuiTestRef ref) +{ + if (ref.ID) + return ref.ID; + + return GetID(ref, RefID); +} + +// Refer to Wiki to read details +// https://github.com/ocornut/imgui_test_engine/wiki/Named-References +// - Meaning of leading "//" ................. "//rootnode" : ignore SetRef +// - Meaning of leading "//$FOCUSED" ......... "//$FOCUSED/node" : "node" in currently focused window +// - Meaning of leading "/" .................. "/node" : move to root of window pointed by SetRef() when SetRef() uses a path +// - Meaning of $$xxxx literal encoding ...... "list/$$1" : hash of "list" + hash if (int)1, equivalent of PushID("hello"); PushID(1); +//// - Meaning of leading "../" .............. "../node" : move back 1 level from SetRef path() when SetRef() uses a path // Unimplemented +// FIXME: "//$FOCUSED/.." is currently not usable. +ImGuiID ImGuiTestContext::GetID(ImGuiTestRef ref, ImGuiTestRef seed_ref) +{ + ImGuiContext& g = *UiContext; + + if (ref.ID) + return ref.ID; // FIXME: What if seed_ref != 0 + + // Handle special $FOCUSED variable. + // (Note that we don't and can't really support a "$HOVERED" equivalent for the hovered window. + // Why? Because it is extremely fragile to use: with late translation of variable held in string, + // it is extremely common that the "expected" hovered window at the time of passing the string has + // changed in later uses of the same reference.) + // You can however easily use: + // SetRef(g.HoveredWindow->ID); + const char* FOCUSED_PREFIX = "//$FOCUSED"; + const size_t FOCUSED_PREFIX_LEN = 10; + + const char* path = ref.Path ? ref.Path : ""; + if (strncmp(path, FOCUSED_PREFIX, FOCUSED_PREFIX_LEN) == 0) + if (path[FOCUSED_PREFIX_LEN] == '/' || path[FOCUSED_PREFIX_LEN] == 0) + { + path += FOCUSED_PREFIX_LEN; + if (path[0] == '/') + path++; + if (g.NavWindow) + seed_ref = g.NavWindow->ID; + else + LogError("\"//$FOCUSED\" was used with no focused window!"); + } + + if (path[0] == '/') + { + path++; + if (path[0] == '/') + { + // "//" : Double-slash prefix resets ID seed to 0. + seed_ref = ImGuiTestRef(); + } + else + { + // "/" : Single-slash prefix sets seed to the "current window", which a parent window containing an item with RefID id. + if (ActiveFunc == ImGuiTestActiveFunc_GuiFunc) + seed_ref = ImGuiTestRef(g.CurrentWindow->ID); + else + seed_ref = RefWindowID; + } + } + + return ImHashDecoratedPath(path, NULL, seed_ref.Path ? GetID(seed_ref) : seed_ref.ID); +} + +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS +ImGuiID ImGuiTestContext::GetIDByInt(int n) +{ + return ImHashData(&n, sizeof(n), GetID(RefID)); +} + +ImGuiID ImGuiTestContext::GetIDByInt(int n, ImGuiTestRef seed_ref) +{ + return ImHashData(&n, sizeof(n), GetID(seed_ref)); +} + +ImGuiID ImGuiTestContext::GetIDByPtr(void* p) +{ + return ImHashData(&p, sizeof(p), GetID(RefID)); +} + +ImGuiID ImGuiTestContext::GetIDByPtr(void* p, ImGuiTestRef seed_ref) +{ + return ImHashData(&p, sizeof(p), GetID(seed_ref)); +} +#endif + +ImVec2 ImGuiTestContext::GetMainMonitorWorkPos() +{ +#ifdef IMGUI_HAS_VIEWPORT + if (UiContext->IO.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) + { + const ImGuiPlatformMonitor* monitor = ImGui::GetViewportPlatformMonitor(ImGui::GetMainViewport()); + return monitor->WorkPos; + } +#endif + return ImGui::GetMainViewport()->WorkPos; +} + +ImVec2 ImGuiTestContext::GetMainMonitorWorkSize() +{ +#ifdef IMGUI_HAS_VIEWPORT + if (UiContext->IO.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) + { + const ImGuiPlatformMonitor* monitor = ImGui::GetViewportPlatformMonitor(ImGui::GetMainViewport()); + return monitor->WorkSize; + } +#endif + return ImGui::GetMainViewport()->WorkSize; +} + +static bool ImGuiTestContext_CanCaptureScreenshot(ImGuiTestContext* ctx) +{ + ImGuiTestEngineIO* io = ctx->EngineIO; + return io->ConfigCaptureEnabled; +} + +static bool ImGuiTestContext_CanCaptureVideo(ImGuiTestContext* ctx) +{ + ImGuiTestEngineIO* io = ctx->EngineIO; + return io->ConfigCaptureEnabled && ImFileExist(io->VideoCaptureEncoderPath); +} + +bool ImGuiTestContext::CaptureAddWindow(ImGuiTestRef ref) +{ + ImGuiWindow* window = GetWindowByRef(ref); + IM_CHECK_SILENT_RETV(window != NULL, false); + CaptureArgs->InCaptureWindows.push_back(window); + return true; +} + +static void CaptureInitAutoFilename(ImGuiTestContext* ctx, const char* ext) +{ + IM_ASSERT(ext != NULL && ext[0] == '.'); + + if (ctx->CaptureArgs->InOutputFile[0] == 0) + ctx->CaptureSetExtension(ext); // Reset extension of specified filename or auto-generate a new filename. +} + +bool ImGuiTestContext::CaptureScreenshot(int capture_flags) +{ + if (IsError()) + return false; + + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + LogInfo("CaptureScreenshot()"); + ImGuiCaptureArgs* args = CaptureArgs; + args->InFlags = capture_flags; + + // Auto filename + CaptureInitAutoFilename(this, ".png"); + +#if IMGUI_TEST_ENGINE_ENABLE_CAPTURE + // Way capture tool is implemented doesn't prevent ClampWindowPos() from running, + // so we disable that feature at the moment. (imgui_test_engine/#33) + ImGuiIO& io = ImGui::GetIO(); + bool backup_io_config_move_window_from_title_bar_only = io.ConfigWindowsMoveFromTitleBarOnly; + if (capture_flags & ImGuiCaptureFlags_StitchAll) + io.ConfigWindowsMoveFromTitleBarOnly = false; + + bool can_capture = ImGuiTestContext_CanCaptureScreenshot(this); + if (!can_capture) + args->InFlags |= ImGuiCaptureFlags_NoSave; + + bool ret = ImGuiTestEngine_CaptureScreenshot(Engine, args); + if (can_capture) + LogInfo("Saved '%s' (%d*%d pixels)", args->InOutputFile, (int)args->OutImageSize.x, (int)args->OutImageSize.y); + else + LogWarning("Skipped saving '%s' (%d*%d pixels) (enable in 'Misc->Options')", args->InOutputFile, (int)args->OutImageSize.x, (int)args->OutImageSize.y); + + if (capture_flags & ImGuiCaptureFlags_StitchAll) + io.ConfigWindowsMoveFromTitleBarOnly = backup_io_config_move_window_from_title_bar_only; + + return ret; +#else + IM_UNUSED(args); + LogWarning("Skipped capturing screenshot: capture disabled by IMGUI_TEST_ENGINE_ENABLE_CAPTURE=0."); + return false; +#endif +} + +void ImGuiTestContext::CaptureReset() +{ + *CaptureArgs = ImGuiCaptureArgs(); +} + +// FIXME-TESTS: Add ImGuiCaptureFlags_NoHideOtherWindows +void ImGuiTestContext::CaptureScreenshotWindow(ImGuiTestRef ref, int capture_flags) +{ + CaptureReset(); + CaptureAddWindow(ref); + CaptureScreenshot(capture_flags); +} + +void ImGuiTestContext::CaptureSetExtension(const char* ext) +{ + IM_ASSERT(ext && ext[0] == '.'); + ImGuiCaptureArgs* args = CaptureArgs; + if (args->InOutputFile[0] == 0) + { + ImFormatString(args->InOutputFile, IM_ARRAYSIZE(args->InOutputFile), "output/captures/%s_%04d%s", Test->Name, CaptureCounter, ext); + CaptureCounter++; + } + else + { + char* filename_ext = (char*)ImPathFindExtension(args->InOutputFile); + ImStrncpy(filename_ext, ext, (size_t)(filename_ext - args->InOutputFile)); + } +} + +bool ImGuiTestContext::CaptureBeginVideo() +{ + if (IsError()) + return false; + + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + LogInfo("CaptureBeginVideo()"); + ImGuiCaptureArgs* args = CaptureArgs; + + // Auto filename + CaptureInitAutoFilename(this, EngineIO->VideoCaptureExtension); + +#if IMGUI_TEST_ENGINE_ENABLE_CAPTURE + bool can_capture = ImGuiTestContext_CanCaptureVideo(this); + if (!can_capture) + args->InFlags |= ImGuiCaptureFlags_NoSave; + return ImGuiTestEngine_CaptureBeginVideo(Engine, args); +#else + IM_UNUSED(args); + LogWarning("Skipped recording GIF: capture disabled by IMGUI_TEST_ENGINE_ENABLE_CAPTURE."); + return false; +#endif +} + +bool ImGuiTestContext::CaptureEndVideo() +{ + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + LogInfo("CaptureEndVideo()"); + ImGuiCaptureArgs* args = CaptureArgs; + + bool ret = Engine->CaptureContext.IsCapturingVideo() && ImGuiTestEngine_CaptureEndVideo(Engine, args); + if (!ret) + return false; + + // In-progress capture was canceled by user. Delete incomplete file. + if (IsError()) + { + //ImFileDelete(args->OutSavedFileName); + return false; + } + bool can_capture = ImGuiTestContext_CanCaptureVideo(this); + if (can_capture) + { + LogInfo("Saved '%s' (%d*%d pixels)", args->InOutputFile, (int)args->OutImageSize.x, (int)args->OutImageSize.y); + } + else + { + if (!EngineIO->ConfigCaptureEnabled) + LogWarning("Skipped saving '%s' video because: io.ConfigCaptureEnabled == false (enable in Misc->Options)", args->InOutputFile); + else + LogWarning("Skipped saving '%s' video because: Video Encoder not found.", args->InOutputFile); + } + + return ret; +} + +// Handle wildcard search on the TestFunc side. +// Results will be resolved on the Gui side via the following call-chain: +// IMGUI_TEST_ENGINE_ITEM_INFO() -> ImGuiTestEngineHook_ItemInfo() -> ImGuiTestEngineHook_ItemInfo_ResolveFindByLabel() +ImGuiID ImGuiTestContext::ItemInfoHandleWildcardSearch(const char* wildcard_prefix_start, const char* wildcard_prefix_end, const char* wildcard_suffix_start) +{ + LogDebug("Wildcard matching.."); + + // Wildcard matching + // Note that task->InPrefixId may be 0 as well (= we don't know the window) + ImGuiTestFindByLabelTask* task = &Engine->FindByLabelTask; + if (wildcard_prefix_start < wildcard_prefix_end) + task->InPrefixId = ImHashDecoratedPath(wildcard_prefix_start, wildcard_prefix_end, RefID); + else + task->InPrefixId = RefID; + task->OutItemId = 0; + + // Advance pointer to point it to the last label + task->InSuffix = task->InSuffixLastItem = wildcard_suffix_start; + for (const char* c = task->InSuffix; *c; c++) + if (*c == '/') + task->InSuffixLastItem = c + 1; + task->InSuffixLastItemHash = ImHashStr(task->InSuffixLastItem, 0, 0); + + // Count number of labels + task->InSuffixDepth = 1; + for (const char* c = wildcard_suffix_start; *c; c++) + if (*c == '/') + task->InSuffixDepth++; + + int retries = 0; + while (retries < 2 && task->OutItemId == 0) + { + ImGuiTestEngine_Yield(Engine); + retries++; + } + + // Wildcard matching requires item to be visible, because clipped items are unaware of their labels. Try panning through entire window, searching for target item. + // (Scrollbar position restoration in theory may be desirable, however it interferes with typical use of found item) + // FIXME-TESTS: This doesn't recurse properly into each child.. + // FIXME: Down the line if we refactor ItemAdd() return value to distinguish render-clipping vs logic-clipping etc, we should instead temporarily enable a "no clip" + // mode without the need for scrolling. + if (task->OutItemId == 0) + { + ImGuiTestItemInfo* base_item = ItemInfo(task->InPrefixId, ImGuiTestOpFlags_NoError); + ImGuiWindow* window = (base_item->ID != 0) ? base_item->Window : GetWindowByRef(task->InPrefixId); + if (window) + { + ImVec2 rect_size = window->InnerRect.GetSize(); + for (float scroll_x = 0.0f; task->OutItemId == 0; scroll_x += rect_size.x) + { + for (float scroll_y = 0.0f; task->OutItemId == 0; scroll_y += rect_size.y) + { + window->Scroll.x = scroll_x; + window->Scroll.y = scroll_y; + + retries = 0; + while (retries < 2 && task->OutItemId == 0) + { + ImGuiTestEngine_Yield(Engine); + retries++; + } + if (window->Scroll.y >= window->ScrollMax.y) + break; + } + if (window->Scroll.x >= window->ScrollMax.x) + break; + } + } + } + ImGuiID full_id = task->OutItemId; + + // FIXME: InFilterItemStatusFlags is intentionally not cleared here, because it is set in ItemAction() and reused in later calls to ItemInfo() to resolve ambiguities. + task->InPrefixId = 0; + task->InSuffix = task->InSuffixLastItem = NULL; + task->InSuffixLastItemHash = 0; + task->InSuffixDepth = 0; + task->OutItemId = 0; // -V1048 // Variable 'OutItemId' was assigned the same value. False-positive, because value of OutItemId could be modified from other thread during ImGuiTestEngine_Yield() call. + + return full_id; +} + +// Return an empty instance so ItemInfo() never returns a NULL pointer by default (unless requested) +ImGuiTestItemInfo* ImGuiTestContext::ItemInfoNull() +{ + DummyItemInfoNull = ImGuiTestItemInfo(); + return &DummyItemInfoNull; +} + +static void ItemInfoErrorLog(ImGuiTestContext* ctx, ImGuiTestRef ref, ImGuiID full_id, ImGuiTestOpFlags flags) +{ + if (flags & ImGuiTestOpFlags_NoError) + return; + + // Prefixing the string with / ignore the reference/current ID + Str256 msg; + if (ref.Path && ref.Path[0] == '/' && ctx->RefStr[0] != 0) + msg.setf("Unable to locate item: '%s'", ref.Path); + else if (ref.Path && full_id != 0) + msg.setf("Unable to locate item: '%s/%s' (0x%08X)", ctx->RefStr, ref.Path, full_id); + else if (ref.Path) + msg.setf("Unable to locate item: '%s/%s'", ctx->RefStr, ref.Path); + else + msg.setf("Unable to locate item: 0x%08X", ref.ID); + + //if (flags & ImGuiTestOpFlags_NoError) + // ctx->LogInfo("Ignored: %s", msg.c_str()); // FIXME + //else + IM_ERRORF_NOHDR("%s", msg.c_str()); +} + +// Supported values for ImGuiTestOpFlags: +// - ImGuiTestOpFlags_NoError +ImGuiTestItemInfo* ImGuiTestContext::ItemInfo(ImGuiTestRef ref, ImGuiTestOpFlags flags) +{ + if (IsError()) + return ItemInfoNull(); + + ImGuiID full_id = 0; + + if (const char* p = ref.Path ? strstr(ref.Path, "**/") : NULL) + { + // Wildcard matching + // FIXME-TESTS: Need to verify that this is not inhibited by a \, so \**/ should not pass, but \\**/ should :) + // We could add a simple helpers that would iterate the strings, handling inhibitors, and let you check if a given characters is inhibited or not. + const char* wildcard_prefix_start = ref.Path; + const char* wildcard_prefix_end = p; + const char* wildcard_suffix_start = wildcard_prefix_end + 3; + full_id = ItemInfoHandleWildcardSearch(wildcard_prefix_start, wildcard_prefix_end, wildcard_suffix_start); + } + else + { + // Regular matching + full_id = GetID(ref); + } + + // If ui_ctx->TestEngineHooksEnabled is not already on (first ItemInfo() task in a while) we'll probably need an extra frame to warmup + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + ImGuiTestItemInfo* item = NULL; + int retries = 0; + int max_retries = 2; + int extra_retries_for_appearing = 0; + while (full_id && retries < max_retries) + { + item = ImGuiTestEngine_FindItemInfo(Engine, full_id, ref.Path); + + // While a window is appearing it is likely to be resizing and items moving. Wait an extra frame for things to settle. (FIXME: Could use another source e.g. Hidden? AutoFitFramesX?) + if (item && item->Window && item->Window->Appearing && extra_retries_for_appearing == 0) + { + item = NULL; + max_retries++; + extra_retries_for_appearing++; + } + + if (item) + return item; + ImGuiTestEngine_Yield(Engine); + retries++; + } + + ItemInfoErrorLog(this, ref, full_id, flags); + + return ItemInfoNull(); +} + +// Supported values for ImGuiTestOpFlags: +// - ImGuiTestOpFlags_NoError +ImGuiTestItemInfo* ImGuiTestContext::ItemInfoOpenFullPath(ImGuiTestRef ref, ImGuiTestOpFlags flags) +{ + // First query + bool can_open_full_path = (ref.Path != NULL); + ImGuiTestItemInfo* item = ItemInfo(ref, (can_open_full_path ? ImGuiTestOpFlags_NoError : ImGuiTestOpFlags_None) | (flags & ImGuiTestOpFlags_NoError)); + if (item->ID != 0) + return item; + if (!can_open_full_path) + return ItemInfoNull(); + + // Tries to auto open intermediaries leading to final path. + // Note that openables cannot be part of the **/ (else it means we would have to open everything). + // - Openables can be before the wildcard "Node2/Node3/**/Button" + // - Openables can be after the wildcard "**/Node2/Node3/Lv4/Button" + int opened_parents = 0; + for (const char* parent_end = strstr(ref.Path, "/"); parent_end != NULL; parent_end = strstr(parent_end + 1, "/")) + { + // Skip "**/* sections + if (strncmp(ref.Path, "**/", parent_end - ref.Path) == 0) + continue; + + Str128 parent_id; + parent_id.set(ref.Path, parent_end); + ImGuiTestItemInfo* parent_item = ItemInfo(parent_id.c_str(), ImGuiTestOpFlags_NoError); + if (parent_item->ID != 0) + { +#ifdef IMGUI_HAS_DOCK + ImGuiWindow* parent_window = parent_item->Window; +#endif + if ((parent_item->StatusFlags & ImGuiItemStatusFlags_Openable) != 0 && (parent_item->StatusFlags & ImGuiItemStatusFlags_Opened) == 0) + { + // Open intermediary item + if ((parent_item->InFlags & ImGuiItemFlags_Disabled) == 0) // FIXME: Report disabled state in log? + { + ItemAction(ImGuiTestAction_Open, parent_item->ID, ImGuiTestOpFlags_NoAutoOpenFullPath); + opened_parents++; + } + } +#ifdef IMGUI_HAS_DOCK + else if (parent_window->ID == parent_item->ID && parent_window->DockIsActive && parent_window->DockTabIsVisible == false) + { + // Make tab visible + ItemClick(parent_item->ID); + opened_parents++; + } +#endif + } + } + if (opened_parents > 0) + item = ItemInfo(ref, (flags & ImGuiTestOpFlags_NoError)); + + if (item->ID == 0) + ItemInfoErrorLog(this, ref, 0, flags); + + return item; +} + +// Find a window given a path or an ID. +// In the case of when a path is passed, this handle finding child windows as well. +// e.g. +// ctx->WindowInfo("//Test Window"); // OK +// ctx->WindowInfo("//Test Window/Child/SubChild"); // OK +// ctx->WindowInfo("//$FOCUSED/Child"); // OK +// ctx->SetRef("Test Window); ctx->WindowInfo("Child"); // OK +// ctx->WindowInfo(GetID("//Test Window")); // OK (find by raw ID without a path) +// ctx->WindowInfo(GetID("//Test Window/Child/SubChild)); // *INCORRECT* GetID() doesn't unmangle child names. +// ctx->WindowInfo("//Test Window/Button"); // *INCORRECT* Only finds windows, not items. +// Return: +// - Return pointer is always valid. +// - Valid fields are: +// - item->ID : window ID (may be == 0, if the window doesn't exist) +// - item->Window : window pointer (may be == NULL, if the window doesn't exist) +// - Other fields correspond to the title-bar/tab item of a window, so likely not what you want (same as using IsItemXXX after Begin) +// - If you want other fields simply get them via the window-> pointer. +// - Likely you may want to feed the return value into SetRef(): e.g. 'ctx->SetRef(item->ID)' or 'ctx->SetRef(WindowInfo("//Window/Child")->ID);' +// Todos: +// - FIXME: Missing support for wildcards. +ImGuiTestItemInfo* ImGuiTestContext::WindowInfo(ImGuiTestRef ref, ImGuiTestOpFlags flags) +{ + if (IsError()) + return ItemInfoNull(); + + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + ImGuiTestVerboseLevel log_level = (flags & ImGuiTestOpFlags_NoError) ? ImGuiTestVerboseLevel_Info : ImGuiTestVerboseLevel_Error; + + // Query by ID (not very useful but supported) + if (ref.ID != 0) + { + LogDebug("WindowInfo: by id: %08X", ref.ID); + IM_ASSERT(ref.Path == NULL); + ImGuiWindow* window = GetWindowByRef(ref); + if (window == NULL) + { + LogEx(log_level, 0, "WindowInfo: error: cannot find window by ID!"); // FIXME: What if we want to query a not-yet-existing window by ID? + return ItemInfoNull(); + } + return ItemInfo(window->ID); + } + + // Query by Path: this is where the meat of our work is. + LogDebug("WindowInfo: by path: '%s'", ref.Path ? ref.Path : "NULL"); + ImGuiWindow* window = NULL; + ImGuiID window_idstack_back = 0; + const char* current = ref.Path; + while (*current || window == NULL) + { + // Handle SetRef(), if any (this will also handle "//$FOCUSED" syntax) + Str128 part_name; + if (window == NULL && RefID != 0 && strncmp(ref.Path, "//", 2) != 0) + { + window = GetWindowByRef(""); + window_idstack_back = window ? window->ID : 0; + } + else + { + // Find next part of the path + create a zero-terminated copy for convenience + const char* part_start = current; + const char* part_end = ImFindNextDecoratedPartInPath(current); + if (part_end == NULL) + { + current = part_end = part_start + strlen(part_start); + } + else if (part_end > part_start) + { + current = part_end; + part_end--; + IM_ASSERT(part_end[0] == '/'); + } + part_name.setf("%.*s", (int)(part_end - part_start), part_start); + + // Find root window or child window + if (window == NULL) + { + // Root: defer first element to GetID(), this will handle SetRef(), "//" and "//$FOCUSED" syntax. + ImGuiID window_id = GetID(part_name.c_str()); + window = GetWindowByRef(window_id); + window_idstack_back = window ? window->ID : 0; + } + else + { + ImGuiID child_window_id = 0; + ImGuiWindow* child_window = NULL; + { + // Child: Attempt 1: Try to BeginChild(const char*) variant and mimic its logic. + Str128 child_window_full_name; +#if (IMGUI_VERSION_NUM >= 18996) && (IMGUI_VERSION_NUM < 18999) + if (window_idstack_back == window->ID) + { + child_window_full_name.setf("%s/%s", window->Name, part_name.c_str()); + } + else +#endif + { + ImGuiID child_item_id = GetID(part_name.c_str(), window_idstack_back); + child_window_full_name.setf("%s/%s_%08X", window->Name, part_name.c_str(), child_item_id); + } + child_window_id = ImHashStr(child_window_full_name.c_str()); // We do NOT use ImHashDecoratedPath() + child_window = GetWindowByRef(child_window_id); + } + if (child_window == NULL) + { + // Child: Attempt 2: Try for BeginChild(ImGuiID id) variant and mimic its logic. + // FIXME: This only really works when ID passed to BeginChild() was derived from a string. + // We could support $$xxxx syntax to encode ID in parameter? + ImGuiID child_item_id = GetID(part_name.c_str(), window_idstack_back); + Str128f child_window_full_name("%s/%08X", window->Name, child_item_id); + child_window_id = ImHashStr(child_window_full_name.c_str()); // We do NOT use ImHashDecoratedPath() + child_window = GetWindowByRef(child_window_id); + } + if (child_window == NULL) + { + // Assume that part is an arbitrary PushID(const char*) + window_idstack_back = GetID(part_name.c_str(), window_idstack_back); + } + else + { + window = child_window; + window_idstack_back = window ? window->ID : 0; + } + } + } + + // Process result + // FIXME: What if we want to query a not-yet-existing window by ID? + if (window == NULL) + { + LogEx(log_level, 0, "WindowInfo: error: element \"%s\" doesn't seem to exist.", part_name.c_str()); + return ItemInfoNull(); + } + } + + IM_ASSERT(window != NULL); + IM_ASSERT(window_idstack_back != 0); + + // Stopped on "window/node/" + if (window_idstack_back != 0 && window_idstack_back != window->ID) + { + LogEx(log_level, 0, "WindowInfo: error: element doesn't seem to exist or isn't a window."); + return ItemInfoNull(); + } + + return ItemInfo(window->ID); +} + +void ImGuiTestContext::ScrollToTop(ImGuiTestRef ref) +{ + if (IsError()) + return; + + ImGuiWindow* window = GetWindowByRef(ref); + IM_CHECK_SILENT(window != NULL); + if (window->Scroll.y == 0.0f) + return; + ScrollToY(ref, 0.0f); + Yield(); +} + +void ImGuiTestContext::ScrollToBottom(ImGuiTestRef ref) +{ + if (IsError()) + return; + + ImGuiWindow* window = GetWindowByRef(ref); + IM_CHECK_SILENT(window != NULL); + if (window->Scroll.y == window->ScrollMax.y) + return; + ScrollToY(ref, window->ScrollMax.y); + Yield(); +} + +bool ImGuiTestContext::ScrollErrorCheck(ImGuiAxis axis, float expected, float actual, int* remaining_attempts) +{ + if (IsError()) + { + (*remaining_attempts)--; + return false; + } + + float THRESHOLD = 1.0f; + if (ImFabs(actual - expected) < THRESHOLD) + return true; + + (*remaining_attempts)--; + if (*remaining_attempts > 0) + { + LogInfo("Failed to set Scroll%c. Requested %.2f, got %.2f. Will try again.", 'X' + axis, expected, actual); + return true; + } + else + { + IM_ERRORF("Failed to set Scroll%c. Requested %.2f, got %.2f. Aborting.", 'X' + axis, expected, actual); + return false; + } +} + +// FIXME-TESTS: Mostly the same code as ScrollbarEx() +static ImVec2 GetWindowScrollbarMousePositionForScroll(ImGuiWindow* window, ImGuiAxis axis, float scroll_v) +{ + ImGuiContext& g = *GImGui; + ImRect bb = ImGui::GetWindowScrollbarRect(window, axis); + + // From Scrollbar(): + //float* scroll_v = &window->Scroll[axis]; + const float size_avail_v = window->InnerRect.Max[axis] - window->InnerRect.Min[axis]; + const float size_contents_v = window->ContentSize[axis] + window->WindowPadding[axis] * 2.0f; + + // From ScrollbarEx() onward: + + // V denote the main, longer axis of the scrollbar (= height for a vertical scrollbar) + const float scrollbar_size_v = bb.Max[axis] - bb.Min[axis]; + + // Calculate the height of our grabbable box. It generally represent the amount visible (vs the total scrollable amount) + // But we maintain a minimum size in pixel to allow for the user to still aim inside. + const float win_size_v = ImMax(ImMax(size_contents_v, size_avail_v), 1.0f); + const float grab_h_pixels = ImClamp(scrollbar_size_v * (size_avail_v / win_size_v), g.Style.GrabMinSize, scrollbar_size_v); + + const float scroll_max = ImMax(1.0f, size_contents_v - size_avail_v); + const float scroll_ratio = ImSaturate(scroll_v / scroll_max); + const float grab_v = scroll_ratio * (scrollbar_size_v - grab_h_pixels); // Grab position + + ImVec2 position; + position[axis] = bb.Min[axis] + grab_v + grab_h_pixels * 0.5f; + position[axis ^ 1] = bb.GetCenter()[axis ^ 1]; + + return position; +} + +#if IMGUI_VERSION_NUM < 18993 +#define ImTrunc ImFloor +#endif + +// Supported values for ImGuiTestOpFlags: +// - ImGuiTestOpFlags_NoFocusWindow +void ImGuiTestContext::ScrollTo(ImGuiTestRef ref, ImGuiAxis axis, float scroll_target, ImGuiTestOpFlags flags) +{ + ImGuiContext& g = *UiContext; + if (IsError()) + return; + + ImGuiWindow* window = GetWindowByRef(ref); + IM_CHECK_SILENT(window != NULL); + + // Early out + const float scroll_target_clamp = ImClamp(scroll_target, 0.0f, window->ScrollMax[axis]); + if (ImFabs(window->Scroll[axis] - scroll_target_clamp) < 1.0f) + return; + + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + const char axis_c = (char)('X' + axis); + LogDebug("ScrollTo %c %.1f/%.1f", axis_c, scroll_target, window->ScrollMax[axis]); + + if (EngineIO->ConfigRunSpeed == ImGuiTestRunSpeed_Cinematic) + SleepStandard(); + + // Try to use Scrollbar if available + const ImGuiTestItemInfo* scrollbar_item = ItemInfo(ImGui::GetWindowScrollbarID(window, axis), ImGuiTestOpFlags_NoError); + if (scrollbar_item->ID != 0 && EngineIO->ConfigRunSpeed != ImGuiTestRunSpeed_Fast && !(flags & ImGuiTestOpFlags_NoFocusWindow)) + { + WindowFocus(window->ID); + + const ImRect scrollbar_rect = ImGui::GetWindowScrollbarRect(window, axis); + const float scrollbar_size_v = scrollbar_rect.Max[axis] - scrollbar_rect.Min[axis]; + const float window_resize_grip_size = ImTrunc(ImMax(g.FontSize * 1.35f, window->WindowRounding + 1.0f + g.FontSize * 0.2f)); + + // In case of a very small window, directly use SetScrollX/Y function to prevent resizing it + // FIXME-TESTS: GetWindowScrollbarMousePositionForScroll doesn't return the exact value when scrollbar grip is too small + if (scrollbar_size_v >= window_resize_grip_size) + { + MouseSetViewport(window); + + const float scroll_src = window->Scroll[axis]; + ImVec2 scrollbar_src_pos = GetWindowScrollbarMousePositionForScroll(window, axis, scroll_src); + scrollbar_src_pos[axis] = ImMin(scrollbar_src_pos[axis], scrollbar_rect.Min[axis] + scrollbar_size_v - window_resize_grip_size); + MouseMoveToPos(scrollbar_src_pos); + MouseDown(0); + SleepStandard(); + + ImVec2 scrollbar_dst_pos = GetWindowScrollbarMousePositionForScroll(window, axis, scroll_target_clamp); + MouseMoveToPos(scrollbar_dst_pos); + MouseUp(0); + SleepStandard(); + + // Verify that things worked + const float scroll_result = window->Scroll[axis]; + if (ImFabs(scroll_result - scroll_target_clamp) < 1.0f) + return; + + // FIXME-TESTS: Investigate + LogWarning("Failed to set Scroll%c. Requested %.2f, got %.2f.", 'X' + axis, scroll_target_clamp, scroll_result); + } + } + + // Fallback: manual slow scroll + // FIXME-TESTS: Consider using mouse wheel, since it can work without taking focus + int remaining_failures = 3; + while (!Abort) + { + if (ImFabs(window->Scroll[axis] - scroll_target_clamp) < 1.0f) + break; + + const float scroll_speed = (EngineIO->ConfigRunSpeed == ImGuiTestRunSpeed_Fast) ? FLT_MAX : ImFloor(EngineIO->ScrollSpeed * g.IO.DeltaTime + 0.99f); + const float scroll_next = ImLinearSweep(window->Scroll[axis], scroll_target, scroll_speed); + if (axis == ImGuiAxis_X) + ImGui::SetScrollX(window, scroll_next); + else + ImGui::SetScrollY(window, scroll_next); + + // Error handling to avoid getting stuck in this function. + Yield(); + if (!ScrollErrorCheck(axis, scroll_next, window->Scroll[axis], &remaining_failures)) + break; + } + + // Need another frame for the result->Rect to stabilize + Yield(); +} + +// Supported values for ImGuiTestOpFlags: +// - ImGuiTestOpFlags_NoFocusWindow +void ImGuiTestContext::ScrollToItem(ImGuiTestRef ref, ImGuiAxis axis, ImGuiTestOpFlags flags) +{ + if (IsError()) + return; + + // If the item is not currently visible, scroll to get it in the center of our window + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + ImGuiTestItemInfo* item = ItemInfo(ref); + ImGuiTestRefDesc desc(ref, item); + LogDebug("ScrollToItem %c %s", 'X' + axis, desc.c_str()); + + if (item->ID == 0) + return; + + // Ensure window size and ScrollMax are up-to-date + Yield(); + + // TabBar are a special case because they have no scrollbar and rely on ScrollButton "<" and ">" + // FIXME-TESTS: Consider moving to its own function. + ImGuiContext& g = *UiContext; + if (axis == ImGuiAxis_X) + if (ImGuiTabBar* tab_bar = g.TabBars.GetByKey(item->ParentID)) + if (tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyScroll) + { + ScrollToTabItem(tab_bar, item->ID); + return; + } + + ImGuiWindow* window = item->Window; + float item_curr = ImFloor(item->RectFull.GetCenter()[axis]); + float item_target = ImFloor(window->InnerClipRect.GetCenter()[axis]); + float scroll_delta = item_target - item_curr; + float scroll_target = ImClamp(window->Scroll[axis] - scroll_delta, 0.0f, window->ScrollMax[axis]); + + ScrollTo(window->ID, axis, scroll_target, (flags & ImGuiTestOpFlags_NoFocusWindow)); +} + +void ImGuiTestContext::ScrollToItemX(ImGuiTestRef ref) +{ + ScrollToItem(ref, ImGuiAxis_X); +} + +void ImGuiTestContext::ScrollToItemY(ImGuiTestRef ref) +{ + ScrollToItem(ref, ImGuiAxis_Y); +} + +void ImGuiTestContext::ScrollToTabItem(ImGuiTabBar* tab_bar, ImGuiID tab_id) +{ + if (IsError()) + return; + + // Cancel if "##v", because it's outside the tab_bar rect, and will be considered as "not visible" even if it is! + //if (GetID("##v") == item->ID) + // return; + + IM_CHECK_SILENT(tab_bar != NULL); + const ImGuiTabItem* selected_tab_item = ImGui::TabBarFindTabByID(tab_bar, tab_bar->SelectedTabId); + const ImGuiTabItem* target_tab_item = ImGui::TabBarFindTabByID(tab_bar, tab_id); + if (target_tab_item == NULL) + return; + + int selected_tab_index = tab_bar->Tabs.index_from_ptr(selected_tab_item); + int target_tab_index = tab_bar->Tabs.index_from_ptr(target_tab_item); + + ImGuiTestRef backup_ref = GetRef(); + SetRef(tab_bar->ID); + + if (selected_tab_index > target_tab_index) + { + MouseMove("##<"); + for (int i = 0; i < selected_tab_index - target_tab_index; ++i) + MouseClick(0); + } + else + { + MouseMove("##>"); + for (int i = 0; i < target_tab_index - selected_tab_index; ++i) + MouseClick(0); + } + + // Skip the scroll animation + if (EngineIO->ConfigRunSpeed == ImGuiTestRunSpeed_Fast) + { + tab_bar->ScrollingAnim = tab_bar->ScrollingTarget; + Yield(); + } + + SetRef(backup_ref); +} + +// Verify that ScrollMax is stable regardless of scrolling position +// - This can break when the layout of clipped items doesn't match layout of unclipped items +// - This can break with non-rounded calls to ItemSize(), namely when the starting position is negative (above visible area) +// We should ideally be more tolerant of non-rounded sizes passed by the users. +// - One of the net visible effect of an unstable ScrollMax is that the End key would put you at a spot that's not exactly the lowest spot, +// and so a second press to End would you move again by a few pixels. +// FIXME-TESTS: Make this an iterative, smooth scroll. +void ImGuiTestContext::ScrollVerifyScrollMax(ImGuiTestRef ref) +{ + ImGuiWindow* window = GetWindowByRef(ref); + ImGui::SetScrollY(window, 0.0f); + Yield(); + float scroll_max_0 = window->ScrollMax.y; + ImGui::SetScrollY(window, window->ScrollMax.y); + Yield(); + float scroll_max_1 = window->ScrollMax.y; + IM_CHECK_EQ(scroll_max_0, scroll_max_1); +} + +void ImGuiTestContext::NavMoveTo(ImGuiTestRef ref) +{ + if (IsError()) + return; + + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + ImGuiContext& g = *UiContext; + ImGuiTestItemInfo* item = ItemInfo(ref); + ImGuiTestRefDesc desc(ref, item); + LogDebug("NavMove to %s", desc.c_str()); + + if (item->ID == 0) + return; + item->RefCount++; + + if (EngineIO->ConfigRunSpeed == ImGuiTestRunSpeed_Cinematic) + SleepStandard(); + + // Focus window before scrolling/moving so things are nicely visible + WindowFocus(item->Window->ID); + + // Teleport + // FIXME-NAV: We should have a nav request feature that does this, + // except it'll have to queue the request to find rect, then set scrolling, which would incur a 2 frame delay :/ + // FIXME-TESTS-NOT_SAME_AS_END_USER + IM_ASSERT(g.NavMoveSubmitted == false); + ImRect rect_rel = item->RectFull; + rect_rel.Translate(ImVec2(-item->Window->Pos.x, -item->Window->Pos.y)); + ImGui::SetNavID(item->ID, (ImGuiNavLayer)item->NavLayer, 0, rect_rel); + g.NavDisableHighlight = false; + g.NavDisableMouseHover = g.NavMousePosDirty = true; + ImGui::ScrollToBringRectIntoView(item->Window, item->RectFull); + while (g.NavMoveSubmitted) + Yield(); + Yield(); + + if (!Abort) + { + if (g.NavId != item->ID) + IM_ERRORF_NOHDR("Unable to set NavId to %s", desc.c_str()); + } + + item->RefCount--; +} + +void ImGuiTestContext::NavActivate() +{ + if (IsError()) + return; + + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + LogDebug("NavActivate"); + Yield(); // ? + KeyPress(ImGuiKey_Space); +} + +void ImGuiTestContext::NavInput() +{ + if (IsError()) + return; + + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + LogDebug("NavInput"); + KeyPress(ImGuiKey_Enter); +} + +// Supported values for ImGuiTestOpFlags: +// - ImGuiTestOpFlags_MoveToEdgeL +// - ImGuiTestOpFlags_MoveToEdgeR +// - ImGuiTestOpFlags_MoveToEdgeU +// - ImGuiTestOpFlags_MoveToEdgeD +static ImVec2 GetMouseAimingPos(ImGuiTestItemInfo* item, ImGuiTestOpFlags flags) +{ + ImRect r = item->RectClipped; + ImVec2 pos; + if (flags & ImGuiTestOpFlags_MoveToEdgeL) + pos.x = (r.Min.x + 1.0f); + else if (flags & ImGuiTestOpFlags_MoveToEdgeR) + pos.x = (r.Max.x - 1.0f); + else + pos.x = (r.Min.x + r.Max.x) * 0.5f; + if (flags & ImGuiTestOpFlags_MoveToEdgeU) + pos.y = (r.Min.y + 1.0f); + else if (flags & ImGuiTestOpFlags_MoveToEdgeD) + pos.y = (r.Max.y - 1.0f); + else + pos.y = (r.Min.y + r.Max.y) * 0.5f; + return pos; +} + +// Conceptucally this could be called ItemHover() +// Supported values for ImGuiTestOpFlags: +// - ImGuiTestOpFlags_NoFocusWindow +// - ImGuiTestOpFlags_NoCheckHoveredId +// - ImGuiTestOpFlags_IsSecondAttempt [used when recursively calling ourself) +// - ImGuiTestOpFlags_MoveToEdgeXXX flags +// FIXME-TESTS: This is too eagerly trying to scroll everything even if already visible. +// FIXME: Maybe ImGuiTestOpFlags_NoCheckHoveredId could be automatic if we detect that another item is active as intended? +void ImGuiTestContext::MouseMove(ImGuiTestRef ref, ImGuiTestOpFlags flags) +{ + if (IsError()) + return; + + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + ImGuiContext& g = *UiContext; + + ImGuiTestItemInfo* item; + if (flags & ImGuiTestOpFlags_NoAutoOpenFullPath) + item = ItemInfo(ref); + else + item = ItemInfoOpenFullPath(ref); + + ImGuiTestRefDesc desc(ref, item); + LogDebug("MouseMove to %s", desc.c_str()); + if (item->ID == 0) + return; + + if (!item->Window->WasActive) + { + LogError("Window '%s' is not active!", item->Window->Name); + return; + } + + item->RefCount++; + + // FIXME-TESTS: If window was not brought to front (because of either ImGuiWindowFlags_NoBringToFrontOnFocus or ImGuiTestOpFlags_NoFocusWindow) + // then we need to make space by moving other windows away. + // An easy to reproduce this bug is to run "docking_dockspace_tab_amend" with Test Engine UI over top-left corner, covering the Tools menu. + + // Check visibility and scroll if necessary + ImGuiWindow* window = item->Window; + if (item->NavLayer == ImGuiNavLayer_Main) + { + ImRect window_r = window->InnerClipRect; + window_r.Expand(ImVec2(-g.WindowsHoverPadding.x, -g.WindowsHoverPadding.y)); + + ImRect item_r_clipped; + item_r_clipped.Min.x = ImClamp(item->RectFull.Min.x, window_r.Min.x, window_r.Max.x); + item_r_clipped.Min.y = ImClamp(item->RectFull.Min.y, window_r.Min.y, window_r.Max.y); + item_r_clipped.Max.x = ImClamp(item->RectFull.Max.x, window_r.Min.x, window_r.Max.x); + item_r_clipped.Max.y = ImClamp(item->RectFull.Max.y, window_r.Min.y, window_r.Max.y); + + // In theory all we need is one visible point, but it is generally nicer if we scroll toward visibility. + // Bias toward reducing amount of horizontal scroll. + float visibility_ratio_x = (item_r_clipped.GetWidth() + 1.0f) / (item->RectFull.GetWidth() + 1.0f); + float visibility_ratio_y = (item_r_clipped.GetHeight() + 1.0f) / (item->RectFull.GetHeight() + 1.0f); + if (visibility_ratio_x < 0.70f) + ScrollToItem(ref, ImGuiAxis_X, ImGuiTestOpFlags_NoFocusWindow); + if (visibility_ratio_y < 0.90f) + ScrollToItem(ref, ImGuiAxis_Y, ImGuiTestOpFlags_NoFocusWindow); + } + else + { + // Menu layer is not scrollable: attempt to resize window. + // FIXME-TESTS: ImGuiItemStatusFlags_Visible is currently not usable for test engine as it relies on ITEM_INFO hook, need moving in ItemAdd(). + //if ((item->StatusFlags & ImGuiItemStatusFlags_Visible) == 0) + { + // FIXME-TESTS: We designed RectClipped as being within RectFull which is not what we want here. Approximate using window's Max.x + ImRect window_r = window->Rect(); + if (item->RectFull.Min.x > window_r.Max.x) + { + float extra_width_desired = item->RectFull.Max.x - window_r.Max.x; // item->RectClipped.Max.x; + if (extra_width_desired > 0.0f && (flags & ImGuiTestOpFlags_IsSecondAttempt) == 0) + { + LogDebug("Will attempt to resize window to make item in menu layer visible."); + WindowResize(window->ID, window->Size + ImVec2(extra_width_desired, 0.0f)); + } + } + } + } + + // FIXME-TESTS-NOT_SAME_AS_END_USER + ImVec2 pos = item->RectFull.GetCenter(); + WindowTeleportToMakePosVisible(window->ID, pos); + + // Keep a deep copy of item info since item-> will be kept updated as we set a RefCount on it. + ImGuiTestItemInfo item_initial_state = *item; + + // Target point + pos = GetMouseAimingPos(item, flags); + + // Focus window + if (!(flags & ImGuiTestOpFlags_NoFocusWindow)) + { + // Avoid unnecessary focus + // While this is generally desirable and much more consistent with user behavior, + // it make test-engine behavior a little less deterministic. + // Incorrectly written tests could possibly succeed or fail based on position of other windows. + bool is_covered = FindHoveredWindowAtPos(pos) != item->Window; + bool is_inhibited = ImGui::IsWindowContentHoverable(item->Window) == false; + + // FIXME-TESTS-NOT_SAME_AS_END_USER: This has too many side effect, could we do without? + // - e.g. This can close a modal. + if (is_covered || is_inhibited) + WindowBringToFront(item->Window->ID); + } + + // Another is window active test (in the case focus change has a side effect but also as we have yield an extra frame) + if (!item->Window->WasActive) + { + LogError("Window '%s' is not active (after aiming)", item->Window->Name); + return; + } + + MouseSetViewport(item->Window); + MouseMoveToPos(pos); + + // Focus again in case something made us lost focus (which could happen on a simple hover) + if (!(flags & ImGuiTestOpFlags_NoFocusWindow)) + { + // Avoid unnecessary focus + bool is_covered = FindHoveredWindowAtPos(pos) != item->Window; + bool is_inhibited = ImGui::IsWindowContentHoverable(item->Window) == false; + + if (is_covered || is_inhibited) + WindowBringToFront(window->ID); + } + + // Check hovering target: may be an item (common) or a window (rare) + if (!Abort && !(flags & ImGuiTestOpFlags_NoCheckHoveredId)) + { + ImGuiID hovered_id; + bool is_hovered_item; + + // Give a few extra frames to validate hovering. + // In the vast majority of case this will be set on the first attempt, + // but e.g. blocking popups may need to close based on external logic. + for (int remaining_attempts = 3; remaining_attempts > 0; remaining_attempts--) + { + hovered_id = g.HoveredIdPreviousFrame; + is_hovered_item = (hovered_id == item->ID); + if (is_hovered_item) + break; + Yield(); + } + + bool is_hovered_window = is_hovered_item ? true : false; + if (!is_hovered_item) + for (ImGuiWindow* hovered_window = g.HoveredWindow; hovered_window != NULL && !is_hovered_window; hovered_window = hovered_window->ParentWindow) + if (hovered_window->ID == item->ID && hovered_window == item->Window) + is_hovered_window = true; + + if (!is_hovered_item && !is_hovered_window) + { + // Check if we are accidentally hovering resize grip (which uses ImGuiButtonFlags_FlattenChildren) + if (!(window->Flags & ImGuiWindowFlags_NoResize) && !(flags & ImGuiTestOpFlags_IsSecondAttempt)) + { + bool is_hovering_resize_corner = false; + for (int n = 0; n < 2; n++) + is_hovering_resize_corner |= (hovered_id == ImGui::GetWindowResizeCornerID(window, n)); + if (is_hovering_resize_corner) + { + LogDebug("Child obstructed by parent's ResizeGrip, trying to resize window and trying again.."); + float extra_size = window->CalcFontSize() * 3.0f; + WindowResize(window->ID, window->Size + ImVec2(extra_size, extra_size)); + MouseMove(ref, flags | ImGuiTestOpFlags_IsSecondAttempt); + item->RefCount--; + return; + } + } + + ImVec2 pos_old = item_initial_state.RectFull.Min; + ImVec2 pos_new = item->RectFull.Min; + ImVec2 size_old = item_initial_state.RectFull.GetSize(); + ImVec2 size_new = item->RectFull.GetSize(); + Str256f error_message( + "Unable to Hover %s:\n" + "- Expected item %08X in window '%s', targeted position: (%.1f,%.1f)'\n" + "- Hovered id was %08X in '%s'.\n" + "- Item Pos: Before mouse move (%6.1f,%6.1f) vs Now (%6.1f,%6.1f) (%s)\n" + "- Item Size: Before mouse move (%6.1f,%6.1f) vs Now (%6.1f,%6.1f) (%s)", + desc.c_str(), + item->ID, item->Window ? item->Window->Name : "", pos.x, pos.y, + hovered_id, g.HoveredWindow ? g.HoveredWindow->Name : "", + pos_old.x, pos_old.y, pos_new.x, pos_new.y, (pos_old.x == pos_new.x && pos_old.y == pos_new.y) ? "Same" : "Changed", + size_old.x, size_old.y, size_new.x, size_new.y, (size_old.x == size_new.x && size_old.y == size_new.y) ? "Same" : "Changed"); + IM_ERRORF_NOHDR("%s", error_message.c_str()); + } + } + + item->RefCount--; +} + +void ImGuiTestContext::MouseSetViewport(ImGuiWindow* window) +{ + IM_CHECK_SILENT(window != NULL); +#ifdef IMGUI_HAS_VIEWPORT + ImGuiViewportP* viewport = window ? window->Viewport : NULL; + ImGuiID viewport_id = viewport ? viewport->ID : 0; + if (window->Viewport == NULL) + IM_CHECK(window->WasActive == false); // only time this is allowed is an inactive window (where the viewport was destroyed) + if (Inputs->MouseHoveredViewport != viewport_id) + { + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + LogDebug("MouseSetViewport changing to 0x%08X (window '%s')", viewport_id, window->Name); + Inputs->MouseHoveredViewport = viewport_id; + Yield(2); + } +#else + IM_UNUSED(window); +#endif +} + +// May be 0 to specify "automatic" (based on platform stack, rarely used) +void ImGuiTestContext::MouseSetViewportID(ImGuiID viewport_id) +{ + if (IsError()) + return; + + if (Inputs->MouseHoveredViewport != viewport_id) + { + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + LogDebug("MouseSetViewportID changing to 0x%08X", viewport_id); + Inputs->MouseHoveredViewport = viewport_id; + ImGuiTestEngine_Yield(Engine); + } +} + +// Make the point at 'pos' (generally expected to be within window's boundaries) visible in the viewport, +// so it can be later focused then clicked. +bool ImGuiTestContext::WindowTeleportToMakePosVisible(ImGuiTestRef ref, ImVec2 pos) +{ + ImGuiContext& g = *UiContext; + if (IsError()) + return false; + ImGuiWindow* window = GetWindowByRef(ref); + IM_CHECK_SILENT_RETV(window != NULL, false); + +#ifdef IMGUI_HAS_DOCK + // This is particularly useful for docked windows, as we have to move root dockspace window instead of docket window + // itself. As a side effect this also adds support for child windows. + window = window->RootWindowDockTree; +#endif + + ImRect visible_r; + visible_r.Min = GetMainMonitorWorkPos(); + visible_r.Max = visible_r.Min + GetMainMonitorWorkSize(); + if (!visible_r.Contains(pos)) + { + // Fallback move window directly to make our item reachable with the mouse. + // FIXME-TESTS-NOT_SAME_AS_END_USER + float pad = g.FontSize; + ImVec2 delta; + delta.x = (pos.x < visible_r.Min.x) ? (visible_r.Min.x - pos.x + pad) : (pos.x > visible_r.Max.x) ? (visible_r.Max.x - pos.x - pad) : 0.0f; + delta.y = (pos.y < visible_r.Min.y) ? (visible_r.Min.y - pos.y + pad) : (pos.y > visible_r.Max.y) ? (visible_r.Max.y - pos.y - pad) : 0.0f; + ImGui::SetWindowPos(window, window->Pos + delta, ImGuiCond_Always); + LogDebug("WindowTeleportToMakePosVisible %s delta (%.1f,%.1f)", window->Name, delta.x, delta.y); + Yield(); + return true; + } + return false; +} + +// ignore_list is a NULL-terminated list of pointers +// Windows that are below all of ignore_list windows are not hidden. +// FIXME-TESTS-NOT_SAME_AS_END_USER: Aim to get rid of this. +void ImGuiTestContext::ForeignWindowsHideOverPos(ImVec2 pos, ImGuiWindow** ignore_list) +{ + ImGuiContext& g = *UiContext; + if (IsError()) + return; + + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + LogDebug("ForeignWindowsHideOverPos (%.0f,%.0f)", pos.x, pos.y); + IM_CHECK_SILENT(ignore_list != NULL); // It makes little sense to call this function with an empty list. + IM_CHECK_SILENT(ignore_list[0] != NULL); + //auto& ctx = this; IM_SUSPEND_TESTFUNC(); + + // Find lowest ignored window index. All windows rendering above this index will be hidden. All windows rendering + // below this index do not prevent interactions with these windows already, and they can be ignored. + int min_window_index = g.Windows.Size; + for (int i = 0; ignore_list[i]; i++) + min_window_index = ImMin(min_window_index, ImGui::FindWindowDisplayIndex(ignore_list[i])); + + bool hidden_windows = false; + for (int i = 0; i < g.Windows.Size; i++) + { + ImGuiWindow* other_window = g.Windows[i]; + if (other_window->RootWindow == other_window && other_window->WasActive) + { + ImRect r = other_window->Rect(); + r.Expand(g.WindowsHoverPadding); + if (r.Contains(pos)) + { + for (int j = 0; ignore_list[j]; j++) +#ifdef IMGUI_HAS_DOCK + if (ignore_list[j]->RootWindowDockTree == other_window->RootWindowDockTree) +#else + if (ignore_list[j] == other_window) +#endif + { + other_window = NULL; + break; + } + + if (other_window && ImGui::FindWindowDisplayIndex(other_window) < min_window_index) + other_window = NULL; + + if (other_window) + { + ForeignWindowsToHide.push_back(other_window); + hidden_windows = true; + } + } + } + } + if (hidden_windows) + Yield(); +} + +void ImGuiTestContext::ForeignWindowsUnhideAll() +{ + ForeignWindowsToHide.clear(); + Yield(); +} + +void ImGuiTestContext::MouseMoveToPos(ImVec2 target) +{ + ImGuiContext& g = *UiContext; + if (IsError()) + return; + + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + LogDebug("MouseMoveToPos from (%.0f,%.0f) to (%.0f,%.0f)", Inputs->MousePosValue.x, Inputs->MousePosValue.y, target.x, target.y); + + if (EngineIO->ConfigRunSpeed == ImGuiTestRunSpeed_Cinematic) + SleepStandard(); + + // Enforce a mouse move if we are already at destination, to enforce g.NavDisableMouseHover gets cleared. + if (g.NavDisableMouseHover && ImLengthSqr(Inputs->MousePosValue - target) < 1.0f) + { + Inputs->MousePosValue = target + ImVec2(1.0f, 0.0f); + ImGuiTestEngine_Yield(Engine); + } + + if (EngineIO->ConfigRunSpeed == ImGuiTestRunSpeed_Fast) + { + Inputs->MousePosValue = target; + ImGuiTestEngine_Yield(Engine); + ImGuiTestEngine_Yield(Engine); + return; + } + + // Simulate slower movements. We use a slightly curved movement to make the movement look less robotic. + + // Calculate some basic parameters + const ImVec2 start_pos = Inputs->MousePosValue; + const ImVec2 delta = target - start_pos; + const float length2 = ImLengthSqr(delta); + const float length = (length2 > 0.0001f) ? ImSqrt(length2) : 1.0f; + const float inv_length = 1.0f / length; + + // Short distance alter speed and wobble + float base_speed = EngineIO->MouseSpeed; + float base_wobble = EngineIO->MouseWobble; + if (length < base_speed * 1.0f) + { + // Time = 1.0f -> wobble max, Time = 0.0f -> no wobble + base_wobble *= length / base_speed; + + // Slow down for short movements(all movement in the 0.0f..1.0f range are remapped to a 0.5f..1.0f seconds) + if (EngineIO->ConfigRunSpeed == ImGuiTestRunSpeed_Cinematic) + { + float approx_time = length / base_speed; + approx_time = 0.5f + ImSaturate(approx_time * 0.5f); + base_speed = length / approx_time; + } + } + + // Calculate a vector perpendicular to the motion delta + const ImVec2 perp = ImVec2(delta.y, -delta.x) * inv_length; + + // Calculate how much wobble we want, clamped to max out when the delta is 100 pixels (shorter movements get less wobble) + const float position_offset_magnitude = ImClamp(length, 1.0f, 100.0f) * base_wobble; + + // Wobble positions, using a sine wave based on position as a cheap way to get a deterministic offset + ImVec2 intermediate_pos_a = start_pos + (delta * 0.3f); + ImVec2 intermediate_pos_b = start_pos + (delta * 0.6f); + intermediate_pos_a += perp * ImSin(intermediate_pos_a.y * 0.1f) * position_offset_magnitude; + intermediate_pos_b += perp * ImCos(intermediate_pos_b.y * 0.1f) * position_offset_magnitude; + + // We manipulate Inputs->MousePosValue without reading back from g.IO.MousePos because the later is rounded. + // To handle high framerate it is easier to bypass this rounding. + float current_dist = 0.0f; // Our current distance along the line (in pixels) + while (true) + { + float move_speed = base_speed * g.IO.DeltaTime; + + //if (g.IO.KeyShift) + // move_speed *= 0.1f; + + current_dist += move_speed; // Move along the line + + // Calculate a parametric position on the direct line that we will use for the curve + float t = current_dist * inv_length; + t = ImClamp(t, 0.0f, 1.0f); + t = 1.0f - ((ImCos(t * IM_PI) + 1.0f) * 0.5f); // Generate a smooth curve with acceleration/deceleration + + //ImGui::GetOverlayDrawList()->AddCircle(target, 10.0f, IM_COL32(255, 255, 0, 255)); + + if (t >= 1.0f) + { + Inputs->MousePosValue = target; + ImGuiTestEngine_Yield(Engine); + ImGuiTestEngine_Yield(Engine); + return; + } + else + { + // Use a bezier curve through the wobble points + Inputs->MousePosValue = ImBezierCubicCalc(start_pos, intermediate_pos_a, intermediate_pos_b, target, t); + //ImGui::GetOverlayDrawList()->AddBezierCurve(start_pos, intermediate_pos_a, intermediate_pos_b, target, IM_COL32(255,0,0,255), 1.0f); + ImGuiTestEngine_Yield(Engine); + } + } +} + +// This always teleport the mouse regardless of fast/slow mode. Useful e.g. to set initial mouse position for a GIF recording. +void ImGuiTestContext::MouseTeleportToPos(ImVec2 target) +{ + if (IsError()) + return; + + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + LogDebug("MouseTeleportToPos from (%.0f,%.0f) to (%.0f,%.0f)", Inputs->MousePosValue.x, Inputs->MousePosValue.y, target.x, target.y); + + Inputs->MousePosValue = target; + ImGuiTestEngine_Yield(Engine); + ImGuiTestEngine_Yield(Engine); +} + +void ImGuiTestContext::MouseDown(ImGuiMouseButton button) +{ + if (IsError()) + return; + + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + LogDebug("MouseDown %d", button); + if (EngineIO->ConfigRunSpeed == ImGuiTestRunSpeed_Cinematic) + SleepStandard(); + + UiContext->IO.MouseClickedTime[button] = -FLT_MAX; // Prevent accidental double-click from happening ever + Inputs->MouseButtonsValue |= (1 << button); + Yield(); +} + +void ImGuiTestContext::MouseUp(ImGuiMouseButton button) +{ + if (IsError()) + return; + + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + LogDebug("MouseUp %d", button); + if (EngineIO->ConfigRunSpeed == ImGuiTestRunSpeed_Cinematic) + SleepShort(); + + Inputs->MouseButtonsValue &= ~(1 << button); + Yield(); +} + +// TODO: click time argument (seconds and/or frames) +void ImGuiTestContext::MouseClick(ImGuiMouseButton button) +{ + if (IsError()) + return; + MouseClickMulti(button, 1); +} + +// TODO: click time argument (seconds and/or frames) +void ImGuiTestContext::MouseClickMulti(ImGuiMouseButton button, int count) +{ + if (IsError()) + return; + + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + if (count > 1) + LogDebug("MouseClickMulti %d x%d", button, count); + else + LogDebug("MouseClick %d", button); + + // Make sure mouse buttons are released + IM_ASSERT(count >= 1); + IM_ASSERT(Inputs->MouseButtonsValue == 0); + Yield(); + + // Press + UiContext->IO.MouseClickedTime[button] = -FLT_MAX; // Prevent accidental double-click from happening ever + + for (int n = 0; n < count; n++) + { + Inputs->MouseButtonsValue = (1 << button); + if (EngineIO->ConfigRunSpeed == ImGuiTestRunSpeed_Cinematic) + SleepShort(); + else if (EngineIO->ConfigRunSpeed != ImGuiTestRunSpeed_Fast) + Yield(2); // Leave enough time for non-alive IDs to expire. (#5325) + else + Yield(); + Inputs->MouseButtonsValue = 0; + + if (EngineIO->ConfigRunSpeed != ImGuiTestRunSpeed_Fast) + Yield(2); // Not strictly necessary but covers more variant. + else + Yield(); + } + + // Now NewFrame() has seen the mouse release. + // Let the imgui frame finish, now e.g. Button() function will return true. Start a new frame. + Yield(); +} + +// TODO: click time argument (seconds and/or frames) +void ImGuiTestContext::MouseDoubleClick(ImGuiMouseButton button) +{ + MouseClickMulti(button, 2); +} + +void ImGuiTestContext::MouseLiftDragThreshold(ImGuiMouseButton button) +{ + if (IsError()) + return; + + ImGuiContext& g = *UiContext; + g.IO.MouseDragMaxDistanceSqr[button] = (g.IO.MouseDragThreshold * g.IO.MouseDragThreshold) + (g.IO.MouseDragThreshold * g.IO.MouseDragThreshold); +} + +// Modeled on FindHoveredWindow() in imgui.cpp. +// Ideally that core function would be refactored to avoid this copy. +// - Need to take account of MovingWindow specificities and early out. +// - Need to be able to skip viewport compare. +// So for now we use a custom function. +ImGuiWindow* ImGuiTestContext::FindHoveredWindowAtPos(const ImVec2& pos) +{ + ImGuiContext& g = *UiContext; + const ImVec2 padding_regular = g.Style.TouchExtraPadding; + const ImVec2 padding_for_resize = g.IO.ConfigWindowsResizeFromEdges ? g.WindowsHoverPadding : padding_regular; + for (int i = g.Windows.Size - 1; i >= 0; i--) + { + ImGuiWindow* window = g.Windows[i]; + if (!window->Active || window->Hidden) + continue; + if (window->Flags & ImGuiWindowFlags_NoMouseInputs) + continue; + + // Using the clipped AABB, a child window will typically be clipped by its parent (not always) + ImRect bb(window->OuterRectClipped); + if (window->Flags & (ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_AlwaysAutoResize)) + bb.Expand(padding_regular); + else + bb.Expand(padding_for_resize); + if (!bb.Contains(pos)) + continue; + + // Support for one rectangular hole in any given window + // FIXME: Consider generalizing hit-testing override (with more generic data, callback, etc.) (#1512) + if (window->HitTestHoleSize.x != 0) + { + ImVec2 hole_pos(window->Pos.x + (float)window->HitTestHoleOffset.x, window->Pos.y + (float)window->HitTestHoleOffset.y); + ImVec2 hole_size((float)window->HitTestHoleSize.x, (float)window->HitTestHoleSize.y); + if (ImRect(hole_pos, hole_pos + hole_size).Contains(pos)) + continue; + } + + return window; + } + return NULL; +} + +static bool IsPosOnVoid(ImGuiContext& g, const ImVec2& pos) +{ + for (ImGuiWindow* window : g.Windows) +#ifdef IMGUI_HAS_DOCK + if (window->RootWindowDockTree == window && window->WasActive) +#else + if (window->RootWindow == window && window->WasActive) +#endif + { + ImRect r = window->Rect(); + r.Expand(g.WindowsHoverPadding); + if (r.Contains(pos)) + return false; + } + return true; +} + +// Sample viewport for an easy location with nothing on it. +// FIXME-OPT: If ever any problematic: +// - (1) could iterate g.WindowsFocusOrder[] now that we made the switch of it only containing root windows +// - (2) increase steps iteratively +// - (3) remember last answer and tries it first. +// - (4) shortpath to failure negative if a window covers the whole viewport? +bool ImGuiTestContext::FindExistingVoidPosOnViewport(ImGuiViewport* viewport, ImVec2* out) +{ + ImGuiContext& g = *UiContext; + if (IsError()) + return false; + + for (int yn = 0; yn < 20; yn++) + for (int xn = 0; xn < 20; xn++) + { + ImVec2 pos = viewport->Pos + viewport->Size * ImVec2(xn / 20.0f, yn / 20.0f); + if (!IsPosOnVoid(g, pos)) + continue; + *out = pos; + return true; + } + return false; +} + +ImVec2 ImGuiTestContext::GetPosOnVoid(ImGuiViewport* viewport) +{ + ImGuiContext& g = *UiContext; + if (IsError()) + return ImVec2(); + + ImVec2 void_pos; + bool found_existing_void_pos = FindExistingVoidPosOnViewport(viewport, &void_pos); + if (found_existing_void_pos) + return void_pos; + + // Move windows away + // FIXME: Should be optional and otherwise error. + void_pos = viewport->Pos + ImVec2(1, 1); + ImVec2 window_min_pos = void_pos + g.WindowsHoverPadding + ImVec2(1.0f, 1.0f); + for (ImGuiWindow* window : g.Windows) + { +#ifdef IMGUI_HAS_DOCK + if (window->Viewport != viewport) + continue; + if (window->RootWindowDockTree == window && window->WasActive) +#else + if (window->RootWindow == window && window->WasActive) +#endif + if (window->Rect().Contains(window_min_pos)) + WindowMove(window->Name, window_min_pos); + } + + return void_pos; +} + +ImVec2 ImGuiTestContext::GetWindowTitlebarPoint(ImGuiTestRef window_ref) +{ + // FIXME-TESTS: Need to find a -visible- click point. drag_pos may end up being outside of main viewport. + if (IsError()) + return ImVec2(); + + ImGuiWindow* window = GetWindowByRef(window_ref); + if (window == NULL) + { + IM_ERRORF_NOHDR("Unable to locate ref window: '%s'", window_ref.Path); + return ImVec2(); + } + + ImVec2 drag_pos; + for (int n = 0; n < 2; n++) + { +#ifdef IMGUI_HAS_DOCK + if (window->DockNode != NULL && window->DockNode->TabBar != NULL) + { + ImGuiTabBar* tab_bar = window->DockNode->TabBar; + ImGuiTabItem* tab = ImGui::TabBarFindTabByID(tab_bar, window->TabId); + IM_ASSERT(tab != NULL); + drag_pos = tab_bar->BarRect.Min + ImVec2(tab->Offset + tab->Width * 0.5f, tab_bar->BarRect.GetHeight() * 0.5f); + } + else +#endif + { + const float h = window->TitleBarHeight(); + drag_pos = ImFloor(window->Pos + ImVec2(window->Size.x, h) * 0.5f); + } + + // If we didn't have to teleport it means we can reach the position already + if (!WindowTeleportToMakePosVisible(window->ID, drag_pos)) + break; + } + return drag_pos; +} + +// Click position which should have no windows. +// Default to last mouse viewport if viewport not specified. +void ImGuiTestContext::MouseMoveToVoid(ImGuiViewport* viewport) +{ + ImGuiContext& g = *UiContext; + if (IsError()) + return; + + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + LogDebug("MouseMoveToVoid"); + +#ifdef IMGUI_HAS_VIEWPORT + if (viewport == NULL && g.MouseViewport && (g.MouseViewport->Flags & ImGuiViewportFlags_CanHostOtherWindows)) + viewport = g.MouseViewport; +#endif + if (viewport == NULL) + viewport = ImGui::GetMainViewport(); + + ImVec2 pos = GetPosOnVoid(viewport); // This may call WindowMove and alter mouse viewport. +#ifdef IMGUI_HAS_VIEWPORT + MouseSetViewportID(viewport->ID); +#endif + MouseMoveToPos(pos); + IM_CHECK(g.HoveredWindow == NULL); +} + +void ImGuiTestContext::MouseClickOnVoid(int mouse_button, ImGuiViewport* viewport) +{ + if (IsError()) + return; + + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + LogDebug("MouseClickOnVoid %d", mouse_button); + MouseMoveToVoid(viewport); + MouseClick(mouse_button); +} + +void ImGuiTestContext::MouseDragWithDelta(ImVec2 delta, ImGuiMouseButton button) +{ + ImGuiContext& g = *UiContext; + if (IsError()) + return; + + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + LogDebug("MouseDragWithDelta %d (%.1f, %.1f)", button, delta.x, delta.y); + + MouseDown(button); + MouseMoveToPos(g.IO.MousePos + delta); + MouseUp(button); +} + +// Important: always call MouseWheelX()/MouseWheelY() with an understand that holding Shift will swap axises. +// - On Windows/Linux, this swap is done in ImGui::NewFrame() +// - On OSX, this swap is generally done by the backends +// - In simulated test engine, always assume Windows/Linux behavior as we will swap in ImGuiTestEngine_ApplyInputToImGuiContext() +void ImGuiTestContext::MouseWheel(ImVec2 delta) +{ + if (IsError()) + return; + + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + + LogDebug("MouseWheel(%g, %g)", delta.x, delta.y); + if (EngineIO->ConfigRunSpeed == ImGuiTestRunSpeed_Cinematic) + SleepStandard(); + + float td = 0.0f; + const float scroll_speed = 15.0f; // Units per second. + while (delta.x != 0.0f || delta.y != 0.0f) + { + ImVec2 scroll; + if (EngineIO->ConfigRunSpeed == ImGuiTestRunSpeed_Fast) + { + scroll = delta; + } + else + { + td += UiContext->IO.DeltaTime; + scroll = ImFloor(delta * ImVec2(td, td) * scroll_speed); + } + + if (scroll.x != 0.0f || scroll.y != 0.0f) + { + scroll = ImClamp(scroll, ImVec2(ImMin(delta.x, 0.0f), ImMin(delta.y, 0.0f)), ImVec2(ImMax(delta.x, 0.0f), ImMax(delta.y, 0.0f))); + Inputs->MouseWheel = scroll; + delta -= scroll; + td = 0; + } + Yield(); + } +} + +void ImGuiTestContext::KeyDown(ImGuiKeyChord key_chord) +{ + if (IsError()) + return; + + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); +#if IMGUI_VERSION_NUM >= 19012 + const char* chord_desc = ImGui::GetKeyChordName(key_chord); +#else + char chord_desc[32]; + ImGui::GetKeyChordName(key_chord, chord_desc, IM_ARRAYSIZE(chord_desc)); +#endif + LogDebug("KeyDown(%s)", chord_desc); + if (EngineIO->ConfigRunSpeed == ImGuiTestRunSpeed_Cinematic) + SleepShort(); + + Inputs->Queue.push_back(ImGuiTestInput::ForKeyChord(key_chord, true)); + Yield(); + Yield(); +} + +void ImGuiTestContext::KeyUp(ImGuiKeyChord key_chord) +{ + if (IsError()) + return; + + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); +#if IMGUI_VERSION_NUM >= 19012 + const char* chord_desc = ImGui::GetKeyChordName(key_chord); +#else + char chord_desc[32]; + ImGui::GetKeyChordName(key_chord, chord_desc, IM_ARRAYSIZE(chord_desc)); +#endif + LogDebug("KeyUp(%s)", chord_desc); + if (EngineIO->ConfigRunSpeed == ImGuiTestRunSpeed_Cinematic) + SleepShort(); + + Inputs->Queue.push_back(ImGuiTestInput::ForKeyChord(key_chord, false)); + Yield(); + Yield(); +} + +void ImGuiTestContext::KeyPress(ImGuiKeyChord key_chord, int count) +{ + if (IsError()) + return; + + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); +#if IMGUI_VERSION_NUM >= 19012 + const char* chord_desc = ImGui::GetKeyChordName(key_chord); +#else + char chord_desc[32]; + ImGui::GetKeyChordName(key_chord, chord_desc, IM_ARRAYSIZE(chord_desc)); +#endif + LogDebug("KeyPress(%s, %d)", chord_desc, count); + if (EngineIO->ConfigRunSpeed == ImGuiTestRunSpeed_Cinematic) + SleepShort(); + + while (count > 0) + { + count--; + Inputs->Queue.push_back(ImGuiTestInput::ForKeyChord(key_chord, true)); + if (EngineIO->ConfigRunSpeed == ImGuiTestRunSpeed_Cinematic) + SleepShort(); + else + Yield(); + Inputs->Queue.push_back(ImGuiTestInput::ForKeyChord(key_chord, false)); + Yield(); + + // Give a frame for items to react + Yield(); + } +} + +void ImGuiTestContext::KeyHold(ImGuiKeyChord key_chord, float time) +{ + if (IsError()) + return; + + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); +#if IMGUI_VERSION_NUM >= 19012 + const char* chord_desc = ImGui::GetKeyChordName(key_chord); +#else + char chord_desc[32]; + ImGui::GetKeyChordName(key_chord, chord_desc, IM_ARRAYSIZE(chord_desc)); +#endif + LogDebug("KeyHold(%s, %.2f sec)", chord_desc, time); + if (EngineIO->ConfigRunSpeed == ImGuiTestRunSpeed_Cinematic) + SleepStandard(); + + Inputs->Queue.push_back(ImGuiTestInput::ForKeyChord(key_chord, true)); + SleepNoSkip(time, 1 / 100.0f); + Inputs->Queue.push_back(ImGuiTestInput::ForKeyChord(key_chord, false)); + Yield(); // Give a frame for items to react +} + +// No extra yield +void ImGuiTestContext::KeySetEx(ImGuiKeyChord key_chord, bool is_down, float time) +{ + if (IsError()) + return; + + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); +#if IMGUI_VERSION_NUM >= 19012 + const char* chord_desc = ImGui::GetKeyChordName(key_chord); +#else + char chord_desc[32]; + ImGui::GetKeyChordName(key_chord, chord_desc, IM_ARRAYSIZE(chord_desc)); +#endif + LogDebug("KeySetEx(%s, is_down=%d, time=%.f)", chord_desc, is_down, time); + Inputs->Queue.push_back(ImGuiTestInput::ForKeyChord(key_chord, is_down)); + if (time > 0.0f) + SleepNoSkip(time, 1.0f / 100.0f); +} + +void ImGuiTestContext::KeyChars(const char* chars) +{ + if (IsError()) + return; + + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + LogDebug("KeyChars('%s')", chars); + if (EngineIO->ConfigRunSpeed == ImGuiTestRunSpeed_Cinematic) + SleepStandard(); + + while (*chars) + { + unsigned int c = 0; + int bytes_count = ImTextCharFromUtf8(&c, chars, NULL); + chars += bytes_count; + if (c > 0 && c <= 0xFFFF) + Inputs->Queue.push_back(ImGuiTestInput::ForChar((ImWchar)c)); + + if (EngineIO->ConfigRunSpeed != ImGuiTestRunSpeed_Fast) + Sleep(1.0f / EngineIO->TypingSpeed); + } + Yield(); +} + +void ImGuiTestContext::KeyCharsAppend(const char* chars) +{ + if (IsError()) + return; + + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + LogDebug("KeyCharsAppend('%s')", chars); + KeyPress(ImGuiKey_End); + KeyChars(chars); +} + +void ImGuiTestContext::KeyCharsAppendEnter(const char* chars) +{ + if (IsError()) + return; + + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + LogDebug("KeyCharsAppendEnter('%s')", chars); + KeyPress(ImGuiKey_End); + KeyChars(chars); + KeyPress(ImGuiKey_Enter); +} + +void ImGuiTestContext::KeyCharsReplace(const char* chars) +{ + if (IsError()) + return; + + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + LogDebug("KeyCharsReplace('%s')", chars); + KeyPress(ImGuiKey_A | ImGuiMod_Shortcut); + if (chars[0]) + KeyChars(chars); + else + KeyPress(ImGuiKey_Delete); +} + +void ImGuiTestContext::KeyCharsReplaceEnter(const char* chars) +{ + if (IsError()) + return; + + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + LogDebug("KeyCharsReplaceEnter('%s')", chars); + KeyPress(ImGuiKey_A | ImGuiMod_Shortcut); + if (chars[0]) + KeyChars(chars); + else + KeyPress(ImGuiKey_Delete); + KeyPress(ImGuiKey_Enter); +} + +// depth = 1 -> immediate child of 'parent' in ID Stack +void ImGuiTestContext::GatherItems(ImGuiTestItemList* out_list, ImGuiTestRef parent, int depth) +{ + IM_ASSERT(out_list != NULL); + IM_ASSERT(depth > 0 || depth == -1); + + if (IsError()) + return; + + ImGuiTestGatherTask* task = &Engine->GatherTask; + IM_ASSERT(task->InParentID == 0); + IM_ASSERT(task->LastItemInfo == NULL); + + // Register gather tasks + if (depth == -1) + depth = 99; + if (parent.ID == 0) + parent.ID = GetID(parent); + task->InParentID = parent.ID; + task->InMaxDepth = depth; + task->InLayerMask = (1 << ImGuiNavLayer_Main); // FIXME: Configurable filter + task->OutList = out_list; + + // Keep running while gathering + // The corresponding hook is ItemAdd() -> ImGuiTestEngineHook_ItemAdd() -> ImGuiTestEngineHook_ItemAdd_GatherTask() + const int begin_gather_size = out_list->GetSize(); + while (true) + { + const int begin_gather_size_for_frame = out_list->GetSize(); + Yield(); + const int end_gather_size_for_frame = out_list->GetSize(); + if (begin_gather_size_for_frame == end_gather_size_for_frame) + break; + } + const int end_gather_size = out_list->GetSize(); + + // FIXME-TESTS: To support filter we'd need to process the list here, + // Because ImGuiTestItemList is a pool (ImVector + map ID->index) we'll need to filter, rewrite, rebuild map + + ImGuiTestItemInfo* parent_item = ItemInfo(parent, ImGuiTestOpFlags_NoError); + LogDebug("GatherItems from %s, %d deep: found %d items.", ImGuiTestRefDesc(parent, parent_item).c_str(), depth, end_gather_size - begin_gather_size); + + task->Clear(); +} + +// Supported values for ImGuiTestOpFlags: +// - ImGuiTestOpFlags_NoAutoOpenFullPath +// - ImGuiTestOpFlags_NoError +void ImGuiTestContext::ItemAction(ImGuiTestAction action, ImGuiTestRef ref, ImGuiTestOpFlags flags, void* action_arg) +{ + if (IsError()) + return; + + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + + // [DEBUG] Breakpoint + //if (ref.ID == 0x0d4af068) + // printf(""); + + // FIXME-TESTS: Fix that stuff + const bool is_wildcard = ref.Path != NULL && strstr(ref.Path, "**/") != 0; + if (is_wildcard) + { + // This is a fragile way to avoid some ambiguities, we're relying on expected action to further filter by status flags. + // These flags are not cleared by ItemInfo() because ItemAction() may call ItemInfo() again to get same item and thus it + // needs these flags to remain in place. + if (action == ImGuiTestAction_Check || action == ImGuiTestAction_Uncheck) + Engine->FindByLabelTask.InFilterItemStatusFlags = ImGuiItemStatusFlags_Checkable; + else if (action == ImGuiTestAction_Open || action == ImGuiTestAction_Close) + Engine->FindByLabelTask.InFilterItemStatusFlags = ImGuiItemStatusFlags_Openable; + } + + // Find item + ImGuiTestItemInfo* item; + if (flags & ImGuiTestOpFlags_NoAutoOpenFullPath) + item = ItemInfo(ref, (flags & ImGuiTestOpFlags_NoError)); + else + item = ItemInfoOpenFullPath(ref, (flags & ImGuiTestOpFlags_NoError)); + + ImGuiTestRefDesc desc(ref, item); + LogDebug("Item%s %s%s", GetActionName(action), desc.c_str(), (InputMode == ImGuiInputSource_Mouse) ? "" : " (w/ Nav)"); + if (item->ID == 0) + { + if (flags & ImGuiTestOpFlags_NoError) + LogDebug("Action skipped: Item doesn't exist + used ImGuiTestOpFlags_NoError."); + return; + } + + // Automatically uncollapse by default + if (item->Window && !(OpFlags & ImGuiTestOpFlags_NoAutoUncollapse)) + WindowCollapse(item->Window->ID, false); + + if (action == ImGuiTestAction_Hover) + { + MouseMove(ref, flags); + } + if (action == ImGuiTestAction_Click || action == ImGuiTestAction_DoubleClick) + { + if (InputMode == ImGuiInputSource_Mouse) + { + const int mouse_button = (int)(intptr_t)action_arg; + IM_ASSERT(mouse_button >= 0 && mouse_button < ImGuiMouseButton_COUNT); + MouseMove(ref, flags); + if (action == ImGuiTestAction_DoubleClick) + MouseDoubleClick(mouse_button); + else + MouseClick(mouse_button); + } + else + { + action = ImGuiTestAction_NavActivate; + } + } + + if (action == ImGuiTestAction_NavActivate) + { + IM_ASSERT(action_arg == NULL); // Unused + NavMoveTo(ref); + NavActivate(); + if (action == ImGuiTestAction_DoubleClick) + IM_ASSERT(0); + } + else if (action == ImGuiTestAction_Input) + { + IM_ASSERT(action_arg == NULL); // Unused + if (InputMode == ImGuiInputSource_Mouse) + { + MouseMove(ref, flags); + KeyDown(ImGuiMod_Ctrl); + MouseClick(0); + KeyUp(ImGuiMod_Ctrl); + } + else + { + NavMoveTo(ref); + NavInput(); + } + } + else if (action == ImGuiTestAction_Open) + { + IM_ASSERT(action_arg == NULL); // Unused + if ((item->StatusFlags & ImGuiItemStatusFlags_Opened) == 0) + { + item->RefCount++; + MouseMove(ref, flags); + + // Some item may open just by hovering, give them that chance + if ((item->StatusFlags & ImGuiItemStatusFlags_Opened) == 0) + { + MouseClick(0); + if ((item->StatusFlags & ImGuiItemStatusFlags_Opened) == 0) + { + MouseDoubleClick(0); // Attempt a double-click // FIXME-TESTS: let's not start doing those fuzzy things.. + if ((item->StatusFlags & ImGuiItemStatusFlags_Opened) == 0) + IM_ERRORF_NOHDR("Unable to Open item: '%s' in '%s'", desc.c_str(), item->Window ? item->Window->Name : "N/A"); + } + } + item->RefCount--; + //Yield(); + } + } + else if (action == ImGuiTestAction_Close) + { + IM_ASSERT(action_arg == NULL); // Unused + if ((item->StatusFlags & ImGuiItemStatusFlags_Opened) != 0) + { + item->RefCount++; + ItemClick(ref, 0, flags); + if ((item->StatusFlags & ImGuiItemStatusFlags_Opened) != 0) + { + ItemDoubleClick(ref, flags); // Attempt a double-click + // FIXME-TESTS: let's not start doing those fuzzy things.. widget should give direction of how to close/open... e.g. do you we close a TabItem? + if ((item->StatusFlags & ImGuiItemStatusFlags_Opened) != 0) + IM_ERRORF_NOHDR("Unable to Close item: %s", ImGuiTestRefDesc(ref, item).c_str()); + } + item->RefCount--; + Yield(); + } + } + else if (action == ImGuiTestAction_Check) + { + IM_ASSERT(action_arg == NULL); // Unused + if ((item->StatusFlags & ImGuiItemStatusFlags_Checkable) && !(item->StatusFlags & ImGuiItemStatusFlags_Checked)) + { + ItemClick(ref, 0, flags); + } + ItemVerifyCheckedIfAlive(ref, true); // We can't just IM_ASSERT(ItemIsChecked()) because the item may disappear and never update its StatusFlags any more! + } + else if (action == ImGuiTestAction_Uncheck) + { + IM_ASSERT(action_arg == NULL); // Unused + if ((item->StatusFlags & ImGuiItemStatusFlags_Checkable) && (item->StatusFlags & ImGuiItemStatusFlags_Checked)) + { + ItemClick(ref, 0, flags); + } + ItemVerifyCheckedIfAlive(ref, false); // We can't just IM_ASSERT(ItemIsChecked()) because the item may disappear and never update its StatusFlags any more! + } + + //if (is_wildcard) + Engine->FindByLabelTask.InFilterItemStatusFlags = ImGuiItemStatusFlags_None; +} + +void ImGuiTestContext::ItemActionAll(ImGuiTestAction action, ImGuiTestRef ref_parent, const ImGuiTestActionFilter* filter) +{ + int max_depth = filter ? filter->MaxDepth : -1; + if (max_depth == -1) + max_depth = 99; + int max_passes = filter ? filter->MaxPasses : -1; + if (max_passes == -1) + max_passes = 99; + IM_ASSERT(max_depth > 0 && max_passes > 0); + + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + LogDebug("ItemActionAll() %s", GetActionName(action)); + + if (!ref_parent.IsEmpty()) + { + // Open parent's parents + ImGuiTestItemInfo* parent_info = ItemInfoOpenFullPath(ref_parent); + if (parent_info->ID != 0) + { + // Open parent + if (action == ImGuiTestAction_Open) + if ((parent_info->StatusFlags & ImGuiItemStatusFlags_Openable) && (parent_info->InFlags & ImGuiItemFlags_Disabled) == 0) + ItemOpen(ref_parent, ImGuiTestOpFlags_NoError); + } + } + + // Find child items + int actioned_total = 0; + for (int pass = 0; pass < max_passes; pass++) + { + ImGuiTestItemList items; + GatherItems(&items, ref_parent, max_depth); + //LogItemList(&items); + + // Find deep most items + int highest_depth = -1; + if (action == ImGuiTestAction_Close) + for (auto& item : items) + if ((item.StatusFlags & ImGuiItemStatusFlags_Openable) && (item.StatusFlags & ImGuiItemStatusFlags_Opened)) // Not checking Disabled state here + highest_depth = ImMax(highest_depth, item.Depth); + + const int actioned_total_at_beginning_of_pass = actioned_total; + + // Process top-to-bottom in most cases + int scan_start = 0; + int scan_end = items.GetSize(); + int scan_dir = +1; + if (action == ImGuiTestAction_Close) + { + // Close bottom-to-top because + // 1) it is more likely to handle same-depth parent/child relationship better (e.g. CollapsingHeader) + // 2) it gives a nicer sense of symmetry with the corresponding open operation. + scan_start = items.GetSize() - 1; + scan_end = -1; + scan_dir = -1; + } + + int processed_count_per_depth[8]; + memset(processed_count_per_depth, 0, sizeof(processed_count_per_depth)); + + for (int n = scan_start; n != scan_end; n += scan_dir) + { + if (IsError()) + break; + + const ImGuiTestItemInfo& item = *items[n]; + + if (filter && filter->RequireAllStatusFlags != 0) + if ((item.StatusFlags & filter->RequireAllStatusFlags) != filter->RequireAllStatusFlags) + continue; + + if (filter && filter->RequireAnyStatusFlags != 0) + if ((item.StatusFlags & filter->RequireAnyStatusFlags) != 0) + continue; + + if (filter && filter->MaxItemCountPerDepth != NULL) + { + if (item.Depth < IM_ARRAYSIZE(processed_count_per_depth)) + { + if (processed_count_per_depth[item.Depth] >= filter->MaxItemCountPerDepth[item.Depth]) + continue; + processed_count_per_depth[item.Depth]++; + } + } + + switch (action) + { + case ImGuiTestAction_Hover: + case ImGuiTestAction_Click: + ItemAction(action, item.ID); + actioned_total++; + break; + case ImGuiTestAction_Check: + if ((item.StatusFlags & ImGuiItemStatusFlags_Checkable) && !(item.StatusFlags & ImGuiItemStatusFlags_Checked)) + if ((item.InFlags & ImGuiItemFlags_Disabled) == 0) + { + ItemAction(action, item.ID); + actioned_total++; + } + break; + case ImGuiTestAction_Uncheck: + if ((item.StatusFlags & ImGuiItemStatusFlags_Checkable) && (item.StatusFlags & ImGuiItemStatusFlags_Checked)) + if ((item.InFlags & ImGuiItemFlags_Disabled) == 0) + { + ItemAction(action, item.ID); + actioned_total++; + } + break; + case ImGuiTestAction_Open: + if ((item.StatusFlags & ImGuiItemStatusFlags_Openable) && !(item.StatusFlags & ImGuiItemStatusFlags_Opened)) + if ((item.InFlags & ImGuiItemFlags_Disabled) == 0) + { + ItemAction(action, item.ID); + actioned_total++; + } + break; + case ImGuiTestAction_Close: + if (item.Depth == highest_depth && (item.StatusFlags & ImGuiItemStatusFlags_Openable) && (item.StatusFlags & ImGuiItemStatusFlags_Opened)) + if ((item.InFlags & ImGuiItemFlags_Disabled) == 0) + { + ItemClose(item.ID); + actioned_total++; + } + break; + default: + IM_ASSERT(0); + } + } + + if (IsError()) + break; + + if (action == ImGuiTestAction_Hover) + break; + if (actioned_total_at_beginning_of_pass == actioned_total) + break; + } + LogDebug("%s %d items in total!", GetActionVerb(action), actioned_total); +} + +void ImGuiTestContext::ItemOpenAll(ImGuiTestRef ref_parent, int max_depth, int max_passes) +{ + ImGuiTestActionFilter filter; + filter.MaxDepth = max_depth; + filter.MaxPasses = max_passes; + ItemActionAll(ImGuiTestAction_Open, ref_parent, &filter); +} + +void ImGuiTestContext::ItemCloseAll(ImGuiTestRef ref_parent, int max_depth, int max_passes) +{ + ImGuiTestActionFilter filter; + filter.MaxDepth = max_depth; + filter.MaxPasses = max_passes; + ItemActionAll(ImGuiTestAction_Close, ref_parent, &filter); +} + +void ImGuiTestContext::ItemInputValue(ImGuiTestRef ref, int value) +{ + char buf[32]; + ImFormatString(buf, IM_ARRAYSIZE(buf), "%d", value); + ItemInput(ref); + KeyCharsReplaceEnter(buf); +} +void ImGuiTestContext::ItemInputValue(ImGuiTestRef ref, float value) +{ + char buf[32]; + ImFormatString(buf, IM_ARRAYSIZE(buf), "%f", value); + ItemInput(ref); + KeyCharsReplaceEnter(buf); +} + +void ImGuiTestContext::ItemInputValue(ImGuiTestRef ref, const char* value) +{ + ItemInput(ref); + KeyCharsReplaceEnter(value); +} + +void ImGuiTestContext::ItemHold(ImGuiTestRef ref, float time) +{ + if (IsError()) + return; + + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + LogDebug("ItemHold '%s' %08X", ref.Path ? ref.Path : "NULL", ref.ID); + + MouseMove(ref); + + Yield(); + Inputs->MouseButtonsValue = (1 << 0); + Sleep(time); + Inputs->MouseButtonsValue = 0; + Yield(); +} + +void ImGuiTestContext::ItemHoldForFrames(ImGuiTestRef ref, int frames) +{ + if (IsError()) + return; + + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + LogDebug("ItemHoldForFrames '%s' %08X", ref.Path ? ref.Path : "NULL", ref.ID); + + MouseMove(ref); + Yield(); + Inputs->MouseButtonsValue = (1 << 0); + Yield(frames); + Inputs->MouseButtonsValue = 0; + Yield(); +} + +// Used to test opening containers (TreeNode, Tabs) while dragging a payload +void ImGuiTestContext::ItemDragOverAndHold(ImGuiTestRef ref_src, ImGuiTestRef ref_dst) +{ + if (IsError()) + return; + + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + ImGuiTestItemInfo* item_src = ItemInfo(ref_src); + ImGuiTestItemInfo* item_dst = ItemInfo(ref_dst); + ImGuiTestRefDesc desc_src(ref_src, item_src); + ImGuiTestRefDesc desc_dst(ref_dst, item_dst); + LogDebug("ItemDragOverAndHold %s to %s", desc_src.c_str(), desc_dst.c_str()); + + MouseMove(ref_src, ImGuiTestOpFlags_NoCheckHoveredId); + SleepStandard(); + MouseDown(0); + + // Enforce lifting drag threshold even if both item are exactly at the same location. + MouseLiftDragThreshold(); + + MouseMove(ref_dst, ImGuiTestOpFlags_NoCheckHoveredId); + SleepNoSkip(1.0f, 1.0f / 10.0f); + MouseUp(0); +} + +void ImGuiTestContext::ItemDragAndDrop(ImGuiTestRef ref_src, ImGuiTestRef ref_dst, ImGuiMouseButton button) +{ + if (IsError()) + return; + + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + ImGuiTestItemInfo* item_src = ItemInfo(ref_src); + ImGuiTestItemInfo* item_dst = ItemInfo(ref_dst); + ImGuiTestRefDesc desc_src(ref_src, item_src); + ImGuiTestRefDesc desc_dst(ref_dst, item_dst); + LogDebug("ItemDragAndDrop %s to %s", desc_src.c_str(), desc_dst.c_str()); + + // Try to keep destination window above other windows. MouseMove() operation will avoid focusing destination window + // as that may steal ActiveID and break operation. + // FIXME-TESTS: This does not handle a case where source and destination windows overlap. + if (item_dst->Window != NULL) + WindowBringToFront(item_dst->Window->ID); + + // Use item_src/item_dst instead of ref_src/ref_dst so references with e.g. //$FOCUSED are latched once in the ItemInfo() call. + MouseMove(item_src->ID, ImGuiTestOpFlags_NoCheckHoveredId); + SleepStandard(); + MouseDown(button); + + // Enforce lifting drag threshold even if both item are exactly at the same location. + MouseLiftDragThreshold(); + + MouseMove(item_dst->ID, ImGuiTestOpFlags_NoCheckHoveredId | ImGuiTestOpFlags_NoFocusWindow); + SleepStandard(); + MouseUp(button); +} + +void ImGuiTestContext::ItemDragWithDelta(ImGuiTestRef ref_src, ImVec2 pos_delta) +{ + if (IsError()) + return; + + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + ImGuiTestItemInfo* item_src = ItemInfo(ref_src); + ImGuiTestRefDesc desc_src(ref_src, item_src); + LogDebug("ItemDragWithDelta %s to (%f, %f)", desc_src.c_str(), pos_delta.x, pos_delta.y); + + MouseMove(ref_src, ImGuiTestOpFlags_NoCheckHoveredId); + SleepStandard(); + MouseDown(0); + + MouseMoveToPos(UiContext->IO.MousePos + pos_delta); + SleepStandard(); + MouseUp(0); +} + +bool ImGuiTestContext::ItemExists(ImGuiTestRef ref) +{ + ImGuiTestItemInfo* item = ItemInfo(ref, ImGuiTestOpFlags_NoError); + return item->ID != 0; +} + +// May want to add support for ImGuiTestOpFlags_NoError if item does not exist? +bool ImGuiTestContext::ItemIsChecked(ImGuiTestRef ref) +{ + ImGuiTestItemInfo* item = ItemInfo(ref); + return (item->StatusFlags & ImGuiItemStatusFlags_Checked) != 0; +} + +// May want to add support for ImGuiTestOpFlags_NoError if item does not exist? +bool ImGuiTestContext::ItemIsOpened(ImGuiTestRef ref) +{ + ImGuiTestItemInfo* item = ItemInfo(ref); + return (item->StatusFlags & ImGuiItemStatusFlags_Opened) != 0; +} + +void ImGuiTestContext::ItemVerifyCheckedIfAlive(ImGuiTestRef ref, bool checked) +{ + // This is designed to deal with disappearing items which will not update their state, + // e.g. a checkable menu item in a popup which closes when checked. + // Otherwise ItemInfo() data is preserved for an additional frame. + Yield(); + ImGuiTestItemInfo* item = ItemInfo(ref, ImGuiTestOpFlags_NoError); + if (item->ID == 0) + return; + if (item->TimestampMain + 1 >= ImGuiTestEngine_GetFrameCount(Engine) && item->TimestampStatus == item->TimestampMain) + IM_CHECK_SILENT(((item->StatusFlags & ImGuiItemStatusFlags_Checked) != 0) == checked); +} + +// FIXME-TESTS: Could this be handled by ItemClose()? +void ImGuiTestContext::TabClose(ImGuiTestRef ref) +{ + if (IsError()) + return; + + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + LogDebug("TabClose '%s' %08X", ref.Path ? ref.Path : "NULL", ref.ID); + + // Move into first, then click close button as it appears + MouseMove(ref); + ImGuiTestRef backup_ref = GetRef(); + SetRef(GetID(ref)); + ItemClick("#CLOSE"); + SetRef(backup_ref); +} + +bool ImGuiTestContext::TabBarCompareOrder(ImGuiTabBar* tab_bar, const char** tab_order) +{ + if (IsError()) + return false; + + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + LogDebug("TabBarCompareOrder"); + IM_CHECK_SILENT_RETV(tab_bar != NULL, false); + + // Display + char buf[256]; + char* buf_end = buf + IM_ARRAYSIZE(buf); + + char* p = buf; + for (int i = 0; i < tab_bar->Tabs.Size; i++) + p += ImFormatString(p, buf_end - p, "%s\"%s\"", i ? ", " : " ", ImGui::TabBarGetTabName(tab_bar, &tab_bar->Tabs[i])); + LogDebug(" Current {%s }", buf); + + p = buf; + for (int i = 0; tab_order[i] != NULL; i++) + p += ImFormatString(p, buf_end - p, "%s\"%s\"", i ? ", " : " ", tab_order[i]); + LogDebug(" Expected {%s }", buf); + + // Compare + for (int i = 0; tab_order[i] != NULL; i++) + { + if (i >= tab_bar->Tabs.Size) + return false; + const char* current = ImGui::TabBarGetTabName(tab_bar, &tab_bar->Tabs[i]); + const char* expected = tab_order[i]; + if (strcmp(current, expected) != 0) + return false; + } + return true; +} + + +void ImGuiTestContext::MenuAction(ImGuiTestAction action, ImGuiTestRef ref) +{ + if (IsError()) + return; + + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + LogDebug("MenuAction '%s' %08X", ref.Path ? ref.Path : "NULL", ref.ID); + + IM_ASSERT(ref.Path != NULL); + + // MenuAction() doesn't support **/ in most case it would be equivalent to opening all menus to "search". + // [01] Works: + // MenuClick("File/New"): + // [02] Works: + // MenuClick("File"); + // MenuClick("File/New"); + // [03] Works: + // MenuClick("File"); + // ItemClick("**/New"); + // [04] Doesn't work: (may work in the future) + // MenuClick("File"); + // MenuClick("**/New"); + // [05] Doesn't work: (unlikely to ever work) + // MenuClick("**/New"); + if (strncmp(ref.Path, "**/", 3) == 0) + { + LogError("\"**/\" is not yet supported by MenuAction()."); + return; + } + + int depth = 0; + const char* path = ref.Path; + const char* path_end = path + strlen(path); + + ImGuiWindow* ref_window = NULL; + if (path[0] == '/' && path[1] == '/') + { + const char* end = strstr(path + 2, "/"); + IM_CHECK_SILENT(end != NULL); // Menu interaction without any menus specified in ref. + Str64 window_name; + window_name.append(path, end); + ref_window = GetWindowByRef(GetID(window_name.c_str())); + path = end + 1; + if (ref_window == NULL) + LogError("MenuAction: missing ref window (invalid name \"//%s\" ?", window_name.c_str()); + } + else if (RefID) + { + ref_window = GetWindowByRef(RefID); + if (ref_window == NULL) + LogError("MenuAction: missing ref window (invalid SetRef value?)"); + } + IM_CHECK_SILENT(ref_window != NULL); // A ref window must always be set + + ImGuiWindow* current_window = ref_window; + Str128 buf; + while (path < path_end && !IsError()) + { + const char* p = ImStrchrRangeWithEscaping(path, path_end, '/'); + if (p == NULL) + p = path_end; + + const bool is_target_item = (p == path_end); + if (current_window->Flags & ImGuiWindowFlags_MenuBar) + buf.setf("//%s/##menubar/%.*s", current_window->Name, (int)(p - path), path); // Click menu in menu bar + else + buf.setf("//%s/%.*s", current_window->Name, (int)(p - path), path); // Click sub menu in its own window + +#if IMGUI_VERSION_NUM < 18520 + if (depth == 0 && (current_window->Flags & ImGuiWindowFlags_Popup)) + depth++; +#endif + + ImGuiTestItemInfo* item = ItemInfo(buf.c_str()); + IM_CHECK_SILENT(item->ID != 0); + if ((item->StatusFlags & ImGuiItemStatusFlags_Opened) == 0) // Open menus can be ignored completely. + { + // We cannot move diagonally to a menu item because depending on the angle and other items we cross on our path we could close our target menu. + // First move horizontally into the menu, then vertically! + if (depth > 0) + { + IM_CHECK_SILENT(item != NULL); + item->RefCount++; + MouseSetViewport(item->Window); + if (depth > 1 && (Inputs->MousePosValue.x <= item->RectFull.Min.x || Inputs->MousePosValue.x >= item->RectFull.Max.x)) + MouseMoveToPos(ImVec2(item->RectFull.GetCenter().x, Inputs->MousePosValue.y)); + if (depth > 0 && (Inputs->MousePosValue.y <= item->RectFull.Min.y || Inputs->MousePosValue.y >= item->RectFull.Max.y)) + MouseMoveToPos(ImVec2(Inputs->MousePosValue.x, item->RectFull.GetCenter().y)); + item->RefCount--; + } + + if (is_target_item) + { + // Final item + ItemAction(action, buf.c_str()); + break; + } + else + { + // Then aim at the menu item. Menus may be navigated by holding mouse button down by hovering a menu. + ItemAction(Inputs->MouseButtonsValue ? ImGuiTestAction_Hover : ImGuiTestAction_Click, buf.c_str()); + } + } + current_window = GetWindowByRef(Str16f("##Menu_%02d", depth).c_str()); + IM_CHECK_SILENT(current_window != NULL); + + path = p + 1; + depth++; + } +} + +void ImGuiTestContext::MenuActionAll(ImGuiTestAction action, ImGuiTestRef ref_parent) +{ + ImGuiTestItemList items; + MenuAction(ImGuiTestAction_Open, ref_parent); + GatherItems(&items, "//$FOCUSED", 1); + //LogItemList(&items); + + for (auto item : items) + { + MenuAction(ImGuiTestAction_Open, ref_parent); // We assume that every interaction will close the menu again + + if (action == ImGuiTestAction_Check || action == ImGuiTestAction_Uncheck) + { + ImGuiTestItemInfo* info2 = ItemInfo(item.ID); // refresh info + if ((info2->InFlags & ImGuiItemFlags_Disabled) != 0) // FIXME: Report disabled state in log? Make that optional? + continue; + if ((info2->StatusFlags & ImGuiItemStatusFlags_Checkable) == 0) + continue; + } + + ItemAction(action, item.ID); + } +} + +static bool IsWindowACombo(ImGuiWindow* window) +{ + if ((window->Flags & ImGuiWindowFlags_Popup) == 0) + return false; + if (strncmp(window->Name, "##Combo_", strlen("##Combo_")) != 0) + return false; + return true; +} + +void ImGuiTestContext::ComboClick(ImGuiTestRef ref) +{ + if (IsError()) + return; + + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + LogDebug("ComboClick '%s' %08X", ref.Path ? ref.Path : "NULL", ref.ID); + + IM_ASSERT(ref.Path != NULL); + + const char* path = ref.Path; + const char* path_end = path + strlen(path); + + const char* p = ImStrchrRangeWithEscaping(path, path_end, '/'); + Str128f combo_popup_buf = Str128f("%.*s", (int)(p-path), path); + ItemClick(combo_popup_buf.c_str()); + + ImGuiWindow* popup = GetWindowByRef("//$FOCUSED"); + IM_CHECK_SILENT(popup && IsWindowACombo(popup)); + + Str128f combo_item_buf = Str128f("//%s/**/%s", popup->Name, p + 1); + ItemClick(combo_item_buf.c_str()); +} + +void ImGuiTestContext::ComboClickAll(ImGuiTestRef ref_parent) +{ + ItemClick(ref_parent); + + ImGuiWindow* popup = GetWindowByRef("//$FOCUSED"); + IM_CHECK_SILENT(popup && IsWindowACombo(popup)); + + ImGuiTestItemList items; + GatherItems(&items, "//$FOCUSED"); + for (auto item : items) + { + ItemClick(ref_parent); // We assume that every interaction will close the combo again + ItemClick(item.ID); + } +} + +static ImGuiTableColumn* HelperTableFindColumnByName(ImGuiTable* table, const char* name) +{ + for (int i = 0; i < table->Columns.size(); i++) + if (strcmp(ImGui::TableGetColumnName(table, i), name) == 0) + return &table->Columns[i]; + return NULL; +} + +void ImGuiTestContext::TableOpenContextMenu(ImGuiTestRef ref, int column_n) +{ + if (IsError()) + return; + + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + LogDebug("TableOpenContextMenu '%s' %08X", ref.Path ? ref.Path : "NULL", ref.ID); + + ImGuiTable* table = ImGui::TableFindByID(GetID(ref)); + IM_CHECK_SILENT(table != NULL); + + if (column_n == -1) + column_n = table->RightMostEnabledColumn; + ItemClick(TableGetHeaderID(table, column_n), ImGuiMouseButton_Right); + Yield(); +} + +ImGuiSortDirection ImGuiTestContext::TableClickHeader(ImGuiTestRef ref, const char* label, ImGuiKeyChord key_mods) +{ + IM_ASSERT((key_mods & ~ImGuiMod_Mask_) == 0); // Cannot pass keys only mods + + ImGuiTable* table = ImGui::TableFindByID(GetID(ref)); + IM_CHECK_SILENT_RETV(table != NULL, ImGuiSortDirection_None); + + ImGuiTableColumn* column = HelperTableFindColumnByName(table, label); + IM_CHECK_SILENT_RETV(column != NULL, ImGuiSortDirection_None); + + if (key_mods != ImGuiMod_None) + KeyDown(key_mods); + + ItemClick(TableGetHeaderID(table, label), ImGuiMouseButton_Left); + + if (key_mods != ImGuiMod_None) + KeyUp(key_mods); + return (ImGuiSortDirection_)column->SortDirection; +} + +void ImGuiTestContext::TableSetColumnEnabled(ImGuiTestRef ref, const char* label, bool enabled) +{ + if (IsError()) + return; + + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + LogDebug("TableSetColumnEnabled '%s' %08X = %d", ref.Path ? ref.Path : "NULL", ref.ID, enabled); + + TableOpenContextMenu(ref); + + ImGuiTestRef backup_ref = GetRef(); + SetRef("//$FOCUSED"); + if (enabled) + ItemCheck(label); + else + ItemUncheck(label); + PopupCloseOne(); + SetRef(backup_ref); +} + +void ImGuiTestContext::TableResizeColumn(ImGuiTestRef ref, int column_n, float width) +{ + if (IsError()) + return; + + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + LogDebug("TableResizeColumn '%s' %08X column %d width %.2f", ref.Path ? ref.Path : "NULL", ref.ID, column_n, width); + + ImGuiTable* table = ImGui::TableFindByID(GetID(ref)); + IM_CHECK_SILENT(table != NULL); + + ImGuiID resize_id = ImGui::TableGetColumnResizeID(table, column_n); + float old_width = table->Columns[column_n].WidthGiven; + ItemDragWithDelta(resize_id, ImVec2(width - old_width, 0)); + + IM_CHECK_EQ(table->Columns[column_n].WidthRequest, width); +} + +const ImGuiTableSortSpecs* ImGuiTestContext::TableGetSortSpecs(ImGuiTestRef ref) +{ + ImGuiTable* table = ImGui::TableFindByID(GetID(ref)); + IM_CHECK_SILENT_RETV(table != NULL, NULL); + + ImGuiContext& g = *UiContext; + ImSwap(table, g.CurrentTable); + const ImGuiTableSortSpecs* sort_specs = ImGui::TableGetSortSpecs(); + ImSwap(table, g.CurrentTable); + return sort_specs; +} + +void ImGuiTestContext::WindowClose(ImGuiTestRef ref) +{ + if (IsError()) + return; + + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + LogDebug("WindowClose"); + ImGuiTestRef backup_ref = GetRef(); + SetRef(GetID(ref)); + +#ifdef IMGUI_HAS_DOCK + // When docked: first move to Tab to make Close Button appear. + if (ImGuiWindow* window = GetWindowByRef("")) + if (window->DockIsActive) + MouseMove(window->TabId); +#endif + + ItemClick("#CLOSE"); + SetRef(backup_ref); +} + +void ImGuiTestContext::WindowCollapse(ImGuiTestRef window_ref, bool collapsed) +{ + if (IsError()) + return; + ImGuiWindow* window = GetWindowByRef(window_ref); + if (window == NULL) + return; + + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + if (window->Collapsed != collapsed) + { + LogDebug("WindowCollapse %d", collapsed); + ImGuiTestOpFlags backup_op_flags = OpFlags; + OpFlags |= ImGuiTestOpFlags_NoAutoUncollapse; + ImGuiTestRef backup_ref = GetRef(); + SetRef(window->ID); + ItemClick("#COLLAPSE"); + SetRef(backup_ref); + OpFlags = backup_op_flags; + Yield(); + IM_CHECK(window->Collapsed == collapsed); + } +} + +// Supported values for ImGuiTestOpFlags: +// - ImGuiTestOpFlags_NoError +void ImGuiTestContext::WindowFocus(ImGuiTestRef ref, ImGuiTestOpFlags flags) +{ + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + ImGuiTestRefDesc desc(ref, NULL); + LogDebug("WindowFocus('%s')", desc.c_str()); + + ImGuiWindow* window = GetWindowByRef(ref); + IM_CHECK_SILENT(window != NULL); + if (window) + { + ImGui::FocusWindow(window); // FIXME-TESTS-NOT_SAME_AS_END_USER: In theory should be replaced by click on title-bar or tab? + Yield(); + } + + // We cannot guarantee this will work 100% + // - Some modal inhibition may kick-in. + // - Because merely hovering an item may e.g. open a window or change focus. + // In particular this can be the case with MenuItem. So trying to Open a MenuItem may lead to its child opening while hovering, + // causing this function to seemingly fail (even if the end goal was reached). + ImGuiContext& g = *UiContext; + if ((window != g.NavWindow) && !(flags & ImGuiTestOpFlags_NoError)) + LogDebug("-- Expected focused window '%s', but '%s' got focus back.", window->Name, g.NavWindow ? g.NavWindow->Name : ""); +} + +// Supported values for ImGuiTestOpFlags: +// - ImGuiTestOpFlags_NoError +// - ImGuiTestOpFlags_NoFocusWindow +// FIXME: In principle most calls to this could be replaced by WindowFocus()? +void ImGuiTestContext::WindowBringToFront(ImGuiTestRef ref, ImGuiTestOpFlags flags) +{ + ImGuiContext& g = *UiContext; + if (IsError()) + return; + + ImGuiWindow* window = GetWindowByRef(ref); + IM_CHECK_SILENT(window != NULL); + + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + if (window != g.NavWindow && !(flags & ImGuiTestOpFlags_NoFocusWindow)) + { + LogDebug("WindowBringToFront()->FocusWindow('%s')", window->Name); + ImGui::FocusWindow(window); // FIXME-TESTS-NOT_SAME_AS_END_USER: In theory should be replaced by click on title-bar or tab? + Yield(2); + } + else if (window->RootWindow != g.Windows.back()->RootWindow) + { + LogDebug("BringWindowToDisplayFront('%s') (window.back=%s)", window->Name, g.Windows.back()->Name); + ImGui::BringWindowToDisplayFront(window); // FIXME-TESTS-NOT_SAME_AS_END_USER: This is not an actually possible action for end-user. + Yield(2); + } + + // Same as WindowFocus() + if ((window != g.NavWindow) && !(flags & ImGuiTestOpFlags_NoError)) + LogDebug("-- Expected focused window '%s', but '%s' got focus back.", window->Name, g.NavWindow ? g.NavWindow->Name : ""); +} + +// Supported values for ImGuiTestOpFlags: +// - ImGuiTestOpFlags_NoFocusWindow +void ImGuiTestContext::WindowMove(ImGuiTestRef ref, ImVec2 input_pos, ImVec2 pivot, ImGuiTestOpFlags flags) +{ + if (IsError()) + return; + + ImGuiWindow* window = GetWindowByRef(ref); + IM_CHECK_SILENT(window != NULL); + + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + LogDebug("WindowMove %s (%.1f,%.1f) ", window->Name, input_pos.x, input_pos.y); + ImVec2 target_pos = ImFloor(input_pos - pivot * window->Size); + if (ImLengthSqr(target_pos - window->Pos) < 0.001f) + { + //MouseMoveToPos(window->Pos); //?? + return; + } + + if ((flags & ImGuiTestOpFlags_NoFocusWindow) == 0) + WindowFocus(window->ID); + WindowCollapse(window->ID, false); + + MouseSetViewport(window); + MouseMoveToPos(GetWindowTitlebarPoint(ref)); + //IM_CHECK_SILENT(UiContext->HoveredWindow == window); + MouseDown(0); + + // Disable docking +#ifdef IMGUI_HAS_DOCK + if (UiContext->IO.ConfigDockingWithShift) + KeyUp(ImGuiMod_Shift); + else + KeyDown(ImGuiMod_Shift); +#endif + + ImVec2 delta = target_pos - window->Pos; + MouseMoveToPos(Inputs->MousePosValue + delta); + Yield(); + + MouseUp(); +#ifdef IMGUI_HAS_DOCK + KeyUp(ImGuiMod_Shift); +#endif + MouseSetViewport(window); // Update in case window has changed viewport +} + +void ImGuiTestContext::WindowResize(ImGuiTestRef ref, ImVec2 size) +{ + if (IsError()) + return; + + ImGuiWindow* window = GetWindowByRef(ref); + IM_CHECK_SILENT(window != NULL); + size = ImFloor(size); + + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + LogDebug("WindowResize %s (%.1f,%.1f)", window->Name, size.x, size.y); + if (ImLengthSqr(size - window->Size) < 0.001f) + return; + + WindowFocus(window->ID); + WindowCollapse(window->ID, false); + + // Extra yield as newly created window that have AutoFitFramesX/AutoFitFramesY set are temporarily not submitting their resize widgets. Give them a bit of slack. + Yield(); + + ImGuiID id = ImGui::GetWindowResizeCornerID(window, 0); + MouseMove(id, ImGuiTestOpFlags_IsSecondAttempt); + + if (size.x <= 0.0f || size.y <= 0.0f) + { + IM_ASSERT(size.x <= 0.0f && size.y <= 0.0f); + MouseDoubleClick(0); + Yield(); + } + else + { + MouseDown(0); + ImVec2 delta = size - window->Size; + MouseMoveToPos(Inputs->MousePosValue + delta); + Yield(); // At this point we don't guarantee the final size! + MouseUp(); + } + MouseSetViewport(window); // Update in case window has changed viewport +} + +void ImGuiTestContext::PopupCloseOne() +{ + if (IsError()) + return; + + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + LogDebug("PopupCloseOne"); + ImGuiContext& g = *UiContext; + if (g.OpenPopupStack.Size > 0) + ImGui::ClosePopupToLevel(g.OpenPopupStack.Size - 1, true); // FIXME-TESTS-NOT_SAME_AS_END_USER + Yield(); +} + +void ImGuiTestContext::PopupCloseAll() +{ + if (IsError()) + return; + + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + LogDebug("PopupCloseAll"); + ImGuiContext& g = *UiContext; + if (g.OpenPopupStack.Size > 0) + ImGui::ClosePopupToLevel(0, true); // FIXME-TESTS-NOT_SAME_AS_END_USER + Yield(); +} + +// Match code in BeginPopupEx() +ImGuiID ImGuiTestContext::PopupGetWindowID(ImGuiTestRef ref) +{ + Str30f popup_name("//##Popup_%08x", GetID(ref)); + return GetID(popup_name.c_str()); +} + +#ifdef IMGUI_HAS_VIEWPORT +// Simulate a platform focus WITHOUT a click perceived by dear imgui. Similare to clicking on Platform title bar. +void ImGuiTestContext::ViewportPlatform_SetWindowFocus(ImGuiViewport* viewport) +{ + if (IsError()) + return; + + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + LogDebug("ViewportPlatform_SetWindowFocus(0x%08X)", viewport->ID); + Inputs->Queue.push_back(ImGuiTestInput::ForViewportFocus(viewport->ID)); // Queued since this will poke into backend, best to do in main thread. + Yield(); // Submit to Platform + Yield(); // Let Dear ImGui next frame see it +} + +// Simulate a platform window closure. +void ImGuiTestContext::ViewportPlatform_CloseWindow(ImGuiViewport* viewport) +{ + if (IsError()) + return; + + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + LogDebug("ViewportPlatform_CloseWindow(0x%08X)", viewport->ID); + Inputs->Queue.push_back(ImGuiTestInput::ForViewportClose(viewport->ID)); // Queued since this will poke into backend, best to do in main thread. + Yield(); // Submit to Platform + Yield(3); // Let Dear ImGui next frame see it +} + +#endif + +#ifdef IMGUI_HAS_DOCK +// Note: unlike DockBuilder functions, for _nodes_ this require the node to be visible. +// Supported values for ImGuiTestOpFlags: +// - ImGuiTestOpFlags_NoFocusWindow +// FIXME-TESTS: USING ImGuiTestOpFlags_NoFocusWindow leads to increase of ForeignWindowsHideOverPos(), best to avoid +void ImGuiTestContext::DockInto(ImGuiTestRef src_id, ImGuiTestRef dst_id, ImGuiDir split_dir, bool split_outer, ImGuiTestOpFlags flags) +{ + ImGuiContext& g = *UiContext; + if (IsError()) + return; + + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + + ImGuiWindow* window_src = GetWindowByRef(src_id); + ImGuiWindow* window_dst = GetWindowByRef(dst_id); + ImGuiDockNode* node_src = ImGui::DockBuilderGetNode(GetID(src_id)); + ImGuiDockNode* node_dst = ImGui::DockBuilderGetNode(GetID(dst_id)); + IM_CHECK_SILENT((window_src != NULL) != (node_src != NULL)); // Src must be either a window either a node + IM_CHECK_SILENT((window_dst != NULL) != (node_dst != NULL)); // Dst must be either a window either a node + + // Infer node from window. Not the opposite as docking a node would imply docking all of it. + if (node_src) + window_src = node_src->HostWindow; + if (node_dst) + window_dst = node_dst->HostWindow; + + Str128f log("DockInto() Src: %s '%s' (0x%08X), Dst: %s '%s' (0x%08X), SplitDir = %d", + node_src ? "node" : "window", node_src ? "" : window_src->Name, node_src ? node_src->ID : window_src->ID, + node_dst ? "node" : "window", node_dst ? "" : window_dst->Name, node_dst ? node_dst->ID : window_dst->ID, split_dir); + LogDebug("%s", log.c_str()); + + IM_CHECK_SILENT(window_src != NULL); + IM_CHECK_SILENT(window_dst != NULL); + IM_CHECK_SILENT(window_src->WasActive); + IM_CHECK_SILENT(window_dst->WasActive); + + // Avoid focusing if we don't need it (this facilitate avoiding focus flashing when recording animated gifs) + if (!(flags & ImGuiTestOpFlags_NoFocusWindow)) + { + if (g.Windows[g.Windows.Size - 2] != window_dst) + WindowFocus(window_dst->ID); + if (g.Windows[g.Windows.Size - 1] != window_src) + WindowFocus(window_src->ID); + } + + // Aim at title bar or tab or node grab + ImGuiTestRef ref_src; + if (node_src) + ref_src = ImGui::DockNodeGetWindowMenuButtonId(node_src); // Whole node grab + else + ref_src = (window_src->DockIsActive ? window_src->TabId : window_src->MoveId); // FIXME-TESTS FIXME-DOCKING: Identify tab + MouseMove(ref_src, ImGuiTestOpFlags_NoCheckHoveredId); + SleepStandard(); + + // Start dragging source, so it gets undocked already, because we calculate target position + // (Consider the possibility that dragging this out will move target position) + MouseDown(0); + if (g.IO.ConfigDockingWithShift) + KeyDown(ImGuiMod_Shift); + MouseLiftDragThreshold(); + if (window_src->DockIsActive) + MouseMoveToPos(g.IO.MousePos + ImVec2(0, ImGui::GetFrameHeight() * 2.0f)); + // (Button still held) + + // Locate target + ImVec2 drop_pos; + bool drop_is_valid = ImGui::DockContextCalcDropPosForDocking(window_dst, node_dst, window_src, node_src, split_dir, split_outer, &drop_pos); + IM_CHECK_SILENT(drop_is_valid); + if (!drop_is_valid) + { + if (g.IO.ConfigDockingWithShift) + KeyUp(ImGuiMod_Shift); + return; + } + + // Ensure we can reach target + WindowTeleportToMakePosVisible(window_dst->ID, drop_pos); + ImGuiWindow* friend_windows[] = { window_src, window_dst, NULL }; + ForeignWindowsHideOverPos(drop_pos, friend_windows); + + // Drag + drop_is_valid = ImGui::DockContextCalcDropPosForDocking(window_dst, node_dst, window_src, node_src, split_dir, split_outer, &drop_pos); + IM_CHECK_SILENT(drop_is_valid); + MouseSetViewport(window_dst); + MouseMoveToPos(drop_pos); + if (node_src) + window_src = node_src->HostWindow; // Dragging a menu button may detach a node and create a new window. + IM_CHECK_SILENT(g.MovingWindow == window_src); + + Yield(2); // Docking to dockspace over viewport (needs extra frame) or moving a dock node to another node (needs two extra frames) fails in fast mode without this. + IM_CHECK_SILENT(g.HoveredWindowUnderMovingWindow && g.HoveredWindowUnderMovingWindow->RootWindowDockTree == window_dst->RootWindowDockTree); + + // Docking will happen on the mouse-up + const ImGuiID prev_dock_id = window_src->DockId; + const ImGuiID prev_dock_parent_id = (window_src->DockNode && window_src->DockNode->ParentNode) ? window_src->DockNode->ParentNode->ID : 0; + const ImGuiID prev_dock_node_as_host_id = window_src->DockNodeAsHost ? window_src->DockNodeAsHost->ID : 0; + + MouseUp(0); + + // Cool down + if (g.IO.ConfigDockingWithShift) + KeyUp(ImGuiMod_Shift); + ForeignWindowsUnhideAll(); + Yield(); + Yield(); + + // Verify docking has succeeded! It's not easy to write a full fledged test, let's go for a simple one. + if (!(flags & ImGuiTestOpFlags_NoError)) + { + const ImGuiID curr_dock_id = window_src->DockId; + const ImGuiID curr_dock_parent_id = (window_src->DockNode && window_src->DockNode->ParentNode) ? window_src->DockNode->ParentNode->ID : 0; + const ImGuiID curr_dock_node_as_host_id = window_src->DockNodeAsHost ? window_src->DockNodeAsHost->ID : 0; + IM_CHECK_SILENT((prev_dock_id != curr_dock_id) || (prev_dock_parent_id != curr_dock_parent_id) || (prev_dock_node_as_host_id != curr_dock_node_as_host_id)); + } +} + +void ImGuiTestContext::DockClear(const char* window_name, ...) +{ + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + LogDebug("DockClear"); + + va_list args; + va_start(args, window_name); + while (window_name != NULL) + { + ImGui::DockBuilderDockWindow(window_name, 0); + window_name = va_arg(args, const char*); + } + va_end(args); + + if (ActiveFunc == ImGuiTestActiveFunc_TestFunc) + Yield(2); // Give time to rebuild dock in case io.ConfigDockingAlwaysTabBar is set +} + +bool ImGuiTestContext::WindowIsUndockedOrStandalone(ImGuiWindow* window) +{ + if (window->DockNode == NULL) + return true; + return DockIdIsUndockedOrStandalone(window->DockId); +} + +bool ImGuiTestContext::DockIdIsUndockedOrStandalone(ImGuiID dock_id) +{ + if (dock_id == 0) + return true; + if (ImGuiDockNode* node = ImGui::DockBuilderGetNode(dock_id)) + if (node->IsFloatingNode() && node->IsLeafNode() && node->Windows.Size == 1) + return true; + return false; +} + +void ImGuiTestContext::DockNodeHideTabBar(ImGuiDockNode* node, bool hidden) +{ + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + LogDebug("DockNodeHideTabBar %d", hidden); + + ImGuiTestRef backup_ref = GetRef(); + if (hidden) + { + SetRef(node->HostWindow); + ItemClick(ImGui::DockNodeGetWindowMenuButtonId(node)); + ImGuiID popup_id = PopupGetWindowID(GetID("#WindowMenu", node->ID)); + SetRef(popup_id); +#if IMGUI_VERSION_NUM >= 18910 + ItemClick("###HideTabBar"); +#else + ItemClick("Hide tab bar"); +#endif + IM_CHECK_SILENT(node->IsHiddenTabBar()); + } + else + { + IM_CHECK_SILENT(node->VisibleWindow != NULL); + SetRef(node->VisibleWindow); + ItemClick("#UNHIDE", 0, ImGuiTestOpFlags_MoveToEdgeD | ImGuiTestOpFlags_MoveToEdgeR); + IM_CHECK_SILENT(!node->IsHiddenTabBar()); + } + SetRef(backup_ref); +} + +void ImGuiTestContext::UndockNode(ImGuiID dock_id) +{ + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + LogDebug("UndockNode 0x%08X", dock_id); + + ImGuiDockNode* node = ImGui::DockBuilderGetNode(dock_id); + if (node == NULL) + return; + if (node->IsFloatingNode()) + return; + if (node->Windows.empty()) + return; + + const float h = node->Windows[0]->TitleBarHeight(); + if (!UiContext->IO.ConfigDockingWithShift) + KeyDown(ImGuiMod_Shift); // Disable docking + ItemDragWithDelta(ImGui::DockNodeGetWindowMenuButtonId(node), ImVec2(h, h) * -2); + if (!UiContext->IO.ConfigDockingWithShift) + KeyUp(ImGuiMod_Shift); + MouseUp(); +} + +void ImGuiTestContext::UndockWindow(const char* window_name) +{ + IM_ASSERT(window_name != NULL); + IMGUI_TEST_CONTEXT_REGISTER_DEPTH(this); + LogDebug("UndockWindow \"%s\"", window_name); + + ImGuiWindow* window = GetWindowByRef(window_name); + if (!window->DockIsActive) + return; + + const float h = window->TitleBarHeight(); + if (!UiContext->IO.ConfigDockingWithShift) + KeyDown(ImGuiMod_Shift); + ItemDragWithDelta(window->TabId, ImVec2(h, h) * -2); + if (!UiContext->IO.ConfigDockingWithShift) + KeyUp(ImGuiMod_Shift); + Yield(); +} + +#endif // #ifdef IMGUI_HAS_DOCK + +//------------------------------------------------------------------------- +// ImGuiTestContext - Performance Tools +//------------------------------------------------------------------------- + +// Calculate the reference DeltaTime, averaged over PerfIterations/500 frames, with GuiFunc disabled. +void ImGuiTestContext::PerfCalcRef() +{ + LogDebug("Measuring ref dt..."); + RunFlags |= ImGuiTestRunFlags_GuiFuncDisable; + + ImMovingAverage delta_times; + delta_times.Init(PerfIterations); + for (int n = 0; n < PerfIterations && !Abort; n++) + { + Yield(); + delta_times.AddSample(UiContext->IO.DeltaTime); + } + + PerfRefDt = delta_times.GetAverage(); + RunFlags &= ~ImGuiTestRunFlags_GuiFuncDisable; +} + +void ImGuiTestContext::PerfCapture(const char* category, const char* test_name, const char* csv_file) +{ + if (IsError()) + return; + + // Calculate reference average DeltaTime if it wasn't explicitly called by TestFunc + if (PerfRefDt < 0.0) + PerfCalcRef(); + IM_ASSERT(PerfRefDt >= 0.0); + + // Yield for the average to stabilize + LogDebug("Measuring GUI dt..."); + ImMovingAverage delta_times; + delta_times.Init(PerfIterations); + for (int n = 0; n < PerfIterations && !Abort; n++) + { + Yield(); + delta_times.AddSample(UiContext->IO.DeltaTime); + } + if (Abort) + return; + + double dt_curr = delta_times.GetAverage(); + double dt_ref_ms = PerfRefDt * 1000; + double dt_delta_ms = (dt_curr - PerfRefDt) * 1000; + + const ImBuildInfo* build_info = ImBuildGetCompilationInfo(); + + // Display results + // FIXME-TESTS: Would be nice if we could submit a custom marker (e.g. branch/feature name) + LogInfo("[PERF] Conditions: Stress x%d, %s, %s, %s, %s, %s", + PerfStressAmount, build_info->Type, build_info->Cpu, build_info->OS, build_info->Compiler, build_info->Date); + LogInfo("[PERF] Result: %+6.3f ms (from ref %+6.3f)", dt_delta_ms, dt_ref_ms); + + ImGuiPerfToolEntry entry; + entry.Timestamp = Engine->BatchStartTime; + entry.Category = category ? category : Test->Category; + entry.TestName = test_name ? test_name : Test->Name; + entry.DtDeltaMs = dt_delta_ms; + entry.PerfStressAmount = PerfStressAmount; + entry.GitBranchName = EngineIO->GitBranchName; + entry.BuildType = build_info->Type; + entry.Cpu = build_info->Cpu; + entry.OS = build_info->OS; + entry.Compiler = build_info->Compiler; + entry.Date = build_info->Date; + ImGuiTestEngine_PerfToolAppendToCSV(Engine->PerfTool, &entry, csv_file); + + // Disable the "Success" message + RunFlags |= ImGuiTestRunFlags_NoSuccessMsg; +} + +//------------------------------------------------------------------------- diff --git a/libs/imgui_test_engine/imgui_te_context.h b/libs/imgui_test_engine/imgui_te_context.h new file mode 100644 index 0000000..a469f60 --- /dev/null +++ b/libs/imgui_test_engine/imgui_te_context.h @@ -0,0 +1,616 @@ +// dear imgui test engine +// (context when a running test + end user automation API) +// This is the main (if not only) interface that your Tests will be using. + +#pragma once + +#include "imgui.h" +#include "imgui_internal.h" // ImGuiAxis, ImGuiItemStatusFlags, ImGuiInputSource, ImGuiWindow +#include "imgui_te_engine.h" // ImGuiTestStatus, ImGuiTestRunFlags, ImGuiTestActiveFunc, ImGuiTestItemInfo, ImGuiTestLogFlags + +/* + +Index of this file: +// [SECTION] Header mess, warnings +// [SECTION] Forward declarations +// [SECTION] ImGuiTestRef +// [SECTION] Helper keys +// [SECTION] ImGuiTestContext related Flags/Enumerations +// [SECTION] ImGuiTestGenericVars, ImGuiTestGenericItemStatus +// [SECTION] ImGuiTestContext +// [SECTION] Debugging macros: IM_SUSPEND_TESTFUNC() +// [SECTION] Testing/Checking macros: IM_CHECK(), IM_ERRORF() etc. + +*/ + +//------------------------------------------------------------------------- +// [SECTION] Header mess, warnings +//------------------------------------------------------------------------- + +// Undo some of the damage done by +#ifdef Yield +#undef Yield +#endif + +#if defined(__clang__) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunknown-pragmas" // warning: unknown warning group 'xxx' +#elif defined(__GNUC__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind +#pragma GCC diagnostic ignored "-Wclass-memaccess" // [__GNUC__ >= 8] warning: 'memset/memcpy' clearing/writing an object of type 'xxxx' with no trivial copy-assignment; use assignment or value-initialization instead +#endif + +//------------------------------------------------------------------------- +// [SECTION] Forward declarations +//------------------------------------------------------------------------- + +// This file +typedef int ImGuiTestOpFlags; // Flags: See ImGuiTestOpFlags_ + +// External: imgui +struct ImGuiDockNode; +struct ImGuiTabBar; +struct ImGuiWindow; + +// External: test engine +struct ImGuiTest; // A test registered with IM_REGISTER_TEST() +struct ImGuiTestEngine; // Test Engine Instance (opaque) +struct ImGuiTestEngineIO; // Test Engine IO structure (configuration flags, state) +struct ImGuiTestItemInfo; // Information gathered about an item: label, status, bounding box etc. +struct ImGuiTestItemList; // Result of an GatherItems() query +struct ImGuiTestInputs; // Test Engine Simulated Inputs structure (opaque) +struct ImGuiTestGatherTask; // Test Engine task for scanning/finding items +struct ImGuiCaptureArgs; // Parameters for ctx->CaptureXXX functions +enum ImGuiTestVerboseLevel : int; + +//------------------------------------------------------------------------- +// [SECTION] ImGuiTestRef +//------------------------------------------------------------------------- + +// Weak reference to an Item/Window given an hashed ID _or_ a string path ID. +// This is most often passed as argument to function and generally has a very short lifetime. +// Documentation: https://github.com/ocornut/imgui_test_engine/wiki/Named-References +// (SUGGESTION: add those constructors to "VA Step Filter" (Visual Assist) or a .natstepfilter file (Visual Studio) so they are skipped by F11 (StepInto) +struct IMGUI_API ImGuiTestRef +{ + ImGuiID ID; // Pre-hashed ID + const char* Path; // Relative or absolute path (string pointed to, not owned, as our lifetime is very short) + + ImGuiTestRef() { ID = 0; Path = NULL; } + ImGuiTestRef(ImGuiID id) { ID = id; Path = NULL; } + ImGuiTestRef(const char* path) { ID = 0; Path = path; } + bool IsEmpty() const { return ID == 0 && (Path == NULL || Path[0] == 0); } +}; + +// Debug helper to output a string showing the Path, ID or Debug Label based on what is available (some items only have ID as we couldn't find/store a Path) +// (The size is arbitrary, this is only used for logging info the user/debugger) +struct IMGUI_API ImGuiTestRefDesc +{ + char Buf[80]; + + const char* c_str() { return Buf; } + ImGuiTestRefDesc(const ImGuiTestRef& ref, const ImGuiTestItemInfo* item); +}; + +//------------------------------------------------------------------------- +// [SECTION] ImGuiTestContext related Flags/Enumerations +//------------------------------------------------------------------------- + +// Named actions. Generally you will call the named helpers e.g. ItemClick(). This is used by shared/low-level functions such as ItemAction(). +enum ImGuiTestAction +{ + ImGuiTestAction_Unknown = 0, + ImGuiTestAction_Hover, // Move mouse + ImGuiTestAction_Click, // Move mouse and click + ImGuiTestAction_DoubleClick, // Move mouse and double-click + ImGuiTestAction_Check, // Check item if unchecked (Checkbox, MenuItem or any widget reporting ImGuiItemStatusFlags_Checkable) + ImGuiTestAction_Uncheck, // Uncheck item if checked + ImGuiTestAction_Open, // Open item if closed (TreeNode, BeginMenu or any widget reporting ImGuiItemStatusFlags_Openable) + ImGuiTestAction_Close, // Close item if opened + ImGuiTestAction_Input, // Start text inputing into a field (e.g. CTRL+Click on Drags/Slider, click on InputText etc.) + ImGuiTestAction_NavActivate, // Activate item with navigation + ImGuiTestAction_COUNT +}; + +// Generic flags for many ImGuiTestContext functions +enum ImGuiTestOpFlags_ +{ + ImGuiTestOpFlags_None = 0, + ImGuiTestOpFlags_NoCheckHoveredId = 1 << 1, // Don't check for HoveredId after aiming for a widget. A few situations may want this: while e.g. dragging or another items prevents hovering, or for items that don't use ItemHoverable() + ImGuiTestOpFlags_NoError = 1 << 2, // Don't abort/error e.g. if the item cannot be found or the operation doesn't succeed. + ImGuiTestOpFlags_NoFocusWindow = 1 << 3, // Don't focus window when aiming at an item + ImGuiTestOpFlags_NoAutoUncollapse = 1 << 4, // Disable automatically uncollapsing windows (useful when specifically testing Collapsing behaviors) + ImGuiTestOpFlags_NoAutoOpenFullPath = 1 << 5, // Disable automatically opening intermediaries (e.g. ItemClick("Hello/OK") will automatically first open "Hello" if "OK" isn't found. Only works if ref is a string path. + ImGuiTestOpFlags_IsSecondAttempt = 1 << 6, // Used by recursing functions to indicate a second attempt + ImGuiTestOpFlags_MoveToEdgeL = 1 << 7, // Simple Dumb aiming helpers to test widget that care about clicking position. May need to replace will better functionalities. + ImGuiTestOpFlags_MoveToEdgeR = 1 << 8, + ImGuiTestOpFlags_MoveToEdgeU = 1 << 9, + ImGuiTestOpFlags_MoveToEdgeD = 1 << 10, +}; + +// Advanced filtering for ItemActionAll() +struct IMGUI_API ImGuiTestActionFilter +{ + int MaxDepth; + int MaxPasses; + const int* MaxItemCountPerDepth; + ImGuiItemStatusFlags RequireAllStatusFlags; + ImGuiItemStatusFlags RequireAnyStatusFlags; + + ImGuiTestActionFilter() { MaxDepth = -1; MaxPasses = -1; MaxItemCountPerDepth = NULL; RequireAllStatusFlags = RequireAnyStatusFlags = 0; } +}; + +//------------------------------------------------------------------------- +// [SECTION] ImGuiTestGenericVars, ImGuiTestGenericItemStatus +//------------------------------------------------------------------------- + +// Helper struct to store various query-able state of an item. +// This facilitate interactions between GuiFunc and TestFunc, since those state are frequently used. +struct IMGUI_API ImGuiTestGenericItemStatus +{ + int RetValue; // return value + int Hovered; // result of IsItemHovered() + int Active; // result of IsItemActive() + int Focused; // result of IsItemFocused() + int Clicked; // result of IsItemClicked() + int Visible; // result of IsItemVisible() + int Edited; // result of IsItemEdited() + int Activated; // result of IsItemActivated() + int Deactivated; // result of IsItemDeactivated() + int DeactivatedAfterEdit; // result of IsItemDeactivatedAfterEdit() + + ImGuiTestGenericItemStatus() { Clear(); } + void Clear() { memset(this, 0, sizeof(*this)); } + void QuerySet(bool ret_val = false) { Clear(); QueryInc(ret_val); } + void QueryInc(bool ret_val = false) { RetValue += ret_val; Hovered += ImGui::IsItemHovered(); Active += ImGui::IsItemActive(); Focused += ImGui::IsItemFocused(); Clicked += ImGui::IsItemClicked(); Visible += ImGui::IsItemVisible(); Edited += ImGui::IsItemEdited(); Activated += ImGui::IsItemActivated(); Deactivated += ImGui::IsItemDeactivated(); DeactivatedAfterEdit += ImGui::IsItemDeactivatedAfterEdit(); } +}; + +// Generic structure with various storage fields. +// This is useful for tests to quickly share data between GuiFunc and TestFunc without creating custom data structure. +// If those fields are not enough: using test->SetVarsDataType<>() + ctx->GetVars<>() it is possible to store custom data. +struct IMGUI_API ImGuiTestGenericVars +{ + // Generic storage with a bit of semantic to make user/test code look neater + int Step; + int Count; + ImGuiID DockId; + ImGuiID OwnerId; + ImGuiWindowFlags WindowFlags; + ImGuiTableFlags TableFlags; + ImGuiPopupFlags PopupFlags; + ImGuiTestGenericItemStatus Status; + bool ShowWindow1, ShowWindow2; + bool UseClipper; + bool UseViewports; + float Width; + ImVec2 Pos; + ImVec2 Size; + ImVec2 Pivot; + ImVec4 Color1, Color2; + + // Generic unnamed storage + int Int1, Int2, IntArray[10]; + float Float1, Float2, FloatArray[10]; + bool Bool1, Bool2, BoolArray[10]; + ImGuiID Id, IdArray[10]; + char Str1[256], Str2[256]; + + ImGuiTestGenericVars() { Clear(); } + void Clear() { memset(this, 0, sizeof(*this)); } +}; + +//------------------------------------------------------------------------- +// [SECTION] ImGuiTestContext +// Context for a running ImGuiTest +// This is the interface that most tests will interact with. +//------------------------------------------------------------------------- + +struct IMGUI_API ImGuiTestContext +{ + // User variables + ImGuiTestGenericVars GenericVars; // Generic variables holder for convenience. + void* UserVars = NULL; // Access using ctx->GetVars(). Setup with test->SetVarsDataType<>(). + + // Public fields + ImGuiContext* UiContext = NULL; // UI context + ImGuiTestEngineIO* EngineIO = NULL; // Test Engine IO/settings + ImGuiTest* Test = NULL; // Test currently running + ImGuiTestOutput* TestOutput = NULL; // Test output (generally == &Test->Output) + ImGuiTestOpFlags OpFlags = ImGuiTestOpFlags_None; // Flags affecting all operation (supported: ImGuiTestOpFlags_NoAutoUncollapse) + int PerfStressAmount = 0; // Convenience copy of engine->IO.PerfStressAmount + int FrameCount = 0; // Test frame count (restarts from zero every time) + int FirstTestFrameCount = 0; // First frame where TestFunc is running (after warm-up frame). This is generally -1 or 0 depending on whether we have warm up enabled + bool FirstGuiFrame = false; + bool HasDock = false; // #ifdef IMGUI_HAS_DOCK expressed in an easier to test value + ImGuiCaptureArgs* CaptureArgs = NULL; // Capture settings used by ctx->Capture*() functions + + //------------------------------------------------------------------------- + // [Internal Fields] + //------------------------------------------------------------------------- + + ImGuiTestEngine* Engine = NULL; + ImGuiTestInputs* Inputs = NULL; + ImGuiTestRunFlags RunFlags = ImGuiTestRunFlags_None; + ImGuiTestActiveFunc ActiveFunc = ImGuiTestActiveFunc_None; // None/GuiFunc/TestFunc + double RunningTime = 0.0; // Amount of wall clock time the Test has been running. Used by safety watchdog. + int ActionDepth = 0; // Nested depth of ctx-> function calls (used to decorate log) + int CaptureCounter = 0; // Number of captures + int ErrorCounter = 0; // Number of errors (generally this maxxes at 1 as most functions will early out) + bool Abort = false; + double PerfRefDt = -1.0; + int PerfIterations = 400; // Number of frames for PerfCapture() measurements + char RefStr[256] = { 0 }; // Reference window/path over which all named references are based + ImGuiID RefID = 0; // Reference ID over which all named references are based + ImGuiID RefWindowID = 0; // ID of a window that contains RefID item + ImGuiInputSource InputMode = ImGuiInputSource_Mouse; // Prefer interacting with mouse/keyboard/gamepad + ImVector Clipboard; // Private clipboard for the test instance + ImVector ForeignWindowsToHide; + ImGuiTestItemInfo DummyItemInfoNull; // Storage for ItemInfoNull() + bool CachedLinesPrintedToTTY = false; + + //------------------------------------------------------------------------- + // Public API + //------------------------------------------------------------------------- + + // Main control + void RecoverFromUiContextErrors(); + void Finish(ImGuiTestStatus status = ImGuiTestStatus_Success); // Set test status and stop running. Usually called when running test logic from GuiFunc() only. + ImGuiTestStatus RunChildTest(const char* test_name, ImGuiTestRunFlags flags = 0); // [Experimental] Run another test from the current test. + template T& GetVars() { IM_ASSERT(UserVars != NULL); return *(T*)(UserVars); }// Campanion to using t->SetVarsDataType<>(). FIXME: Assert to compare sizes + + // Main status queries + bool IsError() const { return TestOutput->Status == ImGuiTestStatus_Error || Abort; } + bool IsWarmUpGuiFrame() const { return FrameCount < FirstTestFrameCount; } // Unless test->Flags has ImGuiTestFlags_NoGuiWarmUp, we run GuiFunc() twice before running TestFunc(). Those frames are called "WarmUp" frames. + bool IsFirstGuiFrame() const { return FirstGuiFrame; } + bool IsFirstTestFrame() const { return FrameCount == FirstTestFrameCount; } // First frame where TestFunc is running (after warm-up frame). + bool IsGuiFuncOnly() const { return (RunFlags & ImGuiTestRunFlags_GuiFuncOnly) != 0; } + + // Debugging + bool SuspendTestFunc(const char* file = NULL, int line = 0); // [DEBUG] Generally called via IM_SUSPEND_TESTFUNC + + // Logging + void LogEx(ImGuiTestVerboseLevel level, ImGuiTestLogFlags flags, const char* fmt, ...) IM_FMTARGS(4); + void LogExV(ImGuiTestVerboseLevel level, ImGuiTestLogFlags flags, const char* fmt, va_list args) IM_FMTLIST(4); + void LogToTTY(ImGuiTestVerboseLevel level, const char* message, const char* message_end = NULL); + void LogToDebugger(ImGuiTestVerboseLevel level, const char* message); + void LogDebug(const char* fmt, ...) IM_FMTARGS(2); // ImGuiTestVerboseLevel_Debug or ImGuiTestVerboseLevel_Trace depending on context depth + void LogInfo(const char* fmt, ...) IM_FMTARGS(2); // ImGuiTestVerboseLevel_Info + void LogWarning(const char* fmt, ...) IM_FMTARGS(2); // ImGuiTestVerboseLevel_Warning + void LogError(const char* fmt, ...) IM_FMTARGS(2); // ImGuiTestVerboseLevel_Error + void LogBasicUiState(); + void LogItemList(ImGuiTestItemList* list); + + // Yield, Timing + void Yield(int count = 1); + void YieldUntil(int frame_count); + void Sleep(float time_in_second); // Sleep for a given simulation time, unless in Fast mode + void SleepShort(); // Standard short delay of io.ActionDelayShort (~0.15f), unless in Fast mode. + void SleepStandard(); // Standard regular delay of io.ActionDelayStandard (~0.40f), unless in Fast mode. + void SleepNoSkip(float time_in_second, float framestep_in_second); + + // Base Reference + // - ItemClick("Window/Button") --> click "Window/Button" + // - SetRef("Window"), ItemClick("Button") --> click "Window/Button" + // - SetRef("Window"), ItemClick("/Button") --> click "Window/Button" + // - SetRef("Window"), ItemClick("//Button") --> click "/Button" + // - SetRef("//$FOCUSED"), ItemClick("Button") --> click "Button" in focused window. + // See https://github.com/ocornut/imgui_test_engine/wiki/Named-References about using ImGuiTestRef in all ImGuiTestContext functions. + // Note: SetRef() may take multiple frames to complete if specified ref is an item id. + void SetRef(ImGuiTestRef ref); + void SetRef(ImGuiWindow* window); // Shortcut to SetRef(window->Name) which works for ChildWindow (see code) + ImGuiTestRef GetRef(); + + // Windows + ImGuiTestItemInfo* WindowInfo(ImGuiTestRef window_ref, ImGuiTestOpFlags flags = ImGuiTestOpFlags_None); + void WindowClose(ImGuiTestRef window_ref); + void WindowCollapse(ImGuiTestRef window_ref, bool collapsed); + void WindowFocus(ImGuiTestRef window_ref, ImGuiTestOpFlags flags = ImGuiTestOpFlags_None); + void WindowBringToFront(ImGuiTestRef window_ref, ImGuiTestOpFlags flags = ImGuiTestOpFlags_None); + void WindowMove(ImGuiTestRef window_ref, ImVec2 pos, ImVec2 pivot = ImVec2(0.0f, 0.0f), ImGuiTestOpFlags flags = ImGuiTestOpFlags_None); + void WindowResize(ImGuiTestRef window_ref, ImVec2 sz); + bool WindowTeleportToMakePosVisible(ImGuiTestRef window_ref, ImVec2 pos_in_window); + ImGuiWindow*GetWindowByRef(ImGuiTestRef window_ref); + + // Popups + void PopupCloseOne(); + void PopupCloseAll(); + ImGuiID PopupGetWindowID(ImGuiTestRef ref); + + // Get hash for a decorated ID Path. + // Note: for windows you may use WindowInfo() + ImGuiID GetID(ImGuiTestRef ref); + ImGuiID GetID(ImGuiTestRef ref, ImGuiTestRef seed_ref); + + // Miscellaneous helpers + ImVec2 GetPosOnVoid(ImGuiViewport* viewport); // Find a point that has no windows // FIXME: This needs error return and flag to enable/disable forcefully finding void. + ImVec2 GetWindowTitlebarPoint(ImGuiTestRef window_ref); // Return a clickable point on window title-bar (window tab for docked windows). + ImVec2 GetMainMonitorWorkPos(); // Work pos and size of main viewport when viewports are disabled, or work pos and size of monitor containing main viewport when viewports are enabled. + ImVec2 GetMainMonitorWorkSize(); + + // Screenshot/Video Captures + void CaptureReset(); // Reset state (use when doing multiple captures) + void CaptureSetExtension(const char* ext); // Set capture file format (otherwise for video this default to EngineIO->VideoCaptureExtension) + bool CaptureAddWindow(ImGuiTestRef ref); // Add window to be captured (default to capture everything) + void CaptureScreenshotWindow(ImGuiTestRef ref, int capture_flags = 0); // Trigger a screen capture of a single window (== CaptureAddWindow() + CaptureScreenshot()) + bool CaptureScreenshot(int capture_flags = 0); // Trigger a screen capture + bool CaptureBeginVideo(); // Start a video capture + bool CaptureEndVideo(); + + // Mouse inputs + void MouseMove(ImGuiTestRef ref, ImGuiTestOpFlags flags = ImGuiTestOpFlags_None); + void MouseMoveToPos(ImVec2 pos); + void MouseTeleportToPos(ImVec2 pos); + void MouseClick(ImGuiMouseButton button = 0); + void MouseClickMulti(ImGuiMouseButton button, int count); + void MouseDoubleClick(ImGuiMouseButton button = 0); + void MouseDown(ImGuiMouseButton button = 0); + void MouseUp(ImGuiMouseButton button = 0); + void MouseLiftDragThreshold(ImGuiMouseButton button = 0); + void MouseDragWithDelta(ImVec2 delta, ImGuiMouseButton button = 0); + void MouseWheel(ImVec2 delta); + void MouseWheelX(float dx) { MouseWheel(ImVec2(dx, 0.0f)); } + void MouseWheelY(float dy) { MouseWheel(ImVec2(0.0f, dy)); } + void MouseMoveToVoid(ImGuiViewport* viewport = NULL); + void MouseClickOnVoid(ImGuiMouseButton button = 0, ImGuiViewport* viewport = NULL); + ImGuiWindow*FindHoveredWindowAtPos(const ImVec2& pos); + bool FindExistingVoidPosOnViewport(ImGuiViewport* viewport, ImVec2* out); + + // Mouse inputs: Viewports + // - This is automatically called by SetRef() and any mouse action taking an item reference (e.g. ItemClick("button"), MouseClick("button")) + // - But when using raw position directy e.g. MouseMoveToPos() / MouseTeleportToPos() without referring to the parent window before, this needs to be set. + void MouseSetViewport(ImGuiWindow* window); + void MouseSetViewportID(ImGuiID viewport_id); + + // Keyboard inputs + void KeyDown(ImGuiKeyChord key_chord); + void KeyUp(ImGuiKeyChord key_chord); + void KeyPress(ImGuiKeyChord key_chord, int count = 1); + void KeyHold(ImGuiKeyChord key_chord, float time); + void KeySetEx(ImGuiKeyChord key_chord, bool is_down, float time); + void KeyChars(const char* chars); // Input characters + void KeyCharsAppend(const char* chars); // Input characters at end of field + void KeyCharsAppendEnter(const char* chars); // Input characters at end of field, press Enter + void KeyCharsReplace(const char* chars); // Delete existing field then input characters + void KeyCharsReplaceEnter(const char* chars); // Delete existing field then input characters, press Enter + + // Navigation inputs + // FIXME: Need some redesign/refactoring: + // - This was initially intended to: replace mouse action with keyboard/gamepad + // - Abstract keyboard vs gamepad actions + // However this is widely inconsistent and unfinished at this point. + void SetInputMode(ImGuiInputSource input_mode); // Mouse or Keyboard or Gamepad. In Keyboard or Gamepad mode, actions such as ItemClick or ItemInput are using nav facilities instead of Mouse. + void NavMoveTo(ImGuiTestRef ref); + void NavActivate(); // Activate current selected item: activate button, tweak sliders/drags. Equivalent of pressing Space on keyboard, ImGuiKey_GamepadFaceUp on a gamepad. + void NavInput(); // Input into select item: input sliders/drags. Equivalent of pressing Enter on keyboard, ImGuiKey_GamepadFaceDown on a gamepad. + + // Scrolling + void ScrollTo(ImGuiTestRef ref, ImGuiAxis axis, float scroll_v, ImGuiTestOpFlags flags = ImGuiTestOpFlags_None); + void ScrollToX(ImGuiTestRef ref, float scroll_x) { ScrollTo(ref, ImGuiAxis_X, scroll_x); } + void ScrollToY(ImGuiTestRef ref, float scroll_y) { ScrollTo(ref, ImGuiAxis_Y, scroll_y); } + void ScrollToTop(ImGuiTestRef ref); + void ScrollToBottom(ImGuiTestRef ref); + void ScrollToItem(ImGuiTestRef ref, ImGuiAxis axis, ImGuiTestOpFlags flags = ImGuiTestOpFlags_None); + void ScrollToItemX(ImGuiTestRef ref); + void ScrollToItemY(ImGuiTestRef ref); + void ScrollToTabItem(ImGuiTabBar* tab_bar, ImGuiID tab_id); + bool ScrollErrorCheck(ImGuiAxis axis, float expected, float actual, int* remaining_attempts); + void ScrollVerifyScrollMax(ImGuiTestRef ref); + + // Low-level queries + // - ItemInfo queries never returns a NULL pointer, instead they return an empty instance (info->IsEmpty(), info->ID == 0) and set contexted as errored. + // - You can use ImGuiTestOpFlags_NoError to do a query without marking context as errored. This is what ItemExists() does. + ImGuiTestItemInfo* ItemInfo(ImGuiTestRef ref, ImGuiTestOpFlags flags = ImGuiTestOpFlags_None); + ImGuiTestItemInfo* ItemInfoOpenFullPath(ImGuiTestRef ref, ImGuiTestOpFlags flags = ImGuiTestOpFlags_None); + ImGuiID ItemInfoHandleWildcardSearch(const char* wildcard_prefix_start, const char* wildcard_prefix_end, const char* wildcard_suffix_start); + ImGuiTestItemInfo* ItemInfoNull(); + void GatherItems(ImGuiTestItemList* out_list, ImGuiTestRef parent, int depth = -1); + + // Item/Widgets manipulation + void ItemAction(ImGuiTestAction action, ImGuiTestRef ref, ImGuiTestOpFlags flags = 0, void* action_arg = NULL); + void ItemClick(ImGuiTestRef ref, ImGuiMouseButton button = 0, ImGuiTestOpFlags flags = 0) { ItemAction(ImGuiTestAction_Click, ref, flags, (void*)(size_t)button); } + void ItemDoubleClick(ImGuiTestRef ref, ImGuiTestOpFlags flags = 0) { ItemAction(ImGuiTestAction_DoubleClick, ref, flags); } + void ItemCheck(ImGuiTestRef ref, ImGuiTestOpFlags flags = 0) { ItemAction(ImGuiTestAction_Check, ref, flags); } + void ItemUncheck(ImGuiTestRef ref, ImGuiTestOpFlags flags = 0) { ItemAction(ImGuiTestAction_Uncheck, ref, flags); } + void ItemOpen(ImGuiTestRef ref, ImGuiTestOpFlags flags = 0) { ItemAction(ImGuiTestAction_Open, ref, flags); } + void ItemClose(ImGuiTestRef ref, ImGuiTestOpFlags flags = 0) { ItemAction(ImGuiTestAction_Close, ref, flags); } + void ItemInput(ImGuiTestRef ref, ImGuiTestOpFlags flags = 0) { ItemAction(ImGuiTestAction_Input, ref, flags); } + void ItemNavActivate(ImGuiTestRef ref, ImGuiTestOpFlags flags = 0) { ItemAction(ImGuiTestAction_NavActivate, ref, flags); } + bool ItemOpenFullPath(ImGuiTestRef); + + // Item/Widgets: Batch actions over an entire scope + void ItemActionAll(ImGuiTestAction action, ImGuiTestRef ref_parent, const ImGuiTestActionFilter* filter = NULL); + void ItemOpenAll(ImGuiTestRef ref_parent, int depth = -1, int passes = -1); + void ItemCloseAll(ImGuiTestRef ref_parent, int depth = -1, int passes = -1); + + // Item/Widgets: Helpers to easily set a value + void ItemInputValue(ImGuiTestRef ref, int v); + void ItemInputValue(ImGuiTestRef ref, float f); + void ItemInputValue(ImGuiTestRef ref, const char* str); + + // Item/Widgets: Drag and Mouse operations + void ItemHold(ImGuiTestRef ref, float time); + void ItemHoldForFrames(ImGuiTestRef ref, int frames); + void ItemDragOverAndHold(ImGuiTestRef ref_src, ImGuiTestRef ref_dst); + void ItemDragAndDrop(ImGuiTestRef ref_src, ImGuiTestRef ref_dst, ImGuiMouseButton button = 0); + void ItemDragWithDelta(ImGuiTestRef ref_src, ImVec2 pos_delta); + + // Item/Widgets: Status query + bool ItemExists(ImGuiTestRef ref); + bool ItemIsChecked(ImGuiTestRef ref); + bool ItemIsOpened(ImGuiTestRef ref); + void ItemVerifyCheckedIfAlive(ImGuiTestRef ref, bool checked); + + // Helpers for Tab Bars widgets + void TabClose(ImGuiTestRef ref); + bool TabBarCompareOrder(ImGuiTabBar* tab_bar, const char** tab_order); + + // Helpers for MenuBar and Menus widgets + // - e.g. MenuCheck("File/Options/Enable grid"); + void MenuAction(ImGuiTestAction action, ImGuiTestRef ref); + void MenuActionAll(ImGuiTestAction action, ImGuiTestRef ref_parent); + void MenuClick(ImGuiTestRef ref) { MenuAction(ImGuiTestAction_Click, ref); } + void MenuCheck(ImGuiTestRef ref) { MenuAction(ImGuiTestAction_Check, ref); } + void MenuUncheck(ImGuiTestRef ref) { MenuAction(ImGuiTestAction_Uncheck, ref); } + void MenuCheckAll(ImGuiTestRef ref_parent) { MenuActionAll(ImGuiTestAction_Check, ref_parent); } + void MenuUncheckAll(ImGuiTestRef ref_parent) { MenuActionAll(ImGuiTestAction_Uncheck, ref_parent); } + + // Helpers for Combo Boxes + void ComboClick(ImGuiTestRef ref); + void ComboClickAll(ImGuiTestRef ref); + + // Helpers for Tables + void TableOpenContextMenu(ImGuiTestRef ref, int column_n = -1); + ImGuiSortDirection TableClickHeader(ImGuiTestRef ref, const char* label, ImGuiKeyChord key_mods = 0); + void TableSetColumnEnabled(ImGuiTestRef ref, const char* label, bool enabled); + void TableResizeColumn(ImGuiTestRef ref, int column_n, float width); + const ImGuiTableSortSpecs* TableGetSortSpecs(ImGuiTestRef ref); + + // Viewports + // IMPORTANT: Those function may alter Platform state (unless using the "Mock Viewport" backend). Use carefully. + // Those are mostly useful to simulate OS actions and testing of viewport-specific features, may not be useful to most users. +#ifdef IMGUI_HAS_VIEWPORT + //void ViewportPlatform_SetWindowPos(ImGuiViewport* viewport, const ImVec2& pos); + //void ViewportPlatform_SetWindowSize(ImGuiViewport* viewport, const ImVec2& size); + void ViewportPlatform_SetWindowFocus(ImGuiViewport* viewport); + void ViewportPlatform_CloseWindow(ImGuiViewport* viewport); +#endif + + // Docking +#ifdef IMGUI_HAS_DOCK + void DockClear(const char* window_name, ...); + void DockInto(ImGuiTestRef src_id, ImGuiTestRef dst_id, ImGuiDir split_dir = ImGuiDir_None, bool is_outer_docking = false, ImGuiTestOpFlags flags = 0); + void UndockNode(ImGuiID dock_id); + void UndockWindow(const char* window_name); + bool WindowIsUndockedOrStandalone(ImGuiWindow* window); + bool DockIdIsUndockedOrStandalone(ImGuiID dock_id); + void DockNodeHideTabBar(ImGuiDockNode* node, bool hidden); +#endif + + // Performances Measurement (use along with Dear ImGui Perf Tool) + void PerfCalcRef(); + void PerfCapture(const char* category = NULL, const char* test_name = NULL, const char* csv_file = NULL); + + // Obsolete functions +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + // Obsoleted 2022/10/11 + ImGuiID GetIDByInt(int n); // Prefer using "$$123" + ImGuiID GetIDByInt(int n, ImGuiTestRef seed_ref); + ImGuiID GetIDByPtr(void* p); // Prefer using "$$(ptr)0xFFFFFFFF" + ImGuiID GetIDByPtr(void* p, ImGuiTestRef seed_ref); + // Obsoleted 2022/09/26 + void KeyModDown(ImGuiModFlags mods) { KeyDown(mods); } + void KeyModUp(ImGuiModFlags mods) { KeyUp(mods); } + void KeyModPress(ImGuiModFlags mods) { KeyPress(mods); } +#endif + + // [Internal] + // FIXME: Aim to remove this system... + void ForeignWindowsHideOverPos(ImVec2 pos, ImGuiWindow** ignore_list); + void ForeignWindowsUnhideAll(); +}; + +//------------------------------------------------------------------------- +// [SECTION] Debugging macros (IM_SUSPEND_TESTFUNC) +//------------------------------------------------------------------------- + +// Debug: Temporarily suspend TestFunc to let user interactively inspect the GUI state (user will need to press the "Continue" button to resume TestFunc execution) +#define IM_SUSPEND_TESTFUNC() do { if (ctx->SuspendTestFunc(__FILE__, __LINE__)) return; } while (0) + +//------------------------------------------------------------------------- +// [SECTION] Testing/Checking macros: IM_CHECK(), IM_ERRORF() etc. +//------------------------------------------------------------------------- + +// Helpers used by IM_CHECK_OP() macros. +// ImGuiTestEngine_GetTempStringBuilder() returns a shared instance of ImGuiTextBuffer to recycle memory allocations +template void ImGuiTestEngineUtil_appendf_auto(ImGuiTextBuffer* buf, T v) { buf->append("???"); IM_UNUSED(v); } // FIXME-TESTS: Could improve with some template magic +template<> inline void ImGuiTestEngineUtil_appendf_auto(ImGuiTextBuffer* buf, const char* v) { buf->appendf("\"%s\"", v); } +template<> inline void ImGuiTestEngineUtil_appendf_auto(ImGuiTextBuffer* buf, bool v) { buf->append(v ? "true" : "false"); } +template<> inline void ImGuiTestEngineUtil_appendf_auto(ImGuiTextBuffer* buf, ImS8 v) { buf->appendf("%d", v); } +template<> inline void ImGuiTestEngineUtil_appendf_auto(ImGuiTextBuffer* buf, ImU8 v) { buf->appendf("%u", v); } +template<> inline void ImGuiTestEngineUtil_appendf_auto(ImGuiTextBuffer* buf, ImS16 v) { buf->appendf("%hd", v); } +template<> inline void ImGuiTestEngineUtil_appendf_auto(ImGuiTextBuffer* buf, ImU16 v) { buf->appendf("%hu", v); } +template<> inline void ImGuiTestEngineUtil_appendf_auto(ImGuiTextBuffer* buf, ImS32 v) { buf->appendf("%d", v); } +template<> inline void ImGuiTestEngineUtil_appendf_auto(ImGuiTextBuffer* buf, ImU32 v) { buf->appendf("0x%08X", v); } // Assuming ImGuiID +template<> inline void ImGuiTestEngineUtil_appendf_auto(ImGuiTextBuffer* buf, ImS64 v) { buf->appendf("%lld", v); } +template<> inline void ImGuiTestEngineUtil_appendf_auto(ImGuiTextBuffer* buf, ImU64 v) { buf->appendf("%llu", v); } +template<> inline void ImGuiTestEngineUtil_appendf_auto(ImGuiTextBuffer* buf, float v) { buf->appendf("%.3f", v); } +template<> inline void ImGuiTestEngineUtil_appendf_auto(ImGuiTextBuffer* buf, double v) { buf->appendf("%f", v); } +template<> inline void ImGuiTestEngineUtil_appendf_auto(ImGuiTextBuffer* buf, ImVec2 v) { buf->appendf("(%.3f, %.3f)", v.x, v.y); } +template<> inline void ImGuiTestEngineUtil_appendf_auto(ImGuiTextBuffer* buf, const void* v) { buf->appendf("%p", v); } +template<> inline void ImGuiTestEngineUtil_appendf_auto(ImGuiTextBuffer* buf, ImGuiWindow* v){ if (v) buf->appendf("\"%s\"", v->Name); else buf->append("NULL"); } + +// We embed every macro in a do {} while(0) statement as a trick to allow using them as regular single statement, e.g. if (XXX) IM_CHECK(A); else IM_CHECK(B) +// We leave the IM_DEBUG_BREAK() outside of the check function to step out faster when using a debugger. It also has the benefit of being lighter than an IM_ASSERT(). +#define IM_CHECK(_EXPR) do { bool res = (bool)(_EXPR); if (ImGuiTestEngine_Check(__FILE__, __func__, __LINE__, ImGuiTestCheckFlags_None, res, #_EXPR)) { IM_DEBUG_BREAK(); } if (!res) return; } while (0) +#define IM_CHECK_NO_RET(_EXPR) do { bool res = (bool)(_EXPR); if (ImGuiTestEngine_Check(__FILE__, __func__, __LINE__, ImGuiTestCheckFlags_None, res, #_EXPR)) { IM_DEBUG_BREAK(); } } while (0) +#define IM_CHECK_SILENT(_EXPR) do { bool res = (bool)(_EXPR); if (ImGuiTestEngine_Check(__FILE__, __func__, __LINE__, ImGuiTestCheckFlags_SilentSuccess, res, #_EXPR)) { IM_DEBUG_BREAK(); } if (!res) return; } while (0) +#define IM_CHECK_RETV(_EXPR,_RETV) do { bool res = (bool)(_EXPR); if (ImGuiTestEngine_Check(__FILE__, __func__, __LINE__, ImGuiTestCheckFlags_None, res, #_EXPR)) { IM_DEBUG_BREAK(); } if (!res) return _RETV; } while (0) +#define IM_CHECK_SILENT_RETV(_EXPR,_RETV) do { bool res = (bool)(_EXPR); if (ImGuiTestEngine_Check(__FILE__, __func__, __LINE__, ImGuiTestCheckFlags_SilentSuccess, res, #_EXPR)) { IM_DEBUG_BREAK(); } if (!res) return _RETV; } while (0) +#define IM_ERRORF(_FMT,...) do { if (ImGuiTestEngine_Error(__FILE__, __func__, __LINE__, ImGuiTestCheckFlags_None, _FMT, __VA_ARGS__)) { IM_DEBUG_BREAK(); } } while (0) +#define IM_ERRORF_NOHDR(_FMT,...) do { if (ImGuiTestEngine_Error(NULL, NULL, 0, ImGuiTestCheckFlags_None, _FMT, __VA_ARGS__)) { IM_DEBUG_BREAK(); } } while (0) + +// Those macros allow us to print out the values of both LHS and RHS expressions involved in a check. +#define IM_CHECK_OP(_LHS, _RHS, _OP, _RETURN) \ + do \ + { \ + auto __lhs = _LHS; /* Cache to avoid side effects */ \ + auto __rhs = _RHS; \ + bool __res = __lhs _OP __rhs; \ + ImGuiTextBuffer* expr_buf = ImGuiTestEngine_GetTempStringBuilder(); \ + expr_buf->append(#_LHS " ["); \ + ImGuiTestEngineUtil_appendf_auto(expr_buf, __lhs); \ + expr_buf->append("] " #_OP " " #_RHS " ["); \ + ImGuiTestEngineUtil_appendf_auto(expr_buf, __rhs); \ + expr_buf->append("]"); \ + if (ImGuiTestEngine_Check(__FILE__, __func__, __LINE__, ImGuiTestCheckFlags_None, __res, expr_buf->c_str())) \ + IM_ASSERT(__res); \ + if (_RETURN && !__res) \ + return; \ + } while (0) + +#define IM_CHECK_STR_OP(_LHS, _RHS, _OP, _RETURN, _FLAGS) \ + do \ + { \ + bool __res; \ + if (ImGuiTestEngine_CheckStrOp(__FILE__, __func__, __LINE__, _FLAGS, #_OP, #_LHS, _LHS, #_RHS, _RHS, &__res)) \ + IM_ASSERT(__res); \ + if (_RETURN && !__res) \ + return; \ + } while (0) + +// Scalar compares +#define IM_CHECK_EQ(_LHS, _RHS) IM_CHECK_OP(_LHS, _RHS, ==, true) // Equal +#define IM_CHECK_NE(_LHS, _RHS) IM_CHECK_OP(_LHS, _RHS, !=, true) // Not Equal +#define IM_CHECK_LT(_LHS, _RHS) IM_CHECK_OP(_LHS, _RHS, < , true) // Less Than +#define IM_CHECK_LE(_LHS, _RHS) IM_CHECK_OP(_LHS, _RHS, <=, true) // Less or Equal +#define IM_CHECK_GT(_LHS, _RHS) IM_CHECK_OP(_LHS, _RHS, > , true) // Greater Than +#define IM_CHECK_GE(_LHS, _RHS) IM_CHECK_OP(_LHS, _RHS, >=, true) // Greater or Equal + +// Scalar compares, without return on failure +#define IM_CHECK_EQ_NO_RET(_LHS, _RHS) IM_CHECK_OP(_LHS, _RHS, ==, false) // Equal +#define IM_CHECK_NE_NO_RET(_LHS, _RHS) IM_CHECK_OP(_LHS, _RHS, !=, false) // Not Equal +#define IM_CHECK_LT_NO_RET(_LHS, _RHS) IM_CHECK_OP(_LHS, _RHS, < , false) // Less Than +#define IM_CHECK_LE_NO_RET(_LHS, _RHS) IM_CHECK_OP(_LHS, _RHS, <=, false) // Less or Equal +#define IM_CHECK_GT_NO_RET(_LHS, _RHS) IM_CHECK_OP(_LHS, _RHS, > , false) // Greater Than +#define IM_CHECK_GE_NO_RET(_LHS, _RHS) IM_CHECK_OP(_LHS, _RHS, >=, false) // Greater or Equal + +// String compares +#define IM_CHECK_STR_EQ(_LHS, _RHS) IM_CHECK_STR_OP(_LHS, _RHS, ==, true, ImGuiTestCheckFlags_None) +#define IM_CHECK_STR_NE(_LHS, _RHS) IM_CHECK_STR_OP(_LHS, _RHS, !=, true, ImGuiTestCheckFlags_None) +#define IM_CHECK_STR_EQ_NO_RET(_LHS, _RHS) IM_CHECK_STR_OP(_LHS, _RHS, ==, false, ImGuiTestCheckFlags_None) +#define IM_CHECK_STR_NE_NO_RET(_LHS, _RHS) IM_CHECK_STR_OP(_LHS, _RHS, !=, false, ImGuiTestCheckFlags_None) +#define IM_CHECK_STR_EQ_SILENT(_LHS, _RHS) IM_CHECK_STR_OP(_LHS, _RHS, ==, true, ImGuiTestCheckFlags_SilentSuccess) + +// Floating point compares +#define IM_CHECK_FLOAT_EQ_EPS(_LHS, _RHS) IM_CHECK_LE(ImFabs(_LHS - (_RHS)), FLT_EPSILON) // Float Equal +#define IM_CHECK_FLOAT_NEAR(_LHS, _RHS, _EPS) IM_CHECK_LE(ImFabs(_LHS - (_RHS)), _EPS) +#define IM_CHECK_FLOAT_NEAR_NO_RET(_LHS, _RHS, _E) IM_CHECK_LE_NO_RET(ImFabs(_LHS - (_RHS)), _E) + +//------------------------------------------------------------------------- + +#if defined(__clang__) +#pragma clang diagnostic pop +#elif defined(__GNUC__) +#pragma GCC diagnostic pop +#endif diff --git a/libs/imgui_test_engine/imgui_te_coroutine.cpp b/libs/imgui_test_engine/imgui_te_coroutine.cpp new file mode 100644 index 0000000..75845c2 --- /dev/null +++ b/libs/imgui_test_engine/imgui_te_coroutine.cpp @@ -0,0 +1,167 @@ +// dear imgui test engine +// (coroutine interface + optional implementation) +// Read https://github.com/ocornut/imgui_test_engine/wiki/Setting-Up + +#include "imgui_te_coroutine.h" +#include "imgui.h" + +#ifdef _MSC_VER +#pragma warning (disable: 4996) // 'This function or variable may be unsafe': strcpy, strdup, sprintf, vsnprintf, sscanf, fopen +#endif + +//------------------------------------------------------------------------ +// Coroutine implementation using std::thread +// This implements a coroutine using std::thread, with a helper thread for each coroutine (with serialised execution, so threads never actually run concurrently) +//------------------------------------------------------------------------ + +#if IMGUI_TEST_ENGINE_ENABLE_COROUTINE_STDTHREAD_IMPL + +#include "imgui_te_utils.h" +#include "thirdparty/Str/Str.h" +#include +#include +#include + +struct Coroutine_ImplStdThreadData +{ + std::thread* Thread; // The thread this coroutine is using + std::condition_variable StateChange; // Condition variable notified when the coroutine state changes + std::mutex StateMutex; // Mutex to protect coroutine state + bool CoroutineRunning; // Is the coroutine currently running? Lock StateMutex before access and notify StateChange on change + bool CoroutineTerminated; // Has the coroutine terminated? Lock StateMutex before access and notify StateChange on change + Str64 Name; // The name of this coroutine +}; + +// The coroutine executing on the current thread (if it is a coroutine thread) +static thread_local Coroutine_ImplStdThreadData* GThreadCoroutine = NULL; + +// The main function for a coroutine thread +static void CoroutineThreadMain(Coroutine_ImplStdThreadData* data, ImGuiTestCoroutineMainFunc func, void* ctx) +{ + // Set our thread name + ImThreadSetCurrentThreadDescription(data->Name.c_str()); + + // Set the thread coroutine + GThreadCoroutine = data; + + // Wait for initial Run() + while (1) + { + std::unique_lock lock(data->StateMutex); + if (data->CoroutineRunning) + break; + data->StateChange.wait(lock); + } + + // Run user code, which will then call Yield() when it wants to yield control + func(ctx); + + // Mark as terminated + { + std::lock_guard lock(data->StateMutex); + + data->CoroutineTerminated = true; + data->CoroutineRunning = false; + data->StateChange.notify_all(); + } +} + + +static ImGuiTestCoroutineHandle Coroutine_ImplStdThread_Create(ImGuiTestCoroutineMainFunc* func, const char* name, void* ctx) +{ + Coroutine_ImplStdThreadData* data = new Coroutine_ImplStdThreadData(); + + data->Name = name; + data->CoroutineRunning = false; + data->CoroutineTerminated = false; + data->Thread = new std::thread(CoroutineThreadMain, data, func, ctx); + + return (ImGuiTestCoroutineHandle)data; +} + +static void Coroutine_ImplStdThread_Destroy(ImGuiTestCoroutineHandle handle) +{ + Coroutine_ImplStdThreadData* data = (Coroutine_ImplStdThreadData*)handle; + + IM_ASSERT(data->CoroutineTerminated); // The coroutine needs to run to termination otherwise it may leak all sorts of things and this will deadlock + if (data->Thread) + { + data->Thread->join(); + + delete data->Thread; + data->Thread = NULL; + } + + delete data; + data = NULL; +} + +// Run the coroutine until the next call to Yield(). Returns TRUE if the coroutine yielded, FALSE if it terminated (or had previously terminated) +static bool Coroutine_ImplStdThread_Run(ImGuiTestCoroutineHandle handle) +{ + Coroutine_ImplStdThreadData* data = (Coroutine_ImplStdThreadData*)handle; + + // Wake up coroutine thread + { + std::lock_guard lock(data->StateMutex); + + if (data->CoroutineTerminated) + return false; // Coroutine has already finished + + data->CoroutineRunning = true; + data->StateChange.notify_all(); + } + + // Wait for coroutine to stop + while (1) + { + std::unique_lock lock(data->StateMutex); + if (!data->CoroutineRunning) + { + // Breakpoint here to catch the point where we return from the coroutine + if (data->CoroutineTerminated) + return false; // Coroutine finished + break; + } + data->StateChange.wait(lock); + } + + return true; +} + +// Yield the current coroutine (can only be called from a coroutine) +static void Coroutine_ImplStdThread_Yield() +{ + IM_ASSERT(GThreadCoroutine); // This can only be called from a coroutine thread + + Coroutine_ImplStdThreadData* data = GThreadCoroutine; + + // Flag that we are not running any more + { + std::lock_guard lock(data->StateMutex); + data->CoroutineRunning = false; + data->StateChange.notify_all(); + } + + // At this point the thread that called RunCoroutine() will leave the "Wait for coroutine to stop" loop + // Wait until we get started up again + while (1) + { + std::unique_lock lock(data->StateMutex); + if (data->CoroutineRunning) + break; // Breakpoint here if you want to catch the point where execution of this coroutine resumes + data->StateChange.wait(lock); + } +} + +ImGuiTestCoroutineInterface* Coroutine_ImplStdThread_GetInterface() +{ + static ImGuiTestCoroutineInterface intf; + intf.CreateFunc = Coroutine_ImplStdThread_Create; + intf.DestroyFunc = Coroutine_ImplStdThread_Destroy; + intf.RunFunc = Coroutine_ImplStdThread_Run; + intf.YieldFunc = Coroutine_ImplStdThread_Yield; + return &intf; +} + +#endif // #if IMGUI_TEST_ENGINE_ENABLE_COROUTINE_STDTHREAD_IMPL diff --git a/libs/imgui_test_engine/imgui_te_coroutine.h b/libs/imgui_test_engine/imgui_te_coroutine.h new file mode 100644 index 0000000..5ed423b --- /dev/null +++ b/libs/imgui_test_engine/imgui_te_coroutine.h @@ -0,0 +1,56 @@ +// dear imgui test engine +// (coroutine interface + optional implementation) +// Read https://github.com/ocornut/imgui_test_engine/wiki/Setting-Up + +#pragma once + +#ifndef IMGUI_VERSION +#include "imgui.h" +#endif + +//------------------------------------------------------------------------ +// Coroutine abstraction +//------------------------------------------------------------------------ +// Coroutines should be used like this: +// ImGuiTestCoroutineHandle handle = CoroutineCreate(, , ); // name being for debugging, and ctx being an arbitrary user context pointer +// while (CoroutineRun(handle)) { // SetUnhandledExceptionFilter() +#undef Yield // Undo some of the damage done by +#else +#include // signal() +#include // sleep() +#endif + +// Warnings +#ifdef _MSC_VER +#pragma warning (disable: 4127) // conditional expression is constant +#endif + +/* + +Index of this file: + +// [SECTION] TODO +// [SECTION] FORWARD DECLARATIONS +// [SECTION] DATA STRUCTURES +// [SECTION] TEST ENGINE FUNCTIONS +// [SECTION] CRASH HANDLING +// [SECTION] HOOKS FOR CORE LIBRARY +// [SECTION] CHECK/ERROR FUNCTIONS FOR TESTS +// [SECTION] SETTINGS +// [SECTION] ImGuiTestLog +// [SECTION] ImGuiTest + +*/ + +//------------------------------------------------------------------------- +// [SECTION] DATA +//------------------------------------------------------------------------- + +static ImGuiTestEngine* GImGuiTestEngine = NULL; + +//------------------------------------------------------------------------- +// [SECTION] FORWARD DECLARATIONS +//------------------------------------------------------------------------- + +// Private functions +static void ImGuiTestEngine_BindImGuiContext(ImGuiTestEngine* engine, ImGuiContext* ui_ctx); +static void ImGuiTestEngine_UnbindImGuiContext(ImGuiTestEngine* engine, ImGuiContext* ui_ctx); +static void ImGuiTestEngine_CoroutineStopAndJoin(ImGuiTestEngine* engine); +static void ImGuiTestEngine_StartCalcSourceLineEnds(ImGuiTestEngine* engine); +static void ImGuiTestEngine_ClearInput(ImGuiTestEngine* engine); +static void ImGuiTestEngine_ApplyInputToImGuiContext(ImGuiTestEngine* engine); +static void ImGuiTestEngine_ProcessTestQueue(ImGuiTestEngine* engine); +static void ImGuiTestEngine_ClearTests(ImGuiTestEngine* engine); +static void ImGuiTestEngine_PreNewFrame(ImGuiTestEngine* engine, ImGuiContext* ui_ctx); +static void ImGuiTestEngine_PostNewFrame(ImGuiTestEngine* engine, ImGuiContext* ui_ctx); +static void ImGuiTestEngine_PreRender(ImGuiTestEngine* engine, ImGuiContext* ui_ctx); +static void ImGuiTestEngine_PostRender(ImGuiTestEngine* engine, ImGuiContext* ui_ctx); +static void ImGuiTestEngine_UpdateHooks(ImGuiTestEngine* engine); +static void ImGuiTestEngine_RunGuiFunc(ImGuiTestEngine* engine); +static void ImGuiTestEngine_TestQueueCoroutineMain(void* engine_opaque); + +// Settings +static void* ImGuiTestEngine_SettingsReadOpen(ImGuiContext*, ImGuiSettingsHandler*, const char* name); +static void ImGuiTestEngine_SettingsReadLine(ImGuiContext*, ImGuiSettingsHandler*, void* entry, const char* line); +static void ImGuiTestEngine_SettingsWriteAll(ImGuiContext* imgui_ctx, ImGuiSettingsHandler* handler, ImGuiTextBuffer* buf); + +//------------------------------------------------------------------------- +// [SECTION] TEST ENGINE FUNCTIONS +//------------------------------------------------------------------------- +// Public +// - ImGuiTestEngine_CreateContext() +// - ImGuiTestEngine_DestroyContext() +// - ImGuiTestEngine_BindImGuiContext() +// - ImGuiTestEngine_UnbindImGuiContext() +// - ImGuiTestEngine_GetIO() +// - ImGuiTestEngine_Abort() +// - ImGuiTestEngine_QueueAllTests() +//------------------------------------------------------------------------- +// - ImGuiTestEngine_FindItemInfo() +// - ImGuiTestEngine_ClearTests() +// - ImGuiTestEngine_ApplyInputToImGuiContext() +// - ImGuiTestEngine_PreNewFrame() +// - ImGuiTestEngine_PostNewFrame() +// - ImGuiTestEngine_Yield() +// - ImGuiTestEngine_ProcessTestQueue() +// - ImGuiTestEngine_QueueTest() +// - ImGuiTestEngine_RunTest() +//------------------------------------------------------------------------- + +ImGuiTestEngine::ImGuiTestEngine() +{ + PerfRefDeltaTime = 0.0f; + PerfDeltaTime100.Init(100); + PerfDeltaTime500.Init(500); + PerfTool = IM_NEW(ImGuiPerfTool); + UiFilterTests = IM_NEW(Str256); // We bite the bullet of adding an extra alloc/indirection in order to avoid including Str.h in our header + UiFilterPerfs = IM_NEW(Str256); + + // Initialize std::thread based coroutine implementation if requested +#if IMGUI_TEST_ENGINE_ENABLE_COROUTINE_STDTHREAD_IMPL + IM_ASSERT(IO.CoroutineFuncs == NULL && "IO.CoroutineFuncs already setup elsewhere!"); + IO.CoroutineFuncs = Coroutine_ImplStdThread_GetInterface(); +#endif +} + +ImGuiTestEngine::~ImGuiTestEngine() +{ + IM_ASSERT(TestQueueCoroutine == NULL); + IM_DELETE(PerfTool); + IM_DELETE(UiFilterTests); + IM_DELETE(UiFilterPerfs); +} + +static void ImGuiTestEngine_BindImGuiContext(ImGuiTestEngine* engine, ImGuiContext* ui_ctx) +{ + IM_ASSERT(engine->UiContextTarget == ui_ctx); + + // Add .ini handle for ImGuiWindow type + if (engine->IO.ConfigSavedSettings) + { + ImGuiSettingsHandler ini_handler; + ini_handler.TypeName = "TestEngine"; + ini_handler.TypeHash = ImHashStr("TestEngine"); + ini_handler.ReadOpenFn = ImGuiTestEngine_SettingsReadOpen; + ini_handler.ReadLineFn = ImGuiTestEngine_SettingsReadLine; + ini_handler.WriteAllFn = ImGuiTestEngine_SettingsWriteAll; + ui_ctx->SettingsHandlers.push_back(ini_handler); + engine->PerfTool->_AddSettingsHandler(); + } + + // Install generic context hooks facility + ImGuiContextHook hook; + hook.Type = ImGuiContextHookType_Shutdown; + hook.Callback = [](ImGuiContext* ui_ctx, ImGuiContextHook* hook) { ImGuiTestEngine_UnbindImGuiContext((ImGuiTestEngine*)hook->UserData, ui_ctx); }; + hook.UserData = (void*)engine; + ImGui::AddContextHook(ui_ctx, &hook); + + hook.Type = ImGuiContextHookType_NewFramePre; + hook.Callback = [](ImGuiContext* ui_ctx, ImGuiContextHook* hook) { ImGuiTestEngine_PreNewFrame((ImGuiTestEngine*)hook->UserData, ui_ctx); }; + hook.UserData = (void*)engine; + ImGui::AddContextHook(ui_ctx, &hook); + + hook.Type = ImGuiContextHookType_NewFramePost; + hook.Callback = [](ImGuiContext* ui_ctx, ImGuiContextHook* hook) { ImGuiTestEngine_PostNewFrame((ImGuiTestEngine*)hook->UserData, ui_ctx); }; + hook.UserData = (void*)engine; + ImGui::AddContextHook(ui_ctx, &hook); + + hook.Type = ImGuiContextHookType_RenderPre; + hook.Callback = [](ImGuiContext* ui_ctx, ImGuiContextHook* hook) { ImGuiTestEngine_PreRender((ImGuiTestEngine*)hook->UserData, ui_ctx); }; + hook.UserData = (void*)engine; + ImGui::AddContextHook(ui_ctx, &hook); + + hook.Type = ImGuiContextHookType_RenderPost; + hook.Callback = [](ImGuiContext* ui_ctx, ImGuiContextHook* hook) { ImGuiTestEngine_PostRender((ImGuiTestEngine*)hook->UserData, ui_ctx); }; + hook.UserData = (void*)engine; + ImGui::AddContextHook(ui_ctx, &hook); + + // Install custom test engine hook data + if (GImGuiTestEngine == NULL) + GImGuiTestEngine = engine; + IM_ASSERT(ui_ctx->TestEngine == NULL); + ui_ctx->TestEngine = engine; +} + +static void ImGuiTestEngine_UnbindImGuiContext(ImGuiTestEngine* engine, ImGuiContext* ui_ctx) +{ + IM_ASSERT(engine->UiContextTarget == ui_ctx); + + // FIXME: Could use ImGui::RemoveContextHook() if we stored our hook ids + for (int hook_n = 0; hook_n < ui_ctx->Hooks.Size; hook_n++) + if (ui_ctx->Hooks[hook_n].UserData == engine) + ImGui::RemoveContextHook(ui_ctx, ui_ctx->Hooks[hook_n].HookId); + + ImGuiTestEngine_CoroutineStopAndJoin(engine); + + IM_ASSERT(ui_ctx->TestEngine == engine); + ui_ctx->TestEngine = NULL; + + // Remove .ini handler + IM_ASSERT(GImGui == ui_ctx); + if (engine->IO.ConfigSavedSettings) + { + ImGui::RemoveSettingsHandler("TestEngine"); + ImGui::RemoveSettingsHandler("TestEnginePerfTool"); + } + + // Remove hook + if (GImGuiTestEngine == engine) + GImGuiTestEngine = NULL; + engine->UiContextTarget = engine->UiContextActive = NULL; +} + +// Create test context (not bound to any dear imgui context yet) +ImGuiTestEngine* ImGuiTestEngine_CreateContext() +{ + ImGuiTestEngine* engine = IM_NEW(ImGuiTestEngine)(); + return engine; +} + +void ImGuiTestEngine_DestroyContext(ImGuiTestEngine* engine) +{ + // We require user to call DestroyContext() before ImGuiTestEngine_DestroyContext() in order to preserve ini data... + // In case of e.g. dynamically creating a TestEngine as runtime and not caring about its settings, you may set io.ConfigSavedSettings to false + // in order to allow earlier destruction of the context. + if (engine->IO.ConfigSavedSettings) + IM_ASSERT(engine->UiContextTarget == NULL && "You need to call ImGui::DestroyContext() BEFORE ImGuiTestEngine_DestroyContext()"); + + // Shutdown coroutine + ImGuiTestEngine_CoroutineStopAndJoin(engine); + if (engine->UiContextTarget != NULL) + ImGuiTestEngine_UnbindImGuiContext(engine, engine->UiContextTarget); + + ImGuiTestEngine_ClearTests(engine); + + for (int n = 0; n < engine->InfoTasks.Size; n++) + IM_DELETE(engine->InfoTasks[n]); + engine->InfoTasks.clear(); + + IM_DELETE(engine); + + // Release hook + if (GImGuiTestEngine == engine) + GImGuiTestEngine = NULL; +} + +void ImGuiTestEngine_Start(ImGuiTestEngine* engine, ImGuiContext* ui_ctx) +{ + IM_ASSERT(engine->Started == false); + IM_ASSERT(engine->UiContextTarget == NULL); + + engine->UiContextTarget = ui_ctx; + ImGuiTestEngine_BindImGuiContext(engine, engine->UiContextTarget); + ImGuiTestEngine_StartCalcSourceLineEnds(engine); + + // Create our coroutine + // (we include the word "Main" in the name to facilitate filtering for both this thread and the "Main Thread" in debuggers) + if (!engine->TestQueueCoroutine) + { + IM_ASSERT(engine->IO.CoroutineFuncs && "Missing CoroutineFuncs! Use '#define IMGUI_TEST_ENGINE_ENABLE_COROUTINE_STDTHREAD_IMPL 1' or define your own implementation!"); + engine->TestQueueCoroutine = engine->IO.CoroutineFuncs->CreateFunc(ImGuiTestEngine_TestQueueCoroutineMain, "Main Dear ImGui Test Thread", engine); + } + engine->Started = true; +} + +void ImGuiTestEngine_Stop(ImGuiTestEngine* engine) +{ + IM_ASSERT(engine->Started); + + engine->Abort = true; + ImGuiTestEngine_CoroutineStopAndJoin(engine); + ImGuiTestEngine_Export(engine); + engine->Started = false; +} + +static void ImGuiTestEngine_CoroutineStopRequest(ImGuiTestEngine* engine) +{ + if (engine->TestQueueCoroutine != NULL) + engine->TestQueueCoroutineShouldExit = true; +} + +static void ImGuiTestEngine_CoroutineStopAndJoin(ImGuiTestEngine* engine) +{ + if (engine->TestQueueCoroutine != NULL) + { + // Run until the coroutine exits + engine->TestQueueCoroutineShouldExit = true; + while (true) + { + if (!engine->IO.CoroutineFuncs->RunFunc(engine->TestQueueCoroutine)) + break; + } + engine->IO.CoroutineFuncs->DestroyFunc(engine->TestQueueCoroutine); + engine->TestQueueCoroutine = NULL; + } +} + +// [EXPERIMENTAL] Destroy and recreate ImGui context +// This potentially allow us to test issues related to handling new windows, restoring settings etc. +// This also gets us once inch closer to more dynamic management of context (e.g. jail tests in their own context) +// FIXME: This is currently called by ImGuiTestEngine_PreNewFrame() in hook but may end up needing to be called +// by main application loop in order to facilitate letting app know of the new pointers. For now none of our backends +// preserve the pointer so may be fine. +void ImGuiTestEngine_RebootUiContext(ImGuiTestEngine* engine) +{ + IM_ASSERT(engine->Started); + ImGuiContext* ctx = engine->UiContextTarget; + ImGuiTestEngine_Stop(engine); + ImGuiTestEngine_UnbindImGuiContext(engine, ctx); + + // Backup + bool backup_atlas_owned_by_context = ctx->FontAtlasOwnedByContext; + ImFontAtlas* backup_atlas = ctx->IO.Fonts; + ImGuiIO backup_io = ctx->IO; +#ifdef IMGUI_HAS_VIEWPORT + // FIXME: Break with multi-viewports as we don't preserve user windowing data properly. + // Backend tend to store e.g. HWND data in viewport 0. + if (ctx->IO.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) + IM_ASSERT(0); + //ImGuiViewport backup_viewport0 = *(ImGuiViewport*)ctx->Viewports[0]; + //ImGuiPlatformIO backup_platform_io = ctx->PlatformIO; + //ImGui::DestroyPlatformWindows(); +#endif + + // Recreate + ctx->FontAtlasOwnedByContext = false; +#if 1 + ImGui::DestroyContext(); + ImGui::CreateContext(backup_atlas); +#else + // Preserve same context pointer, which is probably misleading and not even necessary. + ImGui::Shutdown(ctx); + ctx->~ImGuiContext(); + IM_PLACEMENT_NEW(ctx) ImGuiContext(backup_atlas); + ImGui::Initialize(ctx); +#endif + + // Restore + ctx->FontAtlasOwnedByContext = backup_atlas_owned_by_context; + ctx->IO = backup_io; +#ifdef IMGUI_HAS_VIEWPORT + //backup_platform_io.Viewports.swap(ctx->PlatformIO.Viewports); + //ctx->PlatformIO = backup_platform_io; + //ctx->Viewports[0]->RendererUserData = backup_viewport0.RendererUserData; + //ctx->Viewports[0]->PlatformUserData = backup_viewport0.PlatformUserData; + //ctx->Viewports[0]->PlatformHandle = backup_viewport0.PlatformHandle; + //ctx->Viewports[0]->PlatformHandleRaw = backup_viewport0.PlatformHandleRaw; + //memset(&backup_viewport0, 0, sizeof(backup_viewport0)); +#endif + + ImGuiTestEngine_Start(engine, ctx); +} + +void ImGuiTestEngine_PostSwap(ImGuiTestEngine* engine) +{ + engine->PostSwapCalled = true; + + if (engine->IO.ConfigFixedDeltaTime != 0.0f) + ImGuiTestEngine_SetDeltaTime(engine, engine->IO.ConfigFixedDeltaTime); + + // Sync capture tool configurations from engine IO. + engine->CaptureContext.ScreenCaptureFunc = engine->IO.ScreenCaptureFunc; + engine->CaptureContext.ScreenCaptureUserData = engine->IO.ScreenCaptureUserData; + engine->CaptureContext.VideoCaptureEncoderPath = engine->IO.VideoCaptureEncoderPath; + engine->CaptureContext.VideoCaptureEncoderPathSize = IM_ARRAYSIZE(engine->IO.VideoCaptureEncoderPath); + engine->CaptureContext.VideoCaptureEncoderParams = engine->IO.VideoCaptureEncoderParams; + engine->CaptureContext.VideoCaptureEncoderParamsSize = IM_ARRAYSIZE(engine->IO.VideoCaptureEncoderParams); + engine->CaptureContext.GifCaptureEncoderParams = engine->IO.GifCaptureEncoderParams; + engine->CaptureContext.GifCaptureEncoderParamsSize = IM_ARRAYSIZE(engine->IO.GifCaptureEncoderParams); + engine->CaptureTool.VideoCaptureExtension = engine->IO.VideoCaptureExtension; + engine->CaptureTool.VideoCaptureExtensionSize = IM_ARRAYSIZE(engine->IO.VideoCaptureExtension); + + // Capture a screenshot from main thread while coroutine waits + if (engine->CaptureCurrentArgs != NULL) + { + ImGuiCaptureStatus status = engine->CaptureContext.CaptureUpdate(engine->CaptureCurrentArgs); + if (status != ImGuiCaptureStatus_InProgress) + { + if (status == ImGuiCaptureStatus_Done) + ImStrncpy(engine->CaptureTool.OutputLastFilename, engine->CaptureCurrentArgs->InOutputFile, IM_ARRAYSIZE(engine->CaptureTool.OutputLastFilename)); + engine->CaptureCurrentArgs = NULL; + } + } +} + +ImGuiTestEngineIO& ImGuiTestEngine_GetIO(ImGuiTestEngine* engine) +{ + return engine->IO; +} + +void ImGuiTestEngine_AbortCurrentTest(ImGuiTestEngine* engine) +{ + engine->Abort = true; + if (ImGuiTestContext* test_context = engine->TestContext) + test_context->Abort = true; +} + +bool ImGuiTestEngine_TryAbortEngine(ImGuiTestEngine* engine) +{ + ImGuiTestEngine_AbortCurrentTest(engine); + ImGuiTestEngine_CoroutineStopRequest(engine); + if (ImGuiTestEngine_IsTestQueueEmpty(engine)) + return true; + return false; // Still running coroutine +} + +// FIXME-OPT +ImGuiTest* ImGuiTestEngine_FindTestByName(ImGuiTestEngine* engine, const char* category, const char* name) +{ + IM_ASSERT(category != NULL || name != NULL); + for (int n = 0; n < engine->TestsAll.Size; n++) + { + ImGuiTest* test = engine->TestsAll[n]; + if (name != NULL && strcmp(test->Name, name) != 0) + continue; + if (category != NULL && strcmp(test->Category, category) != 0) + continue; + return test; + } + return NULL; +} + +// FIXME-OPT +static ImGuiTestInfoTask* ImGuiTestEngine_FindInfoTask(ImGuiTestEngine* engine, ImGuiID id) +{ + for (int task_n = 0; task_n < engine->InfoTasks.Size; task_n++) + { + ImGuiTestInfoTask* task = engine->InfoTasks[task_n]; + if (task->ID == id) + return task; + } + return NULL; +} + +// Request information about one item. +// Will push a request for the test engine to process. +// Will return NULL when results are not ready (or not available). +ImGuiTestItemInfo* ImGuiTestEngine_FindItemInfo(ImGuiTestEngine* engine, ImGuiID id, const char* debug_id) +{ + IM_ASSERT(id != 0); + + if (ImGuiTestInfoTask* task = ImGuiTestEngine_FindInfoTask(engine, id)) + { + if (task->Result.TimestampMain + 2 >= engine->FrameCount) + { + task->FrameCount = engine->FrameCount; // Renew task + return &task->Result; + } + return NULL; + } + + // Create task + ImGuiTestInfoTask* task = IM_NEW(ImGuiTestInfoTask)(); + task->ID = id; + task->FrameCount = engine->FrameCount; + if (debug_id) + { + size_t debug_id_sz = strlen(debug_id); + if (debug_id_sz < IM_ARRAYSIZE(task->DebugName) - 1) + { + memcpy(task->DebugName, debug_id, debug_id_sz + 1); + } + else + { + size_t header_sz = (size_t)(IM_ARRAYSIZE(task->DebugName) * 0.30f); + size_t footer_sz = IM_ARRAYSIZE(task->DebugName) - 2 - header_sz; + IM_ASSERT(header_sz > 0 && footer_sz > 0); + ImFormatString(task->DebugName, IM_ARRAYSIZE(task->DebugName), "%.*s..%.*s", (int)header_sz, debug_id, (int)footer_sz, debug_id + debug_id_sz - footer_sz); + } + } + engine->InfoTasks.push_back(task); + + return NULL; +} + +static void ImGuiTestEngine_ClearTests(ImGuiTestEngine* engine) +{ + for (int n = 0; n < engine->TestsAll.Size; n++) + IM_DELETE(engine->TestsAll[n]); + engine->TestsAll.clear(); + engine->TestsQueue.clear(); +} + +// Called at the beginning of a test to ensure no previous inputs leak into the new test +// FIXME-TESTS: Would make sense to reset mouse position as well? +void ImGuiTestEngine_ClearInput(ImGuiTestEngine* engine) +{ + IM_ASSERT(engine->UiContextTarget != NULL); + ImGuiContext& g = *engine->UiContextTarget; + + engine->Inputs.MouseButtonsValue = 0; + engine->Inputs.Queue.clear(); + engine->Inputs.MouseWheel = ImVec2(0, 0); + + // FIXME: Necessary? +#if IMGUI_VERSION_NUM >= 18972 + g.IO.ClearEventsQueue(); +#else + g.InputEventsQueue.resize(0); + g.IO.ClearInputCharacters(); +#endif + g.IO.ClearInputKeys(); + + ImGuiTestEngine_ApplyInputToImGuiContext(engine); +} + +bool ImGuiTestEngine_IsUsingSimulatedInputs(ImGuiTestEngine* engine) +{ + if (engine->UiContextActive) + if (!ImGuiTestEngine_IsTestQueueEmpty(engine)) + if (!(engine->TestContext->RunFlags & ImGuiTestRunFlags_GuiFuncOnly)) + return true; + return false; +} + +// Setup inputs in the tested Dear ImGui context. Essentially we override the work of the backend here. +void ImGuiTestEngine_ApplyInputToImGuiContext(ImGuiTestEngine* engine) +{ + IM_ASSERT(engine->UiContextTarget != NULL); + ImGuiContext& g = *engine->UiContextTarget; + ImGuiIO& io = g.IO; + + const bool use_simulated_inputs = ImGuiTestEngine_IsUsingSimulatedInputs(engine); + if (!use_simulated_inputs) + return; + + // Erase events submitted by backend + for (int n = 0; n < g.InputEventsQueue.Size; n++) + if (g.InputEventsQueue[n].AddedByTestEngine == false) + g.InputEventsQueue.erase(&g.InputEventsQueue[n--]); + + // Special flags to stop submitting events + if (engine->TestContext->RunFlags & ImGuiTestRunFlags_EnableRawInputs) + return; + + // To support using ImGuiKey_NavXXXX shortcuts pointing to gamepad actions + // FIXME-TEST-ENGINE: Should restore + g.IO.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; + g.IO.BackendFlags |= ImGuiBackendFlags_HasGamepad; + + const int input_event_count_prev = g.InputEventsQueue.Size; + + // Apply mouse viewport +#ifdef IMGUI_HAS_VIEWPORT + ImGuiPlatformIO& platform_io = g.PlatformIO; + ImGuiViewport* mouse_hovered_viewport; + if (engine->Inputs.MouseHoveredViewport != 0) + mouse_hovered_viewport = ImGui::FindViewportByID(engine->Inputs.MouseHoveredViewport); // Common case + else + mouse_hovered_viewport = ImGui::FindHoveredViewportFromPlatformWindowStack(engine->Inputs.MousePosValue); // Rarely used, some tests rely on this (e.g. "docking_dockspace_passthru_hover") may make it a opt-in feature instead? + if (mouse_hovered_viewport && (mouse_hovered_viewport->Flags & ImGuiViewportFlags_NoInputs)) + mouse_hovered_viewport = NULL; + //if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) + if (io.BackendFlags & ImGuiBackendFlags_HasMouseHoveredViewport) + io.AddMouseViewportEvent(mouse_hovered_viewport ? mouse_hovered_viewport->ID : 0); + bool mouse_hovered_viewport_focused = mouse_hovered_viewport && (mouse_hovered_viewport->Flags & ImGuiViewportFlags_IsFocused) != 0; +#endif + + // Apply mouse + io.AddMousePosEvent(engine->Inputs.MousePosValue.x, engine->Inputs.MousePosValue.y); + for (int n = 0; n < ImGuiMouseButton_COUNT; n++) + { + bool down = (engine->Inputs.MouseButtonsValue & (1 << n)) != 0; + io.AddMouseButtonEvent(n, down); + + // A click simulate platform focus on the viewport. +#ifdef IMGUI_HAS_VIEWPORT + if (down && mouse_hovered_viewport && !mouse_hovered_viewport_focused) + if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) + { + mouse_hovered_viewport_focused = true; + engine->Inputs.Queue.push_back(ImGuiTestInput::ForViewportFocus(mouse_hovered_viewport->ID)); + } +#endif + } + + // Apply mouse wheel + // [OSX] Simulate OSX behavior of automatically swapping mouse wheel axis when SHIFT is held. + // This is working in conjonction with the fact that ImGuiTestContext::MouseWheel() assume Windows-style behavior. + ImVec2 wheel = engine->Inputs.MouseWheel; + if (io.ConfigMacOSXBehaviors && (io.KeyMods & ImGuiMod_Shift)) // FIXME!! + ImSwap(wheel.x, wheel.y); + if (wheel.x != 0.0f || wheel.y != 0.0f) + io.AddMouseWheelEvent(wheel.x, wheel.y); + engine->Inputs.MouseWheel = ImVec2(0, 0); + + // Process input requests/queues + if (engine->Inputs.Queue.Size > 0) + { + for (int n = 0; n < engine->Inputs.Queue.Size; n++) + { + const ImGuiTestInput& input = engine->Inputs.Queue[n]; + switch (input.Type) + { + case ImGuiTestInputType_Key: + { + ImGuiKeyChord key_chord = input.KeyChord; +#if IMGUI_VERSION_NUM >= 19016 + key_chord = ImGui::FixupKeyChord(&g, key_chord); // This will add ImGuiMod_Alt when pressing ImGuiKey_LeftAlt or ImGuiKey_LeftRight +#endif + ImGuiKey key = (ImGuiKey)(key_chord & ~ImGuiMod_Mask_); + ImGuiKeyChord mods = (key_chord & ImGuiMod_Mask_); + if (mods != 0x00) + { + // OSX conversion +#if IMGUI_VERSION_NUM >= 18912 + if (mods & ImGuiMod_Shortcut) + mods = (mods & ~ImGuiMod_Shortcut) | (g.IO.ConfigMacOSXBehaviors ? ImGuiMod_Super : ImGuiMod_Ctrl); +#endif + // Submitting a ImGuiMod_XXX without associated key needs to add at least one of the key. + if (mods & ImGuiMod_Ctrl) + { + io.AddKeyEvent(ImGuiMod_Ctrl, input.Down); + if (key != ImGuiKey_LeftCtrl && key != ImGuiKey_RightCtrl) + io.AddKeyEvent(ImGuiKey_LeftCtrl, input.Down); + } + if (mods & ImGuiMod_Shift) + { + io.AddKeyEvent(ImGuiMod_Shift, input.Down); + if (key != ImGuiKey_LeftShift && key != ImGuiKey_RightShift) + io.AddKeyEvent(ImGuiKey_LeftShift, input.Down); + } + if (mods & ImGuiMod_Alt) + { + io.AddKeyEvent(ImGuiMod_Alt, input.Down); + if (key != ImGuiKey_LeftAlt && key != ImGuiKey_RightAlt) + io.AddKeyEvent(ImGuiKey_LeftAlt, input.Down); + } + if (mods & ImGuiMod_Super) + { + io.AddKeyEvent(ImGuiMod_Super, input.Down); + if (key != ImGuiKey_LeftSuper && key != ImGuiKey_RightSuper) + io.AddKeyEvent(ImGuiKey_LeftSuper, input.Down); + } + } + + if (key != ImGuiKey_None) + io.AddKeyEvent(key, input.Down); + break; + } + case ImGuiTestInputType_Char: + { + IM_ASSERT(input.Char != 0); + io.AddInputCharacter(input.Char); + break; + } + case ImGuiTestInputType_ViewportFocus: + { +#ifdef IMGUI_HAS_VIEWPORT + if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) + { + IM_ASSERT(engine->TestContext != NULL); + ImGuiViewport* viewport = ImGui::FindViewportByID(input.ViewportId); + if (viewport == NULL) + engine->TestContext->LogError("ViewportPlatform_SetWindowFocus(%08X): cannot find viewport anymore!", input.ViewportId); + else if (platform_io.Platform_SetWindowSize == NULL) + engine->TestContext->LogError("ViewportPlatform_SetWindowFocus(%08X): backend's Platform_SetWindowSize() is not set", input.ViewportId); + else + platform_io.Platform_SetWindowFocus(viewport); + } +#endif + break; + } + case ImGuiTestInputType_ViewportClose: + { +#ifdef IMGUI_HAS_VIEWPORT + if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) + { + IM_ASSERT(engine->TestContext != NULL); + ImGuiViewport* viewport = ImGui::FindViewportByID(input.ViewportId); + if (viewport == NULL) + engine->TestContext->LogError("ViewportPlatform_CloseWindow(%08X): cannot find viewport anymore!", input.ViewportId); + else + viewport->PlatformRequestClose = true; + // FIXME: doesn't apply to actual backend + } +#endif + break; + } + case ImGuiTestInputType_None: + default: + break; + } + } + + engine->Inputs.Queue.resize(0); + } + + const int input_event_count_curr = g.InputEventsQueue.Size; + for (int n = input_event_count_prev; n < input_event_count_curr; n++) + g.InputEventsQueue[n].AddedByTestEngine = true; +} + +// FIXME: Trying to abort a running GUI test won't kill the app immediately. +static void ImGuiTestEngine_UpdateWatchdog(ImGuiTestEngine* engine, ImGuiContext* ui_ctx, double t0, double t1) +{ + IM_UNUSED(ui_ctx); + ImGuiTestContext* test_ctx = engine->TestContext; + + if (engine->IO.ConfigRunSpeed != ImGuiTestRunSpeed_Fast || ImOsIsDebuggerPresent()) + return; + + if (test_ctx->RunFlags & ImGuiTestRunFlags_RunFromGui) + return; + + const float timer_warn = engine->IO.ConfigWatchdogWarning; + const float timer_kill_test = engine->IO.ConfigWatchdogKillTest; + const float timer_kill_app = engine->IO.ConfigWatchdogKillApp; + + // Emit a warning and then fail the test after a given time. + if (t0 < timer_warn && t1 >= timer_warn) + { + test_ctx->LogWarning("[Watchdog] Running time for '%s' is >%.f seconds, may be excessive.", test_ctx->Test->Name, timer_warn); + } + if (t0 < timer_kill_test && t1 >= timer_kill_test) + { + test_ctx->LogError("[Watchdog] Running time for '%s' is >%.f seconds, aborting.", test_ctx->Test->Name, timer_kill_test); + IM_CHECK(false); + } + + // Final safety watchdog in case the TestFunc is calling Yield() but never returning. + // Note that we are not catching infinite loop cases where the TestFunc may be running but not yielding.. + if (t0 < timer_kill_app + 5.0f && t1 >= timer_kill_app + 5.0f) + { + test_ctx->LogError("[Watchdog] Emergency process exit as the test didn't return."); + exit(1); + } +} + +static void ImGuiTestEngine_PreNewFrame(ImGuiTestEngine* engine, ImGuiContext* ui_ctx) +{ + if (engine->UiContextTarget != ui_ctx) + return; + IM_ASSERT(ui_ctx == GImGui); + ImGuiContext& g = *ui_ctx; + + engine->CaptureContext.PreNewFrame(); + + if (engine->ToolDebugRebootUiContext) + { + ImGuiTestEngine_RebootUiContext(engine); + ui_ctx = engine->UiContextTarget; + engine->ToolDebugRebootUiContext = false; + } + + // Inject extra time into the Dear ImGui context + if (engine->OverrideDeltaTime >= 0.0f) + { + ui_ctx->IO.DeltaTime = engine->OverrideDeltaTime; + engine->OverrideDeltaTime = -1.0f; + } + + // NewFrame() will increase this so we are +1 ahead at the time of calling this + engine->FrameCount = g.FrameCount + 1; + if (ImGuiTestContext* test_ctx = engine->TestContext) + { + double t0 = test_ctx->RunningTime; + double t1 = t0 + ui_ctx->IO.DeltaTime; + test_ctx->FrameCount++; + test_ctx->RunningTime = t1; + ImGuiTestEngine_UpdateWatchdog(engine, ui_ctx, t0, t1); + } + + engine->PerfDeltaTime100.AddSample(g.IO.DeltaTime); + engine->PerfDeltaTime500.AddSample(g.IO.DeltaTime); + + if (!ImGuiTestEngine_IsTestQueueEmpty(engine) && !engine->Abort) + { + // Abort testing by holding ESC + // When running GuiFunc only main_io == simulated_io we test for a long hold. + ImGuiIO& main_io = g.IO; + for (auto& e : g.InputEventsQueue) + if (e.Type == ImGuiInputEventType_Key && e.Key.Key == ImGuiKey_Escape) + engine->Inputs.HostEscDown = e.Key.Down; + engine->Inputs.HostEscDownDuration = engine->Inputs.HostEscDown ? (ImMax(engine->Inputs.HostEscDownDuration, 0.0f) + main_io.DeltaTime) : -1.0f; + const bool abort = engine->Inputs.HostEscDownDuration >= 0.20f; + if (abort) + { + if (engine->TestContext) + engine->TestContext->LogWarning("User aborted (pressed ESC)"); + ImGuiTestEngine_AbortCurrentTest(engine); + } + } + else + { + engine->Inputs.HostEscDown = false; + engine->Inputs.HostEscDownDuration = -1.0f; + } + + ImGuiTestEngine_ApplyInputToImGuiContext(engine); + ImGuiTestEngine_UpdateHooks(engine); +} + +static void ImGuiTestEngine_PostNewFrame(ImGuiTestEngine* engine, ImGuiContext* ui_ctx) +{ + if (engine->UiContextTarget != ui_ctx) + return; + IM_ASSERT(ui_ctx == GImGui); + + // Set initial mouse position to a decent value on startup + if (engine->FrameCount == 1) + engine->Inputs.MousePosValue = ImGui::GetMainViewport()->Pos; + + engine->IO.IsCapturing = engine->CaptureContext.IsCapturing(); + + // Garbage collect unused tasks + const int LOCATION_TASK_ELAPSE_FRAMES = 20; + for (int task_n = 0; task_n < engine->InfoTasks.Size; task_n++) + { + ImGuiTestInfoTask* task = engine->InfoTasks[task_n]; + if (task->FrameCount < engine->FrameCount - LOCATION_TASK_ELAPSE_FRAMES && task->Result.RefCount == 0) + { + IM_DELETE(task); + engine->InfoTasks.erase(engine->InfoTasks.Data + task_n); + task_n--; + } + } + + // Slow down whole app + if (engine->ToolSlowDown) + ImThreadSleepInMilliseconds(engine->ToolSlowDownMs); + + // Call user GUI function + ImGuiTestEngine_RunGuiFunc(engine); + + // Process on-going queues in a coroutine + // Run the test coroutine. This will resume the test queue from either the last point the test called YieldFromCoroutine(), + // or the loop in ImGuiTestEngine_TestQueueCoroutineMain that does so if no test is running. + // If you want to breakpoint the point execution continues in the test code, breakpoint the exit condition in YieldFromCoroutine() + const int input_queue_size_before = ui_ctx->InputEventsQueue.Size; + engine->IO.CoroutineFuncs->RunFunc(engine->TestQueueCoroutine); + + // Events added by TestFunc() marked automaticaly to not be deleted + if (engine->TestContext && (engine->TestContext->RunFlags & ImGuiTestRunFlags_EnableRawInputs)) + for (int n = input_queue_size_before; n < ui_ctx->InputEventsQueue.Size; n++) + ui_ctx->InputEventsQueue[n].AddedByTestEngine = true; + + // Update hooks and output flags + ImGuiTestEngine_UpdateHooks(engine); + + // Disable vsync + engine->IO.IsRequestingMaxAppSpeed = engine->IO.ConfigNoThrottle; + if (engine->IO.ConfigRunSpeed == ImGuiTestRunSpeed_Fast && engine->IO.IsRunningTests) + if (engine->TestContext && (engine->TestContext->RunFlags & ImGuiTestRunFlags_GuiFuncOnly) == 0) + engine->IO.IsRequestingMaxAppSpeed = true; +} + +static void ImGuiTestEngine_PreRender(ImGuiTestEngine* engine, ImGuiContext* ui_ctx) +{ + if (engine->UiContextTarget != ui_ctx) + return; + IM_ASSERT(ui_ctx == GImGui); + + engine->CaptureContext.PreRender(); +} + +static void ImGuiTestEngine_PostRender(ImGuiTestEngine* engine, ImGuiContext* ui_ctx) +{ + if (engine->UiContextTarget != ui_ctx) + return; + IM_ASSERT(ui_ctx == GImGui); + + // When test are running make sure real backend doesn't pick mouse cursor shape from tests. + // (If were to instead set io.ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange in ImGuiTestEngine_RunTest() that would get us 99% of the way, + // but unfortunately backend wouldn't restore normal shape after modified by OS decoration such as resize, so not enough..) + ImGuiContext& g = *ui_ctx; + if (!engine->IO.ConfigMouseDrawCursor && !g.IO.MouseDrawCursor && ImGuiTestEngine_IsUsingSimulatedInputs(engine)) + g.MouseCursor = ImGuiMouseCursor_Arrow; + + + // Check ImDrawData integrity + // This is currently a very cheap operation but may later become slower we if e.g. check idx boundaries. +#ifdef IMGUI_HAS_DOCK + if (engine->IO.CheckDrawDataIntegrity) + for (ImGuiViewport* viewport : ImGui::GetPlatformIO().Viewports) + DrawDataVerifyMatchingBufferCount(viewport->DrawData); +#else + if (engine->IO.CheckDrawDataIntegrity) + DrawDataVerifyMatchingBufferCount(ImGui::GetDrawData()); +#endif + + engine->CaptureContext.PostRender(); +} + +static void ImGuiTestEngine_RunGuiFunc(ImGuiTestEngine* engine) +{ + ImGuiTestContext* ctx = engine->TestContext; + if (ctx && ctx->Test->GuiFunc) + { + if (!(ctx->RunFlags & ImGuiTestRunFlags_GuiFuncDisable)) + { + ImGuiTestActiveFunc backup_active_func = ctx->ActiveFunc; + ctx->ActiveFunc = ImGuiTestActiveFunc_GuiFunc; + engine->TestContext->Test->GuiFunc(engine->TestContext); + ctx->ActiveFunc = backup_active_func; + } + + // Safety net + //if (ctx->Test->Status == ImGuiTestStatus_Error) + ctx->RecoverFromUiContextErrors(); + } + if (ctx) + ctx->FirstGuiFrame = false; +} + +// Main function for the test coroutine +static void ImGuiTestEngine_TestQueueCoroutineMain(void* engine_opaque) +{ + ImGuiTestEngine* engine = (ImGuiTestEngine*)engine_opaque; + while (!engine->TestQueueCoroutineShouldExit) + { + ImGuiTestEngine_ProcessTestQueue(engine); + engine->IO.CoroutineFuncs->YieldFunc(); + } +} + +static void ImGuiTestEngine_DisableWindowInputs(ImGuiWindow* window) +{ + window->DisableInputsFrames = 1; + for (ImGuiWindow* child_window : window->DC.ChildWindows) + ImGuiTestEngine_DisableWindowInputs(child_window); +} + +// Yield control back from the TestFunc to the main update + GuiFunc, for one frame. +void ImGuiTestEngine_Yield(ImGuiTestEngine* engine) +{ + ImGuiTestContext* ctx = engine->TestContext; + + // Can only yield in the test func! + if (ctx) + { + IM_ASSERT(ctx->ActiveFunc == ImGuiTestActiveFunc_TestFunc && "Can only yield inside TestFunc()!"); + for (ImGuiWindow* window : ctx->ForeignWindowsToHide) + { + window->HiddenFramesForRenderOnly = 2; // Hide root window + ImGuiTestEngine_DisableWindowInputs(window); // Disable inputs for root window and all it's children recursively + } + } + + engine->IO.CoroutineFuncs->YieldFunc(); +} + +void ImGuiTestEngine_SetDeltaTime(ImGuiTestEngine* engine, float delta_time) +{ + IM_ASSERT(delta_time >= 0.0f); + engine->OverrideDeltaTime = delta_time; +} + +int ImGuiTestEngine_GetFrameCount(ImGuiTestEngine* engine) +{ + return engine->FrameCount; +} + +const char* ImGuiTestEngine_GetStatusName(ImGuiTestStatus v) +{ + static const char* names[ImGuiTestStatus_COUNT] = { "Success", "Queued", "Running", "Error", "Suspended" }; + IM_STATIC_ASSERT(IM_ARRAYSIZE(names) == ImGuiTestStatus_COUNT); + if (v >= 0 && v < IM_ARRAYSIZE(names)) + return names[v]; + return "N/A"; +} + +const char* ImGuiTestEngine_GetRunSpeedName(ImGuiTestRunSpeed v) +{ + static const char* names[ImGuiTestRunSpeed_COUNT] = { "Fast", "Normal", "Cinematic" }; + IM_STATIC_ASSERT(IM_ARRAYSIZE(names) == ImGuiTestRunSpeed_COUNT); + if (v >= 0 && v < IM_ARRAYSIZE(names)) + return names[v]; + return "N/A"; +} + +const char* ImGuiTestEngine_GetVerboseLevelName(ImGuiTestVerboseLevel v) +{ + static const char* names[ImGuiTestVerboseLevel_COUNT] = { "Silent", "Error", "Warning", "Info", "Debug", "Trace" }; + IM_STATIC_ASSERT(IM_ARRAYSIZE(names) == ImGuiTestVerboseLevel_COUNT); + if (v >= 0 && v < IM_ARRAYSIZE(names)) + return names[v]; + return "N/A"; +} + +bool ImGuiTestEngine_CaptureScreenshot(ImGuiTestEngine* engine, ImGuiCaptureArgs* args) +{ + if (engine->IO.ScreenCaptureFunc == NULL) + { + IM_ASSERT(0); + return false; + } + + IM_ASSERT(engine->CaptureCurrentArgs == NULL && "Nested captures are not supported."); + + // Graphics API must render a window so it can be captured + // FIXME: This should work without this, as long as Present vs Vsync are separated (we need a Present, we don't need Vsync) + const ImGuiTestRunSpeed backup_run_speed = engine->IO.ConfigRunSpeed; + engine->IO.ConfigRunSpeed = ImGuiTestRunSpeed_Fast; + + const int frame_count = engine->FrameCount; + + // Because we rely on window->ContentSize for stitching, let 1 extra frame elapse to make sure any + // windows which contents have changed in the last frame get a correct window->ContentSize value. + // FIXME: Can remove this yield if not stitching + if ((args->InFlags & ImGuiCaptureFlags_Instant) == 0) + ImGuiTestEngine_Yield(engine); + + // This will yield until ImGuiTestEngine_PostSwap() -> ImGuiCaptureContext::CaptureUpdate() return false. + // - CaptureUpdate() will call user provided test_io.ScreenCaptureFunc() function + // - Capturing is likely to take multiple frames depending on settings. + int frames_yielded = 0; + engine->CaptureCurrentArgs = args; + engine->PostSwapCalled = false; + while (engine->CaptureCurrentArgs != NULL) + { + ImGuiTestEngine_Yield(engine); + frames_yielded++; + if (frames_yielded > 4) + IM_ASSERT(engine->PostSwapCalled && "ImGuiTestEngine_PostSwap() is not being called by application! Must be called in order."); + } + + // Verify that the ImGuiCaptureFlags_Instant flag got honored + if (args->InFlags & ImGuiCaptureFlags_Instant) + IM_ASSERT(frame_count + 1 == engine->FrameCount); + + engine->IO.ConfigRunSpeed = backup_run_speed; + return true; +} + +bool ImGuiTestEngine_CaptureBeginVideo(ImGuiTestEngine* engine, ImGuiCaptureArgs* args) +{ + if (engine->IO.ScreenCaptureFunc == NULL) + { + IM_ASSERT(0); + return false; + } + + IM_ASSERT(engine->CaptureCurrentArgs == NULL && "Nested captures are not supported."); + + // RunSpeed set to Fast -> Switch to Cinematic, no throttle + // RunSpeed set to Normal -> No change + // RunSpeed set to Cinematic -> No change + engine->BackupConfigRunSpeed = engine->IO.ConfigRunSpeed; + engine->BackupConfigNoThrottle = engine->IO.ConfigNoThrottle; + if (engine->IO.ConfigRunSpeed == ImGuiTestRunSpeed_Fast) + { + engine->IO.ConfigRunSpeed = ImGuiTestRunSpeed_Cinematic; + engine->IO.ConfigNoThrottle = true; + engine->IO.ConfigFixedDeltaTime = 1.0f / 60.0f; + } + engine->CaptureCurrentArgs = args; + engine->CaptureContext.BeginVideoCapture(args); + return true; +} + +bool ImGuiTestEngine_CaptureEndVideo(ImGuiTestEngine* engine, ImGuiCaptureArgs* args) +{ + IM_UNUSED(args); + IM_ASSERT(engine->CaptureContext.IsCapturingVideo() && "No video capture is in progress."); + + engine->CaptureContext.EndVideoCapture(); + while (engine->CaptureCurrentArgs != NULL) // Wait until last frame is captured and gif is saved. + ImGuiTestEngine_Yield(engine); + engine->IO.ConfigRunSpeed = engine->BackupConfigRunSpeed; + engine->IO.ConfigNoThrottle = engine->BackupConfigNoThrottle; + engine->IO.ConfigFixedDeltaTime = 0; + engine->CaptureCurrentArgs = NULL; + return true; +} + +static void ImGuiTestEngine_ProcessTestQueue(ImGuiTestEngine* engine) +{ + // Avoid tracking scrolling in UI when running a single test + const bool track_scrolling = (engine->TestsQueue.Size > 1) || (engine->TestsQueue.Size == 1 && (engine->TestsQueue[0].RunFlags & ImGuiTestRunFlags_RunFromCommandLine)); + + // Backup some state + ImGuiIO& io = ImGui::GetIO(); + const char* backup_ini_filename = io.IniFilename; + ImGuiWindow* backup_nav_window = engine->UiContextTarget->NavWindow; + io.IniFilename = NULL; + + int ran_tests = 0; + engine->BatchStartTime = ImTimeGetInMicroseconds(); + engine->IO.IsRunningTests = true; + for (int n = 0; n < engine->TestsQueue.Size; n++) + { + ImGuiTestRunTask* run_task = &engine->TestsQueue[n]; + IM_ASSERT(run_task->Test->Output.Status == ImGuiTestStatus_Queued); + + // FIXME-TESTS: Blind mode not supported + IM_ASSERT(engine->UiContextTarget != NULL); + IM_ASSERT(engine->UiContextActive == NULL); + engine->UiContextActive = engine->UiContextTarget; + engine->UiSelectedTest = run_task->Test; + if (track_scrolling) + engine->UiSelectAndScrollToTest = run_task->Test; + + // Run test + ImGuiTestEngine_RunTest(engine, NULL, run_task->Test, run_task->RunFlags); + + // Cleanup + IM_ASSERT(engine->TestContext == NULL); + IM_ASSERT(engine->UiContextActive == engine->UiContextTarget); + engine->UiContextActive = NULL; + + // Auto select the first error test + //if (test->Status == ImGuiTestStatus_Error) + // if (engine->UiSelectedTest == NULL || engine->UiSelectedTest->Status != ImGuiTestStatus_Error) + // engine->UiSelectedTest = test; + + ran_tests++; + } + engine->IO.IsRunningTests = false; + engine->BatchEndTime = ImTimeGetInMicroseconds(); + + engine->Abort = false; + engine->TestsQueue.clear(); + + // Restore UI state (done after all ImGuiTestEngine_RunTest() are done) + if (ran_tests) + { + if (engine->IO.ConfigRestoreFocusAfterTests) + ImGui::FocusWindow(backup_nav_window); + } + io.IniFilename = backup_ini_filename; +} + +bool ImGuiTestEngine_IsTestQueueEmpty(ImGuiTestEngine* engine) +{ + return engine->TestsQueue.Size == 0; +} + +static bool ImGuiTestEngine_IsRunningTest(ImGuiTestEngine* engine, ImGuiTest* test) +{ + for (ImGuiTestRunTask& t : engine->TestsQueue) + if (t.Test == test) + return true; + return false; +} + +void ImGuiTestEngine_QueueTest(ImGuiTestEngine* engine, ImGuiTest* test, ImGuiTestRunFlags run_flags) +{ + if (ImGuiTestEngine_IsRunningTest(engine, test)) + return; + + // Detect lack of signal from imgui context, most likely not compiled with IMGUI_ENABLE_TEST_ENGINE=1 + // FIXME: Why is in this function? + if (engine->UiContextTarget && engine->FrameCount < engine->UiContextTarget->FrameCount - 2) + { + ImGuiTestEngine_AbortCurrentTest(engine); + IM_ASSERT(0 && "Not receiving signal from core library. Did you call ImGuiTestEngine_CreateContext() with the correct context? Did you compile imgui/ with IMGUI_ENABLE_TEST_ENGINE=1?"); + test->Output.Status = ImGuiTestStatus_Error; + return; + } + + test->Output.Status = ImGuiTestStatus_Queued; + + ImGuiTestRunTask run_task; + run_task.Test = test; + run_task.RunFlags = run_flags; + engine->TestsQueue.push_back(run_task); +} + +// Called by IM_REGISTER_TEST(). Prefer calling IM_REGISTER_TEST() in your code so src_file/src_line are automatically passed. +ImGuiTest* ImGuiTestEngine_RegisterTest(ImGuiTestEngine* engine, const char* category, const char* name, const char* src_file, int src_line) +{ + ImGuiTestGroup group = ImGuiTestGroup_Tests; + if (strcmp(category, "perf") == 0) + group = ImGuiTestGroup_Perfs; + + ImGuiTest* t = IM_NEW(ImGuiTest)(); + t->Group = group; + t->Category = category; + t->Name = name; + t->SourceFile = src_file; + t->SourceLine = t->SourceLineEnd = src_line; + engine->TestsAll.push_back(t); + + return t; +} + +ImGuiPerfTool* ImGuiTestEngine_GetPerfTool(ImGuiTestEngine* engine) +{ + return engine->PerfTool; +} + +// Filter tests by a specified query. Query is composed of one or more comma-separated filter terms optionally prefixed/suffixed with modifiers. +// Available modifiers: +// - '-' prefix excludes tests matched by the term. +// - '^' prefix anchors term matching to the start of the string. +// - '$' suffix anchors term matching to the end of the string. +// Special keywords: +// - "all" : all tests, no matter what group they are in. +// - "tests" : tests in ImGuiTestGroup_Tests group. +// - "perfs" : tests in ImGuiTestGroup_Perfs group. +// Example queries: +// - "" : empty query matches no tests. +// - "^nav_" : all tests with name starting with "nav_". +// - "_nav$" : all tests with name ending with "_nav". +// - "-xxx" : all tests and perfs that do not contain "xxx". +// - "tests,-scroll,-^nav_" : all tests (but no perfs) that do not contain "scroll" in their name and does not start with "nav_". +// Note: while we borrowed ^ and $ from regex conventions, we do not support actual regex syntax except for behavior of these two modifiers. +bool ImGuiTestEngine_PassFilter(ImGuiTest* test, const char* filter_specs) +{ + IM_ASSERT(filter_specs != NULL); + auto str_iequal = [](const char* s1, const char* s2, const char* s2_end) + { + size_t s2_len = (size_t)(s2_end - s2); + if (strlen(s1) != s2_len) return false; + return ImStrnicmp(s1, s2, s2_len) == 0; + }; + + auto str_iendswith = [&str_iequal](const char* s1, const char* s2, const char* s2_end) + { + size_t s1_len = strlen(s1); + size_t s2_len = (size_t)(s2_end - s2); + if (s1_len < s2_len) return false; + s1 = s1 + s1_len - s2_len; + return str_iequal(s1, s2, s2_end); + }; + + bool include = false; + const char* prefixes = "^-"; + + // When filter starts with exclude condition, we assume we have included all tests from the start. This enables + // writing "-window" instead of "all,-window". + for (int i = 0; filter_specs[i]; i++) + if (filter_specs[i] == '-') + include = true; // First filter is exclusion + else if (strchr(prefixes, filter_specs[i]) == NULL) + break; // End of prefixes + + for (const char* filter_start = filter_specs; filter_start[0];) + { + // Filter modifiers + bool is_exclude = false; + bool is_anchor_to_start = false; + bool is_anchor_to_end = false; + for (;;) + { + if (filter_start[0] == '-') + is_exclude = true; + else if (filter_start[0] == '^') + is_anchor_to_start = true; + else + break; + filter_start++; + } + + const char* filter_end = strstr(filter_start, ","); + filter_end = filter_end ? filter_end : filter_start + strlen(filter_start); + is_anchor_to_end = filter_end[-1] == '$'; + if (is_anchor_to_end) + filter_end--; + + if (str_iequal("all", filter_start, filter_end)) + include = !is_exclude; + else if (str_iequal("tests", filter_start, filter_end)) + include = (test->Group == ImGuiTestGroup_Tests) ? !is_exclude : include; + else if (str_iequal("perfs", filter_start, filter_end)) + include = (test->Group == ImGuiTestGroup_Perfs) ? !is_exclude : include; + else + { + // General filtering + for (int n = 0; n < 2; n++) + { + const char* name = (n == 0) ? test->Name : test->Category; + + bool match = true; + + // "foo" - match a substring. + if (!is_anchor_to_start && !is_anchor_to_end) + match = ImStristr(name, NULL, filter_start, filter_end) != NULL; + + // "^foo" - match start of the string. + // "foo$" - match end of the string. + // FIXME: (minor) '^aaa$' will incorrectly match 'aaabbbaaa'. + if (is_anchor_to_start) + match &= ImStrnicmp(name, filter_start, filter_end - filter_start) == 0; + if (is_anchor_to_end) + match &= str_iendswith(name, filter_start, filter_end); + + if (match) + { + include = is_exclude ? false : true; + break; + } + } + } + + while (filter_end[0] == ',' || filter_end[0] == '$') + filter_end++; + filter_start = filter_end; + } + return include; +} + +void ImGuiTestEngine_QueueTests(ImGuiTestEngine* engine, ImGuiTestGroup group, const char* filter_str, ImGuiTestRunFlags run_flags) +{ + IM_ASSERT(group >= ImGuiTestGroup_Unknown && group < ImGuiTestGroup_COUNT); + for (int n = 0; n < engine->TestsAll.Size; n++) + { + ImGuiTest* test = engine->TestsAll[n]; + if (group != ImGuiTestGroup_Unknown && test->Group != group) + continue; + + if (!ImGuiTestEngine_PassFilter(test, filter_str)) + continue; + + ImGuiTestEngine_QueueTest(engine, test, run_flags); + } +} + +static void ImGuiTestEngine_StartCalcSourceLineEnds(ImGuiTestEngine* engine) +{ + if (engine->TestsAll.empty()) + return; + + ImVector line_starts; + line_starts.reserve(engine->TestsAll.Size); + for (int n = 0; n < engine->TestsAll.Size; n++) + line_starts.push_back(engine->TestsAll[n]->SourceLine); + ImQsort(line_starts.Data, (size_t)line_starts.Size, sizeof(int), [](const void* lhs, const void* rhs) { return (*(const int*)lhs) - *(const int*)rhs; }); + + for (int n = 0; n < engine->TestsAll.Size; n++) + { + ImGuiTest* test = engine->TestsAll[n]; + for (int m = 0; m < line_starts.Size - 1; m++) // FIXME-OPT + if (line_starts[m] == test->SourceLine) + test->SourceLineEnd = ImMax(test->SourceLine, line_starts[m + 1]); + } +} + +void ImGuiTestEngine_GetResult(ImGuiTestEngine* engine, int& count_tested, int& count_success) +{ + count_tested = 0; + count_success = 0; + for (int n = 0; n < engine->TestsAll.Size; n++) + { + ImGuiTest* test = engine->TestsAll[n]; + if (test->Output.Status == ImGuiTestStatus_Unknown) + continue; + IM_ASSERT(test->Output.Status != ImGuiTestStatus_Queued); + IM_ASSERT(test->Output.Status != ImGuiTestStatus_Running); + count_tested++; + if (test->Output.Status == ImGuiTestStatus_Success) + count_success++; + } +} + +// Get a copy of the test list +void ImGuiTestEngine_GetTestList(ImGuiTestEngine* engine, ImVector* out_tests) +{ + *out_tests = engine->TestsAll; +} + +// Get a copy of the test queue +void ImGuiTestEngine_GetTestQueue(ImGuiTestEngine* engine, ImVector* out_tests) +{ + *out_tests = engine->TestsQueue; +} + +static void ImGuiTestEngine_UpdateHooks(ImGuiTestEngine* engine) +{ + ImGuiContext* ui_ctx = engine->UiContextTarget; + IM_ASSERT(ui_ctx->TestEngine == engine); + bool want_hooking = false; + + //if (engine->TestContext != NULL) + // want_hooking = true; + + if (engine->InfoTasks.Size > 0) + want_hooking = true; + if (engine->FindByLabelTask.InSuffix != NULL) + want_hooking = true; + if (engine->GatherTask.InParentID != 0) + want_hooking = true; + + // Update test engine specific hooks + ui_ctx->TestEngineHookItems = want_hooking; +} + +struct ImGuiTestContextUiContextBackup +{ + ImGuiIO IO; + ImGuiStyle Style; + ImGuiDebugLogFlags DebugLogFlags; + ImGuiKeyChord ConfigNavWindowingKeyNext; + ImGuiKeyChord ConfigNavWindowingKeyPrev; + + void Backup(ImGuiContext& g) + { + IO = g.IO; + Style = g.Style; + DebugLogFlags = g.DebugLogFlags; + ConfigNavWindowingKeyNext = g.ConfigNavWindowingKeyNext; + ConfigNavWindowingKeyPrev = g.ConfigNavWindowingKeyPrev; + memset(IO.MouseDown, 0, sizeof(IO.MouseDown)); + for (int n = 0; n < IM_ARRAYSIZE(IO.KeysData); n++) + IO.KeysData[n].Down = false; + } + void Restore(ImGuiContext& g) + { +#if IMGUI_VERSION_NUM < 18993 + IO.MetricsActiveAllocations = g.IO.MetricsActiveAllocations; +#endif + g.IO = IO; + g.Style = Style; + g.DebugLogFlags = DebugLogFlags; + g.ConfigNavWindowingKeyNext = ConfigNavWindowingKeyNext; + g.ConfigNavWindowingKeyPrev = ConfigNavWindowingKeyPrev; + } + void RestoreClipboardFuncs(ImGuiContext& g) + { + g.IO.GetClipboardTextFn = IO.GetClipboardTextFn; + g.IO.SetClipboardTextFn = IO.SetClipboardTextFn; + g.IO.ClipboardUserData = IO.ClipboardUserData; + } +}; + +// FIXME: Work toward simplifying this function? +void ImGuiTestEngine_RunTest(ImGuiTestEngine* engine, ImGuiTestContext* parent_ctx, ImGuiTest* test, ImGuiTestRunFlags run_flags) +{ + ImGuiTestContext stack_ctx; + ImGuiCaptureArgs stack_capture_args; + ImGuiTestContext* ctx; + + if (run_flags & ImGuiTestRunFlags_ShareTestContext) + { + // Reuse existing test context + IM_ASSERT(parent_ctx != NULL); + ctx = parent_ctx; + } + else + { + // Create a test context + ctx = &stack_ctx; + ctx->Engine = engine; + ctx->EngineIO = &engine->IO; + ctx->Inputs = &engine->Inputs; + ctx->CaptureArgs = &stack_capture_args; + ctx->UserVars = NULL; + ctx->PerfStressAmount = engine->IO.PerfStressAmount; +#ifdef IMGUI_HAS_DOCK + ctx->HasDock = true; +#else + ctx->HasDock = false; +#endif + } + + ImGuiTestOutput* test_output; + if (parent_ctx == NULL) + { + ctx->Test = test; + test_output = ctx->TestOutput = &test->Output; + test_output->StartTime = ImTimeGetInMicroseconds(); + } + else + { + ctx->Test = parent_ctx->Test; + test_output = ctx->TestOutput = parent_ctx->TestOutput; + } + + if (engine->Abort) + { + test_output->Status = ImGuiTestStatus_Unknown; + if (parent_ctx == NULL) + test_output->EndTime = test_output->StartTime; + ctx->Test = NULL; + ctx->TestOutput = NULL; + return; + } + + test_output->Status = ImGuiTestStatus_Running; + + ctx->RunFlags = run_flags; + ctx->UiContext = engine->UiContextActive; + + engine->TestContext = ctx; + ImGuiTestEngine_UpdateHooks(engine); + + void* backup_user_vars = NULL; + ImGuiTestGenericVars backup_generic_vars; + if (run_flags & ImGuiTestRunFlags_ShareVars) + { + // Share user vars and generic vars + IM_CHECK_SILENT(parent_ctx != NULL); + IM_CHECK_SILENT(test->VarsSize == parent_ctx->Test->VarsSize); + IM_CHECK_SILENT(test->VarsConstructor == parent_ctx->Test->VarsConstructor); + IM_CHECK_SILENT(test->VarsPostConstructor == parent_ctx->Test->VarsPostConstructor); + IM_CHECK_SILENT(test->VarsPostConstructorUserFn == parent_ctx->Test->VarsPostConstructorUserFn); + IM_CHECK_SILENT(test->VarsDestructor == parent_ctx->Test->VarsDestructor); + if ((run_flags & ImGuiTestRunFlags_ShareTestContext) == 0) + { + ctx->GenericVars = parent_ctx->GenericVars; + ctx->UserVars = parent_ctx->UserVars; + } + } + else + { + // Create user vars + if (run_flags & ImGuiTestRunFlags_ShareTestContext) + { + backup_user_vars = parent_ctx->UserVars; + backup_generic_vars = parent_ctx->GenericVars; + } + ctx->GenericVars.Clear(); + if (test->VarsConstructor != NULL) + { + ctx->UserVars = IM_ALLOC(test->VarsSize); + test->VarsConstructor(ctx->UserVars); + if (test->VarsPostConstructor != NULL && test->VarsPostConstructorUserFn != NULL) + test->VarsPostConstructor(ctx, ctx->UserVars, test->VarsPostConstructorUserFn); + } + } + + // Log header + if (parent_ctx == NULL) + { + ctx->LogEx(ImGuiTestVerboseLevel_Info, ImGuiTestLogFlags_NoHeader, "----------------------------------------------------------------------"); // Intentionally TTY only (just before clear: make it a flag?) + test_output->Log.Clear(); + ctx->LogWarning("Test: '%s' '%s'..", test->Category, test->Name); + } + else + { + ctx->LogWarning("Child Test: '%s' '%s'..", test->Category, test->Name); + ctx->LogWarning("(ShareVars=%d ShareTestContext=%d)", (run_flags & ImGuiTestRunFlags_ShareVars) ? 1 : 0, (run_flags & ImGuiTestRunFlags_ShareTestContext) ? 1 : 0); + } + + // Clear ImGui inputs to avoid key/mouse leaks from one test to another + ImGuiTestEngine_ClearInput(engine); + + ctx->FrameCount = parent_ctx ? parent_ctx->FrameCount : 0; + ctx->ErrorCounter = 0; + ctx->SetRef(""); + ctx->SetInputMode(ImGuiInputSource_Mouse); + ctx->UiContext->NavInputSource = ImGuiInputSource_Keyboard; + ctx->Clipboard.clear(); + + // Backup entire IO and style. Allows tests modifying them and not caring about restoring state. + ImGuiTestContextUiContextBackup backup_ui_context; + backup_ui_context.Backup(*ctx->UiContext); + + // Setup IO: software mouse cursor, viewport support + ImGuiIO& io = ctx->UiContext->IO; + if (engine->IO.ConfigMouseDrawCursor) + io.MouseDrawCursor = true; +#ifdef IMGUI_HAS_VIEWPORT + // We always fill io.MouseHoveredViewport manually (maintained in ImGuiTestInputs::SimulatedIO) + // so ensure we don't leave a chance to Dear ImGui to interpret things differently. + // FIXME: As written, this would prevent tests from toggling ImGuiConfigFlags_ViewportsEnable and have correct value for ImGuiBackendFlags_HasMouseHoveredViewport + if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) + io.BackendFlags |= ImGuiBackendFlags_HasMouseHoveredViewport; + else + io.BackendFlags &= ~ImGuiBackendFlags_HasMouseHoveredViewport; +#endif + + // Setup IO: override clipboard + if ((ctx->RunFlags & ImGuiTestRunFlags_GuiFuncOnly) == 0) + { + io.GetClipboardTextFn = [](void* user_data) -> const char* + { + ImGuiTestContext* ctx = (ImGuiTestContext*)user_data; + return ctx->Clipboard.empty() ? "" : ctx->Clipboard.Data; + }; + io.SetClipboardTextFn = [](void* user_data, const char* text) + { + ImGuiTestContext* ctx = (ImGuiTestContext*)user_data; + ctx->Clipboard.resize((int)strlen(text) + 1); + strcpy(ctx->Clipboard.Data, text); + }; + io.ClipboardUserData = ctx; + } + + // Mark as currently running the TestFunc (this is the only time when we are allowed to yield) + IM_ASSERT(ctx->ActiveFunc == ImGuiTestActiveFunc_None || ctx->ActiveFunc == ImGuiTestActiveFunc_TestFunc); + ImGuiTestActiveFunc backup_active_func = ctx->ActiveFunc; + ctx->ActiveFunc = ImGuiTestActiveFunc_TestFunc; + ctx->FirstGuiFrame = (test->GuiFunc != NULL) ? true : false; + + // Warm up GUI + // - We need one mandatory frame running GuiFunc before running TestFunc + // - We add a second frame, to avoid running tests while e.g. windows are typically appearing for the first time, hidden, + // measuring their initial size. Most tests are going to be more meaningful with this stabilized base. + if (!(test->Flags & ImGuiTestFlags_NoGuiWarmUp)) + { + ctx->FrameCount -= 2; + ctx->Yield(); + if (test_output->Status == ImGuiTestStatus_Running) // To allow GuiFunc calling Finish() in first frame + ctx->Yield(); + } + ctx->FirstTestFrameCount = ctx->FrameCount; + + // Call user test function (optional) + if (ctx->RunFlags & ImGuiTestRunFlags_GuiFuncOnly) + { + // No test function + while (!engine->Abort && test_output->Status == ImGuiTestStatus_Running) + ctx->Yield(); + } + else + { + if (test->TestFunc) + { + // Test function + test->TestFunc(ctx); + + // In case test failed without finishing gif capture - finish it here. This may trigger due to user error or + // due to IM_SUSPEND_TESTFUNC() terminating TestFunc() early. + if (engine->CaptureContext.IsCapturingVideo()) + { + ImGuiCaptureArgs* args = engine->CaptureCurrentArgs; + ImGuiTestEngine_CaptureEndVideo(engine, args); + //ImFileDelete(args->OutSavedFileName); + ctx->LogWarning("Recovered from missing CaptureEndVideo()"); + } + } + else + { + // No test function + if (test->Flags & ImGuiTestFlags_NoAutoFinish) + while (!engine->Abort && test_output->Status == ImGuiTestStatus_Running) + ctx->Yield(); + } + + // Capture failure screenshot. + if (ctx->IsError() && engine->IO.ConfigCaptureOnError) + { + // FIXME-VIEWPORT: Tested windows may be in their own viewport. This only captures everything in main viewport. Capture tool may be extended to capture viewport windows as well. This would leave out OS windows which may be a cause of failure. + ImGuiCaptureArgs args; + args.InFlags = ImGuiCaptureFlags_Instant; + args.InCaptureRect.Min = ImGui::GetMainViewport()->Pos; + args.InCaptureRect.Max = args.InCaptureRect.Min + ImGui::GetMainViewport()->Size; + ImFormatString(args.InOutputFile, IM_ARRAYSIZE(args.InOutputFile), "output/failures/%s_%04d.png", ctx->Test->Name, ctx->ErrorCounter); + if (ImGuiTestEngine_CaptureScreenshot(engine, &args)) + ctx->LogDebug("Saved '%s' (%d*%d pixels)", args.InOutputFile, (int)args.OutImageSize.x, (int)args.OutImageSize.y); + } + + // Recover missing End*/Pop* calls. + ctx->RecoverFromUiContextErrors(); + + if (engine->IO.ConfigRunSpeed != ImGuiTestRunSpeed_Fast) + ctx->SleepStandard(); + + // Stop in GuiFunc mode + if (engine->IO.ConfigKeepGuiFunc && ctx->IsError()) + { + // Position mouse cursor + ctx->UiContext->IO.WantSetMousePos = true; + ctx->UiContext->IO.MousePos = engine->Inputs.MousePosValue; + + // Restore backend clipboard functions + backup_ui_context.RestoreClipboardFuncs(*ctx->UiContext); + + // Unhide foreign windows (may be useful sometimes to inspect GuiFunc state... sometimes not) + //ctx->ForeignWindowsUnhideAll(); + } + + // Keep GuiFunc spinning + // FIXME-TESTS: after an error, this is not visible in the UI because status is not _Running anymore... + if (engine->IO.ConfigKeepGuiFunc) + { + if (engine->TestsQueue.Size == 1 || test_output->Status == ImGuiTestStatus_Error) + { +#if IMGUI_VERSION_NUM >= 18992 + ImGui::TeleportMousePos(engine->Inputs.MousePosValue); +#endif + while (engine->IO.ConfigKeepGuiFunc && !engine->Abort) + { + ctx->RunFlags |= ImGuiTestRunFlags_GuiFuncOnly; + ctx->Yield(); + } + } + } + } + + IM_ASSERT(engine->CaptureCurrentArgs == NULL && "Active capture was not terminated in the test code."); + + // Process and display result/status + test_output->EndTime = ImTimeGetInMicroseconds(); + if (test_output->Status == ImGuiTestStatus_Running) + test_output->Status = ImGuiTestStatus_Success; + if (engine->Abort && test_output->Status != ImGuiTestStatus_Error) + test_output->Status = ImGuiTestStatus_Unknown; + + // Log result + if (test_output->Status == ImGuiTestStatus_Success) + { + if ((ctx->RunFlags & ImGuiTestRunFlags_NoSuccessMsg) == 0) + ctx->LogInfo("Success."); + } + else if (engine->Abort) + ctx->LogWarning("Aborted."); + else if (test_output->Status == ImGuiTestStatus_Error) + ctx->LogError("%s test failed.", test->Name); + else + ctx->LogWarning("Unknown status."); + + // Additional yields to avoid consecutive tests who may share identifiers from missing their window/item activation. + ctx->RunFlags |= ImGuiTestRunFlags_GuiFuncDisable; + ctx->Yield(2); + + // Restore active func + ctx->ActiveFunc = backup_active_func; + if (parent_ctx) + parent_ctx->FrameCount = ctx->FrameCount; + + // Restore backed up IO and style + backup_ui_context.Restore(*ctx->UiContext); + + if (run_flags & ImGuiTestRunFlags_ShareVars) + { + // Share generic vars? + if ((run_flags & ImGuiTestRunFlags_ShareTestContext) == 0) + parent_ctx->GenericVars = ctx->GenericVars; + } + else + { + // Destruct user vars + if (test->VarsConstructor != NULL) + { + test->VarsDestructor(ctx->UserVars); + if (ctx->UserVars) + IM_FREE(ctx->UserVars); + ctx->UserVars = NULL; + } + if (run_flags & ImGuiTestRunFlags_ShareTestContext) + { + parent_ctx->UserVars = backup_user_vars; + parent_ctx->GenericVars = backup_generic_vars; + } + } + + IM_ASSERT(engine->TestContext == ctx); + engine->TestContext = parent_ctx; +} + +//------------------------------------------------------------------------- +// [SECTION] CRASH HANDLING +//------------------------------------------------------------------------- +// - ImGuiTestEngine_CrashHandler() +// - ImGuiTestEngine_InstallDefaultCrashHandler() +//------------------------------------------------------------------------- + +void ImGuiTestEngine_CrashHandler() +{ + static bool handled = false; + if (handled) + return; + handled = true; + + ImGuiContext& g = *GImGui; + ImGuiTestEngine* engine = (ImGuiTestEngine*)g.TestEngine; + + // Write stop times, because thread executing tests will no longer run. + engine->BatchEndTime = ImTimeGetInMicroseconds(); + for (int i = 0; i < engine->TestsAll.Size; i++) + { + if (engine->TestContext) + if (ImGuiTest* test = engine->TestContext->Test) + if (test->Output.Status == ImGuiTestStatus_Running) + { + test->Output.Status = ImGuiTestStatus_Error; + test->Output.EndTime = engine->BatchEndTime; + break; + } + } + + // Export test run results. + ImGuiTestEngine_Export(engine); +} + +#ifdef _WIN32 +static LONG WINAPI ImGuiTestEngine_CrashHandlerWin32(LPEXCEPTION_POINTERS) +{ + ImGuiTestEngine_CrashHandler(); + return EXCEPTION_EXECUTE_HANDLER; +} +#else +static void ImGuiTestEngine_CrashHandlerUnix(int signal) +{ + IM_UNUSED(signal); + ImGuiTestEngine_CrashHandler(); + abort(); +} +#endif + +void ImGuiTestEngine_InstallDefaultCrashHandler() +{ +#ifdef _WIN32 + SetUnhandledExceptionFilter(&ImGuiTestEngine_CrashHandlerWin32); +#else + // Install a crash handler to relevant signals. + struct sigaction action = {}; + action.sa_handler = ImGuiTestEngine_CrashHandlerUnix; + action.sa_flags = SA_SIGINFO; + sigaction(SIGILL, &action, NULL); + sigaction(SIGABRT, &action, NULL); + sigaction(SIGFPE, &action, NULL); + sigaction(SIGSEGV, &action, NULL); + sigaction(SIGPIPE, &action, NULL); + sigaction(SIGBUS, &action, NULL); +#endif +} + + +//------------------------------------------------------------------------- +// [SECTION] HOOKS FOR CORE LIBRARY +//------------------------------------------------------------------------- +// - ImGuiTestEngineHook_ItemAdd() +// - ImGuiTestEngineHook_ItemAdd_GatherTask() +// - ImGuiTestEngineHook_ItemInfo() +// - ImGuiTestEngineHook_ItemInfo_ResolveFindByLabel() +// - ImGuiTestEngineHook_Log() +// - ImGuiTestEngineHook_AssertFunc() +//------------------------------------------------------------------------- + +// This is rather slow at it runs on all items but only during a GatherItems() operations. +static void ImGuiTestEngineHook_ItemAdd_GatherTask(ImGuiContext* ui_ctx, ImGuiTestEngine* engine, ImGuiID id, const ImRect& bb, const ImGuiLastItemData* item_data) +{ + ImGuiContext& g = *ui_ctx; + ImGuiWindow* window = g.CurrentWindow; + ImGuiTestGatherTask* task = &engine->GatherTask; + + if ((task->InLayerMask & (1 << window->DC.NavLayerCurrent)) == 0) + return; + + const ImGuiID parent_id = window->IDStack.Size ? window->IDStack.back() : 0; + const ImGuiID gather_parent_id = task->InParentID; + int result_depth = -1; + if (gather_parent_id == parent_id) + { + result_depth = 0; + } + else + { + const int max_depth = task->InMaxDepth; + + // When using a 'PushID(label); Widget(""); PopID();` pattern flatten as 1 deep instead of 2 for simplicity. + // We do this by offsetting our depth level. + int curr_depth = (id == parent_id) ? -1 : 0; + + ImGuiWindow* curr_window = window; + while (result_depth == -1 && curr_window != NULL) + { + const int id_stack_size = curr_window->IDStack.Size; + for (ImGuiID* p_id_stack = curr_window->IDStack.Data + id_stack_size - 1; p_id_stack >= curr_window->IDStack.Data; p_id_stack--, curr_depth++) + { + if (curr_depth >= max_depth) + break; + if (*p_id_stack == gather_parent_id) + { + result_depth = curr_depth; + break; + } + } + + // Recurse in child (could be policy/option in GatherTask) + if (curr_window->Flags & ImGuiWindowFlags_ChildWindow) + curr_window = curr_window->ParentWindow; + else + curr_window = NULL; + } + } + + if (result_depth != -1) + { + ImGuiTestItemInfo* item = task->OutList->Pool.GetOrAddByKey(id); // Add + item->TimestampMain = engine->FrameCount; + item->ID = id; + item->ParentID = parent_id; + item->Window = window; + item->RectFull = item->RectClipped = bb; + item->RectClipped.ClipWithFull(window->ClipRect); // This two step clipping is important, we want RectClipped to stays within RectFull + item->RectClipped.ClipWithFull(item->RectFull); + item->NavLayer = window->DC.NavLayerCurrent; + item->Depth = result_depth; + item->InFlags = item_data ? item_data->InFlags : ImGuiItemFlags_None; + item->StatusFlags = item_data ? item_data->StatusFlags : ImGuiItemStatusFlags_None; + task->LastItemInfo = item; + } +} + +void ImGuiTestEngineHook_ItemAdd(ImGuiContext* ui_ctx, ImGuiID id, const ImRect& bb, const ImGuiLastItemData* item_data) +{ + ImGuiTestEngine* engine = (ImGuiTestEngine*)ui_ctx->TestEngine; + + IM_ASSERT(id != 0); + ImGuiContext& g = *ui_ctx; + ImGuiWindow* window = g.CurrentWindow; + + // FIXME-OPT: Early out if there are no active Info/Gather tasks. + + // Info Tasks + if (ImGuiTestInfoTask* task = ImGuiTestEngine_FindInfoTask(engine, id)) + { + ImGuiTestItemInfo* item = &task->Result; + item->TimestampMain = engine->FrameCount; + item->ID = id; + item->ParentID = window->IDStack.Size ? window->IDStack.back() : 0; + item->Window = window; + item->RectFull = item->RectClipped = bb; + item->RectClipped.ClipWithFull(window->ClipRect); // This two step clipping is important, we want RectClipped to stays within RectFull + item->RectClipped.ClipWithFull(item->RectFull); + item->NavLayer = window->DC.NavLayerCurrent; + item->Depth = 0; + item->InFlags = item_data ? item_data->InFlags : ImGuiItemFlags_None; + item->StatusFlags = item_data ? item_data->StatusFlags : ImGuiItemStatusFlags_None; + } + + // Gather Task (only 1 can be active) + if (engine->GatherTask.InParentID != 0) + ImGuiTestEngineHook_ItemAdd_GatherTask(ui_ctx, engine, id, bb, item_data); +} + +#if IMGUI_VERSION_NUM < 18934 +void ImGuiTestEngineHook_ItemAdd(ImGuiContext* ui_ctx, const ImRect& bb, ImGuiID id) +{ + ImGuiTestEngineHook_ItemAdd(ui_ctx, id, bb, NULL); +} +#endif + +// Task is submitted in TestFunc by ItemInfo() -> ItemInfoHandleWildcardSearch() +#ifdef IMGUI_HAS_IMSTR +static void ImGuiTestEngineHook_ItemInfo_ResolveFindByLabel(ImGuiContext* ui_ctx, ImGuiID id, const ImStrv label, ImGuiItemStatusFlags flags) +#else +static void ImGuiTestEngineHook_ItemInfo_ResolveFindByLabel(ImGuiContext* ui_ctx, ImGuiID id, const char* label, ImGuiItemStatusFlags flags) +#endif +{ + // At this point "label" is a match for the right-most name in user wildcard (e.g. the "bar" of "**/foo/bar" + ImGuiContext& g = *ui_ctx; + ImGuiTestEngine* engine = (ImGuiTestEngine*)ui_ctx->TestEngine; + IM_UNUSED(label); // Match ABI of caller function (faster call) + + // Test for matching status flags + ImGuiTestFindByLabelTask* label_task = &engine->FindByLabelTask; + if (ImGuiItemStatusFlags filter_flags = label_task->InFilterItemStatusFlags) + if (!(filter_flags & flags)) + return; + + // Test for matching PREFIX (the "window" of "window/**/foo/bar" or the "" of "/**/foo/bar") + // FIXME-TESTS: Stack depth limit? + // FIXME-TESTS: Recurse back into parent window limit? + bool match_prefix = false; + if (label_task->InPrefixId == 0) + { + match_prefix = true; + } + else + { + // Recurse back into parent, so from "WindowA" with SetRef("WindowA") it is possible to use "**/Button" to reach "WindowA/ChildXXXX/Button" + for (ImGuiWindow* window = g.CurrentWindow; window != NULL && !match_prefix; window = window->ParentWindow) + { + const int id_stack_size = window->IDStack.Size; + for (ImGuiID* p_id_stack = window->IDStack.Data + id_stack_size - 1; p_id_stack >= window->IDStack.Data; p_id_stack--) + if (*p_id_stack == label_task->InPrefixId) + { + match_prefix = true; + break; + } + } + } + if (!match_prefix) + return; + + // Test for full matching SUFFIX (the "foo/bar" or "window/**/foo/bar") + // Because at this point we have only compared the prefix and the right-most label (the "window" and "bar" or "window/**/foo/bar") + // FIXME-TESTS: The entire suffix must be inside the final window: + // - In theory, someone could craft a suffix that contains sub-window, e.g. "SomeWindow/**/SomeChild_XXXX/SomeItem" and this will fail. + // - Once we make child path easier to access we can fix that. + if (label_task->InSuffixDepth > 1) // This is merely an early out: for Depth==1 the compare has already been done in ImGuiTestEngineHook_ItemInfo() + { + ImGuiWindow* window = g.CurrentWindow; + const int id_stack_size = window->IDStack.Size; + int id_stack_pos = id_stack_size - label_task->InSuffixDepth; + + // At this point, IN MOST CASES (BUT NOT ALL) this should be the case: + // ImHashStr(label, 0, g.CurrentWindow->IDStack.back()) == id + // It's not always the case as we have situations where we call IMGUI_TEST_ENGINE_ITEM_INFO() outside of the right stack location: + // e.g. Begin(), or items using the PushID(label); SubItem(""); PopID(); idiom. + // If you are curious or need to understand this more in depth, uncomment this assert to detect them: + // ImGuiID tmp_id = ImHashStr(label, 0, g.CurrentWindow->IDStack.back()); + // IM_ASSERT(tmp_id == id); + // The "Try with parent" case is designed to handle that. May need further tuning. + + ImGuiID base_id = id_stack_pos >= 0 ? window->IDStack.Data[id_stack_pos] : 0; // base_id correspond to the "**" + ImGuiID find_id = ImHashDecoratedPath(label_task->InSuffix, NULL, base_id); // hash the whole suffix e.g. "foo/bar" over our base + if (id != find_id) + { + // Try with parent + base_id = id_stack_pos > 0 ? window->IDStack.Data[id_stack_pos - 1] : 0; + find_id = ImHashDecoratedPath(label_task->InSuffix, NULL, base_id); + if (id != find_id) + return; + } + } + + // Success + label_task->OutItemId = id; +} + +// label is optional +#ifdef IMGUI_HAS_IMSTR +void ImGuiTestEngineHook_ItemInfo(ImGuiContext* ui_ctx, ImGuiID id, ImStrv label, ImGuiItemStatusFlags flags) +#else +void ImGuiTestEngineHook_ItemInfo(ImGuiContext* ui_ctx, ImGuiID id, const char* label, ImGuiItemStatusFlags flags) +#endif +{ + ImGuiTestEngine* engine = (ImGuiTestEngine*)ui_ctx->TestEngine; + + IM_ASSERT(id != 0); + ImGuiContext& g = *ui_ctx; + //ImGuiWindow* window = g.CurrentWindow; + //IM_ASSERT(window->DC.LastItemId == id || window->DC.LastItemId == 0); // Need _ItemAdd() to be submitted before _ItemInfo() + + // Update Info Task status flags + if (ImGuiTestInfoTask* task = ImGuiTestEngine_FindInfoTask(engine, id)) + { + ImGuiTestItemInfo* item = &task->Result; + item->TimestampStatus = g.FrameCount; + item->StatusFlags = flags; + if (label) + ImStrncpy(item->DebugLabel, label, IM_ARRAYSIZE(item->DebugLabel)); + } + + // Update Gather Task status flags + if (engine->GatherTask.LastItemInfo && engine->GatherTask.LastItemInfo->ID == id) + { + ImGuiTestItemInfo* item = engine->GatherTask.LastItemInfo; + item->TimestampStatus = g.FrameCount; + item->StatusFlags = flags; + if (label) + ImStrncpy(item->DebugLabel, label, IM_ARRAYSIZE(item->DebugLabel)); + } + + // Update Find by Label Task + // FIXME-TESTS FIXME-OPT: Compare by hashes instead of strcmp to support "###" operator. + // Perhaps we could use strcmp() if we detect that ### is not used, that would be faster. + ImGuiTestFindByLabelTask* label_task = &engine->FindByLabelTask; + if (label && label_task->InSuffixLastItem && label_task->OutItemId == 0) +#ifdef IMGUI_HAS_IMSTR + if (label_task->InSuffixLastItemHash == ImHashStr(label)) +#else + if (label_task->InSuffixLastItemHash == ImHashStr(label, 0)) +#endif + ImGuiTestEngineHook_ItemInfo_ResolveFindByLabel(ui_ctx, id, label, flags); +} + +// Forward core/user-land text to test log +// This is called via the user-land IMGUI_TEST_ENGINE_LOG() macro. +void ImGuiTestEngineHook_Log(ImGuiContext* ui_ctx, const char* fmt, ...) +{ + ImGuiTestEngine* engine = (ImGuiTestEngine*)ui_ctx->TestEngine; + + va_list args; + va_start(args, fmt); + engine->TestContext->LogExV(ImGuiTestVerboseLevel_Debug, ImGuiTestLogFlags_None, fmt, args); + va_end(args); +} + +// Helper to output extra information (e.g. current test) during an assert. +// Your custom assert code may optionally want to call this. +void ImGuiTestEngine_AssertLog(const char* expr, const char* file, const char* function, int line) +{ + ImGuiTestEngine* engine = GImGuiTestEngine; + if (ImGuiTestContext* ctx = engine->TestContext) + { + ctx->LogError("Assert: '%s'", expr); + ctx->LogWarning("In %s:%d, function %s()", file, line, function); + if (ImGuiTest* test = ctx->Test) + ctx->LogWarning("While running test: %s %s", test->Category, test->Name); + } +} + +// Used by IM_CHECK_OP() macros +ImGuiTextBuffer* ImGuiTestEngine_GetTempStringBuilder() +{ + static ImGuiTextBuffer builder; + builder.Buf.resize(1); + builder.Buf[0] = 0; + return &builder; +} + +// Out of convenience for main library we allow this to be called before TestEngine is initialized. +const char* ImGuiTestEngine_FindItemDebugLabel(ImGuiContext* ui_ctx, ImGuiID id) +{ + if (ui_ctx->TestEngine == NULL || id == 0) + return NULL; + if (ImGuiTestItemInfo* id_info = ImGuiTestEngine_FindItemInfo((ImGuiTestEngine*)ui_ctx->TestEngine, id, "")) + return id_info->DebugLabel; + return NULL; +} + +//------------------------------------------------------------------------- +// [SECTION] CHECK/ERROR FUNCTIONS FOR TESTS +//------------------------------------------------------------------------- +// - ImGuiTestEngine_Check() +// - ImGuiTestEngine_Error() +//------------------------------------------------------------------------- + +// Return true to request a debugger break +bool ImGuiTestEngine_Check(const char* file, const char* func, int line, ImGuiTestCheckFlags flags, bool result, const char* expr) +{ + ImGuiTestEngine* engine = GImGuiTestEngine; + (void)func; + + // Removed absolute path from output so we have deterministic output (otherwise __FILE__ gives us machine dending output) + const char* file_without_path = file ? ImPathFindFilename(file) : ""; + + if (ImGuiTestContext* ctx = engine->TestContext) + { + ImGuiTest* test = ctx->Test; + //ctx->LogDebug("IM_CHECK(%s)", expr); + if (!result) + { + if (!(ctx->RunFlags & ImGuiTestRunFlags_GuiFuncOnly)) + test->Output.Status = ImGuiTestStatus_Error; + + if (file) + ctx->LogError("Error %s:%d '%s'", file_without_path, line, expr); + else + ctx->LogError("Error '%s'", expr); + ctx->ErrorCounter++; + } + else if (!(flags & ImGuiTestCheckFlags_SilentSuccess)) + { + if (file) + ctx->LogInfo("OK %s:%d '%s'", file_without_path, line, expr); + else + ctx->LogInfo("OK '%s'", expr); + } + } + else + { + IM_ASSERT(0 && "No active tests!"); + } + + if (result == false && engine->IO.ConfigStopOnError && !engine->Abort) + engine->Abort = true; //ImGuiTestEngine_Abort(engine); + if (result == false && engine->IO.ConfigBreakOnError && !engine->Abort) + return true; + + return false; +} + +bool ImGuiTestEngine_CheckStrOp(const char* file, const char* func, int line, ImGuiTestCheckFlags flags, const char* op, const char* lhs_var, const char* lhs_value, const char* rhs_var, const char* rhs_value, bool* out_res) +{ + int res_strcmp = strcmp(lhs_value, rhs_value); + bool res = 0; + if (strcmp(op, "==") == 0) + res = (res_strcmp == 0); + else if (strcmp(op, "!=") == 0) + res = (res_strcmp != 0); + else + IM_ASSERT(0); + *out_res = res; + + ImGuiTextBuffer buf; // FIXME-OPT: Now we can probably remove that allocation + + bool lhs_is_literal = lhs_var[0] == '\"'; + bool rhs_is_literal = rhs_var[0] == '\"'; + if (strchr(lhs_value, '\n') != NULL || strchr(rhs_value, '\n') != NULL) + { + // Multi line strings + size_t lhs_value_len = strlen(lhs_value); + size_t rhs_value_len = strlen(rhs_value); + if (lhs_value_len > 0 && lhs_value[lhs_value_len - 1] == '\n') // Strip trailing carriage return as we are adding one ourselves + lhs_value_len--; + if (rhs_value_len > 0 && rhs_value[rhs_value_len - 1] == '\n') + rhs_value_len--; + buf.appendf( + "\n" + "---------------------------------------- // lhs: %s\n" + "%.*s\n" + "---------------------------------------- // rhs: %s, compare op: %s\n" + "%.*s\n" + "----------------------------------------\n", + lhs_is_literal ? "literal" : lhs_var, + (int)lhs_value_len, lhs_value, + rhs_is_literal ? "literal" : rhs_var, + op, + (int)rhs_value_len, rhs_value); + } + else + { + // Single line strings + buf.appendf( + "%s [\"%s\"] %s %s [\"%s\"]", + lhs_is_literal ? "" : lhs_var, lhs_value, + op, + rhs_is_literal ? "" : rhs_var, rhs_value); + } + + + return ImGuiTestEngine_Check(file, func, line, flags, res, buf.c_str()); +} + +bool ImGuiTestEngine_Error(const char* file, const char* func, int line, ImGuiTestCheckFlags flags, const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + Str256 buf; + buf.setfv(fmt, args); + bool ret = ImGuiTestEngine_Check(file, func, line, flags, false, buf.c_str()); + va_end(args); + + ImGuiTestEngine* engine = GImGuiTestEngine; + if (engine && engine->Abort) + return false; + return ret; +} + +//------------------------------------------------------------------------- +// [SECTION] SETTINGS +//------------------------------------------------------------------------- +// FIXME: In our wildest dreams we could provide a imgui_club/ serialization helper that would be +// easy to use in both the ReadLine and WriteAll functions. +//------------------------------------------------------------------------- + +static void* ImGuiTestEngine_SettingsReadOpen(ImGuiContext*, ImGuiSettingsHandler*, const char* name) +{ + if (strcmp(name, "Data") != 0) + return NULL; + return (void*)1; +} + +static bool SettingsTryReadString(const char* line, const char* prefix, char* out_buf, size_t out_buf_size) +{ + // Could also use scanf() with "%[^\n]" but it won't bound check. + size_t prefix_len = strlen(prefix); + if (strncmp(line, prefix, prefix_len) != 0) + return false; + line += prefix_len; + IM_ASSERT(out_buf_size >= strlen(line) + 1); + ImFormatString(out_buf, out_buf_size, "%s", line); + return true; +} + +static bool SettingsTryReadString(const char* line, const char* prefix, Str* out_str) +{ + // Could also use scanf() with "%[^\n]" but it won't bound check. + size_t prefix_len = strlen(prefix); + if (strncmp(line, prefix, prefix_len) != 0) + return false; + line += prefix_len; + out_str->set(line); + return true; +} + +static void ImGuiTestEngine_SettingsReadLine(ImGuiContext* ui_ctx, ImGuiSettingsHandler*, void* entry, const char* line) +{ + ImGuiTestEngine* e = (ImGuiTestEngine*)ui_ctx->TestEngine; + IM_ASSERT(e != NULL); + IM_ASSERT(e->UiContextTarget == ui_ctx); + IM_UNUSED(entry); + + int n = 0; + /**/ if (SettingsTryReadString(line, "FilterTests=", e->UiFilterTests)) { } + else if (SettingsTryReadString(line, "FilterPerfs=", e->UiFilterPerfs)) { } + else if (sscanf(line, "LogHeight=%f", &e->UiLogHeight) == 1) { } + else if (sscanf(line, "CaptureTool=%d", &n) == 1) { e->UiCaptureToolOpen = (n != 0); } + else if (sscanf(line, "PerfTool=%d", &n) == 1) { e->UiPerfToolOpen = (n != 0); } + else if (sscanf(line, "StackTool=%d", &n) == 1) { e->UiStackToolOpen = (n != 0); } + else if (sscanf(line, "CaptureEnabled=%d", &n) == 1) { e->IO.ConfigCaptureEnabled = (n != 0); } + else if (sscanf(line, "CaptureOnError=%d", &n) == 1) { e->IO.ConfigCaptureOnError = (n != 0); } + else if (SettingsTryReadString(line, "VideoCapturePathToEncoder=", e->IO.VideoCaptureEncoderPath, IM_ARRAYSIZE(e->IO.VideoCaptureEncoderPath))) { } + else if (SettingsTryReadString(line, "VideoCaptureParamsToEncoder=", e->IO.VideoCaptureEncoderParams, IM_ARRAYSIZE(e->IO.VideoCaptureEncoderParams))) { } + else if (SettingsTryReadString(line, "GifCaptureParamsToEncoder=", e->IO.GifCaptureEncoderParams, IM_ARRAYSIZE(e->IO.GifCaptureEncoderParams))) { } + else if (SettingsTryReadString(line, "VideoCaptureExtension=", e->IO.VideoCaptureExtension, IM_ARRAYSIZE(e->IO.VideoCaptureExtension))) { } +} + +static void ImGuiTestEngine_SettingsWriteAll(ImGuiContext* ui_ctx, ImGuiSettingsHandler* handler, ImGuiTextBuffer* buf) +{ + ImGuiTestEngine* engine = (ImGuiTestEngine*)ui_ctx->TestEngine; + IM_ASSERT(engine != NULL); + IM_ASSERT(engine->UiContextTarget == ui_ctx); + + buf->appendf("[%s][Data]\n", handler->TypeName); + buf->appendf("FilterTests=%s\n", engine->UiFilterTests->c_str()); + buf->appendf("FilterPerfs=%s\n", engine->UiFilterPerfs->c_str()); + buf->appendf("LogHeight=%.0f\n", engine->UiLogHeight); + buf->appendf("CaptureTool=%d\n", engine->UiCaptureToolOpen); + buf->appendf("PerfTool=%d\n", engine->UiPerfToolOpen); + buf->appendf("StackTool=%d\n", engine->UiStackToolOpen); + buf->appendf("CaptureEnabled=%d\n", engine->IO.ConfigCaptureEnabled); + buf->appendf("CaptureOnError=%d\n", engine->IO.ConfigCaptureOnError); + buf->appendf("VideoCapturePathToEncoder=%s\n", engine->IO.VideoCaptureEncoderPath); + buf->appendf("VideoCaptureParamsToEncoder=%s\n", engine->IO.VideoCaptureEncoderParams); + buf->appendf("GifCaptureParamsToEncoder=%s\n", engine->IO.GifCaptureEncoderParams); + buf->appendf("VideoCaptureExtension=%s\n", engine->IO.VideoCaptureExtension); + buf->appendf("\n"); +} + +//------------------------------------------------------------------------- +// [SECTION] ImGuiTestLog +//------------------------------------------------------------------------- + +void ImGuiTestLog::Clear() +{ + Buffer.clear(); + LineInfo.clear(); + memset(&CountPerLevel, 0, sizeof(CountPerLevel)); +} + +// Output: +// - If 'buffer != NULL': all extracted lines are appended to 'buffer'. Use 'buffer->c_str()' on your side to obtain the text. +// - Return value: number of lines extracted (should be equivalent to number of '\n' inside buffer->c_str()). +// - You may call the function with buffer == NULL to only obtain a count without getting the data. +// Verbose levels are inclusive: +// - To get ONLY Error: Use level_min == ImGuiTestVerboseLevel_Error, level_max = ImGuiTestVerboseLevel_Error +// - To get ONLY Error and Warnings: Use level_min == ImGuiTestVerboseLevel_Error, level_max = ImGuiTestVerboseLevel_Warning +// - To get All Errors, Warnings, Debug... Use level_min == ImGuiTestVerboseLevel_Error, level_max = ImGuiTestVerboseLevel_Trace +int ImGuiTestLog::ExtractLinesForVerboseLevels(ImGuiTestVerboseLevel level_min, ImGuiTestVerboseLevel level_max, ImGuiTextBuffer* out_buffer) +{ + IM_ASSERT(level_min <= level_max); + + // Return count + int count = 0; + if (out_buffer == NULL) + { + for (int n = level_min; n <= level_max; n++) + count += CountPerLevel[n]; + return count; + } + + // Extract lines and return count + for (auto& line_info : LineInfo) + if (line_info.Level >= level_min && line_info.Level <= level_max) + { + const char* line_begin = Buffer.c_str() + line_info.LineOffset; + const char* line_end = strchr(line_begin, '\n'); + out_buffer->append(line_begin, line_end[0] == '\n' ? line_end + 1 : line_end); + count++; + } + return count; +} + +void ImGuiTestLog::UpdateLineOffsets(ImGuiTestEngineIO* engine_io, ImGuiTestVerboseLevel level, const char* start) +{ + IM_UNUSED(engine_io); + IM_ASSERT(Buffer.begin() <= start && start < Buffer.end()); + const char* p_begin = start; + const char* p_end = Buffer.end(); + const char* p = p_begin; + while (p < p_end) + { + const char* p_bol = p; + const char* p_eol = strchr(p, '\n'); + + bool last_empty_line = (p_bol + 1 == p_end); + if (!last_empty_line) + { + int offset = (int)(p_bol - Buffer.c_str()); + LineInfo.push_back({level, offset}); + CountPerLevel[level] += 1; + } + p = p_eol ? p_eol + 1 : NULL; + } +} + +//------------------------------------------------------------------------- +// [SECTION] ImGuiTest +//------------------------------------------------------------------------- + +ImGuiTest::~ImGuiTest() +{ + if (NameOwned) + ImGui::MemFree((char*)Name); +} + +void ImGuiTest::SetOwnedName(const char* name) +{ + IM_ASSERT(!NameOwned); + NameOwned = true; + Name = ImStrdup(name); +} + +//------------------------------------------------------------------------- diff --git a/libs/imgui_test_engine/imgui_te_engine.h b/libs/imgui_test_engine/imgui_te_engine.h new file mode 100644 index 0000000..c5a585d --- /dev/null +++ b/libs/imgui_test_engine/imgui_te_engine.h @@ -0,0 +1,429 @@ +// dear imgui test engine +// (core) +// This is the interface that your initial setup (app init, main loop) will mostly be using. +// Actual tests will mostly use the interface of imgui_te_context.h + +#pragma once + +#include "imgui.h" +#include "imgui_internal.h" // ImPool<>, ImRect, ImGuiItemStatusFlags, ImFormatString +#include "imgui_te_utils.h" // ImFuncPtr +#include "imgui_capture_tool.h" // ImGuiScreenCaptureFunc + +//------------------------------------------------------------------------- +// Forward Declarations +//------------------------------------------------------------------------- + +struct ImGuiTest; // Data for a test registered with IM_REGISTER_TEST() +struct ImGuiTestContext; // Context while a test is running +struct ImGuiTestCoroutineInterface; // Interface to expose coroutine functions (imgui_te_coroutine provides a default implementation for C++11 using std::thread, but you may use your own) +struct ImGuiTestEngine; // Test engine instance +struct ImGuiTestEngineIO; // Test engine public I/O +struct ImGuiTestItemInfo; // Info queried from item (id, geometry, status flags, debug label) +struct ImGuiTestItemList; // A list of items +struct ImGuiTestInputs; // Simulated user inputs (will be fed into ImGuiIO by the test engine) +struct ImGuiTestRunTask; // A queued test (test + runflags) + +typedef int ImGuiTestFlags; // Flags: See ImGuiTestFlags_ +typedef int ImGuiTestCheckFlags; // Flags: See ImGuiTestCheckFlags_ +typedef int ImGuiTestLogFlags; // Flags: See ImGuiTestLogFlags_ +typedef int ImGuiTestRunFlags; // Flags: See ImGuiTestRunFlags_ + +enum ImGuiTestActiveFunc : int; +enum ImGuiTestGroup : int; +enum ImGuiTestRunSpeed : int; +enum ImGuiTestStatus : int; +enum ImGuiTestVerboseLevel : int; +enum ImGuiTestEngineExportFormat : int; + +//------------------------------------------------------------------------- +// Types +//------------------------------------------------------------------------- + +// Stored in ImGuiTestContext: where we are currently running GuiFunc or TestFunc +enum ImGuiTestActiveFunc : int +{ + ImGuiTestActiveFunc_None, + ImGuiTestActiveFunc_GuiFunc, + ImGuiTestActiveFunc_TestFunc +}; + +enum ImGuiTestRunSpeed : int +{ + ImGuiTestRunSpeed_Fast = 0, // Run tests as fast as possible (teleport mouse, skip delays, etc.) + ImGuiTestRunSpeed_Normal = 1, // Run tests at human watchable speed (for debugging) + ImGuiTestRunSpeed_Cinematic = 2, // Run tests with pauses between actions (for e.g. tutorials) + ImGuiTestRunSpeed_COUNT +}; + +enum ImGuiTestVerboseLevel : int +{ + ImGuiTestVerboseLevel_Silent = 0, // -v0 + ImGuiTestVerboseLevel_Error = 1, // -v1 + ImGuiTestVerboseLevel_Warning = 2, // -v2 + ImGuiTestVerboseLevel_Info = 3, // -v3 + ImGuiTestVerboseLevel_Debug = 4, // -v4 + ImGuiTestVerboseLevel_Trace = 5, + ImGuiTestVerboseLevel_COUNT +}; + +// Test status (stored in ImGuiTest) +enum ImGuiTestStatus : int +{ + ImGuiTestStatus_Unknown = -1, + ImGuiTestStatus_Success = 0, + ImGuiTestStatus_Queued = 1, + ImGuiTestStatus_Running = 2, + ImGuiTestStatus_Error = 3, + ImGuiTestStatus_Suspended = 4, + ImGuiTestStatus_COUNT +}; + +// Test group: this is mostly used to categorize tests in our testing UI. (Stored in ImGuiTest) +enum ImGuiTestGroup : int +{ + ImGuiTestGroup_Unknown = -1, + ImGuiTestGroup_Tests = 0, + ImGuiTestGroup_Perfs = 1, + ImGuiTestGroup_COUNT +}; + +// Flags (stored in ImGuiTest) +enum ImGuiTestFlags_ +{ + ImGuiTestFlags_None = 0, + ImGuiTestFlags_NoGuiWarmUp = 1 << 0, // Disable running the GUI func for 2 frames before starting test code. For tests which absolutely need to start before GuiFunc. + ImGuiTestFlags_NoAutoFinish = 1 << 1, // By default, tests with no TestFunc (only a GuiFunc) will end after warmup. Setting this require test to call ctx->Finish(). + ImGuiTestFlags_NoRecoveryWarnings = 1 << 2 // Disable state recovery warnings (missing End/Pop calls etc.) for tests which may rely on those. + //ImGuiTestFlags_RequireViewports = 1 << 10 +}; + +// Flags for IM_CHECK* macros. +enum ImGuiTestCheckFlags_ +{ + ImGuiTestCheckFlags_None = 0, + ImGuiTestCheckFlags_SilentSuccess = 1 << 0 +}; + +// Flags for ImGuiTestContext::Log* functions. +enum ImGuiTestLogFlags_ +{ + ImGuiTestLogFlags_None = 0, + ImGuiTestLogFlags_NoHeader = 1 << 0 // Do not display frame count and depth padding +}; + +enum ImGuiTestRunFlags_ +{ + ImGuiTestRunFlags_None = 0, + ImGuiTestRunFlags_GuiFuncDisable = 1 << 0, // Used internally to temporarily disable the GUI func (at the end of a test, etc) + ImGuiTestRunFlags_GuiFuncOnly = 1 << 1, // Set when user selects "Run GUI func" + ImGuiTestRunFlags_NoSuccessMsg = 1 << 2, + ImGuiTestRunFlags_EnableRawInputs = 1 << 3, // Disable input submission to let test submission raw input event (in order to test e.g. IO queue) + ImGuiTestRunFlags_RunFromGui = 1 << 4, // Test ran manually from GUI, will disable watchdog. + ImGuiTestRunFlags_RunFromCommandLine= 1 << 5, // Test queued from command-line. + + // Flags for ImGuiTestContext::RunChildTest() + ImGuiTestRunFlags_NoError = 1 << 10, + ImGuiTestRunFlags_ShareVars = 1 << 11, // Share generic vars and custom vars between child and parent tests (custom vars need to be same type) + ImGuiTestRunFlags_ShareTestContext = 1 << 12, // Share ImGuiTestContext instead of creating a new one (unsure what purpose this may be useful for yet) + // TODO: Add GuiFunc options +}; + +//------------------------------------------------------------------------- +// Functions +//------------------------------------------------------------------------- + +// Hooks for core imgui/ library (generally called via macros) +extern void ImGuiTestEngineHook_ItemAdd(ImGuiContext* ui_ctx, ImGuiID id, const ImRect& bb, const ImGuiLastItemData* item_data); +#if IMGUI_VERSION_NUM < 18934 +extern void ImGuiTestEngineHook_ItemAdd(ImGuiContext* ui_ctx, const ImRect& bb, ImGuiID id); +#endif +#ifdef IMGUI_HAS_IMSTR +extern void ImGuiTestEngineHook_ItemInfo(ImGuiContext* ui_ctx, ImGuiID id, ImStrv label, ImGuiItemStatusFlags flags); +#else +extern void ImGuiTestEngineHook_ItemInfo(ImGuiContext* ui_ctx, ImGuiID id, const char* label, ImGuiItemStatusFlags flags); +#endif +extern void ImGuiTestEngineHook_Log(ImGuiContext* ui_ctx, const char* fmt, ...); +extern const char* ImGuiTestEngine_FindItemDebugLabel(ImGuiContext* ui_ctx, ImGuiID id); + +// Functions (generally called via IM_CHECK() macros) +IMGUI_API bool ImGuiTestEngine_Check(const char* file, const char* func, int line, ImGuiTestCheckFlags flags, bool result, const char* expr); +IMGUI_API bool ImGuiTestEngine_CheckStrOp(const char* file, const char* func, int line, ImGuiTestCheckFlags flags, const char* op, const char* lhs_var, const char* lhs_value, const char* rhs_var, const char* rhs_value, bool* out_result); +IMGUI_API bool ImGuiTestEngine_Error(const char* file, const char* func, int line, ImGuiTestCheckFlags flags, const char* fmt, ...); +IMGUI_API void ImGuiTestEngine_AssertLog(const char* expr, const char* file, const char* function, int line); +IMGUI_API ImGuiTextBuffer* ImGuiTestEngine_GetTempStringBuilder(); + +//------------------------------------------------------------------------- +// ImGuiTestEngine API +//------------------------------------------------------------------------- + +// Functions: Initialization +IMGUI_API ImGuiTestEngine* ImGuiTestEngine_CreateContext(); // Create test engine +IMGUI_API void ImGuiTestEngine_DestroyContext(ImGuiTestEngine* engine); // Destroy test engine. Call after ImGui::DestroyContext() so test engine specific ini data gets saved. +IMGUI_API void ImGuiTestEngine_Start(ImGuiTestEngine* engine, ImGuiContext* ui_ctx); // Bind to a dear imgui context. Start coroutine. +IMGUI_API void ImGuiTestEngine_Stop(ImGuiTestEngine* engine); // Stop coroutine and export if any. (Unbind will lazily happen on context shutdown) +IMGUI_API void ImGuiTestEngine_PostSwap(ImGuiTestEngine* engine); // Call every frame after framebuffer swap, will process screen capture and call test_io.ScreenCaptureFunc() +IMGUI_API ImGuiTestEngineIO& ImGuiTestEngine_GetIO(ImGuiTestEngine* engine); + +// Macros: Register Test +#define IM_REGISTER_TEST(_ENGINE, _CATEGORY, _NAME) ImGuiTestEngine_RegisterTest(_ENGINE, _CATEGORY, _NAME, __FILE__, __LINE__) +IMGUI_API ImGuiTest* ImGuiTestEngine_RegisterTest(ImGuiTestEngine* engine, const char* category, const char* name, const char* src_file = NULL, int src_line = 0); // Prefer calling IM_REGISTER_TEST() + +// Functions: Main +IMGUI_API void ImGuiTestEngine_QueueTest(ImGuiTestEngine* engine, ImGuiTest* test, ImGuiTestRunFlags run_flags = 0); +IMGUI_API void ImGuiTestEngine_QueueTests(ImGuiTestEngine* engine, ImGuiTestGroup group, const char* filter = NULL, ImGuiTestRunFlags run_flags = 0); +IMGUI_API bool ImGuiTestEngine_TryAbortEngine(ImGuiTestEngine* engine); +IMGUI_API void ImGuiTestEngine_AbortCurrentTest(ImGuiTestEngine* engine); +IMGUI_API ImGuiTest* ImGuiTestEngine_FindTestByName(ImGuiTestEngine* engine, const char* category, const char* name); + +// Functions: Status Queries +// FIXME: Clarify API to avoid function calls vs raw bools in ImGuiTestEngineIO +IMGUI_API bool ImGuiTestEngine_IsTestQueueEmpty(ImGuiTestEngine* engine); +IMGUI_API bool ImGuiTestEngine_IsUsingSimulatedInputs(ImGuiTestEngine* engine); +IMGUI_API void ImGuiTestEngine_GetResult(ImGuiTestEngine* engine, int& count_tested, int& success_count); +IMGUI_API void ImGuiTestEngine_GetTestList(ImGuiTestEngine* engine, ImVector* out_tests); +IMGUI_API void ImGuiTestEngine_GetTestQueue(ImGuiTestEngine* engine, ImVector* out_tests); + +// Functions: Crash Handling +// Ensure past test results are properly exported even if application crash during a test. +IMGUI_API void ImGuiTestEngine_InstallDefaultCrashHandler(); // Install default crash handler (if you don't have one) +IMGUI_API void ImGuiTestEngine_CrashHandler(); // Default crash handler, should be called from a custom crash handler if such exists + +//----------------------------------------------------------------------------- +// IO structure to configure the test engine +//----------------------------------------------------------------------------- + +// Function bound to right-clicking on a test and selecting "Open source" in the UI +// - Easy: you can make this function call OS shell to "open" the file (e.g. ImOsOpenInShell() helper). +// - Better: bind this function to a custom setup which can pass line number to a text editor (e.g. see 'imgui_test_suite/tools/win32_open_with_sublime.cmd' example) +typedef void (ImGuiTestEngineSrcFileOpenFunc)(const char* filename, int line_no, void* user_data); + +struct IMGUI_API ImGuiTestEngineIO +{ + //------------------------------------------------------------------------- + // Functions + //------------------------------------------------------------------------- + + // Options: Functions + ImGuiTestCoroutineInterface* CoroutineFuncs = NULL; // (Required) Coroutine functions (see imgui_te_coroutines.h) + ImFuncPtr(ImGuiTestEngineSrcFileOpenFunc) SrcFileOpenFunc = NULL; // (Optional) To open source files from test engine UI + ImFuncPtr(ImGuiScreenCaptureFunc) ScreenCaptureFunc = NULL; // (Optional) To capture graphics output (application _MUST_ call ImGuiTestEngine_PostSwap() function after swapping is framebuffer) + void* SrcFileOpenUserData = NULL; // (Optional) User data for SrcFileOpenFunc + void* ScreenCaptureUserData = NULL; // (Optional) User data for ScreenCaptureFunc + + // Options: Main + bool ConfigSavedSettings = true; // Load/Save settings in main context .ini file. + ImGuiTestRunSpeed ConfigRunSpeed = ImGuiTestRunSpeed_Fast; // Run tests in fast/normal/cinematic mode + bool ConfigStopOnError = false; // Stop queued tests on test error + bool ConfigBreakOnError = false; // Break debugger on test error by calling IM_DEBUG_BREAK() + bool ConfigKeepGuiFunc = false; // Keep test GUI running at the end of the test + ImGuiTestVerboseLevel ConfigVerboseLevel = ImGuiTestVerboseLevel_Warning; + ImGuiTestVerboseLevel ConfigVerboseLevelOnError = ImGuiTestVerboseLevel_Info; + bool ConfigLogToTTY = false; + bool ConfigLogToDebugger = false; + bool ConfigRestoreFocusAfterTests = true;// Restore focus back after running tests + bool ConfigCaptureEnabled = true; // Master enable flags for capturing and saving captures. Disable to avoid e.g. lengthy saving of large PNG files. + bool ConfigCaptureOnError = false; + bool ConfigNoThrottle = false; // Disable vsync for performance measurement or fast test running + bool ConfigMouseDrawCursor = true; // Enable drawing of Dear ImGui software mouse cursor when running tests + float ConfigFixedDeltaTime = 0.0f; // Use fixed delta time instead of calculating it from wall clock + int PerfStressAmount = 1; // Integer to scale the amount of items submitted in test + char GitBranchName[64] = ""; // e.g. fill in branch name (e.g. recorded in perf samples .csv) + + // Options: Speed of user simulation + float MouseSpeed = 600.0f; // Mouse speed (pixel/second) when not running in fast mode + float MouseWobble = 0.25f; // (0.0f..1.0f) How much wobble to apply to the mouse (pixels per pixel of move distance) when not running in fast mode + float ScrollSpeed = 1400.0f; // Scroll speed (pixel/second) when not running in fast mode + float TypingSpeed = 20.0f; // Char input speed (characters/second) when not running in fast mode + float ActionDelayShort = 0.15f; // Time between short actions + float ActionDelayStandard = 0.40f; // Time between most actions + + // Options: Screen/video capture + char VideoCaptureEncoderPath[256] = ""; // Video encoder executable path, e.g. "path/to/ffmpeg.exe". + char VideoCaptureEncoderParams[256] = "";// Video encoder parameters for .MP4 captures, e.g. see IMGUI_CAPTURE_DEFAULT_VIDEO_PARAMS_FOR_FFMPEG + char GifCaptureEncoderParams[512] = ""; // Video encoder parameters for .GIF captures, e.g. see IMGUI_CAPTURE_DEFAULT_GIF_PARAMS_FOR_FFMPEG + char VideoCaptureExtension[8] = ".mp4"; // Video file extension (default, may be overridden by test). + + // Options: Watchdog. Set values to FLT_MAX to disable. + // Interactive GUI applications that may be slower tend to use higher values. + float ConfigWatchdogWarning = 30.0f; // Warn when a test exceed this time (in second) + float ConfigWatchdogKillTest = 60.0f; // Attempt to stop running a test when exceeding this time (in second) + float ConfigWatchdogKillApp = FLT_MAX; // Stop application when exceeding this time (in second) + + // Options: Export + // While you can manually call ImGuiTestEngine_Export(), registering filename/format here ensure the crash handler will always export if application crash. + const char* ExportResultsFilename = NULL; + ImGuiTestEngineExportFormat ExportResultsFormat = (ImGuiTestEngineExportFormat)0; + + // Options: Sanity Checks + bool CheckDrawDataIntegrity = false; // Check ImDrawData integrity (buffer count, etc.). Currently cheap but may become a slow operation. + + //------------------------------------------------------------------------- + // Output + //------------------------------------------------------------------------- + + // Output: State of test engine + bool IsRunningTests = false; + bool IsRequestingMaxAppSpeed = false; // When running in fast mode: request app to skip vsync or even skip rendering if it wants + bool IsCapturing = false; // Capture is in progress +}; + +//------------------------------------------------------------------------- +// ImGuiTestItemInfo +//------------------------------------------------------------------------- + +// Information about a given item or window, result of an ItemInfo() or WindowInfo() query +struct ImGuiTestItemInfo +{ + int RefCount : 8; // User can increment this if they want to hold on the result pointer across frames, otherwise the task will be GC-ed. + unsigned int NavLayer : 1; // Nav layer of the item (ImGuiNavLayer) + int Depth : 16; // Depth from requested parent id. 0 == ID is immediate child of requested parent id. + int TimestampMain = -1; // Timestamp of main result (all fields) + int TimestampStatus = -1; // Timestamp of StatusFlags + ImGuiID ID = 0; // Item ID + ImGuiID ParentID = 0; // Item Parent ID (value at top of the ID stack) + ImGuiWindow* Window = NULL; // Item Window + ImRect RectFull = ImRect(); // Item Rectangle + ImRect RectClipped = ImRect(); // Item Rectangle (clipped with window->ClipRect at time of item submission) + ImGuiItemFlags InFlags = 0; // Item flags + ImGuiItemStatusFlags StatusFlags = 0; // Item Status flags (fully updated for some items only, compare TimestampStatus to FrameCount) + char DebugLabel[32] = {}; // Shortened label for debugging purpose + + ImGuiTestItemInfo() { RefCount = 0; NavLayer = 0; Depth = 0; } + bool IsEmpty() const { return ID == 0; } +}; + +// Result of an GatherItems() query +struct IMGUI_API ImGuiTestItemList +{ + ImPool Pool; + + void Clear() { Pool.Clear(); } + void Reserve(int capacity) { Pool.Reserve(capacity); } + int GetSize() const { return Pool.GetMapSize(); } + const ImGuiTestItemInfo* GetByIndex(int n) { return Pool.GetByIndex(n); } + const ImGuiTestItemInfo* GetByID(ImGuiID id) { return Pool.GetByKey(id); } + + // For range-for + size_t size() const { return (size_t)Pool.GetMapSize(); } + const ImGuiTestItemInfo* begin() const { return Pool.Buf.begin(); } + const ImGuiTestItemInfo* end() const { return Pool.Buf.end(); } + const ImGuiTestItemInfo* operator[] (size_t n) { return &Pool.Buf[(int)n]; } +}; + +//------------------------------------------------------------------------- +// ImGuiTestLog: store textual output of one given Test. +//------------------------------------------------------------------------- + +struct IMGUI_API ImGuiTestLogLineInfo +{ + ImGuiTestVerboseLevel Level; + int LineOffset; +}; + +struct IMGUI_API ImGuiTestLog +{ + ImGuiTextBuffer Buffer; + ImVector LineInfo; + int CountPerLevel[ImGuiTestVerboseLevel_COUNT] = {}; + + // Functions + ImGuiTestLog() {} + bool IsEmpty() const { return Buffer.empty(); } + void Clear(); + + // Extract log contents filtered per log-level. + // Output: + // - If 'buffer != NULL': all extracted lines are appended to 'buffer'. Use 'buffer->c_str()' on your side to obtain the text. + // - Return value: number of lines extracted (should be equivalent to number of '\n' inside buffer->c_str()). + // - You may call the function with buffer == NULL to only obtain a count without getting the data. + // Verbose levels are inclusive: + // - To get ONLY Error: Use level_min == ImGuiTestVerboseLevel_Error, level_max = ImGuiTestVerboseLevel_Error + // - To get ONLY Error and Warnings: Use level_min == ImGuiTestVerboseLevel_Error, level_max = ImGuiTestVerboseLevel_Warning + // - To get All Errors, Warnings, Debug... Use level_min == ImGuiTestVerboseLevel_Error, level_max = ImGuiTestVerboseLevel_Trace + int ExtractLinesForVerboseLevels(ImGuiTestVerboseLevel level_min, ImGuiTestVerboseLevel level_max, ImGuiTextBuffer* out_buffer); + + // [Internal] + void UpdateLineOffsets(ImGuiTestEngineIO* engine_io, ImGuiTestVerboseLevel level, const char* start); +}; + +//------------------------------------------------------------------------- +// ImGuiTest +//------------------------------------------------------------------------- + +typedef void (ImGuiTestGuiFunc)(ImGuiTestContext* ctx); +typedef void (ImGuiTestTestFunc)(ImGuiTestContext* ctx); + +// Wraps a placement new of a given type (where 'buffer' is the allocated memory) +typedef void (ImGuiTestVarsConstructor)(void* buffer); +typedef void (ImGuiTestVarsPostConstructor)(ImGuiTestContext* ctx, void* ptr, void* fn); +typedef void (ImGuiTestVarsDestructor)(void* ptr); + +// Storage for the output of a test run +struct IMGUI_API ImGuiTestOutput +{ + ImGuiTestStatus Status = ImGuiTestStatus_Unknown; + ImGuiTestLog Log; + ImU64 StartTime = 0; + ImU64 EndTime = 0; +}; + +// Storage for one test +struct IMGUI_API ImGuiTest +{ + // Test Definition + const char* Category = NULL; // Literal, not owned + const char* Name = NULL; // Literal, generally not owned unless NameOwned=true + ImGuiTestGroup Group = ImGuiTestGroup_Unknown; // Coarse groups: 'Tests' or 'Perf' + bool NameOwned = false; // + const char* SourceFile = NULL; // __FILE__ + int SourceLine = 0; // __LINE__ + int SourceLineEnd = 0; // Calculated by ImGuiTestEngine_StartCalcSourceLineEnds() + int ArgVariant = 0; // User parameter. Generally we use it to run variations of a same test by sharing GuiFunc/TestFunc + ImGuiTestFlags Flags = ImGuiTestFlags_None; // See ImGuiTestFlags_ + ImFuncPtr(ImGuiTestGuiFunc) GuiFunc = NULL; // GUI function (optional if your test are running over an existing GUI application) + ImFuncPtr(ImGuiTestTestFunc) TestFunc = NULL; // Test function + void* UserData = NULL; // General purpose user data (if assigning capturing lambdas on GuiFunc/TestFunc you may not need to use this) + //ImVector Dependencies; // Registered via AddDependencyTest(), ran automatically before our test. This is a simpler wrapper to calling ctx->RunChildTest() + + // Last Test Output/Status + // (this is the only part that may change after registration) + ImGuiTestOutput Output; + + // User variables (which are instantiated when running the test) + // Setup after test registration with SetVarsDataType<>(), access instance during test with GetVars<>(). + // This is mostly useful to communicate between GuiFunc and TestFunc. If you don't use both you may not want to use it! + size_t VarsSize = 0; + ImGuiTestVarsConstructor* VarsConstructor = NULL; + ImGuiTestVarsPostConstructor* VarsPostConstructor = NULL; // To override constructor default (in case the default are problematic on the first GuiFunc frame) + void* VarsPostConstructorUserFn = NULL; + ImGuiTestVarsDestructor* VarsDestructor = NULL; + + // Functions + ImGuiTest() {} + ~ImGuiTest(); + + void SetOwnedName(const char* name); + + template + void SetVarsDataType(void(*post_initialize)(ImGuiTestContext* ctx, T& vars) = NULL) + { + VarsSize = sizeof(T); + VarsConstructor = [](void* ptr) { IM_PLACEMENT_NEW(ptr) T; }; + VarsDestructor = [](void* ptr) { IM_UNUSED(ptr); reinterpret_cast(ptr)->~T(); }; + if (post_initialize != NULL) + { + VarsPostConstructorUserFn = (void*)post_initialize; + VarsPostConstructor = [](ImGuiTestContext* ctx, void* ptr, void* fn) { ((void (*)(ImGuiTestContext*, T&))(fn))(ctx, *(T*)ptr); }; + } + } +}; + +// Stored in test queue +struct IMGUI_API ImGuiTestRunTask +{ + ImGuiTest* Test = NULL; + ImGuiTestRunFlags RunFlags = ImGuiTestRunFlags_None; +}; + +//------------------------------------------------------------------------- diff --git a/libs/imgui_test_engine/imgui_te_exporters.cpp b/libs/imgui_test_engine/imgui_te_exporters.cpp new file mode 100644 index 0000000..8290c52 --- /dev/null +++ b/libs/imgui_test_engine/imgui_te_exporters.cpp @@ -0,0 +1,303 @@ +// dear imgui test engine +// (result exporters) +// Read https://github.com/ocornut/imgui_test_engine/wiki/Exporting-Results + +#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS) +#define _CRT_SECURE_NO_WARNINGS +#endif +#include "imgui_te_exporters.h" +#include "imgui_te_engine.h" +#include "imgui_te_internal.h" +#include "thirdparty/Str/Str.h" + +//------------------------------------------------------------------------- +// [SECTION] FORWARD DECLARATIONS +//------------------------------------------------------------------------- + +static void ImGuiTestEngine_ExportJUnitXml(ImGuiTestEngine* engine, const char* output_file); + +//------------------------------------------------------------------------- +// [SECTION] TEST ENGINE EXPORTER FUNCTIONS +//------------------------------------------------------------------------- +// - ImGuiTestEngine_PrintResultSummary() +// - ImGuiTestEngine_Export() +// - ImGuiTestEngine_ExportEx() +// - ImGuiTestEngine_ExportJUnitXml() +//------------------------------------------------------------------------- + +void ImGuiTestEngine_PrintResultSummary(ImGuiTestEngine* engine) +{ + int count_tested = 0; + int count_success = 0; + ImGuiTestEngine_GetResult(engine, count_tested, count_success); + + if (count_success < count_tested) + { + printf("\nFailing tests:\n"); + for (ImGuiTest* test : engine->TestsAll) + if (test->Output.Status == ImGuiTestStatus_Error) + printf("- %s\n", test->Name); + } + + ImOsConsoleSetTextColor(ImOsConsoleStream_StandardOutput, (count_success == count_tested) ? ImOsConsoleTextColor_BrightGreen : ImOsConsoleTextColor_BrightRed); + printf("\nTests Result: %s\n", (count_success == count_tested) ? "OK" : "Errors"); + printf("(%d/%d tests passed)\n", count_success, count_tested); + ImOsConsoleSetTextColor(ImOsConsoleStream_StandardOutput, ImOsConsoleTextColor_White); +} + +// This is mostly a copy of ImGuiTestEngine_PrintResultSummary with few additions. +static void ImGuiTestEngine_ExportResultSummary(ImGuiTestEngine* engine, FILE* fp, int indent_count, ImGuiTestGroup group) +{ + int count_tested = 0; + int count_success = 0; + + for (ImGuiTest* test : engine->TestsAll) + { + if (test->Group != group) + continue; + if (test->Output.Status != ImGuiTestStatus_Unknown) + count_tested++; + if (test->Output.Status == ImGuiTestStatus_Success) + count_success++; + } + + Str64 indent_str; + indent_str.reserve(indent_count + 1); + memset(indent_str.c_str(), ' ', indent_count); + indent_str[indent_count] = 0; + const char* indent = indent_str.c_str(); + + if (count_success < count_tested) + { + fprintf(fp, "\n%sFailing tests:\n", indent); + for (ImGuiTest* test : engine->TestsAll) + { + if (test->Group != group) + continue; + if (test->Output.Status == ImGuiTestStatus_Error) + fprintf(fp, "%s- %s\n", indent, test->Name); + } + fprintf(fp, "\n"); + } + + fprintf(fp, "%sTests Result: %s\n", indent, (count_success == count_tested) ? "OK" : "Errors"); + fprintf(fp, "%s(%d/%d tests passed)\n", indent, count_success, count_tested); +} + +static bool ImGuiTestEngine_HasAnyLogLines(ImGuiTestLog* test_log, ImGuiTestVerboseLevel level) +{ + for (auto& line_info : test_log->LineInfo) + if (line_info.Level <= level) + return true; + return false; +} + +static void ImGuiTestEngine_PrintLogLines(FILE* fp, ImGuiTestLog* test_log, int indent, ImGuiTestVerboseLevel level) +{ + Str128 log_line; + for (auto& line_info : test_log->LineInfo) + { + if (line_info.Level > level) + continue; + const char* line_start = test_log->Buffer.c_str() + line_info.LineOffset; + const char* line_end = strstr(line_start, "\n"); // FIXME: Incorrect. + log_line.set(line_start, line_end); + ImStrXmlEscape(&log_line); // FIXME: Should not be here considering the function name. + + // Some users may want to disable indenting? + fprintf(fp, "%*s%s\n", indent, "", log_line.c_str()); + } +} + +// Export using settings stored in ImGuiTestEngineIO +// This is called by ImGuiTestEngine_CrashHandler(). +void ImGuiTestEngine_Export(ImGuiTestEngine* engine) +{ + ImGuiTestEngineIO& io = engine->IO; + ImGuiTestEngine_ExportEx(engine, io.ExportResultsFormat, io.ExportResultsFilename); +} + +// Export using custom settings. +void ImGuiTestEngine_ExportEx(ImGuiTestEngine* engine, ImGuiTestEngineExportFormat format, const char* filename) +{ + if (format == ImGuiTestEngineExportFormat_None) + return; + IM_ASSERT(filename != NULL); + + if (format == ImGuiTestEngineExportFormat_JUnitXml) + ImGuiTestEngine_ExportJUnitXml(engine, filename); + else + IM_ASSERT(0); +} + +void ImGuiTestEngine_ExportJUnitXml(ImGuiTestEngine* engine, const char* output_file) +{ + IM_ASSERT(engine != NULL); + IM_ASSERT(output_file != NULL); + + FILE* fp = fopen(output_file, "w+b"); + if (fp == NULL) + { + fprintf(stderr, "Writing '%s' failed.\n", output_file); + return; + } + + // Per-testsuite test statistics. + struct + { + const char* Name = NULL; + int Tests = 0; + int Failures = 0; + int Disabled = 0; + } testsuites[ImGuiTestGroup_COUNT]; + testsuites[ImGuiTestGroup_Tests].Name = "tests"; + testsuites[ImGuiTestGroup_Perfs].Name = "perfs"; + + for (int n = 0; n < engine->TestsAll.Size; n++) + { + ImGuiTest* test = engine->TestsAll[n]; + auto* stats = &testsuites[test->Group]; + stats->Tests += 1; + if (test->Output.Status == ImGuiTestStatus_Error) + stats->Failures += 1; + else if (test->Output.Status == ImGuiTestStatus_Unknown) + stats->Disabled += 1; + } + + // Attributes for tag. + const char* testsuites_name = "Dear ImGui"; + int testsuites_failures = 0; + int testsuites_tests = 0; + int testsuites_disabled = 0; + float testsuites_time = (float)((double)(engine->BatchEndTime - engine->BatchStartTime) / 1000000.0); + for (int testsuite_id = ImGuiTestGroup_Tests; testsuite_id < ImGuiTestGroup_COUNT; testsuite_id++) + { + testsuites_tests += testsuites[testsuite_id].Tests; + testsuites_failures += testsuites[testsuite_id].Failures; + testsuites_disabled += testsuites[testsuite_id].Disabled; + } + + // FIXME: "errors" attribute and tag in may be supported if we have means to catch unexpected errors like assertions. + fprintf(fp, "\n" + "\n", + testsuites_disabled, testsuites_failures, testsuites_name, testsuites_tests, testsuites_time); + + for (int testsuite_id = ImGuiTestGroup_Tests; testsuite_id < ImGuiTestGroup_COUNT; testsuite_id++) + { + // Attributes for tag. + auto* testsuite = &testsuites[testsuite_id]; + float testsuite_time = testsuites_time; // FIXME: We do not differentiate between tests and perfs, they are executed in one big batch. + Str30 testsuite_timestamp = ""; + ImTimestampToISO8601(engine->BatchStartTime, &testsuite_timestamp); + fprintf(fp, " \n", + testsuite->Name, testsuite->Tests, testsuite->Disabled, testsuite->Failures, testsuite_id, testsuite_time, testsuite_timestamp.c_str()); + + for (int n = 0; n < engine->TestsAll.Size; n++) + { + ImGuiTest* test = engine->TestsAll[n]; + if (test->Group != testsuite_id) + continue; + + ImGuiTestOutput* test_output = &test->Output; + ImGuiTestLog* test_log = &test_output->Log; + + // Attributes for tag. + const char* testcase_name = test->Name; + const char* testcase_classname = test->Category; + const char* testcase_status = ImGuiTestEngine_GetStatusName(test_output->Status); + const float testcase_time = (float)((double)(test_output->EndTime - test_output->StartTime) / 1000000.0); + + fprintf(fp, " \n", + testcase_name, testcase_classname, testcase_status, testcase_time); + + if (test_output->Status == ImGuiTestStatus_Error) + { + // Skip last error message because it is generic information that test failed. + Str128 log_line; + for (int i = test_log->LineInfo.Size - 2; i >= 0; i--) + { + ImGuiTestLogLineInfo* line_info = &test_log->LineInfo[i]; + if (line_info->Level > engine->IO.ConfigVerboseLevelOnError) + continue; + if (line_info->Level == ImGuiTestVerboseLevel_Error) + { + const char* line_start = test_log->Buffer.c_str() + line_info->LineOffset; + const char* line_end = strstr(line_start, "\n"); + log_line.set(line_start, line_end); + ImStrXmlEscape(&log_line); + break; + } + } + + // Failing tests save their "on error" log output in text element of tag. + fprintf(fp, " \n", log_line.c_str()); + ImGuiTestEngine_PrintLogLines(fp, test_log, 8, engine->IO.ConfigVerboseLevelOnError); + fprintf(fp, " \n"); + } + + if (test_output->Status == ImGuiTestStatus_Unknown) + { + fprintf(fp, " \n"); + } + else + { + // Succeeding tests save their default log output output as "stdout". + if (ImGuiTestEngine_HasAnyLogLines(test_log, engine->IO.ConfigVerboseLevel)) + { + fprintf(fp, " \n"); + ImGuiTestEngine_PrintLogLines(fp, test_log, 8, engine->IO.ConfigVerboseLevel); + fprintf(fp, " \n"); + } + + // Save error messages as "stderr". + if (ImGuiTestEngine_HasAnyLogLines(test_log, ImGuiTestVerboseLevel_Error)) + { + fprintf(fp, " \n"); + ImGuiTestEngine_PrintLogLines(fp, test_log, 8, ImGuiTestVerboseLevel_Error); + fprintf(fp, " \n"); + } + } + fprintf(fp, " \n"); + } + + if (testsuites[testsuite_id].Disabled < testsuites[testsuite_id].Tests) // Any tests executed + { + // Log all log messages as "stdout". + fprintf(fp, " \n"); + for (int n = 0; n < engine->TestsAll.Size; n++) + { + ImGuiTest* test = engine->TestsAll[n]; + ImGuiTestOutput* test_output = &test->Output; + if (test->Group != testsuite_id) + continue; + if (test_output->Status == ImGuiTestStatus_Unknown) + continue; + fprintf(fp, " [0000] Test: '%s' '%s'..\n", test->Category, test->Name); + ImGuiTestVerboseLevel level = test_output->Status == ImGuiTestStatus_Error ? engine->IO.ConfigVerboseLevelOnError : engine->IO.ConfigVerboseLevel; + ImGuiTestEngine_PrintLogLines(fp, &test_output->Log, 6, level); + } + ImGuiTestEngine_ExportResultSummary(engine, fp, 6, (ImGuiTestGroup)testsuite_id); + fprintf(fp, " \n"); + + // Log all warning and error messages as "stderr". + fprintf(fp, " \n"); + for (int n = 0; n < engine->TestsAll.Size; n++) + { + ImGuiTest* test = engine->TestsAll[n]; + ImGuiTestOutput* test_output = &test->Output; + if (test->Group != testsuite_id) + continue; + if (test_output->Status == ImGuiTestStatus_Unknown) + continue; + fprintf(fp, " [0000] Test: '%s' '%s'..\n", test->Category, test->Name); + ImGuiTestEngine_PrintLogLines(fp, &test_output->Log, 6, ImGuiTestVerboseLevel_Warning); + } + ImGuiTestEngine_ExportResultSummary(engine, fp, 6, (ImGuiTestGroup)testsuite_id); + fprintf(fp, " \n"); + } + fprintf(fp, " \n"); + } + fprintf(fp, "\n"); + fclose(fp); + fprintf(stdout, "Saved test results to '%s' successfully.\n", output_file); +} diff --git a/libs/imgui_test_engine/imgui_te_exporters.h b/libs/imgui_test_engine/imgui_te_exporters.h new file mode 100644 index 0000000..49392a8 --- /dev/null +++ b/libs/imgui_test_engine/imgui_te_exporters.h @@ -0,0 +1,59 @@ +// dear imgui test engine +// (result exporters) +// Read https://github.com/ocornut/imgui_test_engine/wiki/Exporting-Results + +#pragma once + +//------------------------------------------------------------------------- +// Description +//------------------------------------------------------------------------- +// +// Test results may be exported in one of supported formats. +// To enable result exporting please configure test engine as follows: +// +// ImGuiTestEngineIO& test_io = ImGuiTestEngine_GetIO(engine); +// test_io.ExportResultsFile = "output_file.xml"; +// test_io.ExportResultsFormat = ImGuiTestEngineExportFormat_<...>; +// +// JUnit XML format +//------------------ +// JUnit XML format described at https://llg.cubic.org/docs/junit/. Many +// third party applications support consumption of this format. Some of +// of them are listed here: +// - Jenkins +// - Installation guide: https://www.jenkins.io/doc/book/installing/docker/ +// - JUnit plugin: https://plugins.jenkins.io/junit/ +// - xunit-viewer +// - Project: https://github.com/lukejpreston/xunit-viewer +// - Install npm: https://docs.npmjs.com/downloading-and-installing-node-js-and-npm +// - Install viewer and view test results: +// npm install xunit-viewer +// imgui_test_suite -nopause -v2 -ve4 -nogui -export-file junit.xml tests +// node_modules/xunit-viewer/bin/xunit-viewer -r junit.xml -o junit.html +// - Open junit.html +// + +//------------------------------------------------------------------------- +// Forward Declarations +//------------------------------------------------------------------------- + +struct ImGuiTestEngine; + +//------------------------------------------------------------------------- +// Types +//------------------------------------------------------------------------- + +enum ImGuiTestEngineExportFormat : int +{ + ImGuiTestEngineExportFormat_None = 0, + ImGuiTestEngineExportFormat_JUnitXml, +}; + +//------------------------------------------------------------------------- +// Functions +//------------------------------------------------------------------------- + +void ImGuiTestEngine_PrintResultSummary(ImGuiTestEngine* engine); + +void ImGuiTestEngine_Export(ImGuiTestEngine* engine); +void ImGuiTestEngine_ExportEx(ImGuiTestEngine* engine, ImGuiTestEngineExportFormat format, const char* filename); diff --git a/libs/imgui_test_engine/imgui_te_imconfig.h b/libs/imgui_test_engine/imgui_te_imconfig.h new file mode 100644 index 0000000..e68b7d9 --- /dev/null +++ b/libs/imgui_test_engine/imgui_te_imconfig.h @@ -0,0 +1,57 @@ +// dear imgui test engine +// (template for compile-time configuration) +// Replicate or #include this file in your imconfig.h to enable test engine. + +// Compile Dear ImGui with test engine hooks +// (Important: This is a value-less define, to be consistent with other defines used in core dear imgui.) +#define IMGUI_ENABLE_TEST_ENGINE + +// [Optional, default 0] Enable plotting of perflog data for comparing performance of different runs. +// This feature requires ImPlot to be linked in the application. +#ifndef IMGUI_TEST_ENGINE_ENABLE_IMPLOT +#define IMGUI_TEST_ENGINE_ENABLE_IMPLOT 0 +#endif + +// [Optional, default 1] Enable screen capture and PNG/GIF saving functionalities +// There's not much point to disable this but we provide it to reassure user that the dependencies on imstb_image_write.h and ffmpeg are technically optional. +#ifndef IMGUI_TEST_ENGINE_ENABLE_CAPTURE +#define IMGUI_TEST_ENGINE_ENABLE_CAPTURE 1 +#endif + +// [Optional, default 0] Using std::function and for function pointers such as ImGuiTest::TestFunc and ImGuiTest::GuiFunc +#ifndef IMGUI_TEST_ENGINE_ENABLE_STD_FUNCTION +#define IMGUI_TEST_ENGINE_ENABLE_STD_FUNCTION 0 +#endif + +// [Optional, default 0] Automatically fill ImGuiTestEngineIO::CoroutineFuncs with a default implementation using std::thread +#ifndef IMGUI_TEST_ENGINE_ENABLE_COROUTINE_STDTHREAD_IMPL +#define IMGUI_TEST_ENGINE_ENABLE_COROUTINE_STDTHREAD_IMPL 0 +#endif + +// Define IM_DEBUG_BREAK macros so it is accessible in imgui.h +// (this is a conveniance for app using test engine may define an IM_ASSERT() that uses this instead of an actual assert) +// (this is a copy of the block in imgui_internal.h. if the one in imgui_internal.h were to be defined at the top of imgui.h we wouldn't need this) +#ifndef IM_DEBUG_BREAK +#if defined (_MSC_VER) +#define IM_DEBUG_BREAK() __debugbreak() +#elif defined(__clang__) +#define IM_DEBUG_BREAK() __builtin_debugtrap() +#elif defined(__GNUC__) && (defined(__i386__) || defined(__x86_64__)) +#define IM_DEBUG_BREAK() __asm__ volatile("int $0x03") +#elif defined(__GNUC__) && defined(__thumb__) +#define IM_DEBUG_BREAK() __asm__ volatile(".inst 0xde01") +#elif defined(__GNUC__) && defined(__arm__) && !defined(__thumb__) +#define IM_DEBUG_BREAK() __asm__ volatile(".inst 0xe7f001f0"); +#else +#define IM_DEBUG_BREAK() IM_ASSERT(0) // It is expected that you define IM_DEBUG_BREAK() into something that will break nicely in a debugger! +#endif +#endif // #ifndef IMGUI_DEBUG_BREAK + +// [Options] We provide custom assert macro used by our our test suite, which you may use: +// - Calling IM_DEBUG_BREAK() instead of an actual assert, so we can easily recover and step over (compared to many assert implementations). +// - If a test is running, test name will be included in the log. +// - Macro is calling IM_DEBUG_BREAK() inline to get debugger to break in the calling function (instead of a deeper callstack level). +// - Macro is using comma operator instead of an if() to avoid "conditional expression is constant" warnings. +extern void ImGuiTestEngine_AssertLog(const char* expr, const char* file, const char* func, int line); +#define IM_TEST_ENGINE_ASSERT(_EXPR) do { if ((void)0, !(_EXPR)) { ImGuiTestEngine_AssertLog(#_EXPR, __FILE__, __func__, __LINE__); IM_DEBUG_BREAK(); } } while (0) +// V_ASSERT_CONTRACT, assertMacro:IM_ASSERT diff --git a/libs/imgui_test_engine/imgui_te_internal.h b/libs/imgui_test_engine/imgui_te_internal.h new file mode 100644 index 0000000..4b20438 --- /dev/null +++ b/libs/imgui_test_engine/imgui_te_internal.h @@ -0,0 +1,213 @@ +// dear imgui test engine +// (internal api) + +#pragma once + +#include "imgui_te_coroutine.h" +#include "imgui_te_utils.h" // ImMovingAverage +#include "imgui_capture_tool.h" // ImGuiCaptureTool // FIXME + +//------------------------------------------------------------------------- +// FORWARD DECLARATIONS +//------------------------------------------------------------------------- + +class Str; // Str<> from thirdparty/Str/Str.h +struct ImGuiPerfTool; + +//------------------------------------------------------------------------- +// DATA STRUCTURES +//------------------------------------------------------------------------- + +// Query item position/window/state given ID. +struct ImGuiTestInfoTask +{ + // Input + ImGuiID ID = 0; + int FrameCount = -1; // Timestamp of request + char DebugName[64] = ""; // Debug string representing the queried ID + + // Output + ImGuiTestItemInfo Result; +}; + +// Gather item list in given parent ID. +struct ImGuiTestGatherTask +{ + // Input + ImGuiID InParentID = 0; + int InMaxDepth = 0; + short InLayerMask = 0; + + // Output/Temp + ImGuiTestItemList* OutList = NULL; + ImGuiTestItemInfo* LastItemInfo = NULL; + + void Clear() { memset(this, 0, sizeof(*this)); } +}; + +// Find item ID given a label and a parent id +// Usually used by queries with wildcards such as ItemInfo("hello/**/foo/bar") +struct ImGuiTestFindByLabelTask +{ + // Input + ImGuiID InPrefixId = 0; // A known base ID which appears BEFORE the wildcard ID (for "hello/**/foo/bar" it would be hash of "hello") + int InSuffixDepth = 0; // Number of labels in a path, after unknown base ID (for "hello/**/foo/bar" it would be 2) + const char* InSuffix = NULL; // A label string which appears on ID stack after unknown base ID (for "hello/**/foo/bar" it would be "foo/bar") + const char* InSuffixLastItem = NULL; // A last label string (for "hello/**/foo/bar" it would be "bar") + ImGuiID InSuffixLastItemHash = 0; + ImGuiItemStatusFlags InFilterItemStatusFlags = 0; // Flags required for item to be returned + + // Output + ImGuiID OutItemId = 0; // Result item ID +}; + +enum ImGuiTestInputType +{ + ImGuiTestInputType_None, + ImGuiTestInputType_Key, + ImGuiTestInputType_Char, + ImGuiTestInputType_ViewportFocus, + ImGuiTestInputType_ViewportClose +}; + +// FIXME: May want to strip further now that core imgui is using its own input queue +struct ImGuiTestInput +{ + ImGuiTestInputType Type = ImGuiTestInputType_None; + ImGuiKeyChord KeyChord = ImGuiKey_None; + ImWchar Char = 0; + bool Down = false; + ImGuiID ViewportId = 0; + + static ImGuiTestInput ForKeyChord(ImGuiKeyChord key_chord, bool down) + { + ImGuiTestInput inp; + inp.Type = ImGuiTestInputType_Key; + inp.KeyChord = key_chord; + inp.Down = down; + return inp; + } + + static ImGuiTestInput ForChar(ImWchar v) + { + ImGuiTestInput inp; + inp.Type = ImGuiTestInputType_Char; + inp.Char = v; + return inp; + } + + static ImGuiTestInput ForViewportFocus(ImGuiID viewport_id) + { + ImGuiTestInput inp; + inp.Type = ImGuiTestInputType_ViewportFocus; + inp.ViewportId = viewport_id; + return inp; + } + + static ImGuiTestInput ForViewportClose(ImGuiID viewport_id) + { + ImGuiTestInput inp; + inp.Type = ImGuiTestInputType_ViewportClose; + inp.ViewportId = viewport_id; + return inp; + } +}; + +struct ImGuiTestInputs +{ + ImVec2 MousePosValue; // Own non-rounded copy of MousePos in order facilitate simulating mouse movement very slow speed and high-framerate + ImVec2 MouseWheel; + ImGuiID MouseHoveredViewport = 0; + int MouseButtonsValue = 0x00; // FIXME-TESTS: Use simulated_io.MouseDown[] ? + ImVector Queue; + bool HostEscDown = false; + float HostEscDownDuration = -1.0f; // Maintain our own DownDuration for host/backend ESC key so we can abort. +}; + +// [Internal] Test Engine Context +struct ImGuiTestEngine +{ + ImGuiTestEngineIO IO; + ImGuiContext* UiContextTarget = NULL; // imgui context for testing + ImGuiContext* UiContextActive = NULL; // imgui context for testing == UiContextTarget or NULL + + bool Started = false; + ImU64 BatchStartTime = 0; + ImU64 BatchEndTime = 0; + int FrameCount = 0; + float OverrideDeltaTime = -1.0f; // Inject custom delta time into imgui context to simulate clock passing faster than wall clock time. + ImVector TestsAll; + ImVector TestsQueue; + ImGuiTestContext* TestContext = NULL; + ImVectorInfoTasks; + ImGuiTestGatherTask GatherTask; + ImGuiTestFindByLabelTask FindByLabelTask; + ImGuiTestCoroutineHandle TestQueueCoroutine = NULL; // Coroutine to run the test queue + bool TestQueueCoroutineShouldExit = false; // Flag to indicate that we are shutting down and the test queue coroutine should stop + + // Inputs + ImGuiTestInputs Inputs; + + // UI support + bool Abort = false; + ImGuiTest* UiSelectAndScrollToTest = NULL; + ImGuiTest* UiSelectedTest = NULL; + Str* UiFilterTests; + Str* UiFilterPerfs; + ImU32 UiFilterByStatusMask = ~0u; + bool UiMetricsOpen = false; + bool UiDebugLogOpen = false; + bool UiCaptureToolOpen = false; + bool UiStackToolOpen = false; + bool UiPerfToolOpen = false; + float UiLogHeight = 150.0f; + + // Performance Monitor + double PerfRefDeltaTime; + ImMovingAverage PerfDeltaTime100; + ImMovingAverage PerfDeltaTime500; + ImGuiPerfTool* PerfTool = NULL; + + // Screen/Video Capturing + ImGuiCaptureToolUI CaptureTool; // Capture tool UI + ImGuiCaptureContext CaptureContext; // Capture context used in tests + ImGuiCaptureArgs* CaptureCurrentArgs = NULL; + + // Tools + bool PostSwapCalled = false; + bool ToolDebugRebootUiContext = false; // Completely shutdown and recreate the dear imgui context in place + bool ToolSlowDown = false; + int ToolSlowDownMs = 100; + ImGuiTestRunSpeed BackupConfigRunSpeed = ImGuiTestRunSpeed_Fast; + bool BackupConfigNoThrottle = false; + + // Functions + ImGuiTestEngine(); + ~ImGuiTestEngine(); +}; + +//------------------------------------------------------------------------- +// INTERNAL FUNCTIONS +//------------------------------------------------------------------------- + +ImGuiTestItemInfo* ImGuiTestEngine_FindItemInfo(ImGuiTestEngine* engine, ImGuiID id, const char* debug_id); +void ImGuiTestEngine_Yield(ImGuiTestEngine* engine); +void ImGuiTestEngine_SetDeltaTime(ImGuiTestEngine* engine, float delta_time); +int ImGuiTestEngine_GetFrameCount(ImGuiTestEngine* engine); +bool ImGuiTestEngine_PassFilter(ImGuiTest* test, const char* filter); +void ImGuiTestEngine_RunTest(ImGuiTestEngine* engine, ImGuiTestContext* ctx, ImGuiTest* test, ImGuiTestRunFlags run_flags); + +void ImGuiTestEngine_RebootUiContext(ImGuiTestEngine* engine); +ImGuiPerfTool* ImGuiTestEngine_GetPerfTool(ImGuiTestEngine* engine); + +// Screen/Video Capturing +bool ImGuiTestEngine_CaptureScreenshot(ImGuiTestEngine* engine, ImGuiCaptureArgs* args); +bool ImGuiTestEngine_CaptureBeginVideo(ImGuiTestEngine* engine, ImGuiCaptureArgs* args); +bool ImGuiTestEngine_CaptureEndVideo(ImGuiTestEngine* engine, ImGuiCaptureArgs* args); + +// Helper functions +const char* ImGuiTestEngine_GetStatusName(ImGuiTestStatus v); +const char* ImGuiTestEngine_GetRunSpeedName(ImGuiTestRunSpeed v); +const char* ImGuiTestEngine_GetVerboseLevelName(ImGuiTestVerboseLevel v); + +//------------------------------------------------------------------------- diff --git a/libs/imgui_test_engine/imgui_te_perftool.cpp b/libs/imgui_test_engine/imgui_te_perftool.cpp new file mode 100644 index 0000000..65b853a --- /dev/null +++ b/libs/imgui_test_engine/imgui_te_perftool.cpp @@ -0,0 +1,1949 @@ +// dear imgui test engine +// (performance tool) +// Browse and visualize samples recorded by ctx->PerfCapture() calls. +// User access via 'Test Engine UI -> Tools -> Perf Tool' + +/* + +Index of this file: +// [SECTION] Header mess +// [SECTION] ImGuiPerflogEntry +// [SECTION] Types & everything else +// [SECTION] USER INTERFACE +// [SECTION] SETTINGS +// [SECTION] TESTS + +*/ + +// Terminology: +// * Entry: information about execution of a single perf test. This corresponds to one line in CSV file. +// * Batch: a group of entries that were created together during a single execution. A new batch is created each time +// one or more perf tests are executed. All entries in a single batch will have a matching ImGuiPerflogEntry::Timestamp. +// * Build: A group of batches that have matching BuildType, OS, Cpu, Compiler, GitBranchName. +// * Baseline: A batch that we are comparing against. Baselines are identified by batch timestamp and build id. + +//------------------------------------------------------------------------- +// [SECTION] Header mess +//------------------------------------------------------------------------- + +#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS) +#define _CRT_SECURE_NO_WARNINGS +#endif + +#define IMGUI_DEFINE_MATH_OPERATORS +#include "imgui_te_perftool.h" +#include "imgui.h" +#include "imgui_internal.h" +#include "imgui_te_utils.h" +#include "thirdparty/Str/Str.h" +#include // time(), localtime() +#if IMGUI_TEST_ENGINE_ENABLE_IMPLOT +#include "implot.h" +#include "implot_internal.h" +#endif + +// For tests +#include "imgui_te_engine.h" +#include "imgui_te_context.h" +#include "imgui_te_internal.h" // ImGuiTestEngine_GetPerfTool() +#include "imgui_capture_tool.h" + +//------------------------------------------------------------------------- +// [SECTION] ImGuiPerflogEntry +//------------------------------------------------------------------------- + +void ImGuiPerfToolEntry::Set(const ImGuiPerfToolEntry& other) +{ + Timestamp = other.Timestamp; + Category = other.Category; + TestName = other.TestName; + DtDeltaMs = other.DtDeltaMs; + DtDeltaMsMin = other.DtDeltaMsMin; + DtDeltaMsMax = other.DtDeltaMsMax; + NumSamples = other.NumSamples; + PerfStressAmount = other.PerfStressAmount; + GitBranchName = other.GitBranchName; + BuildType = other.BuildType; + Cpu = other.Cpu; + OS = other.OS; + Compiler = other.Compiler; + Date = other.Date; + //DateMax = ... + VsBaseline = other.VsBaseline; + LabelIndex = other.LabelIndex; +} + +//------------------------------------------------------------------------- +// [SECTION] Types & everything else +//------------------------------------------------------------------------- + +typedef ImGuiID(*HashEntryFn)(ImGuiPerfToolEntry* entry); +typedef void(*FormatEntryLabelFn)(ImGuiPerfTool* perftool, Str* result, ImGuiPerfToolEntry* entry); + +struct ImGuiPerfToolColumnInfo +{ + const char* Title; + int Offset; + ImGuiDataType Type; + bool ShowAlways; + ImGuiTableFlags Flags; + + template + T GetValue(const ImGuiPerfToolEntry* entry) const { return *(T*)((const char*)entry + Offset); } +}; + +// Update _ShowEntriesTable() and SaveHtmlReport() when adding new entries. +static const ImGuiPerfToolColumnInfo PerfToolColumnInfo[] = +{ + { /* 00 */ "Date", offsetof(ImGuiPerfToolEntry, Timestamp), ImGuiDataType_U64, true, ImGuiTableColumnFlags_DefaultHide }, + { /* 01 */ "Test Name", offsetof(ImGuiPerfToolEntry, TestName), ImGuiDataType_COUNT, true, 0 }, + { /* 02 */ "Branch", offsetof(ImGuiPerfToolEntry, GitBranchName), ImGuiDataType_COUNT, true, 0 }, + { /* 03 */ "Compiler", offsetof(ImGuiPerfToolEntry, Compiler), ImGuiDataType_COUNT, true, 0 }, + { /* 04 */ "OS", offsetof(ImGuiPerfToolEntry, OS), ImGuiDataType_COUNT, true, 0 }, + { /* 05 */ "CPU", offsetof(ImGuiPerfToolEntry, Cpu), ImGuiDataType_COUNT, true, 0 }, + { /* 06 */ "Build", offsetof(ImGuiPerfToolEntry, BuildType), ImGuiDataType_COUNT, true, 0 }, + { /* 07 */ "Stress", offsetof(ImGuiPerfToolEntry, PerfStressAmount), ImGuiDataType_S32, true, 0 }, + { /* 08 */ "Avg ms", offsetof(ImGuiPerfToolEntry, DtDeltaMs), ImGuiDataType_Double, true, 0 }, + { /* 09 */ "Min ms", offsetof(ImGuiPerfToolEntry, DtDeltaMsMin), ImGuiDataType_Double, false, 0 }, + { /* 00 */ "Max ms", offsetof(ImGuiPerfToolEntry, DtDeltaMsMax), ImGuiDataType_Double, false, 0 }, + { /* 11 */ "Samples", offsetof(ImGuiPerfToolEntry, NumSamples), ImGuiDataType_S32, false, 0 }, + { /* 12 */ "VS Baseline", offsetof(ImGuiPerfToolEntry, VsBaseline), ImGuiDataType_Float, true, 0 }, +}; + +static const char* PerfToolReportDefaultOutputPath = "./output/capture_perf_report.html"; + +// This is declared as a standalone function in order to run without a PerfTool instance +void ImGuiTestEngine_PerfToolAppendToCSV(ImGuiPerfTool* perf_log, ImGuiPerfToolEntry* entry, const char* filename) +{ + if (filename == NULL) + filename = IMGUI_PERFLOG_DEFAULT_FILENAME; + + if (!ImFileCreateDirectoryChain(filename, ImPathFindFilename(filename))) + { + fprintf(stderr, "Unable to create missing directory '%*s', perftool entry was not saved.\n", (int)(ImPathFindFilename(filename) - filename), filename); + return; + } + + // Appends to .csv + FILE* f = fopen(filename, "a+b"); + if (f == NULL) + { + fprintf(stderr, "Unable to open '%s', perftool entry was not saved.\n", filename); + return; + } + fprintf(f, "%llu,%s,%s,%.3f,x%d,%s,%s,%s,%s,%s,%s\n", entry->Timestamp, entry->Category, entry->TestName, + entry->DtDeltaMs, entry->PerfStressAmount, entry->GitBranchName, entry->BuildType, entry->Cpu, entry->OS, + entry->Compiler, entry->Date); + fflush(f); + fclose(f); + + // Register to runtime perf tool if any + if (perf_log != NULL) + perf_log->AddEntry(entry); +} + +// Tri-state button. Copied and modified ButtonEx(). +static bool Button3(const char* label, int* value) +{ + ImGuiWindow* window = ImGui::GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + const ImGuiID id = window->GetID(label); + const ImVec2 label_size = ImGui::CalcTextSize(label, NULL, true); + float dot_radius2 = g.FontSize; + ImVec2 btn_size(dot_radius2 * 2, dot_radius2); + + ImVec2 pos = window->DC.CursorPos; + ImVec2 size = ImGui::CalcItemSize(ImVec2(), btn_size.x + label_size.x + style.FramePadding.x * 2.0f + style.ItemInnerSpacing.x, label_size.y + style.FramePadding.y * 2.0f); + + const ImRect bb(pos, pos + size); + ImGui::ItemSize(size, style.FramePadding.y); + if (!ImGui::ItemAdd(bb, id)) + return false; + + bool hovered, held; + bool pressed = ImGui::ButtonBehavior(ImRect(pos, pos + style.FramePadding + btn_size), id, &hovered, &held, 0); + + // Render + const ImU32 col = ImGui::GetColorU32(ImGuiCol_FrameBg); + ImGui::RenderNavHighlight(bb, id); + ImGui::RenderFrame(bb.Min + style.FramePadding, bb.Min + style.FramePadding + btn_size, col, true, /*style.FrameRounding*/ 5.0f); + + ImColor btn_col; + if (held) + btn_col = style.Colors[ImGuiCol_SliderGrabActive]; + else if (hovered) + btn_col = style.Colors[ImGuiCol_ButtonHovered]; + else + btn_col = style.Colors[ImGuiCol_SliderGrab]; + ImVec2 center = bb.Min + ImVec2(dot_radius2 + (dot_radius2 * (float)*value), dot_radius2) * 0.5f + style.FramePadding; + window->DrawList->AddCircleFilled(center, dot_radius2 * 0.5f, btn_col); + + ImRect text_bb; + text_bb.Min = bb.Min + style.FramePadding + ImVec2(btn_size.x + style.ItemInnerSpacing.x, 0); + text_bb.Max = text_bb.Min + label_size; + ImGui::RenderTextClipped(text_bb.Min, text_bb.Max, label, NULL, &label_size, style.ButtonTextAlign, &bb); + + *value = (*value + pressed) % 3; + return pressed; +} + +static ImGuiID GetBuildID(const ImGuiPerfToolEntry* entry) +{ + IM_ASSERT(entry != NULL); + ImGuiID build_id = ImHashStr(entry->BuildType); + build_id = ImHashStr(entry->OS, 0, build_id); + build_id = ImHashStr(entry->Cpu, 0, build_id); + build_id = ImHashStr(entry->Compiler, 0, build_id); + build_id = ImHashStr(entry->GitBranchName, 0, build_id); + return build_id; +} + +static ImGuiID GetBuildID(const ImGuiPerfToolBatch* batch) +{ + IM_ASSERT(batch != NULL); + IM_ASSERT(!batch->Entries.empty()); + return GetBuildID(&batch->Entries.Data[0]); +} + +// Batch ID depends on display type. It is either a build ID (when combinding by build type) or batch timestamp otherwise. +static ImGuiID GetBatchID(const ImGuiPerfTool* perftool, const ImGuiPerfToolEntry* entry) +{ + IM_ASSERT(perftool != NULL); + IM_ASSERT(entry != NULL); + if (perftool->_DisplayType == ImGuiPerfToolDisplayType_CombineByBuildInfo) + return GetBuildID(entry); + else + return (ImU32)entry->Timestamp; +} + +static int PerfToolComparerStr(const void* a, const void* b) +{ + return strcmp(*(const char**)b, *(const char**)a); +} + +static int IMGUI_CDECL PerfToolComparerByEntryInfo(const void* lhs, const void* rhs) +{ + const ImGuiPerfToolEntry* a = (const ImGuiPerfToolEntry*)lhs; + const ImGuiPerfToolEntry* b = (const ImGuiPerfToolEntry*)rhs; + + // While build ID does include git branch it wont ensure branches are grouped together, therefore we do branch + // sorting manually. + int result = strcmp(a->GitBranchName, b->GitBranchName); + + // Now that we have groups of branches - sort individual builds within those groups. + if (result == 0) + result = ImClamp((int)((ImS64)GetBuildID(a) - (ImS64)GetBuildID(b)), -1, +1); + + // Group individual runs together within build groups. + if (result == 0) + result = (int)ImClamp((ImS64)b->Timestamp - (ImS64)a->Timestamp, -1, +1); + + // And finally sort individual runs by perf name so we can have a predictable order (used to optimize in _Rebuild()). + if (result == 0) + result = (int)strcmp(a->TestName, b->TestName); + + return result; +} + +static ImGuiPerfTool* PerfToolInstance = NULL; +static int IMGUI_CDECL CompareWithSortSpecs(const void* lhs, const void* rhs) +{ + IM_ASSERT(PerfToolInstance != NULL); + ImGuiPerfTool* tool = PerfToolInstance; + const ImGuiTableSortSpecs* sort_specs = PerfToolInstance->_InfoTableSortSpecs; + int batch_index_a, entry_index_a, mono_index_a, batch_index_b, entry_index_b, mono_index_b; + tool->_UnpackSortedKey(*(ImU64*)lhs, &batch_index_a, &entry_index_a, &mono_index_a); + tool->_UnpackSortedKey(*(ImU64*)rhs, &batch_index_b, &entry_index_b, &mono_index_b); + for (int i = 0; i < sort_specs->SpecsCount; i++) + { + const ImGuiTableColumnSortSpecs* specs = &sort_specs->Specs[i]; + const ImGuiPerfToolColumnInfo& col_info = PerfToolColumnInfo[specs->ColumnIndex]; + const ImGuiPerfToolBatch* batch_a = &tool->_Batches[batch_index_a]; + const ImGuiPerfToolBatch* batch_b = &tool->_Batches[batch_index_b]; + ImGuiPerfToolEntry* a = &batch_a->Entries.Data[entry_index_a]; + ImGuiPerfToolEntry* b = &batch_b->Entries.Data[entry_index_b]; + if (specs->SortDirection == ImGuiSortDirection_Ascending) + ImSwap(a, b); + + int result = 0; + switch (col_info.Type) + { + case ImGuiDataType_S32: + result = col_info.GetValue(a) - col_info.GetValue(b); + break; + case ImGuiDataType_U64: + result = (int)(col_info.GetValue(a) - col_info.GetValue(b)); + break; + case ImGuiDataType_Float: + result = (int)((col_info.GetValue(a) - col_info.GetValue(b)) * 1000.0f); + break; + case ImGuiDataType_Double: + result = (int)((col_info.GetValue(a) - col_info.GetValue(b)) * 1000.0); + break; + case ImGuiDataType_COUNT: + result = strcmp(col_info.GetValue(a), col_info.GetValue(b)); + break; + default: + IM_ASSERT(false); + } + if (result != 0) + return result; + } + return mono_index_a - mono_index_b; +} + +// Dates are in format "YYYY-MM-DD" +static bool IsDateValid(const char* date) +{ + if (date[4] != '-' || date[7] != '-') + return false; + for (int i = 0; i < 10; i++) + { + if (i == 4 || i == 7) + continue; + if (date[i] < '0' || date[i] > '9') + return false; + } + return true; +} + +static float FormatVsBaseline(ImGuiPerfToolEntry* entry, ImGuiPerfToolEntry* baseline_entry, Str& out_label) +{ + if (baseline_entry == NULL) + { + out_label.appendf("--"); + return FLT_MAX; + } + + if (entry == baseline_entry) + { + out_label.append("baseline"); + return FLT_MAX; + } + + double percent_vs_first = 100.0 / baseline_entry->DtDeltaMs * entry->DtDeltaMs; + double dt_change = -(100.0 - percent_vs_first); + if (dt_change == INFINITY) + out_label.appendf("--"); + else if (ImAbs(dt_change) > 0.001f) + out_label.appendf("%+.2lf%% (%s)", dt_change, dt_change < 0.0f ? "faster" : "slower"); + else + out_label.appendf("=="); + return (float)dt_change; +} + +#if IMGUI_TEST_ENGINE_ENABLE_IMPLOT +static void PerfToolFormatBuildInfo(ImGuiPerfTool* perftool, Str* result, ImGuiPerfToolBatch* batch) +{ + IM_ASSERT(perftool != NULL); + IM_ASSERT(result != NULL); + IM_ASSERT(batch != NULL); + IM_ASSERT(batch->Entries.Size > 0); + ImGuiPerfToolEntry* entry = &batch->Entries.Data[0]; + Str64f legend_format("x%%-%dd %%-%ds %%-%ds %%-%ds %%-%ds %%-%ds %%s%%s%%s%%s(%%-%dd sample%%s)%%s", + perftool->_AlignStress, perftool->_AlignType, perftool->_AlignCpu, perftool->_AlignOs, perftool->_AlignCompiler, + perftool->_AlignBranch, perftool->_AlignSamples); + result->appendf(legend_format.c_str(), entry->PerfStressAmount, entry->BuildType, entry->Cpu, entry->OS, + entry->Compiler, entry->GitBranchName, entry->Date, +#if 0 + // Show min-max dates. + perftool->_CombineByBuildInfo ? " - " : "", + entry->DateMax ? entry->DateMax : "", +#else + "", "", +#endif + *entry->Date ? " " : "", + batch->NumSamples, + batch->NumSamples > 1 ? "s" : "", // Singular/plural form of "sample(s)" + batch->NumSamples > 1 || perftool->_AlignSamples == 1 ? "" : " " // Space after legend entry to separate * marking baseline + ); +} +#endif + +static int PerfToolCountBuilds(ImGuiPerfTool* perftool, bool only_visible) +{ + int num_builds = 0; + ImU64 build_id = 0; + for (ImGuiPerfToolEntry& entry : perftool->_SrcData) + { + if (build_id != GetBuildID(&entry)) + { + if (!only_visible || perftool->_IsVisibleBuild(&entry)) + num_builds++; + build_id = GetBuildID(&entry); + } + } + return num_builds; +} + +static bool InputDate(const char* label, char* date, int date_len, bool valid) +{ + ImGui::SetNextItemWidth(ImGui::CalcTextSize("YYYY-MM-DD").x + ImGui::GetStyle().FramePadding.x * 2.0f); + const bool date_valid = date[0] == 0 || (IsDateValid(date) && valid); + if (!date_valid) + { + ImGui::PushStyleColor(ImGuiCol_Border, IM_COL32(255, 0, 0, 255)); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1); + } + bool date_changed = ImGui::InputTextWithHint(label, "YYYY-MM-DD", date, date_len); + if (!date_valid) + { + ImGui::PopStyleVar(); + ImGui::PopStyleColor(); + } + return date_changed; +} + +static void FormatDate(ImU64 microseconds, char* buf, size_t buf_size) +{ + time_t timestamp = (time_t)(microseconds / 1000000); + tm* time = localtime(×tamp); + ImFormatString(buf, buf_size, "%04d-%02d-%02d", time->tm_year + 1900, time->tm_mon + 1, time->tm_mday); +} + +static void FormatDateAndTime(ImU64 microseconds, char* buf, size_t buf_size) +{ + time_t timestamp = (time_t)(microseconds / 1000000); + tm* time = localtime(×tamp); + ImFormatString(buf, buf_size, "%04d-%02d-%02d %02d:%02d:%02d", time->tm_year + 1900, time->tm_mon + 1, time->tm_mday, time->tm_hour, time->tm_min, time->tm_sec); +} + +static void RenderFilterInput(ImGuiPerfTool* perf, const char* hint, float width = -FLT_MIN) +{ + if (ImGui::IsWindowAppearing()) + strcpy(perf->_Filter, ""); + ImGui::SetNextItemWidth(width); + ImGui::InputTextWithHint("##filter", hint, perf->_Filter, IM_ARRAYSIZE(perf->_Filter)); + if (ImGui::IsWindowAppearing()) + ImGui::SetKeyboardFocusHere(); +} + +static bool RenderMultiSelectFilter(ImGuiPerfTool* perf, const char* filter_hint, ImVector* labels) +{ + ImGuiContext& g = *ImGui::GetCurrentContext(); + ImGuiIO& io = ImGui::GetIO(); + ImGuiStorage& visibility = perf->_Visibility; + bool modified = false; + RenderFilterInput(perf, filter_hint, -(ImGui::CalcTextSize("(?)").x + g.Style.ItemSpacing.x)); + ImGui::SameLine(); + ImGui::TextDisabled("(?)"); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Hold CTRL to invert other items.\nHold SHIFT to close popup instantly."); + + // Keep popup open for multiple actions if SHIFT is pressed. + if (!io.KeyShift) + ImGui::PushItemFlag(ImGuiItemFlags_SelectableDontClosePopup, true); + + if (ImGui::MenuItem("Show All")) + { + for (const char* label : *labels) + if (strstr(label, perf->_Filter) != NULL) + visibility.SetBool(ImHashStr(label), true); + modified = true; + } + + if (ImGui::MenuItem("Hide All")) + { + for (const char* label : *labels) + if (strstr(label, perf->_Filter) != NULL) + visibility.SetBool(ImHashStr(label), false); + modified = true; + } + + // Render perf labels in reversed order. Labels are sorted, but stored in reversed order to render them on the plot + // from top down (ImPlot renders stuff from bottom up). + int filtered_entries = 0; + for (int i = labels->Size - 1; i >= 0; i--) + { + const char* label = (*labels)[i]; + if (strstr(label, perf->_Filter) == NULL) // Filter out entries not matching a filter query + continue; + + if (filtered_entries == 0) + ImGui::Separator(); + + ImGuiID build_id = ImHashStr(label); + bool visible = visibility.GetBool(build_id, true); + if (ImGui::MenuItem(label, NULL, &visible)) + { + modified = true; + if (io.KeyCtrl) + { + for (const char* label2 : *labels) + { + ImGuiID build_id2 = ImHashStr(label2); + visibility.SetBool(build_id2, !visibility.GetBool(build_id2, true)); + } + } + else + { + visibility.SetBool(build_id, !visibility.GetBool(build_id, true)); + } + } + filtered_entries++; + } + + if (!io.KeyShift) + ImGui::PopItemFlag(); + + return modified; +} + +// Based on ImPlot::SetupFinish(). +#if IMGUI_TEST_ENGINE_ENABLE_IMPLOT +static ImRect ImPlotGetYTickRect(int t, int y = 0) +{ + ImPlotContext& gp = *GImPlot; + ImPlotPlot& plot = *gp.CurrentPlot; + ImPlotAxis& ax = plot.YAxis(y); + const ImPlotTicker& tkc = ax.Ticker; + const bool opp = ax.IsOpposite(); + ImRect result(1.0f, 1.0f, -1.0f, -1.0f); + if (ax.HasTickLabels()) + { + const ImPlotTick& tk = tkc.Ticks[t]; + const float datum = ax.Datum1 + (opp ? gp.Style.LabelPadding.x : (-gp.Style.LabelPadding.x - tk.LabelSize.x)); + if (tk.ShowLabel && tk.PixelPos >= plot.PlotRect.Min.y - 1 && tk.PixelPos <= plot.PlotRect.Max.y + 1) + { + ImVec2 start(datum, tk.PixelPos - 0.5f * tk.LabelSize.y); + result.Min = start; + result.Max = start + tk.LabelSize; + } + } + return result; +} +#endif // #if IMGUI_TEST_ENGINE_ENABLE_IMPLOT + +ImGuiPerfTool::ImGuiPerfTool() +{ + _CsvParser = IM_NEW(ImGuiCsvParser)(); + Clear(); +} + +ImGuiPerfTool::~ImGuiPerfTool() +{ + _SrcData.clear_destruct(); + _Batches.clear_destruct(); + IM_DELETE(_CsvParser); +} + +void ImGuiPerfTool::AddEntry(ImGuiPerfToolEntry* entry) +{ + if (strcmp(_FilterDateFrom, entry->Date) > 0) + ImStrncpy(_FilterDateFrom, entry->Date, IM_ARRAYSIZE(_FilterDateFrom)); + if (strcmp(_FilterDateTo, entry->Date) < 0) + ImStrncpy(_FilterDateTo, entry->Date, IM_ARRAYSIZE(_FilterDateTo)); + + _SrcData.push_back(*entry); + _Batches.clear_destruct(); +} + +void ImGuiPerfTool::_Rebuild() +{ + if (_SrcData.empty()) + return; + + ImGuiStorage& temp_set = _TempSet; + _Labels.resize(0); + _LabelsVisible.resize(0); + _InfoTableSort.resize(0); + _Batches.clear_destruct(); + _InfoTableSortDirty = true; + + // Gather all visible labels. Legend batches will store data in this order. + temp_set.Data.resize(0); // name_id:IsLabelSeen + for (ImGuiPerfToolEntry& entry : _SrcData) + { + ImGuiID name_id = ImHashStr(entry.TestName); + if (!temp_set.GetBool(name_id)) + { + temp_set.SetBool(name_id, true); + _Labels.push_back(entry.TestName); + if (_IsVisibleTest(entry.TestName)) + _LabelsVisible.push_front(entry.TestName); + } + } + int num_visible_labels = _LabelsVisible.Size; + + // Labels are sorted in reverse order so they appear to be oredered from top down. + ImQsort(_Labels.Data, _Labels.Size, sizeof(const char*), &PerfToolComparerStr); + ImQsort(_LabelsVisible.Data, num_visible_labels, sizeof(const char*), &PerfToolComparerStr); + + // _SrcData vector stores sorted raw entries of imgui_perflog.csv. Sorting is very important, + // algorithm depends on data being correctly sorted. Sorting _SrcData is OK, because it is only + // ever appended to and never written out to disk. Entries are sorted by multiple criteria, + // in specified order: + // 1. By branch name + // 2. By build ID + // 3. By run timestamp + // 4. By test name + // This results in a neatly partitioned dataset where similar data is grouped together and where perf test order + // is consistent in all batches. Sorting by build ID _before_ timestamp is also important as we will be aggregating + // entries by build ID instead of timestamp, when appropriate display mode is enabled. + ImQsort(_SrcData.Data, _SrcData.Size, sizeof(ImGuiPerfToolEntry), &PerfToolComparerByEntryInfo); + + // Sort groups of entries into batches. + const bool combine_by_build_info = _DisplayType == ImGuiPerfToolDisplayType_CombineByBuildInfo; + _LabelBarCounts.Data.resize(0); + + // Process all batches. `entry` is always a first batch element (guaranteed by _SrcData being sorted by timestamp). + // At the end of this loop we fast-forward until next batch (first entry having different batch id (which is a + // timestamp or build info)). + for (ImGuiPerfToolEntry* entry = _SrcData.begin(); entry < _SrcData.end();) + { + // Filtered out entries can be safely ignored. Note that entry++ does not follow logic of fast-forwarding to the + // next batch, as found at the end of this loop. This is OK, because all entries belonging to a same batch will + // also have same date. + if ((_FilterDateFrom[0] && strcmp(entry->Date, _FilterDateFrom) < 0) || (_FilterDateTo[0] && strcmp(entry->Date, _FilterDateTo) > 0)) + { + entry++; + continue; + } + + _Batches.push_back(ImGuiPerfToolBatch()); + ImGuiPerfToolBatch& batch = _Batches.back(); + batch.BatchID = GetBatchID(this, entry); + batch.Entries.resize(num_visible_labels); + + // Fill in defaults. Done once before data aggregation loop, because same entry may be touched multiple times in + // the following loop when entries are being combined by build info. + for (int i = 0; i < num_visible_labels; i++) + { + ImGuiPerfToolEntry* e = &batch.Entries.Data[i]; + *e = *entry; + e->DtDeltaMs = 0; + e->NumSamples = 0; + e->LabelIndex = i; + e->TestName = _LabelsVisible.Data[i]; + } + + // Find perf test runs for this particular batch and accumulate them. + for (int i = 0; i < num_visible_labels; i++) + { + // This inner loop walks all entries that belong to current batch. Due to sorting we are sure that batch + // always starts with `entry`, and all entries that belong to a batch (whether we combine by build info or not) + // will be grouped in _SrcData. + ImGuiPerfToolEntry* aggregate = &batch.Entries.Data[i]; + for (ImGuiPerfToolEntry* e = entry; e < _SrcData.end() && GetBatchID(this, e) == batch.BatchID; e++) + { + if (strcmp(e->TestName, aggregate->TestName) != 0) + continue; + aggregate->DtDeltaMs += e->DtDeltaMs; + aggregate->NumSamples++; + aggregate->DtDeltaMsMin = ImMin(aggregate->DtDeltaMsMin, e->DtDeltaMs); + aggregate->DtDeltaMsMax = ImMax(aggregate->DtDeltaMsMax, e->DtDeltaMs); + } + } + + // In case data is combined by build info, DtDeltaMs will be a sum of all combined entries. Average it out. + if (combine_by_build_info) + for (int i = 0; i < num_visible_labels; i++) + { + ImGuiPerfToolEntry* aggregate = &batch.Entries.Data[i]; + if (aggregate->NumSamples > 0) + aggregate->DtDeltaMs /= aggregate->NumSamples; + } + + // Advance to the next batch. + batch.NumSamples = 1; + if (combine_by_build_info) + { + ImU64 last_timestamp = entry->Timestamp; + for (ImGuiID build_id = GetBuildID(entry); entry < _SrcData.end() && build_id == GetBuildID(entry);) + { + // Also count how many unique batches participate in this aggregated batch. + if (entry->Timestamp != last_timestamp) + { + batch.NumSamples++; + last_timestamp = entry->Timestamp; + } + entry++; + } + } + else + { + for (ImU64 timestamp = entry->Timestamp; entry < _SrcData.end() && timestamp == entry->Timestamp;) + entry++; + } + } + + // Create man entries for every batch. + // Pushed after sorting so they are always at the start of the chart. + const char* mean_labels[] = { "harmonic mean", "arithmetic mean", "geometric mean" }; + int num_visible_mean_labels = 0; + for (const char* label : mean_labels) + { + _Labels.push_back(label); + if (_IsVisibleTest(label)) + { + _LabelsVisible.push_back(label); + num_visible_mean_labels++; + } + } + for (ImGuiPerfToolBatch& batch : _Batches) + { + double delta_sum = 0.0; + double delta_prd = 1.0; + double delta_rec = 0.0; + for (int i = 0; i < batch.Entries.Size; i++) + { + ImGuiPerfToolEntry* entry = &batch.Entries.Data[i]; + delta_sum += entry->DtDeltaMs; + delta_prd *= entry->DtDeltaMs; + delta_rec += 1 / entry->DtDeltaMs; + } + + int visible_label_i = 0; + for (int i = 0; i < IM_ARRAYSIZE(mean_labels); i++) + { + if (!_IsVisibleTest(mean_labels[i])) + continue; + + batch.Entries.push_back(ImGuiPerfToolEntry()); + ImGuiPerfToolEntry* mean_entry = &batch.Entries.back(); + *mean_entry = batch.Entries.Data[0]; + mean_entry->LabelIndex = _LabelsVisible.Size - num_visible_mean_labels + visible_label_i; + mean_entry->TestName = _LabelsVisible.Data[mean_entry->LabelIndex]; + mean_entry->GitBranchName = ""; + mean_entry->BuildType = ""; + mean_entry->Compiler = ""; + mean_entry->OS = ""; + mean_entry->Cpu = ""; + mean_entry->Date = ""; + visible_label_i++; + if (i == 0) + mean_entry->DtDeltaMs = num_visible_labels / delta_rec; + else if (i == 1) + mean_entry->DtDeltaMs = delta_sum / num_visible_labels; + else if (i == 2) + mean_entry->DtDeltaMs = pow(delta_prd, 1.0 / num_visible_labels); + else + IM_ASSERT(0); + } + IM_ASSERT(batch.Entries.Size == _LabelsVisible.Size); + } + + // Find number of bars (batches) each label will render. + for (ImGuiPerfToolBatch& batch : _Batches) + { + if (!_IsVisibleBuild(&batch)) + continue; + + for (ImGuiPerfToolEntry& entry : batch.Entries) + { + ImGuiID label_id = ImHashStr(entry.TestName); + int num_bars = _LabelBarCounts.GetInt(label_id) + 1; + _LabelBarCounts.SetInt(label_id, num_bars); + } + } + + // Index branches, used for per-branch colors. + temp_set.Data.resize(0); // ImHashStr(branch_name):linear_index + int branch_index_last = 0; + _BaselineBatchIndex = -1; + for (ImGuiPerfToolBatch& batch : _Batches) + { + if (batch.Entries.empty()) + continue; + ImGuiPerfToolEntry* entry = &batch.Entries.Data[0]; + ImGuiID branch_hash = ImHashStr(entry->GitBranchName); + batch.BranchIndex = temp_set.GetInt(branch_hash, -1); + if (batch.BranchIndex < 0) + { + batch.BranchIndex = branch_index_last++; + temp_set.SetInt(branch_hash, batch.BranchIndex); + } + + if (_BaselineBatchIndex < 0) + if ((combine_by_build_info && GetBuildID(entry) == _BaselineBuildId) || _BaselineTimestamp == entry->Timestamp) + _BaselineBatchIndex = _Batches.index_from_ptr(&batch); + } + + // When per-branch colors are enabled we aggregate sample counts and set them to all batches with identical build info. + temp_set.Data.resize(0); // build_id:TotalSamples + if (_DisplayType == ImGuiPerfToolDisplayType_PerBranchColors) + { + // Aggregate totals to temp_set. + for (ImGuiPerfToolBatch& batch : _Batches) + { + ImGuiID build_id = GetBuildID(&batch); + temp_set.SetInt(build_id, temp_set.GetInt(build_id, 0) + batch.NumSamples); + } + + // Fill in batch sample counts. + for (ImGuiPerfToolBatch& batch : _Batches) + { + ImGuiID build_id = GetBuildID(&batch); + batch.NumSamples = temp_set.GetInt(build_id, 1); + } + } + + _NumVisibleBuilds = PerfToolCountBuilds(this, true); + _NumUniqueBuilds = PerfToolCountBuilds(this, false); + + _CalculateLegendAlignment(); + temp_set.Data.resize(0); +} + +void ImGuiPerfTool::Clear() +{ + _Labels.clear(); + _LabelsVisible.clear(); + _Batches.clear_destruct(); + _Visibility.Clear(); + _SrcData.clear_destruct(); + _CsvParser->Clear(); + + ImStrncpy(_FilterDateFrom, "9999-99-99", IM_ARRAYSIZE(_FilterDateFrom)); + ImStrncpy(_FilterDateTo, "0000-00-00", IM_ARRAYSIZE(_FilterDateFrom)); +} + +bool ImGuiPerfTool::LoadCSV(const char* filename) +{ + if (filename == NULL) + filename = IMGUI_PERFLOG_DEFAULT_FILENAME; + + Clear(); + + ImGuiCsvParser* parser = _CsvParser; + parser->Columns = 11; + if (!parser->Load(filename)) + return false; + + // Read perf test entries from CSV + for (int row = 0; row < parser->Rows; row++) + { + ImGuiPerfToolEntry entry; + int col = 0; + sscanf(parser->GetCell(row, col++), "%llu", &entry.Timestamp); + entry.Category = parser->GetCell(row, col++); + entry.TestName = parser->GetCell(row, col++); + sscanf(parser->GetCell(row, col++), "%lf", &entry.DtDeltaMs); + sscanf(parser->GetCell(row, col++), "x%d", &entry.PerfStressAmount); + entry.GitBranchName = parser->GetCell(row, col++); + entry.BuildType = parser->GetCell(row, col++); + entry.Cpu = parser->GetCell(row, col++); + entry.OS = parser->GetCell(row, col++); + entry.Compiler = parser->GetCell(row, col++); + entry.Date = parser->GetCell(row, col++); + AddEntry(&entry); + } + + return true; +} + +void ImGuiPerfTool::ViewOnly(const char** perf_names) +{ + // Data would not be built if we tried to view perftool of a particular test without first opening perftool via button. We need data to be built to hide perf tests. + if (_Batches.empty()) + _Rebuild(); + + // Hide other perf tests. + for (const char* label : _Labels) + { + bool visible = false; + for (const char** p_name = perf_names; !visible && *p_name; p_name++) + visible |= strcmp(label, *p_name) == 0; + _Visibility.SetBool(ImHashStr(label), visible); + } +} + +void ImGuiPerfTool::ViewOnly(const char* perf_name) +{ + const char* names[] = { perf_name, NULL }; + ViewOnly(names); +} + +ImGuiPerfToolEntry* ImGuiPerfTool::GetEntryByBatchIdx(int idx, const char* perf_name) +{ + if (idx < 0) + return NULL; + IM_ASSERT(idx < _Batches.Size); + ImGuiPerfToolBatch& batch = _Batches.Data[idx]; + for (int i = 0; i < batch.Entries.Size; i++) + if (ImGuiPerfToolEntry* entry = &batch.Entries.Data[i]) + if (strcmp(entry->TestName, perf_name) == 0) + return entry; + return NULL; +} + +bool ImGuiPerfTool::_IsVisibleBuild(ImGuiPerfToolBatch* batch) +{ + IM_ASSERT(batch != NULL); + if (batch->Entries.empty()) + return false; // All entries are hidden. + return _IsVisibleBuild(&batch->Entries.Data[0]); +} + +bool ImGuiPerfTool::_IsVisibleBuild(ImGuiPerfToolEntry* entry) +{ + return _Visibility.GetBool(ImHashStr(entry->GitBranchName), true) && + _Visibility.GetBool(ImHashStr(entry->Compiler), true) && + _Visibility.GetBool(ImHashStr(entry->Cpu), true) && + _Visibility.GetBool(ImHashStr(entry->OS), true) && + _Visibility.GetBool(ImHashStr(entry->BuildType), true); +} + +bool ImGuiPerfTool::_IsVisibleTest(const char* test_name) +{ + return _Visibility.GetBool(ImHashStr(test_name), true); +} + +void ImGuiPerfTool::_CalculateLegendAlignment() +{ + // Estimate paddings for legend format so it looks nice and aligned + // FIXME: Rely on font being monospace. May need to recalculate every frame on a per-need basis based on font? + _AlignStress = _AlignType = _AlignCpu = _AlignOs = _AlignCompiler = _AlignBranch = _AlignSamples = 0; + for (ImGuiPerfToolBatch& batch : _Batches) + { + if (batch.Entries.empty()) + continue; + ImGuiPerfToolEntry* entry = &batch.Entries.Data[0]; + if (!_IsVisibleBuild(entry)) + continue; + _AlignStress = ImMax(_AlignStress, (int)ceil(log10(entry->PerfStressAmount))); + _AlignType = ImMax(_AlignType, (int)strlen(entry->BuildType)); + _AlignCpu = ImMax(_AlignCpu, (int)strlen(entry->Cpu)); + _AlignOs = ImMax(_AlignOs, (int)strlen(entry->OS)); + _AlignCompiler = ImMax(_AlignCompiler, (int)strlen(entry->Compiler)); + _AlignBranch = ImMax(_AlignBranch, (int)strlen(entry->GitBranchName)); + _AlignSamples = ImMax(_AlignSamples, (int)Str16f("%d", entry->NumSamples).length()); + } +} + +bool ImGuiPerfTool::SaveHtmlReport(const char* file_name, const char* image_file) +{ + if (!ImFileCreateDirectoryChain(file_name, ImPathFindFilename(file_name))) + return false; + + FILE* fp = fopen(file_name, "w+"); + if (fp == NULL) + return false; + + fprintf(fp, "\n" + "\n" + "\n" + " \n" + " Dear ImGui perf report\n" + "\n" + "\n" + "
\n");
+
+    // Embed performance chart.
+    fprintf(fp, "## Dear ImGui perf report\n\n");
+
+    if (image_file != NULL)
+    {
+        FILE* fp_img = fopen(image_file, "rb");
+        if (fp_img != NULL)
+        {
+            ImVector image_buffer;
+            ImVector base64_buffer;
+            fseek(fp_img, 0, SEEK_END);
+            image_buffer.resize((int)ftell(fp_img));
+            base64_buffer.resize(((image_buffer.Size / 3) + 1) * 4 + 1);
+            rewind(fp_img);
+            fread(image_buffer.Data, 1, image_buffer.Size, fp_img);
+            fclose(fp_img);
+            int len = ImStrBase64Encode((unsigned char*)image_buffer.Data, base64_buffer.Data, image_buffer.Size);
+            base64_buffer.Data[len] = 0;
+            fprintf(fp, "![](data:image/png;base64,%s)\n\n", base64_buffer.Data);
+        }
+    }
+
+    // Print info table.
+    const bool combine_by_build_info = _DisplayType == ImGuiPerfToolDisplayType_CombineByBuildInfo;
+    for (const auto& column_info : PerfToolColumnInfo)
+        if (column_info.ShowAlways || combine_by_build_info)
+            fprintf(fp, "| %s ", column_info.Title);
+    fprintf(fp, "|\n");
+    for (const auto& column_info : PerfToolColumnInfo)
+        if (column_info.ShowAlways || combine_by_build_info)
+            fprintf(fp, "| -- ");
+    fprintf(fp, "|\n");
+
+    for (int row_index = _InfoTableSort.Size - 1; row_index >= 0; row_index--)
+    {
+        int batch_index_sorted, entry_index_sorted;
+        _UnpackSortedKey(_InfoTableSort[row_index], &batch_index_sorted, &entry_index_sorted);
+        ImGuiPerfToolBatch* batch = &_Batches[batch_index_sorted];
+        ImGuiPerfToolEntry* entry = &batch->Entries[entry_index_sorted];
+        const char* test_name = entry->TestName;
+        if (!_IsVisibleBuild(entry) || entry->NumSamples == 0)
+            continue;
+
+        ImGuiPerfToolEntry* baseline_entry = GetEntryByBatchIdx(_BaselineBatchIndex, test_name);
+        for (int i = 0; i < IM_ARRAYSIZE(PerfToolColumnInfo); i++)
+        {
+            Str30f label("");
+            const ImGuiPerfToolColumnInfo& column_info = PerfToolColumnInfo[i];
+            if (column_info.ShowAlways || combine_by_build_info)
+            {
+                switch (i)
+                {
+                case 0:
+                {
+                    char date[64];
+                    FormatDateAndTime(entry->Timestamp, date, IM_ARRAYSIZE(date));
+                    fprintf(fp, "| %s ", date);
+                    break;
+                }
+                case 1:  fprintf(fp, "| %s ", entry->TestName);             break;
+                case 2:  fprintf(fp, "| %s ", entry->GitBranchName);        break;
+                case 3:  fprintf(fp, "| %s ", entry->Compiler);             break;
+                case 4:  fprintf(fp, "| %s ", entry->OS);                   break;
+                case 5:  fprintf(fp, "| %s ", entry->Cpu);                  break;
+                case 6:  fprintf(fp, "| %s ", entry->BuildType);            break;
+                case 7:  fprintf(fp, "| x%d ", entry->PerfStressAmount);    break;
+                case 8:  fprintf(fp, "| %.2f ", entry->DtDeltaMs);          break;
+                case 9:  fprintf(fp, "| %.2f ", entry->DtDeltaMsMin);       break;
+                case 10: fprintf(fp, "| %.2f ", entry->DtDeltaMsMax);       break;
+                case 11: fprintf(fp, "| %d ", entry->NumSamples);           break;
+                case 12: FormatVsBaseline(entry, baseline_entry, label); fprintf(fp, "| %s ", label.c_str()); break;
+                default: IM_ASSERT(0); break;
+                }
+            }
+        }
+        fprintf(fp, "|\n");
+    }
+
+    fprintf(fp, "
\n" + " \n" + " \n" + "\n" + "\n"); + + fclose(fp); + return true; +} + +void ImGuiPerfTool::_SetBaseline(int batch_index) +{ + IM_ASSERT(batch_index < _Batches.Size); + _BaselineBatchIndex = batch_index; + if (batch_index >= 0) + { + _BaselineTimestamp = _Batches.Data[batch_index].Entries.Data[0].Timestamp; + _BaselineBuildId = GetBuildID(&_Batches.Data[batch_index]); + } +} + +//------------------------------------------------------------------------- +// [SECTION] USER INTERFACE +//------------------------------------------------------------------------- + +void ImGuiPerfTool::ShowPerfToolWindow(ImGuiTestEngine* engine, bool* p_open) +{ + if (!ImGui::Begin("Dear ImGui Perf Tool", p_open)) + { + ImGui::End(); + return; + } + + if (ImGui::IsWindowAppearing() && Empty()) + LoadCSV(); + + ImGuiStyle& style = ImGui::GetStyle(); + + // ----------------------------------------------------------------------------------------------------------------- + // Render utility buttons + // ----------------------------------------------------------------------------------------------------------------- + + // Date filter + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted("Date Range:"); + ImGui::SameLine(); + + bool dirty = _Batches.empty(); + bool date_changed = InputDate("##date-from", _FilterDateFrom, IM_ARRAYSIZE(_FilterDateFrom), + (strcmp(_FilterDateFrom, _FilterDateTo) <= 0 || !*_FilterDateTo)); + if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) + ImGui::OpenPopup("InputDate From Menu"); + ImGui::SameLine(0, 0.0f); + ImGui::TextUnformatted(".."); + ImGui::SameLine(0, 0.0f); + date_changed |= InputDate("##date-to", _FilterDateTo, IM_ARRAYSIZE(_FilterDateTo), + (strcmp(_FilterDateFrom, _FilterDateTo) <= 0 || !*_FilterDateFrom)); + if (date_changed) + { + dirty = (!_FilterDateFrom[0] || IsDateValid(_FilterDateFrom)) && (!_FilterDateTo[0] || IsDateValid(_FilterDateTo)); + if (_FilterDateFrom[0] && _FilterDateTo[0]) + dirty &= strcmp(_FilterDateFrom, _FilterDateTo) <= 0; + } + if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) + ImGui::OpenPopup("InputDate To Menu"); + ImGui::SameLine(); + + for (int i = 0; i < 2; i++) + { + if (ImGui::BeginPopup(i == 0 ? "InputDate From Menu" : "InputDate To Menu")) + { + char* date = i == 0 ? _FilterDateFrom : _FilterDateTo; + int date_size = i == 0 ? IM_ARRAYSIZE(_FilterDateFrom) : IM_ARRAYSIZE(_FilterDateTo); + if (i == 0 && ImGui::MenuItem("Set Min")) + { + for (ImGuiPerfToolEntry& entry : _SrcData) + if (strcmp(date, entry.Date) > 0) + { + ImStrncpy(date, entry.Date, date_size); + dirty = true; + } + } + if (ImGui::MenuItem("Set Max")) + { + for (ImGuiPerfToolEntry& entry : _SrcData) + if (strcmp(date, entry.Date) < 0) + { + ImStrncpy(date, entry.Date, date_size); + dirty = true; + } + } + if (ImGui::MenuItem("Set Today")) + { + time_t now = time(NULL); + FormatDate((ImU64)now * 1000000, date, date_size); + dirty = true; + } + ImGui::EndPopup(); + } + } + + if (ImGui::Button(Str64f("Filter builds (%d/%d)###Filter builds", _NumVisibleBuilds, _NumUniqueBuilds).c_str())) + ImGui::OpenPopup("Filter builds"); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Hide or show individual builds."); + ImGui::SameLine(); + if (ImGui::Button(Str64f("Filter tests (%d/%d)###Filter tests", _LabelsVisible.Size, _Labels.Size).c_str())) + ImGui::OpenPopup("Filter perfs"); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Hide or show individual tests."); + ImGui::SameLine(); + + dirty |= Button3("Combine", (int*)&_DisplayType); + if (ImGui::IsItemHovered()) + { + ImGui::BeginTooltip(); + ImGui::RadioButton("Display each run separately", _DisplayType == ImGuiPerfToolDisplayType_Simple); + ImGui::RadioButton("Use one color per branch. Disables baseline comparisons!", _DisplayType == ImGuiPerfToolDisplayType_PerBranchColors); + ImGui::RadioButton("Combine multiple runs with same build info into one averaged build entry.", _DisplayType == ImGuiPerfToolDisplayType_CombineByBuildInfo); + ImGui::EndTooltip(); + } + + ImGui::SameLine(); + if (_ReportGenerating && ImGuiTestEngine_IsTestQueueEmpty(engine)) + { + _ReportGenerating = false; + ImOsOpenInShell(PerfToolReportDefaultOutputPath); + } + if (_Batches.empty()) + ImGui::BeginDisabled(); + if (ImGui::Button("Html Export")) + { + // In order to capture a screenshot Report is saved by executing a "capture_perf_report" test. + _ReportGenerating = true; + ImGuiTestEngine_QueueTests(engine, ImGuiTestGroup_Tests, "capture_perf_report"); + } + if (_Batches.empty()) + ImGui::EndDisabled(); + ImGui::SameLine(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Generate a report and open it in the browser."); + + // Align help button to the right. + float help_pos = ImGui::GetWindowContentRegionMax().x - style.FramePadding.x * 2 - ImGui::CalcTextSize("(?)").x; + if (help_pos > ImGui::GetCursorPosX()) + ImGui::SetCursorPosX(help_pos); + + ImGui::TextDisabled("(?)"); + if (ImGui::IsItemHovered()) + { + ImGui::BeginTooltip(); + ImGui::BulletText("To change baseline build, double-click desired build in the legend."); + ImGui::BulletText("Extra information is displayed when hovering bars of a particular perf test and holding SHIFT."); + ImGui::BulletText("Double-click plot to fit plot into available area."); + ImGui::EndTooltip(); + } + + if (ImGui::BeginPopup("Filter builds")) + { + ImGuiStorage& temp_set = _TempSet; + temp_set.Data.resize(0); // ImHashStr(BuildProperty):seen + + static const char* columns[] = { "Branch", "Build", "CPU", "OS", "Compiler" }; + bool show_all = ImGui::Button("Show All"); + ImGui::SameLine(); + bool hide_all = ImGui::Button("Hide All"); + if (ImGui::BeginTable("Builds", IM_ARRAYSIZE(columns), ImGuiTableFlags_Borders | ImGuiTableFlags_SizingFixedFit)) + { + for (int i = 0; i < IM_ARRAYSIZE(columns); i++) + ImGui::TableSetupColumn(columns[i]); + ImGui::TableHeadersRow(); + + // Find columns with nothing checked. + bool checked_any[] = { false, false, false, false, false }; + for (ImGuiPerfToolEntry& entry : _SrcData) + { + const char* properties[] = { entry.GitBranchName, entry.BuildType, entry.Cpu, entry.OS, entry.Compiler }; + for (int i = 0; i < IM_ARRAYSIZE(properties); i++) + { + ImGuiID hash = ImHashStr(properties[i]); + checked_any[i] |= _Visibility.GetBool(hash, true); + } + } + + int property_offsets[] = + { + offsetof(ImGuiPerfToolEntry, GitBranchName), + offsetof(ImGuiPerfToolEntry, BuildType), + offsetof(ImGuiPerfToolEntry, Cpu), + offsetof(ImGuiPerfToolEntry, OS), + offsetof(ImGuiPerfToolEntry, Compiler), + }; + + ImGui::TableNextRow(); + for (int i = 0; i < IM_ARRAYSIZE(property_offsets); i++) + { + ImGui::TableSetColumnIndex(i); + for (ImGuiPerfToolEntry& entry : _SrcData) + { + const char* property = *(const char**)((const char*)&entry + property_offsets[i]); + ImGuiID hash = ImHashStr(property); + if (temp_set.GetBool(hash)) + continue; + temp_set.SetBool(hash, true); + + bool visible = _Visibility.GetBool(hash, true) || show_all; + if (hide_all) + visible = false; + bool modified = ImGui::Checkbox(property, &visible) || show_all || hide_all; + _Visibility.SetBool(hash, visible); + if (modified) + { + _CalculateLegendAlignment(); + _NumVisibleBuilds = PerfToolCountBuilds(this, true); + dirty = true; + } + if (!checked_any[i]) + { + ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, ImColor(1.0f, 0.0f, 0.0f, 0.2f)); + if (ImGui::TableGetColumnFlags() & ImGuiTableColumnFlags_IsHovered) + ImGui::SetTooltip("Check at least one item in each column to see any data."); + } + } + } + ImGui::EndTable(); + } + ImGui::EndPopup(); + } + + if (ImGui::BeginPopup("Filter perfs")) + { + dirty |= RenderMultiSelectFilter(this, "Filter by perf test", &_Labels); + if (ImGui::IsKeyPressed(ImGuiKey_Escape)) + ImGui::CloseCurrentPopup(); + ImGui::EndPopup(); + } + + if (dirty) + _Rebuild(); + + // Rendering a plot of empty dataset is not possible. + if (_Batches.empty() || _LabelsVisible.Size == 0 || _NumVisibleBuilds == 0) + { + ImGui::TextUnformatted("No data is available. Run some perf tests or adjust filter settings."); + } + else + { +#if IMGUI_TEST_ENGINE_ENABLE_IMPLOT + // Splitter between two following child windows is rendered first. + float plot_height = 0.0f; + float& table_height = _InfoTableHeight; + ImGui::Splitter("splitter", &plot_height, &table_height, ImGuiAxis_Y, +1); + + // Double-click to move splitter to bottom + if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) + { + table_height = 0; + plot_height = ImGui::GetContentRegionAvail().y - style.ItemSpacing.y; + ImGui::ClearActiveID(); + } + + // Render entries plot + if (ImGui::BeginChild(ImGui::GetID("plot"), ImVec2(0, plot_height))) + _ShowEntriesPlot(); + ImGui::EndChild(); + + // Render entries tables + if (table_height > 0.0f) + { + if (ImGui::BeginChild(ImGui::GetID("info-table"), ImVec2(0, table_height))) + _ShowEntriesTable(); + ImGui::EndChild(); + } +#else + _ShowEntriesTable(); +#endif + } + ImGui::End(); +} + +#if IMGUI_TEST_ENGINE_ENABLE_IMPLOT +static double GetLabelVerticalOffset(double occupy_h, int max_visible_builds, int now_visible_builds) +{ + const double h = occupy_h / (float)max_visible_builds; + double offset = -h * ((max_visible_builds - 1) * 0.5); + return (double)now_visible_builds * h + offset; +} +#endif + +void ImGuiPerfTool::_ShowEntriesPlot() +{ +#if IMGUI_TEST_ENGINE_ENABLE_IMPLOT + ImGuiIO& io = ImGui::GetIO(); + ImGuiStyle& style = ImGui::GetStyle(); + Str256 label; + Str256 display_label; + + ImPlot::PushStyleColor(ImPlotCol_AxisBgHovered, IM_COL32(0, 0, 0, 0)); + ImPlot::PushStyleColor(ImPlotCol_AxisBgActive, IM_COL32(0, 0, 0, 0)); + if (!ImPlot::BeginPlot("PerfTool", ImVec2(-1, -1), ImPlotFlags_NoTitle)) + return; + + ImPlot::SetupAxis(ImAxis_X1, NULL, ImPlotAxisFlags_NoTickLabels); + if (_LabelsVisible.Size > 1) + { + ImPlot::SetupAxisTicks(ImAxis_Y1, 0, _LabelsVisible.Size, _LabelsVisible.Size, _LabelsVisible.Data); + } + else if (_LabelsVisible.Size == 1) + { + const char* labels[] = { _LabelsVisible[0], "" }; + ImPlot::SetupAxisTicks(ImAxis_Y1, 0, _LabelsVisible.Size, 2, labels); + } + ImPlot::SetupLegend(ImPlotLocation_NorthEast); + + // Amount of vertical space bars of one label will occupy. 1.0 would leave no space between bars of adjacent labels. + const float occupy_h = 0.8f; + + // Plot bars + bool legend_hovered = false; + ImGuiStorage& temp_set = _TempSet; + temp_set.Data.resize(0); // ImHashStr(TestName):now_visible_builds_i + int current_baseline_batch_index = _BaselineBatchIndex; // Cache this value before loop, so toggling it does not create flicker. + for (int batch_index = 0; batch_index < _Batches.Size; batch_index++) + { + ImGuiPerfToolBatch& batch = _Batches[batch_index]; + if (!_IsVisibleBuild(&batch.Entries.Data[0])) + continue; + + // Plot bars. + label.clear(); + display_label.clear(); + PerfToolFormatBuildInfo(this, &label, &batch); + display_label.append(label.c_str()); + ImGuiID batch_label_id; + bool baseline_match = false; + if (_DisplayType == ImGuiPerfToolDisplayType_PerBranchColors) + { + // No "vs baseline" comparison for per-branch colors, because runs are combined in the legend, but not in the info table. + batch_label_id = GetBuildID(&batch); + } + else + { + batch_label_id = ImHashData(&batch.BatchID, sizeof(batch.BatchID)); + baseline_match = current_baseline_batch_index == batch_index; + } + display_label.appendf("%s###%08X", baseline_match ? " *" : "", batch_label_id); + + // Plot all bars one by one, so batches with varying number of bars would not contain empty holes. + for (ImGuiPerfToolEntry& entry : batch.Entries) + { + if (entry.NumSamples == 0) + continue; // Dummy entry, perf did not run for this test in this batch. + ImGuiID label_id = ImHashStr(entry.TestName); + const int max_visible_builds = _LabelBarCounts.GetInt(label_id); + const int now_visible_builds = temp_set.GetInt(label_id); + temp_set.SetInt(label_id, now_visible_builds + 1); + double y_pos = (double)entry.LabelIndex + GetLabelVerticalOffset(occupy_h, max_visible_builds, now_visible_builds); + ImPlot::SetNextFillStyle(ImPlot::GetColormapColor(_DisplayType == ImGuiPerfToolDisplayType_PerBranchColors ? batch.BranchIndex : batch_index)); + ImPlot::PlotBars(display_label.c_str(), &entry.DtDeltaMs, &y_pos, 1, occupy_h / (double)max_visible_builds, ImPlotBarsFlags_Horizontal); + } + legend_hovered |= ImPlot::IsLegendEntryHovered(display_label.c_str()); + + // Set baseline. + if (ImPlot::IsLegendEntryHovered(display_label.c_str())) + { + if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) + _SetBaseline(batch_index); + } + } + + // Plot highlights. + ImPlotContext& gp = *GImPlot; + ImPlotPlot& plot = *gp.CurrentPlot; + _PlotHoverTest = -1; + _PlotHoverBatch = -1; + _PlotHoverTestLabel = false; + bool can_highlight = !legend_hovered && (ImPlot::IsPlotHovered() || ImPlot::IsAxisHovered(ImAxis_Y1)); + ImDrawList* plot_draw_list = ImPlot::GetPlotDrawList(); + + // Highlight bars when hovering a label. + int hovered_label_index = -1; + for (int i = 0; i < _LabelsVisible.Size && can_highlight; i++) + { + ImRect label_rect_loose = ImPlotGetYTickRect(i); // Rect around test label + ImRect label_rect_tight; // Rect around test label, covering bar height and label area width + label_rect_tight.Min.y = ImPlot::PlotToPixels(0, (float)i + 0.5f).y; + label_rect_tight.Max.y = ImPlot::PlotToPixels(0, (float)i - 0.5f).y; + label_rect_tight.Min.x = plot.CanvasRect.Min.x; + label_rect_tight.Max.x = plot.PlotRect.Min.x; + + ImRect rect_bars; // Rect around bars only + rect_bars.Min.x = plot.PlotRect.Min.x; + rect_bars.Max.x = plot.PlotRect.Max.x; + rect_bars.Min.y = ImPlot::PlotToPixels(0, (float)i + 0.5f).y; + rect_bars.Max.y = ImPlot::PlotToPixels(0, (float)i - 0.5f).y; + + // Render underline signaling it is clickable. Clicks are handled when rendering info table. + if (label_rect_loose.Contains(io.MousePos)) + { + ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + plot_draw_list->AddLine(ImFloor(label_rect_loose.GetBL()), ImFloor(label_rect_loose.GetBR()), + ImColor(style.Colors[ImGuiCol_Text])); + } + + // Highlight bars belonging to hovered label. + if (label_rect_tight.Contains(io.MousePos)) + { + plot_draw_list->AddRectFilled(rect_bars.Min, rect_bars.Max, ImColor(style.Colors[ImGuiCol_TextSelectedBg])); + _PlotHoverTestLabel = true; + _PlotHoverTest = i; + } + + if (rect_bars.Contains(io.MousePos)) + hovered_label_index = i; + } + + // Highlight individual bars when hovering them on the plot or info table. + temp_set.Data.resize(0); // ImHashStr(hovered_label):now_visible_builds_i + if (hovered_label_index < 0) + hovered_label_index = _TableHoveredTest; + if (hovered_label_index >= 0) + { + const char* hovered_label = _LabelsVisible.Data[hovered_label_index]; + ImGuiID label_id = ImHashStr(hovered_label); + for (ImGuiPerfToolBatch& batch : _Batches) + { + int batch_index = _Batches.index_from_ptr(&batch); + if (!_IsVisibleBuild(&batch)) + continue; + + ImGuiPerfToolEntry* entry = &batch.Entries.Data[hovered_label_index]; + if (entry->NumSamples == 0) + continue; // Dummy entry, perf did not run for this test in this batch. + + int max_visible_builds = _LabelBarCounts.GetInt(label_id); + const int now_visible_builds = temp_set.GetInt(label_id); + temp_set.SetInt(label_id, now_visible_builds + 1); + float h = occupy_h / (float)max_visible_builds; + float y_pos = (float)entry->LabelIndex; + y_pos += (float)GetLabelVerticalOffset(occupy_h, max_visible_builds, now_visible_builds); + ImRect rect_bar; // Rect around hovered bar only + rect_bar.Min.x = plot.PlotRect.Min.x; + rect_bar.Max.x = plot.PlotRect.Max.x; + rect_bar.Min.y = ImPlot::PlotToPixels(0, y_pos - h * 0.5f + h).y; // ImPlot y_pos is for bar center, therefore we adjust positions by half-height to get a bounding box. + rect_bar.Max.y = ImPlot::PlotToPixels(0, y_pos - h * 0.5f).y; + + // Mouse is hovering label or bars of a perf test - highlight them in info table. + if (_PlotHoverTest < 0 && rect_bar.Min.y <= io.MousePos.y && io.MousePos.y < rect_bar.Max.y && io.MousePos.x > plot.PlotRect.Min.x) + { + // _LabelsVisible is inverted to make perf test order match info table order. Revert it back. + _PlotHoverTest = hovered_label_index; + _PlotHoverBatch = batch_index; + plot_draw_list->AddRectFilled(rect_bar.Min, rect_bar.Max, ImColor(style.Colors[ImGuiCol_TextSelectedBg])); + } + + // Mouse is hovering a row in info table - highlight relevant bars on the plot. + if (_TableHoveredBatch == batch_index && _TableHoveredTest == hovered_label_index) + plot_draw_list->AddRectFilled(rect_bar.Min, rect_bar.Max, ImColor(style.Colors[ImGuiCol_TextSelectedBg])); + } + } + + if (io.KeyShift && _PlotHoverTest >= 0) + { + // Info tooltip with delta times of each batch for a hovered test. + const char* test_name = _LabelsVisible.Data[_PlotHoverTest]; + ImGui::BeginTooltip(); + float w = ImGui::CalcTextSize(test_name).x; + float total_w = ImGui::GetContentRegionAvail().x; + if (total_w > w) + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (total_w - w) * 0.5f); + ImGui::TextUnformatted(test_name); + + for (int i = 0; i < _Batches.Size; i++) + { + if (ImGuiPerfToolEntry* hovered_entry = GetEntryByBatchIdx(i, test_name)) + ImGui::Text("%s %.3fms", label.c_str(), hovered_entry->DtDeltaMs); + else + ImGui::Text("%s --", label.c_str()); + } + ImGui::EndTooltip(); + } + + ImPlot::EndPlot(); + ImPlot::PopStyleColor(2); +#else + ImGui::TextUnformatted("Not enabled because ImPlot is not available (IMGUI_TEST_ENGINE_ENABLE_IMPLOT=0)."); +#endif +} + +void ImGuiPerfTool::_ShowEntriesTable() +{ + ImGuiTableFlags table_flags = ImGuiTableFlags_Hideable | ImGuiTableFlags_Borders | ImGuiTableFlags_Sortable | + ImGuiTableFlags_SortMulti | ImGuiTableFlags_SortTristate | ImGuiTableFlags_Resizable | + ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_ScrollY; + if (!ImGui::BeginTable("PerfInfo", IM_ARRAYSIZE(PerfToolColumnInfo), table_flags)) + return; + + ImGuiStyle& style = ImGui::GetStyle(); + int num_visible_labels = _LabelsVisible.Size; + + // Test name column is not sorted because we do sorting only within perf runs of a particular tests, + // so as far as sorting function is concerned all items in first column are identical. + for (int i = 0; i < IM_ARRAYSIZE(PerfToolColumnInfo); i++) + { + const ImGuiPerfToolColumnInfo& info = PerfToolColumnInfo[i]; + ImGuiTableColumnFlags column_flags = info.Flags; + if (i == 0 && _DisplayType != ImGuiPerfToolDisplayType_Simple) + column_flags |= ImGuiTableColumnFlags_Disabled; // Date only visible in non-combining mode. + if (!info.ShowAlways && _DisplayType != ImGuiPerfToolDisplayType_CombineByBuildInfo) + column_flags |= ImGuiTableColumnFlags_Disabled; + ImGui::TableSetupColumn(info.Title, column_flags); + } + ImGui::TableSetupScrollFreeze(0, 1); + + if (ImGuiTableSortSpecs* sorts_specs = ImGui::TableGetSortSpecs()) + if (sorts_specs->SpecsDirty || _InfoTableSortDirty) + { + // Fill sort table with unsorted indices. + sorts_specs->SpecsDirty = _InfoTableSortDirty = false; + + // Reinitialize sorting table to unsorted state. + _InfoTableSort.resize(num_visible_labels * _Batches.Size); + for (int entry_index = 0, i = 0; entry_index < num_visible_labels; entry_index++) + for (int batch_index = 0; batch_index < _Batches.Size; batch_index++, i++) + _InfoTableSort.Data[i] = (((ImU64)batch_index * num_visible_labels + entry_index) << 24) | i; + + // Sort batches of each label. + if (sorts_specs->SpecsCount > 0) + { + _InfoTableSortSpecs = sorts_specs; + PerfToolInstance = this; + ImQsort(_InfoTableSort.Data, (size_t)_InfoTableSort.Size, sizeof(_InfoTableSort.Data[0]), CompareWithSortSpecs); + _InfoTableSortSpecs = NULL; + PerfToolInstance = NULL; + } + } + + ImGui::TableHeadersRow(); + + // ImPlot renders bars from bottom to the top. We want bars to render from top to the bottom, therefore we loop + // labels and batches in reverse order. + _TableHoveredTest = -1; + _TableHoveredBatch = -1; + const bool scroll_into_view = _PlotHoverTestLabel && ImGui::IsMouseClicked(ImGuiMouseButton_Left); + const float header_row_height = ImGui::TableGetCellBgRect(ImGui::GetCurrentTable(), 0).GetHeight(); + ImRect scroll_into_view_rect(FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX); + + for (int row_index = _InfoTableSort.Size - 1; row_index >= 0; row_index--) + { + int batch_index_sorted, entry_index_sorted; + _UnpackSortedKey(_InfoTableSort[row_index], &batch_index_sorted, &entry_index_sorted); + ImGuiPerfToolBatch* batch = &_Batches[batch_index_sorted]; + ImGuiPerfToolEntry* entry = &batch->Entries[entry_index_sorted]; + const char* test_name = entry->TestName; + + if (!_IsVisibleBuild(entry) || !_IsVisibleTest(entry->TestName) || entry->NumSamples == 0) + continue; + + ImGui::PushID(entry); + ImGui::TableNextRow(); + if (row_index & 1) + ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImGui::GetColorU32(ImGuiCol_TableRowBgAlt, 0.5f)); + else + ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImGui::GetColorU32(ImGuiCol_TableRowBg, 0.5f)); + + if (_PlotHoverTest == entry_index_sorted) + { + // Highlight a row that corresponds to hovered bar, or all rows that correspond to hovered perf test label. + if (_PlotHoverBatch == batch_index_sorted || _PlotHoverTestLabel) + ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImColor(style.Colors[ImGuiCol_TextSelectedBg])); + } + + ImGuiPerfToolEntry* baseline_entry = GetEntryByBatchIdx(_BaselineBatchIndex, test_name); + + // Date + if (ImGui::TableNextColumn()) + { + char date[64]; + FormatDateAndTime(entry->Timestamp, date, IM_ARRAYSIZE(date)); + ImGui::TextUnformatted(date); + } + + // Build info + if (ImGui::TableNextColumn()) + { + // ImGuiSelectableFlags_Disabled + changing ImGuiCol_TextDisabled color prevents selectable from overriding table highlight behavior. + ImGui::PushStyleColor(ImGuiCol_Header, style.Colors[ImGuiCol_Text]); + ImGui::PushStyleColor(ImGuiCol_HeaderHovered, style.Colors[ImGuiCol_TextSelectedBg]); + ImGui::PushStyleColor(ImGuiCol_HeaderActive, style.Colors[ImGuiCol_TextSelectedBg]); + ImGui::Selectable(entry->TestName, false, ImGuiSelectableFlags_SpanAllColumns); + ImGui::PopStyleColor(3); + if (ImGui::IsItemHovered()) + { + _TableHoveredTest = entry_index_sorted; + _TableHoveredBatch = batch_index_sorted; + } + + if (ImGui::BeginPopupContextItem()) + { + if (entry == baseline_entry) + ImGui::BeginDisabled(); + if (ImGui::MenuItem("Set as baseline")) + _SetBaseline(batch_index_sorted); + if (entry == baseline_entry) + ImGui::EndDisabled(); + ImGui::EndPopup(); + } + } + if (ImGui::TableNextColumn()) + ImGui::TextUnformatted(entry->GitBranchName); + if (ImGui::TableNextColumn()) + ImGui::TextUnformatted(entry->Compiler); + if (ImGui::TableNextColumn()) + ImGui::TextUnformatted(entry->OS); + if (ImGui::TableNextColumn()) + ImGui::TextUnformatted(entry->Cpu); + if (ImGui::TableNextColumn()) + ImGui::TextUnformatted(entry->BuildType); + if (ImGui::TableNextColumn()) + ImGui::Text("x%d", entry->PerfStressAmount); + + // Avg ms + if (ImGui::TableNextColumn()) + ImGui::Text("%.3lf", entry->DtDeltaMs); + + // Min ms + if (ImGui::TableNextColumn()) + ImGui::Text("%.3lf", entry->DtDeltaMsMin); + + // Max ms + if (ImGui::TableNextColumn()) + ImGui::Text("%.3lf", entry->DtDeltaMsMax); + + // Num samples + if (ImGui::TableNextColumn()) + ImGui::Text("%d", entry->NumSamples); + + // VS Baseline + if (ImGui::TableNextColumn()) + { + float dt_change = (float)entry->VsBaseline; + if (_DisplayType == ImGuiPerfToolDisplayType_PerBranchColors) + { + ImGui::TextUnformatted("--"); + } + else + { + Str30 label; + dt_change = FormatVsBaseline(entry, baseline_entry, label); + ImGui::TextUnformatted(label.c_str()); + if (dt_change != entry->VsBaseline) + { + entry->VsBaseline = dt_change; + _InfoTableSortDirty = true; // Force re-sorting. + } + } + } + + if (_PlotHoverTest == entry_index_sorted && scroll_into_view) + { + ImGuiTable* table = ImGui::GetCurrentTable(); + scroll_into_view_rect.Add(ImGui::TableGetCellBgRect(table, 0)); + } + + ImGui::PopID(); + } + + if (scroll_into_view) + { + scroll_into_view_rect.Min.y -= header_row_height; // FIXME-TABLE: Compensate for frozen header row covering a first content row scrolled into view. + ImGui::ScrollToRect(ImGui::GetCurrentWindow(), scroll_into_view_rect, ImGuiScrollFlags_NoScrollParent); + } + + ImGui::EndTable(); +} + +//------------------------------------------------------------------------- +// [SECTION] SETTINGS +//------------------------------------------------------------------------- + +static void PerflogSettingsHandler_ClearAll(ImGuiContext*, ImGuiSettingsHandler* ini_handler) +{ + ImGuiPerfTool* perftool = (ImGuiPerfTool*)ini_handler->UserData; + perftool->_Visibility.Clear(); +} + +static void* PerflogSettingsHandler_ReadOpen(ImGuiContext*, ImGuiSettingsHandler*, const char*) +{ + return (void*)1; +} + +static void PerflogSettingsHandler_ReadLine(ImGuiContext*, ImGuiSettingsHandler* ini_handler, void*, const char* line) +{ + ImGuiPerfTool* perftool = (ImGuiPerfTool*)ini_handler->UserData; + char buf[128]; + int visible = -1, display_type = -1; + /**/ if (sscanf(line, "DateFrom=%10s", perftool->_FilterDateFrom)) {} + else if (sscanf(line, "DateTo=%10s", perftool->_FilterDateTo)) {} + else if (sscanf(line, "DisplayType=%d", &display_type)) { perftool->_DisplayType = (ImGuiPerfToolDisplayType)display_type; } + else if (sscanf(line, "BaselineBuildId=%llu", &perftool->_BaselineBuildId)) {} + else if (sscanf(line, "BaselineTimestamp=%llu", &perftool->_BaselineTimestamp)) {} + else if (sscanf(line, "TestVisibility=%[^,],%d", buf, &visible) == 2) { perftool->_Visibility.SetBool(ImHashStr(buf), !!visible); } + else if (sscanf(line, "BuildVisibility=%[^,],%d", buf, &visible) == 2) { perftool->_Visibility.SetBool(ImHashStr(buf), !!visible); } +} + +static void PerflogSettingsHandler_ApplyAll(ImGuiContext*, ImGuiSettingsHandler* ini_handler) +{ + ImGuiPerfTool* perftool = (ImGuiPerfTool*)ini_handler->UserData; + perftool->_Batches.clear_destruct(); + perftool->_SetBaseline(-1); +} + +static void PerflogSettingsHandler_WriteAll(ImGuiContext*, ImGuiSettingsHandler* ini_handler, ImGuiTextBuffer* buf) +{ + ImGuiPerfTool* perftool = (ImGuiPerfTool*)ini_handler->UserData; + if (perftool->_Batches.empty()) + return; + buf->appendf("[%s][Data]\n", ini_handler->TypeName); + buf->appendf("DateFrom=%s\n", perftool->_FilterDateFrom); + buf->appendf("DateTo=%s\n", perftool->_FilterDateTo); + buf->appendf("DisplayType=%d\n", perftool->_DisplayType); + buf->appendf("BaselineBuildId=%llu\n", perftool->_BaselineBuildId); + buf->appendf("BaselineTimestamp=%llu\n", perftool->_BaselineTimestamp); + for (const char* label : perftool->_Labels) + buf->appendf("TestVisibility=%s,%d\n", label, perftool->_Visibility.GetBool(ImHashStr(label), true)); + + ImGuiStorage& temp_set = perftool->_TempSet; + temp_set.Data.clear(); + for (ImGuiPerfToolEntry& entry : perftool->_SrcData) + { + const char* properties[] = { entry.GitBranchName, entry.BuildType, entry.Cpu, entry.OS, entry.Compiler }; + for (int i = 0; i < IM_ARRAYSIZE(properties); i++) + { + ImGuiID hash = ImHashStr(properties[i]); + if (!temp_set.GetBool(hash)) + { + temp_set.SetBool(hash, true); + buf->appendf("BuildVisibility=%s,%d\n", properties[i], perftool->_Visibility.GetBool(hash, true)); + } + } + } + buf->append("\n"); +} + +void ImGuiPerfTool::_AddSettingsHandler() +{ + ImGuiSettingsHandler ini_handler; + ini_handler.TypeName = "TestEnginePerfTool"; + ini_handler.TypeHash = ImHashStr("TestEnginePerfTool"); + ini_handler.ClearAllFn = PerflogSettingsHandler_ClearAll; + ini_handler.ReadOpenFn = PerflogSettingsHandler_ReadOpen; + ini_handler.ReadLineFn = PerflogSettingsHandler_ReadLine; + ini_handler.ApplyAllFn = PerflogSettingsHandler_ApplyAll; + ini_handler.WriteAllFn = PerflogSettingsHandler_WriteAll; + ini_handler.UserData = this; + ImGui::AddSettingsHandler(&ini_handler); +} + +void ImGuiPerfTool::_UnpackSortedKey(ImU64 key, int* batch_index, int* entry_index, int* monotonic_index) +{ + IM_ASSERT(batch_index != NULL); + IM_ASSERT(entry_index != NULL); + const int num_visible_labels = _LabelsVisible.Size; + *batch_index = (int)((key >> 24) / num_visible_labels); + *entry_index = (int)((key >> 24) % num_visible_labels); + if (monotonic_index) + *monotonic_index = (int)(key & 0xFFFFFF); +} + +//------------------------------------------------------------------------- +// [SECTION] TESTS +//------------------------------------------------------------------------- + +static bool SetPerfToolWindowOpen(ImGuiTestContext* ctx, bool is_open) +{ + ctx->MenuClick("//Dear ImGui Test Engine/Tools"); + bool was_open = ctx->ItemIsChecked("//##Menu_00/Perf Tool"); + ctx->MenuAction(is_open ? ImGuiTestAction_Check : ImGuiTestAction_Uncheck, "//Dear ImGui Test Engine/Tools/Perf Tool"); + return was_open; +} + +void RegisterTests_TestEnginePerfTool(ImGuiTestEngine* e) +{ + ImGuiTest* t = NULL; + + // ## Flex perf tool code. + t = IM_REGISTER_TEST(e, "testengine", "testengine_cov_perftool"); + t->GuiFunc = [](ImGuiTestContext* ctx) + { + IM_UNUSED(ctx); + ImGui::Begin("Test Func", NULL, ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_AlwaysAutoResize); + int loop_count = 1000; + bool v1 = false, v2 = true; + for (int n = 0; n < loop_count / 2; n++) + { + ImGui::PushID(n); + ImGui::Checkbox("Hello, world", &v1); + ImGui::Checkbox("Hello, world", &v2); + ImGui::PopID(); + } + ImGui::End(); + }; + t->TestFunc = [](ImGuiTestContext* ctx) + { + ImGuiPerfTool* perftool = ImGuiTestEngine_GetPerfTool(ctx->Engine); + const char* temp_perf_csv = "output/misc_cov_perf_tool.csv"; + + Str16f min_date_bkp = perftool->_FilterDateFrom; + Str16f max_date_bkp = perftool->_FilterDateTo; + + // Execute few perf tests, serialize them to temporary csv file. + ctx->PerfIterations = 50; // Make faster + ctx->PerfCapture("perf", "misc_cov_perf_tool_1", temp_perf_csv); + ctx->PerfCapture("perf", "misc_cov_perf_tool_2", temp_perf_csv); + + // Load perf data from csv file and open perf tool. + perftool->Clear(); + perftool->LoadCSV(temp_perf_csv); + bool perf_was_open = SetPerfToolWindowOpen(ctx, true); + ctx->Yield(); + + ImGuiWindow* window = ctx->GetWindowByRef("Dear ImGui Perf Tool"); + IM_CHECK(window != NULL); + ImVec2 pos_bkp = window->Pos; + ImVec2 size_bkp = window->Size; + ctx->SetRef(window); + ctx->WindowMove("", ImVec2(50, 50)); + ctx->WindowResize("", ImVec2(1400, 900)); +#if IMGUI_TEST_ENGINE_ENABLE_IMPLOT + ImGuiWindow* plot_child = ctx->WindowInfo("plot")->Window; // "plot/PerfTool" prior to implot 2023/08/21 + IM_CHECK(plot_child != NULL); + + // Move legend to right side. + ctx->MouseMoveToPos(plot_child->Rect().GetCenter()); + ctx->MouseDoubleClick(ImGuiMouseButton_Left); // Auto-size plots while at it + ctx->MouseClick(ImGuiMouseButton_Right); + ctx->MenuClick("//$FOCUSED/Legend/NE"); + + // Click some stuff for more coverage. + ctx->MouseMoveToPos(plot_child->Rect().GetCenter()); + ctx->KeyPress(ImGuiMod_Shift); +#endif + ctx->ItemClick("##date-from", ImGuiMouseButton_Right); + ctx->ItemClick(ctx->GetID("//$FOCUSED/Set Min")); + ctx->ItemClick("##date-to", ImGuiMouseButton_Right); + ctx->ItemClick(ctx->GetID("//$FOCUSED/Set Max")); + ctx->ItemClick("###Filter builds"); + ctx->ItemClick("###Filter tests"); + ctx->ItemClick("Combine", 0, ImGuiTestOpFlags_MoveToEdgeL); // Toggle thrice to leave state unchanged + ctx->ItemClick("Combine", 0, ImGuiTestOpFlags_MoveToEdgeL); + ctx->ItemClick("Combine", 0, ImGuiTestOpFlags_MoveToEdgeL); + + // Restore original state. + perftool->Clear(); // Clear test data and load original data + ImFileDelete(temp_perf_csv); + perftool->LoadCSV(); + ctx->Yield(); +#if IMGUI_TEST_ENGINE_ENABLE_IMPLOT + ctx->MouseMoveToPos(plot_child->Rect().GetCenter()); + ctx->MouseDoubleClick(ImGuiMouseButton_Left); // Fit plot to original data +#endif + ImStrncpy(perftool->_FilterDateFrom, min_date_bkp.c_str(), IM_ARRAYSIZE(perftool->_FilterDateFrom)); + ImStrncpy(perftool->_FilterDateTo, max_date_bkp.c_str(), IM_ARRAYSIZE(perftool->_FilterDateTo)); + ImGui::SetWindowPos(window, pos_bkp); + ImGui::SetWindowSize(window, size_bkp); + SetPerfToolWindowOpen(ctx, perf_was_open); // Restore window visibility + }; + + // ## Capture perf tool graph. + t = IM_REGISTER_TEST(e, "capture", "capture_perf_report"); + t->TestFunc = [](ImGuiTestContext* ctx) + { + ImGuiPerfTool* perftool = ImGuiTestEngine_GetPerfTool(ctx->Engine); + const char* perf_report_image = NULL; + if (!ImFileExist(IMGUI_PERFLOG_DEFAULT_FILENAME)) + { + ctx->LogWarning("Perf tool has no data. Perf report generation was aborted."); + return; + } + + char min_date_bkp[sizeof(perftool->_FilterDateFrom)], max_date_bkp[sizeof(perftool->_FilterDateTo)]; + ImStrncpy(min_date_bkp, perftool->_FilterDateFrom, IM_ARRAYSIZE(min_date_bkp)); + ImStrncpy(max_date_bkp, perftool->_FilterDateTo, IM_ARRAYSIZE(max_date_bkp)); + bool perf_was_open = SetPerfToolWindowOpen(ctx, true); + ctx->Yield(); + + ImGuiWindow* window = ctx->GetWindowByRef("Dear ImGui Perf Tool"); + IM_CHECK_SILENT(window != NULL); + ImVec2 pos_bkp = window->Pos; + ImVec2 size_bkp = window->Size; + ctx->SetRef(window); + ctx->WindowMove("", ImVec2(50, 50)); + ctx->WindowResize("", ImVec2(1400, 900)); +#if IMGUI_TEST_ENGINE_ENABLE_IMPLOT + ctx->ItemDoubleClick("splitter"); // Hide info table + + ImGuiWindow* plot_child = ctx->WindowInfo("plot")->Window; // "plot/PerfTool" prior to implot 2023/08/21 + IM_CHECK(plot_child != NULL); + + // Move legend to right side. + ctx->MouseMoveToPos(plot_child->Rect().GetCenter()); + ctx->MouseDoubleClick(ImGuiMouseButton_Left); // Auto-size plots while at it + ctx->MouseClick(ImGuiMouseButton_Right); + ctx->MenuClick("//$FOCUSED/Legend/NE"); +#endif + // Click some stuff for more coverage. + ctx->ItemClick("##date-from", ImGuiMouseButton_Right); + ctx->ItemClick(ctx->GetID("//$FOCUSED/Set Min")); + ctx->ItemClick("##date-to", ImGuiMouseButton_Right); + ctx->ItemClick(ctx->GetID("//$FOCUSED/Set Max")); +#if IMGUI_TEST_ENGINE_ENABLE_IMPLOT + // Take a screenshot. + ImGuiCaptureArgs* args = ctx->CaptureArgs; + args->InCaptureRect = plot_child->Rect(); + ctx->CaptureAddWindow(window->Name); + ctx->CaptureScreenshot(ImGuiCaptureFlags_HideMouseCursor); + ctx->ItemDragWithDelta("splitter", ImVec2(0, -180)); // Show info table + perf_report_image = args->InOutputFile; +#endif + ImStrncpy(perftool->_FilterDateFrom, min_date_bkp, IM_ARRAYSIZE(min_date_bkp)); + ImStrncpy(perftool->_FilterDateTo, max_date_bkp, IM_ARRAYSIZE(max_date_bkp)); + ImGui::SetWindowPos(window, pos_bkp); + ImGui::SetWindowSize(window, size_bkp); + SetPerfToolWindowOpen(ctx, perf_was_open); // Restore window visibility + + const char* perf_report_output = getenv("CAPTURE_PERF_REPORT_OUTPUT"); + if (perf_report_output == NULL) + perf_report_output = PerfToolReportDefaultOutputPath; + perftool->SaveHtmlReport(perf_report_output, perf_report_image); + }; +} + +//------------------------------------------------------------------------- diff --git a/libs/imgui_test_engine/imgui_te_perftool.h b/libs/imgui_test_engine/imgui_te_perftool.h new file mode 100644 index 0000000..d75e667 --- /dev/null +++ b/libs/imgui_test_engine/imgui_te_perftool.h @@ -0,0 +1,131 @@ +// dear imgui test engine +// (performance tool) +// Browse and visualize samples recorded by ctx->PerfCapture() calls. +// User access via 'Test Engine UI -> Tools -> Perf Tool' + +#pragma once + +#include "imgui.h" + +// Forward Declaration +struct ImGuiPerfToolColumnInfo; +struct ImGuiTestEngine; +struct ImGuiCsvParser; + +// Configuration +#define IMGUI_PERFLOG_DEFAULT_FILENAME "output/imgui_perflog.csv" + +// [Internal] Perf log entry. Changes to this struct should be reflected in ImGuiTestContext::PerfCapture() and ImGuiTestEngine_Start(). +// This struct assumes strings stored here will be available until next ImGuiPerfTool::Clear() call. Fortunately we do not have to actively +// manage lifetime of these strings. New entries are created only in two cases: +// 1. ImGuiTestEngine_PerfToolAppendToCSV() call after perf test has run. This call receives ImGuiPerfToolEntry with const strings stored indefinitely by application. +// 2. As a consequence of ImGuiPerfTool::LoadCSV() call, we persist the ImGuiCSVParser instance, which keeps parsed CSV text, from which strings are referenced. +// As a result our solution also doesn't make many allocations. +struct IMGUI_API ImGuiPerfToolEntry +{ + ImU64 Timestamp = 0; // Title of a particular batch of perftool entries. + const char* Category = NULL; // Name of category perf test is in. + const char* TestName = NULL; // Name of perf test. + double DtDeltaMs = 0.0; // Result of perf test. + double DtDeltaMsMin = +FLT_MAX; // May be used by perftool. + double DtDeltaMsMax = -FLT_MAX; // May be used by perftool. + int NumSamples = 1; // Number aggregated samples. + int PerfStressAmount = 0; // + const char* GitBranchName = NULL; // Build information. + const char* BuildType = NULL; // + const char* Cpu = NULL; // + const char* OS = NULL; // + const char* Compiler = NULL; // + const char* Date = NULL; // Date of this entry or min date of combined entries. + //const char* DateMax = NULL; // Max date of combined entries, or NULL. + double VsBaseline = 0.0; // Percent difference vs baseline. + int LabelIndex = 0; // Index of TestName in ImGuiPerfTool::_LabelsVisible. + + ImGuiPerfToolEntry() { } + ImGuiPerfToolEntry(const ImGuiPerfToolEntry& rhs) { Set(rhs); } + ImGuiPerfToolEntry& operator=(const ImGuiPerfToolEntry& rhs){ Set(rhs); return *this; } + void Set(const ImGuiPerfToolEntry& rhs); +}; + +// [Internal] Perf log batch. +struct ImGuiPerfToolBatch +{ + ImU64 BatchID = 0; // Timestamp of the batch, or unique ID of the build in combined mode. + int NumSamples = 0; // A number of unique batches aggregated. + int BranchIndex = 0; // For per-branch color mapping. + ImVector Entries; // Aggregated perf test entries. Order follows ImGuiPerfTool::_LabelsVisible order. + ~ImGuiPerfToolBatch() { Entries.clear_destruct(); } // FIXME: Misleading: nothing to destruct in that struct? +}; + +enum ImGuiPerfToolDisplayType : int +{ + ImGuiPerfToolDisplayType_Simple, // Each run will be displayed individually. + ImGuiPerfToolDisplayType_PerBranchColors, // Use one bar color per branch. + ImGuiPerfToolDisplayType_CombineByBuildInfo, // Entries with same build information will be averaged. +}; + +// +struct IMGUI_API ImGuiPerfTool +{ + ImVector _SrcData; // Raw entries from CSV file (with string pointer into CSV data). + ImVector _Labels; + ImVector _LabelsVisible; // ImPlot requires a pointer of all labels beforehand. Always contains a dummy "" entry at the end! + ImVector _Batches; + ImGuiStorage _LabelBarCounts; // Number bars each label will render. + int _NumVisibleBuilds = 0; // Cached number of visible builds. + int _NumUniqueBuilds = 0; // Cached number of unique builds. + ImGuiPerfToolDisplayType _DisplayType = ImGuiPerfToolDisplayType_CombineByBuildInfo; + int _BaselineBatchIndex = 0; // Index of baseline build. + ImU64 _BaselineTimestamp = 0; + ImU64 _BaselineBuildId = 0; + char _Filter[128]; // Context menu filtering substring. + char _FilterDateFrom[11] = {}; + char _FilterDateTo[11] = {}; + float _InfoTableHeight = 180.0f; + int _AlignStress = 0; // Alignment values for build info components, so they look aligned in the legend. + int _AlignType = 0; + int _AlignOs = 0; + int _AlignCpu = 0; + int _AlignCompiler = 0; + int _AlignBranch = 0; + int _AlignSamples = 0; + bool _InfoTableSortDirty = false; + ImVector _InfoTableSort; // _InfoTableSort[_LabelsVisible.Size * _Batches.Size]. Contains sorted batch indices for each label. + const ImGuiTableSortSpecs* _InfoTableSortSpecs = NULL; // Current table sort specs. + ImGuiStorage _TempSet; // Used as a set + int _TableHoveredTest = -1; // Index within _VisibleLabelPointers array. + int _TableHoveredBatch = -1; + int _PlotHoverTest = -1; + int _PlotHoverBatch = -1; + bool _PlotHoverTestLabel = false; + bool _ReportGenerating = false; + ImGuiStorage _Visibility; + ImGuiCsvParser* _CsvParser = NULL; // We keep this around and point to its fields + + ImGuiPerfTool(); + ~ImGuiPerfTool(); + + void Clear(); + bool LoadCSV(const char* filename = NULL); + void AddEntry(ImGuiPerfToolEntry* entry); + + void ShowPerfToolWindow(ImGuiTestEngine* engine, bool* p_open); + void ViewOnly(const char* perf_name); + void ViewOnly(const char** perf_names); + ImGuiPerfToolEntry* GetEntryByBatchIdx(int idx, const char* perf_name = NULL); + bool SaveHtmlReport(const char* file_name, const char* image_file = NULL); + inline bool Empty() { return _SrcData.empty(); } + + void _Rebuild(); + bool _IsVisibleBuild(ImGuiPerfToolBatch* batch); + bool _IsVisibleBuild(ImGuiPerfToolEntry* batch); + bool _IsVisibleTest(const char* test_name); + void _CalculateLegendAlignment(); + void _ShowEntriesPlot(); + void _ShowEntriesTable(); + void _SetBaseline(int batch_index); + void _AddSettingsHandler(); + void _UnpackSortedKey(ImU64 key, int* batch_index, int* entry_index, int* monotonic_index = NULL); +}; + +IMGUI_API void ImGuiTestEngine_PerfToolAppendToCSV(ImGuiPerfTool* perf_log, ImGuiPerfToolEntry* entry, const char* filename = NULL); diff --git a/libs/imgui_test_engine/imgui_te_ui.cpp b/libs/imgui_test_engine/imgui_te_ui.cpp new file mode 100644 index 0000000..2d42bde --- /dev/null +++ b/libs/imgui_test_engine/imgui_te_ui.cpp @@ -0,0 +1,842 @@ +// dear imgui test engine +// (ui) +// If you run tests in an interactive or visible application, you may want to call ImGuiTestEngine_ShowTestEngineWindows() + +#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS) +#define _CRT_SECURE_NO_WARNINGS +#endif + +#define IMGUI_DEFINE_MATH_OPERATORS +#include "imgui_te_ui.h" +#include "imgui.h" +#include "imgui_internal.h" +#include "imgui_te_engine.h" +#include "imgui_te_context.h" +#include "imgui_te_internal.h" +#include "imgui_te_perftool.h" +#include "thirdparty/Str/Str.h" + +//------------------------------------------------------------------------- +// TEST ENGINE: USER INTERFACE +//------------------------------------------------------------------------- +// - DrawTestLog() [internal] +// - GetVerboseLevelName() [internal] +// - ShowTestGroup() [internal] +// - ImGuiTestEngine_ShowTestWindows() +//------------------------------------------------------------------------- + +// Look for " filename:number " in the string and add menu option to open source. +static bool ParseLineAndDrawFileOpenItemForSourceFile(ImGuiTestEngine* e, ImGuiTest* test, const char* line_start, const char* line_end) +{ + const char* separator = ImStrchrRange(line_start, line_end, ':'); + if (separator == NULL) + return false; + + const char* path_end = separator; + const char* path_begin = separator - 1; + while (path_begin > line_start&& path_begin[-1] != ' ') + path_begin--; + if (path_begin == path_end) + return false; + + int line_no = -1; + sscanf(separator + 1, "%d ", &line_no); + if (line_no == -1) + return false; + + Str256f buf("Open '%.*s' at line %d", (int)(path_end - path_begin), path_begin, line_no); + if (ImGui::MenuItem(buf.c_str())) + { + // FIXME-TESTS: Assume folder is same as folder of test->SourceFile! + const char* src_path = test->SourceFile; + const char* src_name = ImPathFindFilename(src_path); + buf.setf("%.*s%.*s", (int)(src_name - src_path), src_path, (int)(path_end - path_begin), path_begin); + + ImGuiTestEngineIO& e_io = ImGuiTestEngine_GetIO(e); + e_io.SrcFileOpenFunc(buf.c_str(), line_no, e_io.SrcFileOpenUserData); + } + + return true; +} + +// Look for "[ ,"]filename.png" in the string and add menu option to open image. +static bool ParseLineAndDrawFileOpenItemForImageFile(ImGuiTestEngine* e, ImGuiTest* test, const char* line_start, const char* line_end, const char* file_ext) +{ + IM_UNUSED(e); + IM_UNUSED(test); + + const char* extension = ImStristr(line_start, line_end, file_ext, NULL); + if (extension == NULL) + return false; + + const char* path_end = extension + strlen(file_ext); + const char* path_begin = extension - 1; + while (path_begin > line_start && path_begin[-1] != ' ' && path_begin[-1] != '\'' && path_begin[-1] != '\"') + path_begin--; + if (path_begin == path_end) + return false; + + Str256 buf; + + // Open file + buf.setf("Open file: %.*s", (int)(path_end - path_begin), path_begin); + if (ImGui::MenuItem(buf.c_str())) + { + buf.setf("%.*s", (int)(path_end - path_begin), path_begin); + ImPathFixSeparatorsForCurrentOS(buf.c_str()); + ImOsOpenInShell(buf.c_str()); + } + + // Open folder + const char* folder_begin = path_begin; + const char* folder_end = ImPathFindFilename(path_begin, path_end); + buf.setf("Open folder: %.*s", (int)(folder_end - folder_begin), path_begin); + if (ImGui::MenuItem(buf.c_str())) + { + buf.setf("%.*s", (int)(folder_end - folder_begin), folder_begin); + ImPathFixSeparatorsForCurrentOS(buf.c_str()); + ImOsOpenInShell(buf.c_str()); + } + + return true; +} + +static bool ParseLineAndDrawFileOpenItem(ImGuiTestEngine* e, ImGuiTest* test, const char* line_start, const char* line_end) +{ + if (ParseLineAndDrawFileOpenItemForSourceFile(e, test, line_start, line_end)) + return true; + if (ParseLineAndDrawFileOpenItemForImageFile(e, test, line_start, line_end, ".png")) + return true; + if (ParseLineAndDrawFileOpenItemForImageFile(e, test, line_start, line_end, ".gif")) + return true; + if (ParseLineAndDrawFileOpenItemForImageFile(e, test, line_start, line_end, ".mp4")) + return true; + return false; +} + +static float GetDpiScale() +{ +#ifdef IMGUI_HAS_VIEWPORT + return ImGui::GetWindowViewport()->DpiScale; +#else + return 1.0f; +#endif +} + +static void DrawTestLog(ImGuiTestEngine* e, ImGuiTest* test) +{ + const ImU32 error_col = IM_COL32(255, 150, 150, 255); + const ImU32 warning_col = IM_COL32(240, 240, 150, 255); + const ImU32 unimportant_col = IM_COL32(190, 190, 190, 255); + const float dpi_scale = GetDpiScale(); + + ImGuiTestOutput* test_output = &test->Output; + + ImGuiTestLog* log = &test_output->Log; + const char* text = log->Buffer.begin(); + const char* text_end = log->Buffer.end(); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(6.0f, 2.0f) * dpi_scale); + ImGuiListClipper clipper; + ImGuiTestVerboseLevel max_log_level = test_output->Status == ImGuiTestStatus_Error ? e->IO.ConfigVerboseLevelOnError : e->IO.ConfigVerboseLevel; + int line_count = log->ExtractLinesForVerboseLevels(ImGuiTestVerboseLevel_Silent, max_log_level, NULL); + int current_index_clipped = -1; + int current_index_abs = 0; + clipper.Begin(line_count); + while (clipper.Step()) + { + for (int line_no = clipper.DisplayStart; line_no < clipper.DisplayEnd; line_no++) + { + // Advance index_by_log_level to find log entry indicated by line_no. + ImGuiTestLogLineInfo* line_info = NULL; + while (current_index_clipped < line_no) + { + line_info = &log->LineInfo[current_index_abs]; + if (line_info->Level <= max_log_level) + current_index_clipped++; + current_index_abs++; + } + + const char* line_start = text + line_info->LineOffset; + const char* line_end = strchr(line_start, '\n'); + if (line_end == NULL) + line_end = text_end; + + switch (line_info->Level) + { + case ImGuiTestVerboseLevel_Error: + ImGui::PushStyleColor(ImGuiCol_Text, error_col); + break; + case ImGuiTestVerboseLevel_Warning: + ImGui::PushStyleColor(ImGuiCol_Text, warning_col); + break; + case ImGuiTestVerboseLevel_Debug: + case ImGuiTestVerboseLevel_Trace: + ImGui::PushStyleColor(ImGuiCol_Text, unimportant_col); + break; + default: + ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32_WHITE); + break; + } + ImGui::TextUnformatted(line_start, line_end); + ImGui::PopStyleColor(); + + ImGui::PushID(line_no); + if (ImGui::BeginPopupContextItem("Context", 1)) + { + if (!ParseLineAndDrawFileOpenItem(e, test, line_start, line_end)) + ImGui::MenuItem("No options", NULL, false, false); + ImGui::EndPopup(); + } + ImGui::PopID(); + } + } + ImGui::PopStyleVar(); +} + +#if IMGUI_VERSION_NUM <= 18963 +namespace ImGui +{ + void SetItemTooltip(const char* fmt, ...) + { + if (ImGui::IsItemHovered()) + { + va_list args; + va_start(args, fmt); + ImGui::SetTooltipV(fmt, args); + va_end(args); + } + } +} // namespace ImGui +#endif + +static bool ShowTestGroupFilterTest(ImGuiTestEngine* e, ImGuiTestGroup group, const char* filter, ImGuiTest* test) +{ + if (test->Group != group) + return false; + if (!ImGuiTestEngine_PassFilter(test, *filter ? filter : "all")) + return false; + if ((e->UiFilterByStatusMask & (1 << test->Output.Status)) == 0) + return false; + return true; +} + +static void GetFailingTestsAsString(ImGuiTestEngine* e, ImGuiTestGroup group, char separator, Str* out_string) +{ + IM_ASSERT(out_string != NULL); + bool first = true; + for (int i = 0; i < e->TestsAll.Size; i++) + { + ImGuiTest* failing_test = e->TestsAll[i]; + Str* filter = (group == ImGuiTestGroup_Tests) ? e->UiFilterTests : e->UiFilterPerfs; + if (failing_test->Group != group) + continue; + if (failing_test->Output.Status != ImGuiTestStatus_Error) + continue; + if (!ImGuiTestEngine_PassFilter(failing_test, filter->empty() ? "all" : filter->c_str())) + continue; + if (!first) + out_string->append(separator); + out_string->append(failing_test->Name); + first = false; + } +} + +static void TestStatusButton(const char* id, const ImVec4& color, bool running, int display_counter) +{ + ImGuiContext& g = *GImGui; + ImGui::PushItemFlag(ImGuiItemFlags_NoTabStop, true); + ImGui::ColorButton(id, color, ImGuiColorEditFlags_NoTooltip); + ImGui::PopItemFlag(); + if (running) + { + //ImRect r = g.LastItemData.Rect; + ImVec2 center = g.LastItemData.Rect.GetCenter(); + float radius = ImFloor(ImMin(g.LastItemData.Rect.GetWidth(), g.LastItemData.Rect.GetHeight()) * 0.40f); + float t = (float)(ImGui::GetTime() * 20.0f); + ImVec2 off(ImCos(t) * radius, ImSin(t) * radius); + ImGui::GetWindowDrawList()->AddLine(center - off, center + off, ImGui::GetColorU32(ImGuiCol_Text), 1.5f); + //ImGui::RenderText(r.Min + style.FramePadding + ImVec2(0, 0), &"|\0/\0-\0\\"[(((ImGui::GetFrameCount() / 5) & 3) << 1)], NULL); + } + else if (display_counter >= 0) + { + ImVec2 center = g.LastItemData.Rect.GetCenter(); + Str30f buf("%d", display_counter); + ImGui::GetWindowDrawList()->AddText(center - ImGui::CalcTextSize(buf.c_str()) * 0.5f, ImGui::GetColorU32(ImGuiCol_Text), buf.c_str()); + } +} + +static void ShowTestGroup(ImGuiTestEngine* e, ImGuiTestGroup group, Str* filter) +{ + ImGuiStyle& style = ImGui::GetStyle(); + ImGuiIO& io = ImGui::GetIO(); + const float dpi_scale = GetDpiScale(); + + // Colored Status button: will be displayed later below + // - Save position of test run status button and make space for it. + const ImVec2 status_button_pos = ImGui::GetCursorPos(); + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + ImGui::GetFrameHeight() + style.ItemInnerSpacing.x); + + //ImGui::Text("TESTS (%d)", engine->TestsAll.Size); +#if IMGUI_VERSION_NUM >= 18837 + bool run = ImGui::Button("Run") || ImGui::Shortcut(ImGuiMod_Ctrl | ImGuiKey_R); +#else + bool = ImGui::Button("Run"); +#endif +#if IMGUI_VERSION_NUM > 18963 + ImGui::SetItemTooltip("Ctrl+R"); +#endif + if (run) + { + for (int n = 0; n < e->TestsAll.Size; n++) + { + ImGuiTest* test = e->TestsAll[n]; + if (!ShowTestGroupFilterTest(e, group, filter->c_str(), test)) + continue; + ImGuiTestEngine_QueueTest(e, test, ImGuiTestRunFlags_None); + } + } + ImGui::SameLine(); + + { + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 6.0f); + const char* filter_by_status_desc = ""; + if (e->UiFilterByStatusMask == ~0u) + filter_by_status_desc = "All"; + else if (e->UiFilterByStatusMask == ~(1u << ImGuiTestStatus_Success)) + filter_by_status_desc = "Not OK"; + else if (e->UiFilterByStatusMask == (1u << ImGuiTestStatus_Error)) + filter_by_status_desc = "Errors"; + if (ImGui::BeginCombo("##filterbystatus", filter_by_status_desc)) + { + if (ImGui::Selectable("All", e->UiFilterByStatusMask == ~0u)) + e->UiFilterByStatusMask = (ImU32)~0u; + if (ImGui::Selectable("Not OK", e->UiFilterByStatusMask == ~(1u << ImGuiTestStatus_Success))) + e->UiFilterByStatusMask = (ImU32)~(1u << ImGuiTestStatus_Success); + if (ImGui::Selectable("Errors", e->UiFilterByStatusMask == (1u << ImGuiTestStatus_Error))) + e->UiFilterByStatusMask = (ImU32)(1u << ImGuiTestStatus_Error); + ImGui::EndCombo(); + } + } + + ImGui::SameLine(); + const char* perflog_label = "Perf Tool"; + float filter_width = ImGui::GetWindowContentRegionMax().x - ImGui::GetCursorPos().x; + float perf_stress_factor_width = (30 * dpi_scale); + if (group == ImGuiTestGroup_Perfs) + { + filter_width -= style.ItemSpacing.x + perf_stress_factor_width; + filter_width -= style.ItemSpacing.x + style.FramePadding.x * 2 + ImGui::CalcTextSize(perflog_label).x; + } + filter_width -= ImGui::CalcTextSize("(?)").x + style.ItemSpacing.x; + ImGui::SetNextItemWidth(ImMax(20.0f, filter_width)); + ImGui::InputText("##filter", filter); + ImGui::SameLine(); + ImGui::TextDisabled("(?)"); + ImGui::SetItemTooltip("Query is composed of one or more comma-separated filter terms with optional modifiers.\n" + "Available modifiers:\n" + "- '-' prefix excludes tests matched by the term.\n" + "- '^' prefix anchors term matching to the start of the string.\n" + "- '$' suffix anchors term matching to the end of the string."); + if (group == ImGuiTestGroup_Perfs) + { + ImGui::SameLine(); + ImGui::SetNextItemWidth(perf_stress_factor_width); + ImGui::DragInt("##PerfStress", &e->IO.PerfStressAmount, 0.1f, 1, 20, "x%d"); + ImGui::SetItemTooltip("Increase workload of performance tests (higher means longer run)."); // FIXME: Move? + ImGui::SameLine(); + if (ImGui::Button(perflog_label)) + { + e->UiPerfToolOpen = true; + ImGui::FocusWindow(ImGui::FindWindowByName("Dear ImGui Perf Tool")); + } + } + + int tests_completed = 0; + int tests_succeeded = 0; + int tests_failed = 0; + if (ImGui::BeginTable("Tests", 3, ImGuiTableFlags_ScrollY | ImGuiTableFlags_Resizable | ImGuiTableFlags_NoBordersInBody | ImGuiTableFlags_SizingFixedFit)) + { + ImGui::TableSetupScrollFreeze(0, 1); + ImGui::TableSetupColumn("Status"); + ImGui::TableSetupColumn("Category"); + ImGui::TableSetupColumn("Test", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableHeadersRow(); + + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(6, 4) * dpi_scale); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(4, 0) * dpi_scale); + //ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(100, 10) * dpi_scale); + for (int test_n = 0; test_n < e->TestsAll.Size; test_n++) + { + ImGuiTest* test = e->TestsAll[test_n]; + if (!ShowTestGroupFilterTest(e, group, filter->c_str(), test)) + continue; + + ImGuiTestOutput* test_output = &test->Output; + ImGuiTestContext* test_context = (e->TestContext && e->TestContext->Test == test) ? e->TestContext : NULL; // Running context, if any + + ImGui::TableNextRow(); + ImGui::PushID(test_n); + + // Colors match general test status colors defined below. + ImVec4 status_color; + switch (test_output->Status) + { + case ImGuiTestStatus_Error: + status_color = ImVec4(0.9f, 0.1f, 0.1f, 1.0f); + tests_completed++; + tests_failed++; + break; + case ImGuiTestStatus_Success: + status_color = ImVec4(0.1f, 0.9f, 0.1f, 1.0f); + tests_completed++; + tests_succeeded++; + break; + case ImGuiTestStatus_Queued: + case ImGuiTestStatus_Running: + case ImGuiTestStatus_Suspended: + if (test_context && (test_context->RunFlags & ImGuiTestRunFlags_GuiFuncOnly)) + status_color = ImVec4(0.8f, 0.0f, 0.8f, 1.0f); + else + status_color = ImVec4(0.8f, 0.4f, 0.1f, 1.0f); + break; + default: + status_color = ImVec4(0.4f, 0.4f, 0.4f, 1.0f); + break; + } + + ImGui::TableNextColumn(); + TestStatusButton("status", status_color, test_output->Status == ImGuiTestStatus_Running || test_output->Status == ImGuiTestStatus_Suspended, -1); + ImGui::SameLine(); + + bool queue_test = false; + bool queue_gui_func_toggle = false; + bool select_test = false; + + if (test_output->Status == ImGuiTestStatus_Suspended) + { + // Resume IM_SUSPEND_TESTFUNC + // FIXME: Terrible user experience to have this here. + if (ImGui::Button("Con###Run")) + test_output->Status = ImGuiTestStatus_Running; + ImGui::SetItemTooltip("CTRL+Space to continue."); + if (ImGui::IsKeyPressed(ImGuiKey_Space) && io.KeyCtrl) + test_output->Status = ImGuiTestStatus_Running; + } + else + { + if (ImGui::Button("Run###Run")) + queue_test = select_test = true; + } + + ImGui::TableNextColumn(); + if (ImGui::Selectable(test->Category, test == e->UiSelectedTest, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_SelectOnNav)) + select_test = true; + + // Double-click to run test, CTRL+Double-click to run GUI function + const bool is_running_gui_func = (test_context && (test_context->RunFlags & ImGuiTestRunFlags_GuiFuncOnly)); + const bool has_gui_func = (test->GuiFunc != NULL); + if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0)) + { + if (ImGui::GetIO().KeyCtrl) + queue_gui_func_toggle = true; + else + queue_test = true; + } + + /*if (ImGui::IsItemHovered() && test->TestLog.size() > 0) + { + ImGui::BeginTooltip(); + DrawTestLog(engine, test, false); + ImGui::EndTooltip(); + }*/ + + if (e->UiSelectAndScrollToTest == test) + ImGui::SetScrollHereY(); + + bool view_source = false; + if (ImGui::BeginPopupContextItem()) + { + select_test = true; + + if (ImGui::MenuItem("Run test")) + queue_test = true; + if (ImGui::MenuItem("Run GUI func", "Ctrl+DblClick", is_running_gui_func, has_gui_func)) + queue_gui_func_toggle = true; + + ImGui::Separator(); + + const bool open_source_available = (test->SourceFile != NULL) && (e->IO.SrcFileOpenFunc != NULL); + + Str128 buf; + if (test->SourceFile != NULL) // This is normally set by IM_REGISTER_TEST() but custom registration may omit it. + buf.setf("Open source (%s:%d)", ImPathFindFilename(test->SourceFile), test->SourceLine); + else + buf.set("Open source"); + if (ImGui::MenuItem(buf.c_str(), NULL, false, open_source_available)) + e->IO.SrcFileOpenFunc(test->SourceFile, test->SourceLine, e->IO.SrcFileOpenUserData); + if (ImGui::MenuItem("View source...", NULL, false, test->SourceFile != NULL)) + view_source = true; + + if (group == ImGuiTestGroup_Perfs && ImGui::MenuItem("View perflog")) + { + e->PerfTool->ViewOnly(test->Name); + e->UiPerfToolOpen = true; + } + + ImGui::Separator(); + if (ImGui::MenuItem("Copy name", NULL, false)) + ImGui::SetClipboardText(test->Name); + + if (test_output->Status == ImGuiTestStatus_Error) + if (ImGui::MenuItem("Copy names of all failing tests")) + { + Str256 failing_tests; + GetFailingTestsAsString(e, group, ',', &failing_tests); + ImGui::SetClipboardText(failing_tests.c_str()); + } + + ImGuiTestLog* test_log = &test_output->Log; + if (ImGui::BeginMenu("Copy log", !test_log->IsEmpty())) + { + for (int level_n = ImGuiTestVerboseLevel_Error; level_n < ImGuiTestVerboseLevel_COUNT; level_n++) + { + ImGuiTestVerboseLevel level = (ImGuiTestVerboseLevel)level_n; + int count = test_log->ExtractLinesForVerboseLevels((ImGuiTestVerboseLevel)0, level, NULL); + if (ImGui::MenuItem(Str64f("%s (%d lines)", ImGuiTestEngine_GetVerboseLevelName(level), count).c_str(), NULL, false, count > 0)) + { + ImGuiTextBuffer buffer; + test_log->ExtractLinesForVerboseLevels((ImGuiTestVerboseLevel)0, level, &buffer); + ImGui::SetClipboardText(buffer.c_str()); + } + } + ImGui::EndMenu(); + } + + if (ImGui::MenuItem("Clear log", NULL, false, !test_log->IsEmpty())) + test_log->Clear(); + + ImGui::EndPopup(); + } + + // Process source popup + static ImGuiTextBuffer source_blurb; + static int goto_line = -1; + if (view_source) + { + source_blurb.clear(); + size_t file_size = 0; + char* file_data = (char*)ImFileLoadToMemory(test->SourceFile, "rb", &file_size); + if (file_data) + source_blurb.append(file_data, file_data + file_size); + else + source_blurb.append(""); + goto_line = (test->SourceLine + test->SourceLineEnd) / 2; + ImGui::OpenPopup("Source"); + } + if (ImGui::BeginPopup("Source")) + { + // FIXME: Local vs screen pos too messy :( + const ImVec2 start_pos = ImGui::GetCursorStartPos(); + const float line_height = ImGui::GetTextLineHeight(); + if (goto_line != -1) + ImGui::SetScrollFromPosY(start_pos.y + (goto_line - 1) * line_height, 0.5f); + goto_line = -1; + + ImRect r(0.0f, test->SourceLine * line_height, ImGui::GetWindowWidth(), (test->SourceLine + 1) * line_height); // SourceLineEnd is too flaky + ImGui::GetWindowDrawList()->AddRectFilled(ImGui::GetWindowPos() + start_pos + r.Min, ImGui::GetWindowPos() + start_pos + r.Max, IM_COL32(80, 80, 150, 150)); + + ImGui::TextUnformatted(source_blurb.c_str(), source_blurb.end()); + ImGui::EndPopup(); + } + + ImGui::TableNextColumn(); + ImGui::TextUnformatted(test->Name); + + // Process selection + if (select_test) + e->UiSelectedTest = test; + + // Process queuing + if (queue_gui_func_toggle && is_running_gui_func) + ImGuiTestEngine_AbortCurrentTest(e); + else if (queue_gui_func_toggle && !e->IO.IsRunningTests) + ImGuiTestEngine_QueueTest(e, test, ImGuiTestRunFlags_RunFromGui | ImGuiTestRunFlags_GuiFuncOnly); + if (queue_test && !e->IO.IsRunningTests) + ImGuiTestEngine_QueueTest(e, test, ImGuiTestRunFlags_RunFromGui); + + ImGui::PopID(); + } + ImGui::Spacing(); + ImGui::PopStyleVar(2); + ImGui::EndTable(); + } + + // Display test status recap (colors match per-test run button colors defined above) + { + ImVec4 status_color; + if (tests_failed > 0) + status_color = ImVec4(0.9f, 0.1f, 0.1f, 1.0f); // Red + else if (e->IO.IsRunningTests) + status_color = ImVec4(0.8f, 0.4f, 0.1f, 1.0f); + else if (tests_succeeded > 0 && tests_completed == tests_succeeded) + status_color = ImVec4(0.1f, 0.9f, 0.1f, 1.0f); + else + status_color = ImVec4(0.4f, 0.4f, 0.4f, 1.0f); + //ImVec2 cursor_pos_bkp = ImGui::GetCursorPos(); + ImGui::SetCursorPos(status_button_pos); + TestStatusButton("status", status_color, false, tests_failed > 0 ? tests_failed : -1);// e->IO.IsRunningTests); + ImGui::SetItemTooltip("Filtered: %d\n- OK: %d\n- Errors: %d", tests_completed, tests_succeeded, tests_failed); + //ImGui::SetCursorPos(cursor_pos_bkp); // Restore cursor position for rendering further widgets + } +} + +static void ImGuiTestEngine_ShowLogAndTools(ImGuiTestEngine* engine) +{ + ImGuiContext& g = *GImGui; + const float dpi_scale = GetDpiScale(); + + if (!ImGui::BeginTabBar("##tools")) + return; + + if (ImGui::BeginTabItem("LOG")) + { + ImGuiTest* selected_test = engine->UiSelectedTest; + + if (selected_test != NULL) + ImGui::Text("Log for '%s' '%s'", selected_test->Category, selected_test->Name); + else + ImGui::Text("N/A"); + if (ImGui::SmallButton("Clear")) + if (selected_test) + selected_test->Output.Log.Clear(); + ImGui::SameLine(); + if (ImGui::SmallButton("Copy to clipboard")) + if (engine->UiSelectedTest) + ImGui::SetClipboardText(selected_test->Output.Log.Buffer.c_str()); + ImGui::Separator(); + + ImGui::BeginChild("Log"); + if (engine->UiSelectedTest) + { + DrawTestLog(engine, engine->UiSelectedTest); + if (ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) + ImGui::SetScrollHereY(); + } + ImGui::EndChild(); + ImGui::EndTabItem(); + } + + // Options + if (ImGui::BeginTabItem("OPTIONS")) + { + ImGuiIO& io = ImGui::GetIO(); + ImGui::Text("%.3f ms/frame (%.1f FPS)", 1000.0f / io.Framerate, io.Framerate); + ImGui::Text("TestEngine: HookItems: %d, HookPushId: %d, InfoTasks: %d", g.TestEngineHookItems, g.DebugHookIdInfo != 0, engine->InfoTasks.Size); + ImGui::Separator(); + + if (ImGui::Button("Reboot UI context")) + engine->ToolDebugRebootUiContext = true; + + const ImGuiInputTextCallback filter_callback = [](ImGuiInputTextCallbackData* data) { return (data->EventChar == ',' || data->EventChar == ';') ? 1 : 0; }; + ImGui::InputText("Branch/Annotation", engine->IO.GitBranchName, IM_ARRAYSIZE(engine->IO.GitBranchName), ImGuiInputTextFlags_CallbackCharFilter, filter_callback, NULL); + ImGui::SetItemTooltip("This will be stored in the CSV file for performance tools."); + + ImGui::Separator(); + + if (ImGui::TreeNode("Screen/video capture")) + { + ImGui::Checkbox("Capture when requested by API", &engine->IO.ConfigCaptureEnabled); + ImGui::SetItemTooltip("Enable or disable screen capture API completely."); + ImGui::Checkbox("Capture screen on error", &engine->IO.ConfigCaptureOnError); + ImGui::SetItemTooltip("Capture a screenshot on test failure."); + + // Fields modified by in this call will be synced to engine->CaptureContext. + engine->CaptureTool._ShowEncoderConfigFields(&engine->CaptureContext); + + ImGui::TreePop(); + } + + if (ImGui::TreeNode("Performances")) + { + ImGui::Checkbox("Slow down whole app", &engine->ToolSlowDown); + ImGui::SameLine(); ImGui::SetNextItemWidth(70 * dpi_scale); + ImGui::SliderInt("##ms", &engine->ToolSlowDownMs, 0, 400, "%d ms"); + + // FIXME-TESTS: Need to be visualizing the samples/spikes. + double dt_1 = 1.0 / ImGui::GetIO().Framerate; + double fps_now = 1.0 / dt_1; + double dt_100 = engine->PerfDeltaTime100.GetAverage(); + double dt_500 = engine->PerfDeltaTime500.GetAverage(); + + //if (engine->PerfRefDeltaTime <= 0.0 && engine->PerfRefDeltaTime.IsFull()) + // engine->PerfRefDeltaTime = dt_2000; + + ImGui::Checkbox("Unthrolled", &engine->IO.ConfigNoThrottle); + ImGui::SameLine(); + if (ImGui::Button("Pick ref dt")) + engine->PerfRefDeltaTime = dt_500; + + double dt_ref = engine->PerfRefDeltaTime; + ImGui::Text("[ref dt] %6.3f ms", engine->PerfRefDeltaTime * 1000); + ImGui::Text("[last 001] %6.3f ms (%.1f FPS) ++ %6.3f ms", dt_1 * 1000.0, 1.0 / dt_1, (dt_1 - dt_ref) * 1000); + ImGui::Text("[last 100] %6.3f ms (%.1f FPS) ++ %6.3f ms ~ converging in %.1f secs", dt_100 * 1000.0, 1.0 / dt_100, (dt_1 - dt_ref) * 1000, 100.0 / fps_now); + ImGui::Text("[last 500] %6.3f ms (%.1f FPS) ++ %6.3f ms ~ converging in %.1f secs", dt_500 * 1000.0, 1.0 / dt_500, (dt_1 - dt_ref) * 1000, 500.0 / fps_now); + + //ImGui::PlotLines("Last 100", &engine->PerfDeltaTime100.Samples.Data, engine->PerfDeltaTime100.Samples.Size, engine->PerfDeltaTime100.Idx, NULL, 0.0f, dt_1000 * 1.10f, ImVec2(0.0f, ImGui::GetFontSize())); + ImVec2 plot_size(0.0f, ImGui::GetFrameHeight() * 3); + ImMovingAverage* ma = &engine->PerfDeltaTime500; + ImGui::PlotLines("Last 500", + [](void* data, int n) { ImMovingAverage* ma = (ImMovingAverage*)data; return (float)(ma->Samples[n] * 1000); }, + ma, ma->Samples.Size, 0 * ma->Idx, NULL, 0.0f, (float)(ImMax(dt_100, dt_500) * 1000.0 * 1.2f), plot_size); + + ImGui::TreePop(); + } + + if (ImGui::TreeNode("Dear ImGui Configuration Flags")) + { + ImGui::CheckboxFlags("io.ConfigFlags: NavEnableKeyboard", &io.ConfigFlags, ImGuiConfigFlags_NavEnableKeyboard); + ImGui::CheckboxFlags("io.ConfigFlags: NavEnableGamepad", &io.ConfigFlags, ImGuiConfigFlags_NavEnableGamepad); +#ifdef IMGUI_HAS_DOCK + ImGui::Checkbox("io.ConfigDockingAlwaysTabBar", &io.ConfigDockingAlwaysTabBar); +#endif + ImGui::TreePop(); + } + + ImGui::EndTabItem(); + } + ImGui::EndTabBar(); +} + +static void ImGuiTestEngine_ShowTestTool(ImGuiTestEngine* engine, bool* p_open) +{ + const float dpi_scale = GetDpiScale(); + + ImGui::SetNextWindowSize(ImVec2(ImGui::GetFontSize() * 50, ImGui::GetFontSize() * 40), ImGuiCond_FirstUseEver); + if (!ImGui::Begin("Dear ImGui Test Engine", p_open, ImGuiWindowFlags_MenuBar)) + { + ImGui::End(); + return; + } + + if (ImGui::BeginMenuBar()) + { + if (ImGui::BeginMenu("Tools")) + { + ImGuiContext& g = *GImGui; + ImGui::MenuItem("Metrics/Debugger", "", &engine->UiMetricsOpen); + ImGui::MenuItem("Debug Log", "", &engine->UiDebugLogOpen); + ImGui::MenuItem("Stack Tool", "", &engine->UiStackToolOpen); + ImGui::MenuItem("Item Picker", "", &g.DebugItemPickerActive); + ImGui::Separator(); + ImGui::MenuItem("Capture Tool", "", &engine->UiCaptureToolOpen); + ImGui::MenuItem("Perf Tool", "", &engine->UiPerfToolOpen); + ImGui::EndMenu(); + } + ImGui::EndMenuBar(); + } + + ImGui::SetNextItemWidth(90 * dpi_scale); + if (ImGui::BeginCombo("##RunSpeed", ImGuiTestEngine_GetRunSpeedName(engine->IO.ConfigRunSpeed), ImGuiComboFlags_None)) + { + for (ImGuiTestRunSpeed level = (ImGuiTestRunSpeed)0; level < ImGuiTestRunSpeed_COUNT; level = (ImGuiTestRunSpeed)(level + 1)) + if (ImGui::Selectable(ImGuiTestEngine_GetRunSpeedName(level), engine->IO.ConfigRunSpeed == level)) + engine->IO.ConfigRunSpeed = level; + ImGui::EndCombo(); + } + ImGui::SetItemTooltip( + "Running speed\n" + "- Fast: Run tests as fast as possible (no delay/vsync, teleport mouse, etc.).\n" + "- Normal: Run tests at human watchable speed (for debugging).\n" + "- Cinematic: Run tests with pauses between actions (for e.g. tutorials)." + ); + ImGui::SameLine(); + //ImGui::Checkbox("Fast", &engine->IO.ConfigRunFast); + //ImGui::SameLine(); + ImGui::Checkbox("Stop", &engine->IO.ConfigStopOnError); + ImGui::SetItemTooltip("Stop running tests when hitting an error."); + ImGui::SameLine(); + ImGui::Checkbox("DbgBrk", &engine->IO.ConfigBreakOnError); + ImGui::SetItemTooltip("Break in debugger when hitting an error."); + ImGui::SameLine(); + ImGui::Checkbox("KeepGUI", &engine->IO.ConfigKeepGuiFunc); + ImGui::SetItemTooltip("Keep GUI function running after a test fails, or when a single queued test is finished.\nHold ESC to abort a running GUI function."); + ImGui::SameLine(); + ImGui::Checkbox("Refocus", &engine->IO.ConfigRestoreFocusAfterTests); + ImGui::SetItemTooltip("Restore focus back after running tests."); + ImGui::SameLine(); + ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical); + ImGui::SameLine(); + ImGui::SetNextItemWidth(70 * dpi_scale); + if (ImGui::BeginCombo("##Verbose", ImGuiTestEngine_GetVerboseLevelName(engine->IO.ConfigVerboseLevel), ImGuiComboFlags_None)) + { + for (ImGuiTestVerboseLevel level = (ImGuiTestVerboseLevel)0; level < ImGuiTestVerboseLevel_COUNT; level = (ImGuiTestVerboseLevel)(level + 1)) + if (ImGui::Selectable(ImGuiTestEngine_GetVerboseLevelName(level), engine->IO.ConfigVerboseLevel == level)) + engine->IO.ConfigVerboseLevel = engine->IO.ConfigVerboseLevelOnError = level; + ImGui::EndCombo(); + } + ImGui::SetItemTooltip("Verbose level."); + //ImGui::PopStyleVar(); + ImGui::Separator(); + + // SPLITTER + // FIXME-OPT: A better splitter API supporting arbitrary number of splits would be useful. + float list_height = 0.0f; + float& log_height = engine->UiLogHeight; + ImGui::Splitter("splitter", &list_height, &log_height, ImGuiAxis_Y, +1); + + // TESTS + ImGui::BeginChild("List", ImVec2(0, list_height), false, ImGuiWindowFlags_NoScrollbar); + if (ImGui::BeginTabBar("##Tests", ImGuiTabBarFlags_NoTooltip)) // Add _NoPushId flag in TabBar? + { + if (ImGui::BeginTabItem("TESTS", NULL, ImGuiTabItemFlags_NoPushId)) + { + ShowTestGroup(engine, ImGuiTestGroup_Tests, engine->UiFilterTests); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("PERFS", NULL, ImGuiTabItemFlags_NoPushId)) + { + ShowTestGroup(engine, ImGuiTestGroup_Perfs, engine->UiFilterPerfs); + ImGui::EndTabItem(); + } + ImGui::EndTabBar(); + } + ImGui::EndChild(); + engine->UiSelectAndScrollToTest = NULL; + + // LOG & TOOLS + ImGui::BeginChild("Log", ImVec2(0, log_height)); + ImGuiTestEngine_ShowLogAndTools(engine); + ImGui::EndChild(); + + ImGui::End(); +} + +void ImGuiTestEngine_ShowTestEngineWindows(ImGuiTestEngine* e, bool* p_open) +{ + // Test Tool + ImGuiTestEngine_ShowTestTool(e, p_open); + + // Stack Tool +#if IMGUI_VERSION_NUM < 18993 + if (e->UiStackToolOpen) + ImGui::ShowStackToolWindow(&e->UiStackToolOpen); +#else + if (e->UiStackToolOpen) + ImGui::ShowIDStackToolWindow(&e->UiStackToolOpen); +#endif + + // Capture Tool + if (e->UiCaptureToolOpen) + e->CaptureTool.ShowCaptureToolWindow(&e->CaptureContext, &e->UiCaptureToolOpen); + + // Performance tool + if (e->UiPerfToolOpen) + e->PerfTool->ShowPerfToolWindow(e, &e->UiPerfToolOpen);; + + // Show Dear ImGui windows + // (we cannot show demo window here because it could lead to duplicate display, which demo windows isn't guarded for) + if (e->UiMetricsOpen) + ImGui::ShowMetricsWindow(&e->UiMetricsOpen); + if (e->UiDebugLogOpen) + ImGui::ShowDebugLogWindow(&e->UiDebugLogOpen); +} diff --git a/libs/imgui_test_engine/imgui_te_ui.h b/libs/imgui_test_engine/imgui_te_ui.h new file mode 100644 index 0000000..4527fb7 --- /dev/null +++ b/libs/imgui_test_engine/imgui_te_ui.h @@ -0,0 +1,21 @@ +// dear imgui test engine +// (ui) +// If you run tests in an interactive or visible application, you may want to call ImGuiTestEngine_ShowTestEngineWindows() + +// Provide access to: +// - "Dear ImGui Test Engine" main interface +// - "Dear ImGui Capture Tool" +// - "Dear ImGui Perf Tool" +// - other core debug functions: Metrics, Debug Log + +#pragma once + +#ifndef IMGUI_VERSION +#include "imgui.h" // IMGUI_API +#endif + +// Forward declarations +struct ImGuiTestEngine; + +// Functions +IMGUI_API void ImGuiTestEngine_ShowTestEngineWindows(ImGuiTestEngine* engine, bool* p_open); diff --git a/libs/imgui_test_engine/imgui_te_utils.cpp b/libs/imgui_test_engine/imgui_te_utils.cpp new file mode 100644 index 0000000..ad473a7 --- /dev/null +++ b/libs/imgui_test_engine/imgui_te_utils.cpp @@ -0,0 +1,1306 @@ +// dear imgui test engine +// (helpers/utilities. do NOT use this as a general purpose library) + +#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS) +#define _CRT_SECURE_NO_WARNINGS +#endif + +#include "imgui_te_utils.h" +#include "imgui.h" +#include "imgui_internal.h" +#define STR_IMPLEMENTATION +#include "thirdparty/Str/Str.h" + +#if defined(_WIN32) +#if !defined(_WINDOWS_) +#define WIN32_LEAN_AND_MEAN +#include +#endif +#include // ShellExecuteA() +#include +#else +#include +#include +#endif +#ifndef _MSC_VER +#include +#include // stat() +#endif + +#if defined(__linux) || defined(__linux__) || defined(__MACH__) || defined(__MSL__) || defined(__MINGW32__) +#include // pthread_setname_np() +#endif +#include // high_resolution_clock::now() +#include // this_thread::sleep_for() + +//----------------------------------------------------------------------------- +// Hashing Helpers +//----------------------------------------------------------------------------- +// - ImHashDecoratedPathParseLiteral() [internal] +// - ImHashDecoratedPath() +// - ImFindNextDecoratedPartInPath() +//----------------------------------------------------------------------------- + +// - Parse literals encoded as "$$xxxx/" and incorporate into our hash based on type. +// - $$ not passed by caller. +static ImGuiID ImHashDecoratedPathParseLiteral(ImGuiID crc, const unsigned char* str, const unsigned char* str_end, const unsigned char** out_str_remaining) +{ + // Parse type (default to int) + ImGuiDataType type = ImGuiDataType_S32; + if (*str == '(') + { + // "$$(int)????" where ???? is s32 or u32 + if (str + 5 < str_end && memcmp(str, "(int)", 5) == 0) + { + type = ImGuiDataType_S32; + str += 5; + } + // "$$(ptr)0x????" where ???? is ptr size + else if (str + 7 < str_end && memcmp(str, "(ptr)0x", 7) == 0) + { + type = ImGuiDataType_Pointer; + str += 7; + } + } + + // Parse value + switch (type) + { + case ImGuiDataType_S32: + { + // e.g. "$$(int)123" for s32/u32/ImGuiID, same as PushID(int) + int v = 0; + { + int negative = 0; + if (str < str_end && *str == '-') { negative = 1; str++; } + if (str < str_end && *str == '+') { str++; } + for (char c = *str; str < str_end; c = *(++str)) + { + if (c >= '0' && c <= '9') { v = (v * 10) + (c - '0'); } + else break; + } + if (negative) + v = -v; + } + crc = ~ImHashData(&v, sizeof(int), ~crc); + break; + } + case ImGuiDataType_Pointer: + { + // e.g. "$$(ptr)0x1234FFFF" for pointers, same as PushID(void*) + intptr_t v = 0; + { + for (char c = *str; str < str_end; c = *(++str)) + { + if (c >= '0' && c <= '9') { v = (v << 4) + (c - '0'); } + else if (c >= 'A' && c <= 'F') { v = (v << 4) + 10 + (c - 'A'); } + else if (c >= 'a' && c <= 'f') { v = (v << 4) + 10 + (c - 'a'); } + else break; + } + } + crc = ~ImHashData(&v, sizeof(void*), ~crc); + break; + } + } + + // "$$xxxx" must always be either end of string, either leading to a next section e.g. "$$xxxx/" + IM_ASSERT(str == str_end || *str == '/'); + + *out_str_remaining = str; + return crc; +} + +// Hash "hello/world" as if it was "helloworld" +// To hash a forward slash we need to use "hello\\/world" +// IM_ASSERT(ImHashDecoratedPath("Hello/world") == ImHashStr("Helloworld", 0)); +// IM_ASSERT(ImHashDecoratedPath("Hello\\/world") == ImHashStr("Hello/world", 0)); +// IM_ASSERT(ImHashDecoratedPath("$$1") == (n = 1, ImHashData(&n, sizeof(int)))); +// Adapted from ImHash(). Not particularly fast! +ImGuiID ImHashDecoratedPath(const char* str, const char* str_end, ImGuiID seed) +{ + static ImU32 crc32_lut[256] = { 0 }; + if (!crc32_lut[1]) + { + const ImU32 polynomial = 0xEDB88320; + for (ImU32 i = 0; i < 256; i++) + { + ImU32 crc = i; + for (ImU32 j = 0; j < 8; j++) + crc = (crc >> 1) ^ (ImU32(-int(crc & 1)) & polynomial); + crc32_lut[i] = crc; + } + } + + // Prefixing the string with / ignore the seed + if (str != str_end && str[0] == '/') + seed = 0; + + seed = ~seed; + ImU32 crc = seed; + + // Focus for non-zero terminated string for consistency + if (str_end == NULL) + str_end = str + strlen(str); + + bool inhibit_one = false; + bool new_section = true; + const unsigned char* current = (const unsigned char*)str; + while (current < (const unsigned char*)str_end) + { + const unsigned char c = *current++; + + // Backslash to inhibit special behavior of following character + if (c == '\\' && !inhibit_one) + { + inhibit_one = true; + continue; + } + + // Forward slashes are ignored unless prefixed with a backward slash + if (c == '/' && !inhibit_one) + { + inhibit_one = false; + new_section = true; + seed = crc; // Set seed to the new path + continue; + } + + // $$ at the beginning of a section to encode literals. + // - Currently: "$$????" = hash of 1 as int + // - May add pointers and other types. + if (c == '$' && current[0] == '$' && !inhibit_one && new_section) + { + crc = ImHashDecoratedPathParseLiteral(crc, current + 1, (const unsigned char*)str_end, ¤t); + continue; + } + + // Reset the hash when encountering ### + if (c == '#' && current[0] == '#' && current[1] == '#') + crc = seed; + + // Hash byte + crc = (crc >> 8) ^ crc32_lut[(crc & 0xFF) ^ c]; + + inhibit_one = new_section = false; + } + return ~crc; +} + +// Returns a next element of decorated hash path. +// "//hello/world/child" --> "world/child" +// "world/child" --> "child" +// This is a helper for code needing to do some parsing of individual nodes in a path. +// Note: we need the (unsigned char*) stuff in order to keep code similar to ImHashDecoratedPath(). They are not really necessary in this function tho. +const char* ImFindNextDecoratedPartInPath(const char* str, const char* str_end) +{ + const unsigned char* current = (const unsigned char*)str; + while (*current == '/') + current++; + + bool inhibit_one = false; + while (true) + { + if (str_end != NULL && current == (const unsigned char*)str_end) + break; + + const unsigned char c = *current++; + if (c == 0) + break; + if (c == '\\' && !inhibit_one) + { + inhibit_one = true; + continue; + } + + // Forward slashes are ignored unless prefixed with a backward slash + if (c == '/' && !inhibit_one) + return (const char*)current; + + inhibit_one = false; + } + return NULL; +} + +//----------------------------------------------------------------------------- +// File/Directory Helpers +//----------------------------------------------------------------------------- +// - ImFileExist() +// - ImFileCreateDirectoryChain() +// - ImFileFindInParents() +// - ImFileLoadSourceBlurb() +//----------------------------------------------------------------------------- + +#if _WIN32 +static const char IM_DIR_SEPARATOR = '\\'; + +static void ImUtf8ToWideChar(const char* multi_byte, ImVector* buf) +{ + const int wsize = ::MultiByteToWideChar(CP_UTF8, 0, multi_byte, -1, NULL, 0); + buf->resize(wsize); + ::MultiByteToWideChar(CP_UTF8, 0, multi_byte, -1, (wchar_t*)buf->Data, wsize); +} +#else +static const char IM_DIR_SEPARATOR = '/'; +#endif + +bool ImFileExist(const char* filename) +{ + struct stat dir_stat; + int ret = stat(filename, &dir_stat); + return (ret == 0); +} + +bool ImFileDelete(const char* filename) +{ +#if _WIN32 + ImVector buf; + ImUtf8ToWideChar(filename, &buf); + return ::DeleteFileW(&buf[0]) == TRUE; +#else + unlink(filename); +#endif + return false; +} + +// Create directories for specified path. Slashes will be replaced with platform directory separators. +// e.g. ImFileCreateDirectoryChain("aaaa/bbbb/cccc.png") +// will try to create "aaaa/" then "aaaa/bbbb/". +bool ImFileCreateDirectoryChain(const char* path, const char* path_end) +{ + IM_ASSERT(path != NULL); + IM_ASSERT(path[0] != 0); + + if (path_end == NULL) + path_end = path + strlen(path); + + // Copy in a local, zero-terminated buffer + size_t path_len = (size_t)(path_end - path); + char* path_local = (char*)IM_ALLOC(path_len + 1); + memcpy(path_local, path, path_len); + path_local[path_len] = 0; + +#if defined(_WIN32) + ImVector buf; +#endif + // Modification of passed file_name allows us to avoid extra temporary memory allocation. + // strtok() pokes \0 into places where slashes are, we create a directory using directory_name and restore slash. + for (char* token = strtok(path_local, "\\/"); token != NULL; token = strtok(NULL, "\\/")) + { + // strtok() replaces slashes with NULLs. Overwrite removed slashes here with the type of slashes the OS needs (win32 functions need backslashes). + if (token != path_local) + *(token - 1) = IM_DIR_SEPARATOR; + +#if defined(_WIN32) + // Use ::CreateDirectoryW() because ::CreateDirectoryA() treat filenames in the local code-page instead of UTF-8. + const int filename_wsize = ImTextCountCharsFromUtf8(path_local, NULL) + 1; + buf.resize(filename_wsize); + ImTextStrFromUtf8(&buf[0], filename_wsize, path_local, NULL); + if (!::CreateDirectoryW((wchar_t*)&buf[0], NULL) && GetLastError() != ERROR_ALREADY_EXISTS) +#else + if (mkdir(path_local, S_IRWXU) != 0 && errno != EEXIST) +#endif + { + IM_FREE(path_local); + return false; + } + } + IM_FREE(path_local); + return true; +} + +bool ImFileFindInParents(const char* sub_path, int max_parent_count, Str* output) +{ + IM_ASSERT(sub_path != NULL); + IM_ASSERT(output != NULL); + for (int parent_level = 0; parent_level < max_parent_count; parent_level++) + { + output->clear(); + for (int j = 0; j < parent_level; j++) + output->append("../"); + output->append(sub_path); + if (ImFileExist(output->c_str())) + return true; + } + output->clear(); + return false; +} + +bool ImFileLoadSourceBlurb(const char* file_name, int line_no_start, int line_no_end, ImGuiTextBuffer* out_buf) +{ + size_t file_size = 0; + char* file_begin = (char*)ImFileLoadToMemory(file_name, "rb", &file_size, 1); + if (file_begin == NULL) + return false; + + char* file_end = file_begin + file_size; + int line_no = 0; + const char* test_src_begin = NULL; + const char* test_src_end = NULL; + for (const char* p = file_begin; p < file_end; ) + { + line_no++; + const char* line_begin = p; + const char* line_end = ImStrchrRange(line_begin + 1, file_end, '\n'); + if (line_end == NULL) + line_end = file_end; + if (line_no >= line_no_start && line_no <= line_no_end) + { + if (test_src_begin == NULL) + test_src_begin = line_begin; + test_src_end = ImMax(test_src_end, line_end); + } + p = line_end + 1; + } + + if (test_src_begin != NULL) + out_buf->append(test_src_begin, test_src_end); + else + out_buf->clear(); + + ImGui::MemFree(file_begin); + return true; +} + +//----------------------------------------------------------------------------- +// Path Helpers +//----------------------------------------------------------------------------- +// - ImPathFindFilename() +// - ImPathFindFileExt() +// - ImPathFixSeparatorsForCurrentOS() +//----------------------------------------------------------------------------- + +const char* ImPathFindFilename(const char* path, const char* path_end) +{ + IM_ASSERT(path != NULL); + if (!path_end) + path_end = path + strlen(path); + const char* p = path_end; + while (p > path) + { + if (p[-1] == '/' || p[-1] == '\\') + break; + p--; + } + return p; +} + +// "folder/filename" -> return pointer to "" (end of string) +// "folder/filename.png" -> return pointer to ".png" +// "folder/filename.png.bak" -> return pointer to ".png.bak" +const char* ImPathFindExtension(const char* path, const char* path_end) +{ + if (!path_end) + path_end = path + strlen(path); + const char* filename = ImPathFindFilename(path, path_end); + const char* p = filename; + while (p < path_end) + { + if (p[0] == '.') + break; + p++; + } + return p; +} + +void ImPathFixSeparatorsForCurrentOS(char* buf) +{ +#ifdef _WIN32 + for (char* p = buf; *p != 0; p++) + if (*p == '/') + *p = '\\'; +#else + for (char* p = buf; *p != 0; p++) + if (*p == '\\') + *p = '/'; +#endif +} + +//----------------------------------------------------------------------------- +// String Helpers +//----------------------------------------------------------------------------- + +static const char* ImStrStr(const char* haystack, size_t hlen, const char* needle, int nlen) +{ + const char* end = haystack + hlen; + const char* p = haystack; + while ((p = (const char*)memchr(p, *needle, end - p)) != NULL) + { + if (end - p < nlen) + return NULL; + if (memcmp(p, needle, nlen) == 0) + return p; + p++; + } + return NULL; +} + +void ImStrReplace(Str* s, const char* find, const char* repl) +{ + IM_ASSERT(find != NULL && *find); + IM_ASSERT(repl != NULL); + int find_len = (int)strlen(find); + int repl_len = (int)strlen(repl); + int repl_diff = repl_len - find_len; + + // Estimate required length of new buffer if string size increases. + int need_capacity = s->capacity(); + int num_matches = INT_MAX; + if (repl_diff > 0) + { + num_matches = 0; + need_capacity = s->length() + 1; + for (char* p = s->c_str(), *end = s->c_str() + s->length(); p != NULL && p < end;) + { + p = (char*)ImStrStr(p, end - p, find, find_len); + if (p) + { + need_capacity += repl_diff; + p += find_len; + num_matches++; + } + } + } + + if (num_matches == 0) + return; + + const char* not_owned_data = s->owned() ? NULL : s->c_str(); + if (!s->owned() || need_capacity > s->capacity()) + s->reserve(need_capacity); + if (not_owned_data != NULL) + s->set(not_owned_data); + + // Replace data. + for (char* p = s->c_str(), *end = s->c_str() + s->length(); p != NULL && p < end && num_matches--;) + { + p = (char*)ImStrStr(p, end - p, find, find_len); + if (p) + { + memmove(p + repl_len, p + find_len, end - p - find_len + 1); + memcpy(p, repl, repl_len); + p += repl_len; + end += repl_diff; + } + } +} + +const char* ImStrchrRangeWithEscaping(const char* str, const char* str_end, char find_c) +{ + while (str < str_end) + { + const char c = *str; + if (c == '\\') + { + str += 2; + continue; + } + if (c == find_c) + return str; + str++; + } + return NULL; +} + +// Suboptimal but ok for the data size we are dealing with (see commit on 2022/08/22 for a faster and more complicated version) +void ImStrXmlEscape(Str* s) +{ + ImStrReplace(s, "&", "&"); + ImStrReplace(s, "<", "<"); + ImStrReplace(s, ">", ">"); + ImStrReplace(s, "\"", """); + ImStrReplace(s, "\'", "'"); +} + +// Based on code from https://github.com/EddieBreeg/C_b64 by @EddieBreeg. +int ImStrBase64Encode(const unsigned char* src, char* dst, int length) +{ + static const char* b64Table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; + int i, j, k, l, encoded_len = 0; + + while (length > 0) + { + switch (length) + { + case 1: + i = src[0] >> 2; + j = (src[0] & 3) << 4; + k = 64; + l = 64; + break; + case 2: + i = src[0] >> 2; + j = ((src[0] & 3) << 4) | (src[1] >> 4); + k = (src[1] & 15) << 2; + l = 64; + break; + default: + i = src[0] >> 2; + j = ((src[0] & 3) << 4) | (src[1] >> 4); + k = ((src[1] & 0xf) << 2) | (src[2] >> 6 & 3); + l = src[2] & 0x3f; + break; + } + dst[0] = b64Table[i]; + dst[1] = b64Table[j]; + dst[2] = b64Table[k]; + dst[3] = b64Table[l]; + src += 3; + dst += 4; + length -= 3; + encoded_len += 4; + } + return encoded_len; +} + +//----------------------------------------------------------------------------- +// Parsing Helpers +//----------------------------------------------------------------------------- +// - ImParseSplitCommandLine() +// - ImParseFindIniSection() +//----------------------------------------------------------------------------- + +void ImParseExtractArgcArgvFromCommandLine(int* out_argc, char const*** out_argv, const char* cmd_line) +{ + size_t cmd_line_len = strlen(cmd_line); + + int n = 1; + { + const char* p = cmd_line; + while (*p != 0) + { + const char* arg = p; + while (*arg == ' ') + arg++; + const char* arg_end = strchr(arg, ' '); + if (arg_end == NULL) + p = arg_end = cmd_line + cmd_line_len; + else + p = arg_end + 1; + n++; + } + } + + int argc = n; + char const** argv = (char const**)malloc(sizeof(char*) * ((size_t)argc + 1) + (cmd_line_len + 1)); + IM_ASSERT(argv != NULL); + char* cmd_line_dup = (char*)argv + sizeof(char*) * ((size_t)argc + 1); + strcpy(cmd_line_dup, cmd_line); + + { + argv[0] = "main.exe"; + argv[argc] = NULL; + + char* p = cmd_line_dup; + for (n = 1; n < argc; n++) + { + char* arg = p; + char* arg_end = strchr(arg, ' '); + if (arg_end == NULL) + p = arg_end = cmd_line_dup + cmd_line_len; + else + p = arg_end + 1; + argv[n] = arg; + arg_end[0] = 0; + } + } + + *out_argc = argc; + *out_argv = argv; +} + +bool ImParseFindIniSection(const char* ini_config, const char* header, ImVector* result) +{ + IM_ASSERT(ini_config != NULL); + IM_ASSERT(header != NULL); + IM_ASSERT(result != NULL); + + size_t ini_len = strlen(ini_config); + size_t header_len = strlen(header); + + IM_ASSERT(header_len > 0); + + if (ini_len == 0) + return false; + + const char* section_start = strstr(ini_config, header); + if (section_start == NULL) + return false; + + const char* section_end = strstr(section_start + header_len, "\n["); + if (section_end == NULL) + section_end = section_start + ini_len; + + // "\n[" matches next header start on all platforms, but it cuts new line marker in half on windows. + if (*(section_end - 1) == '\r') + --section_end; + + size_t section_len = (size_t)(section_end - section_start); + result->resize((int)section_len + 1); + ImStrncpy(result->Data, section_start, section_len); + + return true; +} + +//----------------------------------------------------------------------------- +// Time Helpers +//----------------------------------------------------------------------------- +// - ImTimeGetInMicroseconds() +// - ImTimestampToISO8601() +//----------------------------------------------------------------------------- + +uint64_t ImTimeGetInMicroseconds() +{ + // Trying std::chrono out of unfettered optimism that it may actually work.. + using namespace std; + chrono::microseconds ms = chrono::duration_cast(chrono::high_resolution_clock::now().time_since_epoch()); + return (uint64_t)ms.count(); +} + +void ImTimestampToISO8601(uint64_t timestamp, Str* out_date) +{ + time_t unix_time = (time_t)(timestamp / 1000000); // Convert to seconds. + tm* time = gmtime(&unix_time); + const char* time_format = "%Y-%m-%dT%H:%M:%S"; + size_t size_req = strftime(out_date->c_str(), out_date->capacity(), time_format, time); + if (size_req >= (size_t)out_date->capacity()) + { + out_date->reserve((int)size_req); + strftime(out_date->c_str(), out_date->capacity(), time_format, time); + } +} + +//----------------------------------------------------------------------------- +// Threading Helpers +//----------------------------------------------------------------------------- +// - ImThreadSleepInMilliseconds() +// - ImThreadSetCurrentThreadDescription() +//----------------------------------------------------------------------------- + +void ImThreadSleepInMilliseconds(int ms) +{ + using namespace std; + this_thread::sleep_for(chrono::milliseconds(ms)); +} + +#if defined(_MSC_VER) +// Helper function for setting thread name on Win32 +// This is a separate function because __try cannot coexist with local objects that need destructors called on stack unwind +static void ImThreadSetCurrentThreadDescriptionWin32OldStyle(const char* description) +{ + // Old-style Win32 thread name setting method + // See https://docs.microsoft.com/en-us/visualstudio/debugger/how-to-set-a-thread-name-in-native-code + const DWORD MS_VC_EXCEPTION = 0x406D1388; +#pragma pack(push,8) + typedef struct tagTHREADNAME_INFO + { + DWORD dwType; // Must be 0x1000. + LPCSTR szName; // Pointer to name (in user addr space). + DWORD dwThreadID; // Thread ID (-1=caller thread). + DWORD dwFlags; // Reserved for future use, must be zero. + } THREADNAME_INFO; +#pragma pack(pop) + + THREADNAME_INFO info; + info.dwType = 0x1000; + info.szName = description; + info.dwThreadID = (DWORD)-1; + info.dwFlags = 0; +#pragma warning(push) +#pragma warning(disable: 6320 6322) + __try + { + RaiseException(MS_VC_EXCEPTION, 0, sizeof(info) / sizeof(ULONG_PTR), (ULONG_PTR*)&info); + } + __except (EXCEPTION_EXECUTE_HANDLER) + { + } +#pragma warning(pop) +} +#endif // #ifdef _WIN32 + +// Set the description (name) of the current thread for debugging purposes +void ImThreadSetCurrentThreadDescription(const char* description) +{ +#if defined(_MSC_VER) // Windows + Visual Studio + // New-style thread name setting + // Only supported from Win 10 version 1607/Server 2016 onwards, hence the need for dynamic linking + + typedef HRESULT(WINAPI* SetThreadDescriptionFunc)(HANDLE hThread, PCWSTR lpThreadDescription); + + SetThreadDescriptionFunc set_thread_description = (SetThreadDescriptionFunc)::GetProcAddress(GetModuleHandleA("Kernel32.dll"), "SetThreadDescription"); + if (set_thread_description) + { + ImVector buf; + const int description_wsize = ImTextCountCharsFromUtf8(description, NULL) + 1; + buf.resize(description_wsize); + ImTextStrFromUtf8(&buf[0], description_wsize, description, NULL); + set_thread_description(::GetCurrentThread(), (wchar_t*)&buf[0]); + } + + // Also do the old-style method too even if the new-style one worked, as the two work in slightly different sets of circumstances + ImThreadSetCurrentThreadDescriptionWin32OldStyle(description); +#elif defined(__linux) || defined(__linux__) || defined(__MINGW32__) // Linux or MingW + pthread_setname_np(pthread_self(), description); +#elif defined(__MACH__) || defined(__MSL__) // OSX + pthread_setname_np(description); +#else + // This is a nice-to-have rather than critical functionality, so fail silently if we don't support this platform +#endif +} + +//----------------------------------------------------------------------------- +// Build info helpers +//----------------------------------------------------------------------------- +// - ImBuildGetCompilationInfo() +// - ImBuildGetGitBranchName() +//----------------------------------------------------------------------------- + +// Turn __DATE__ "Jan 10 2019" into "2019-01-10" +static void ImBuildParseDateFromCompilerIntoYMD(const char* in_date, char* out_buf, size_t out_buf_size) +{ + char month_str[5]; + int year, month, day; + sscanf(in_date, "%3s %d %d", month_str, &day, &year); + const char month_names[] = "JanFebMarAprMayJunJulAugSepOctNovDec"; + const char* p = strstr(month_names, month_str); + month = p ? (int)(1 + (p - month_names) / 3) : 0; + ImFormatString(out_buf, out_buf_size, "%04d-%02d-%02d", year, month, day); +} + +// Those strings are used to output easily identifiable markers in compare logs. We only need to support what we use for testing. +// We can probably grab info in eaplatform.h/eacompiler.h etc. in EASTL +const ImBuildInfo* ImBuildGetCompilationInfo() +{ + static ImBuildInfo build_info; + + if (build_info.Type[0] == '\0') + { + // Build Type +#if defined(DEBUG) || defined(_DEBUG) + build_info.Type = "Debug"; +#else + build_info.Type = "Release"; +#endif + + // CPU +#if defined(_M_X86) || defined(_M_IX86) || defined(__i386) || defined(__i386__) || defined(_X86_) || defined(_M_AMD64) || defined(_AMD64_) || defined(__x86_64__) + build_info.Cpu = (sizeof(size_t) == 4) ? "X86" : "X64"; +#elif defined(__aarch64__) + build_info.Cpu = "ARM64"; +#else +#error + build_info.Cpu = (sizeof(size_t) == 4) ? "Unknown32" : "Unknown64"; +#endif + + // Platform/OS +#if defined(_WIN32) + build_info.OS = "Windows"; +#elif defined(__linux) || defined(__linux__) + build_info.OS = "Linux"; +#elif defined(__MACH__) || defined(__MSL__) + build_info.OS = "OSX"; +#elif defined(__ORBIS__) + build_info.OS = "PS4"; +#elif defined(_DURANGO) + build_info.OS = "XboxOne"; +#else + build_info.OS = "Unknown"; +#endif + + // Compiler +#if defined(_MSC_VER) + build_info.Compiler = "MSVC"; +#elif defined(__clang__) + build_info.Compiler = "Clang"; +#elif defined(__GNUC__) + build_info.Compiler = "GCC"; +#else + build_info.Compiler = "Unknown"; +#endif + + // Date/Time + ImBuildParseDateFromCompilerIntoYMD(__DATE__, build_info.Date, IM_ARRAYSIZE(build_info.Date)); + build_info.Time = __TIME__; + } + + return &build_info; +} + +bool ImBuildFindGitBranchName(const char* git_repo_path, Str* branch_name) +{ + IM_ASSERT(git_repo_path != NULL); + IM_ASSERT(branch_name != NULL); + Str256f head_path("%s/.git/HEAD", git_repo_path); + size_t head_size = 0; + bool result = false; + if (char* git_head = (char*)ImFileLoadToMemory(head_path.c_str(), "r", &head_size, 1)) + { + const char prefix[] = "ref: refs/heads/"; // Branch name is prefixed with this in HEAD file. + const int prefix_length = IM_ARRAYSIZE(prefix) - 1; + strtok(git_head, "\r\n"); // Trim new line + if (head_size > prefix_length && strncmp(git_head, prefix, prefix_length) == 0) + { + // "ref: refs/heads/master" -> "master" + branch_name->set(git_head + prefix_length); + } + else + { + // Should be git hash, keep first 8 characters (see #42) + branch_name->setf("%.8s", git_head); + } + result = true; + IM_FREE(git_head); + } + return result; +} + +//----------------------------------------------------------------------------- +// Operating System Helpers +//----------------------------------------------------------------------------- +// - ImOsCreateProcess() +// - ImOsPOpen() +// - ImOsPClose() +// - ImOsOpenInShell() +// - ImOsConsoleSetTextColor() +// - ImOsIsDebuggerPresent() +//----------------------------------------------------------------------------- + +bool ImOsCreateProcess(const char* cmd_line) +{ +#ifdef _WIN32 + STARTUPINFOA siStartInfo; + PROCESS_INFORMATION piProcInfo; + ZeroMemory(&siStartInfo, sizeof(STARTUPINFOA)); + char* cmd_line_copy = ImStrdup(cmd_line); + BOOL ret = ::CreateProcessA(NULL, cmd_line_copy, NULL, NULL, FALSE, 0, NULL, NULL, &siStartInfo, &piProcInfo); + free(cmd_line_copy); + ::CloseHandle(siStartInfo.hStdInput); + ::CloseHandle(siStartInfo.hStdOutput); + ::CloseHandle(siStartInfo.hStdError); + ::CloseHandle(piProcInfo.hProcess); + ::CloseHandle(piProcInfo.hThread); + return ret != 0; +#else + IM_UNUSED(cmd_line); + return false; +#endif +} + +FILE* ImOsPOpen(const char* cmd_line, const char* mode) +{ + IM_ASSERT(cmd_line != NULL && *cmd_line); + IM_ASSERT(mode != NULL && *mode); +#if _WIN32 + ImVector w_cmd_line; + ImVector w_mode; + ImUtf8ToWideChar(cmd_line, &w_cmd_line); + ImUtf8ToWideChar(mode, &w_mode); + w_mode.resize(w_mode.Size + 1); + wcscat(w_mode.Data, L"b"); // Windows requires 'b' mode while unixes do not support it and default to binary. + return _wpopen(w_cmd_line.Data, w_mode.Data); +#else + return popen(cmd_line, mode); +#endif +} + +void ImOsPClose(FILE* fp) +{ + IM_ASSERT(fp != NULL); +#if _WIN32 + _pclose(fp); +#else + pclose(fp); +#endif +} + +void ImOsOpenInShell(const char* path) +{ + Str256 command(path); +#ifdef _WIN32 + ImPathFixSeparatorsForCurrentOS(command.c_str()); + ::ShellExecuteA(NULL, "open", command.c_str(), NULL, NULL, SW_SHOWDEFAULT); +#else +#if __APPLE__ + const char* open_executable = "open"; +#else + const char* open_executable = "xdg-open"; +#endif + command.setf("%s \"%s\"", open_executable, path); + ImPathFixSeparatorsForCurrentOS(command.c_str()); + system(command.c_str()); +#endif +} + +void ImOsConsoleSetTextColor(ImOsConsoleStream stream, ImOsConsoleTextColor color) +{ +#ifdef _WIN32 + HANDLE hConsole = 0; + switch (stream) + { + case ImOsConsoleStream_StandardOutput: hConsole = ::GetStdHandle(STD_OUTPUT_HANDLE); break; + case ImOsConsoleStream_StandardError: hConsole = ::GetStdHandle(STD_ERROR_HANDLE); break; + } + WORD wAttributes = FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE; + switch (color) + { + case ImOsConsoleTextColor_Black: wAttributes = 0x00; break; + case ImOsConsoleTextColor_White: wAttributes = FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE; break; + case ImOsConsoleTextColor_BrightWhite: wAttributes = FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE | FOREGROUND_INTENSITY; break; + case ImOsConsoleTextColor_BrightRed: wAttributes = FOREGROUND_RED | FOREGROUND_INTENSITY; break; + case ImOsConsoleTextColor_BrightGreen: wAttributes = FOREGROUND_GREEN | FOREGROUND_INTENSITY; break; + case ImOsConsoleTextColor_BrightBlue: wAttributes = FOREGROUND_BLUE | FOREGROUND_INTENSITY; break; + case ImOsConsoleTextColor_BrightYellow: wAttributes = FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY; break; + default: IM_ASSERT(0); + } + ::SetConsoleTextAttribute(hConsole, wAttributes); +#elif defined(__linux) || defined(__linux__) || defined(__MACH__) || defined(__MSL__) + // FIXME: check system capabilities (with environment variable TERM) + FILE* handle = 0; + switch (stream) + { + case ImOsConsoleStream_StandardOutput: handle = stdout; break; + case ImOsConsoleStream_StandardError: handle = stderr; break; + } + + const char* modifier = ""; + switch (color) + { + case ImOsConsoleTextColor_Black: modifier = "\033[30m"; break; + case ImOsConsoleTextColor_White: modifier = "\033[0m"; break; + case ImOsConsoleTextColor_BrightWhite: modifier = "\033[1;37m"; break; + case ImOsConsoleTextColor_BrightRed: modifier = "\033[1;31m"; break; + case ImOsConsoleTextColor_BrightGreen: modifier = "\033[1;32m"; break; + case ImOsConsoleTextColor_BrightBlue: modifier = "\033[1;34m"; break; + case ImOsConsoleTextColor_BrightYellow: modifier = "\033[1;33m"; break; + default: IM_ASSERT(0); + } + + fprintf(handle, "%s", modifier); +#endif +} + +bool ImOsIsDebuggerPresent() +{ +#ifdef _WIN32 + return ::IsDebuggerPresent() != 0; +#elif defined(__linux__) + int debugger_pid = 0; + char buf[2048]; // TracerPid is located near the start of the file. If end of the buffer gets cut off thats fine. + FILE* fp = fopen("/proc/self/status", "rb"); // Can not use ImFileLoadToMemory because size detection of /proc/self/status would fail. + if (fp == NULL) + return false; + fread(buf, 1, IM_ARRAYSIZE(buf), fp); + fclose(fp); + buf[IM_ARRAYSIZE(buf) - 1] = 0; + if (char* tracer_pid = strstr(buf, "TracerPid:")) + { + tracer_pid += 10; // Skip label + while (isspace(*tracer_pid)) + tracer_pid++; + debugger_pid = atoi(tracer_pid); + } + return debugger_pid != 0; +#else + // FIXME + return false; +#endif +} + +void ImOsOutputDebugString(const char* message) +{ +#ifdef _WIN32 + OutputDebugStringA(message); +#else + IM_UNUSED(message); +#endif +} + +//----------------------------------------------------------------------------- +// Str.h + InputText bindings +//----------------------------------------------------------------------------- + +struct InputTextCallbackStr_UserData +{ + Str* StrObj; + ImGuiInputTextCallback ChainCallback; + void* ChainCallbackUserData; +}; + +static int InputTextCallbackStr(ImGuiInputTextCallbackData* data) +{ + InputTextCallbackStr_UserData* user_data = (InputTextCallbackStr_UserData*)data->UserData; + if (data->EventFlag == ImGuiInputTextFlags_CallbackResize) + { + // Resize string callback + // If for some reason we refuse the new length (BufTextLen) and/or capacity (BufSize) we need to set them back to what we want. + Str* str = user_data->StrObj; + IM_ASSERT(data->Buf == str->c_str()); + str->reserve(data->BufTextLen + 1); + data->Buf = (char*)str->c_str(); + } + else if (user_data->ChainCallback) + { + // Forward to user callback, if any + data->UserData = user_data->ChainCallbackUserData; + return user_data->ChainCallback(data); + } + return 0; +} + +// Draw an extra colored frame over the previous item +// Similar to DebugDrawItemRect() but use Max(1.0f, FrameBorderSize) +void ImGui::ItemErrorFrame(ImU32 col) +{ + ImGuiContext& g = *GetCurrentContext(); + ImDrawList* drawlist = GetWindowDrawList(); + ImGuiStyle& style = GetStyle(); + // FIXME: GetItemRectMin() / GetItemRectMax() will include label. NavRect is not probably defined :( + drawlist->AddRect(g.LastItemData.NavRect.Min, g.LastItemData.NavRect.Max, GetColorU32(col), style.FrameRounding, ImDrawFlags_None, ImMax(1.0f, style.FrameBorderSize)); +} + +bool ImGui::InputText(const char* label, Str* str, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data) +{ + IM_ASSERT((flags & ImGuiInputTextFlags_CallbackResize) == 0); + flags |= ImGuiInputTextFlags_CallbackResize; + + InputTextCallbackStr_UserData cb_user_data; + cb_user_data.StrObj = str; + cb_user_data.ChainCallback = callback; + cb_user_data.ChainCallbackUserData = user_data; + return InputText(label, (char*)str->c_str(), (size_t)str->capacity() + 1, flags, InputTextCallbackStr, &cb_user_data); +} + +bool ImGui::InputTextWithHint(const char* label, const char* hint, Str* str, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data) +{ + IM_ASSERT((flags & ImGuiInputTextFlags_CallbackResize) == 0); + flags |= ImGuiInputTextFlags_CallbackResize; + + InputTextCallbackStr_UserData cb_user_data; + cb_user_data.StrObj = str; + cb_user_data.ChainCallback = callback; + cb_user_data.ChainCallbackUserData = user_data; + return InputTextWithHint(label, hint, (char*)str->c_str(), (size_t)str->capacity() + 1, flags, InputTextCallbackStr, &cb_user_data); +} + +bool ImGui::InputTextMultiline(const char* label, Str* str, const ImVec2& size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data) +{ + IM_ASSERT((flags & ImGuiInputTextFlags_CallbackResize) == 0); + flags |= ImGuiInputTextFlags_CallbackResize; + + InputTextCallbackStr_UserData cb_user_data; + cb_user_data.StrObj = str; + cb_user_data.ChainCallback = callback; + cb_user_data.ChainCallbackUserData = user_data; + return InputTextMultiline(label, (char*)str->c_str(), (size_t)str->capacity() + 1, size, flags, InputTextCallbackStr, &cb_user_data); +} + +// anchor parameter indicates which split would retain it's constant size. +// anchor = 0 - both splits resize when parent container size changes. Both value_1 and value_2 should be persistent. +// anchor = -1 - top/left split would have a constant size. bottom/right split would resize when parent container size changes. value_1 should be persistent, value_2 will always be recalculated from value_1. +// anchor = +1 - bottom/right split would have a constant size. top/left split would resize when parent container size changes. value_2 should be persistent, value_1 will always be recalculated from value_2. +bool ImGui::Splitter(const char* id, float* value_1, float* value_2, int axis, int anchor, float min_size_0, float min_size_1) +{ + // FIXME-DOGFOODING: This needs further refining. + // FIXME-SCROLL: When resizing either we'd like to keep scroll focus on something (e.g. last clicked item for list, bottom for log) + // See https://github.com/ocornut/imgui/issues/319 + ImGuiContext& g = *GImGui; + ImGuiStyle& style = g.Style; + ImGuiWindow* window = ImGui::GetCurrentWindow(); + if (min_size_0 < 0) + min_size_0 = ImGui::GetFrameHeight(); + if (min_size_1) + min_size_1 = ImGui::GetFrameHeight(); + + IM_ASSERT(axis == ImGuiAxis_X || axis == ImGuiAxis_Y); + + float& v_1 = *value_1; + float& v_2 = *value_2; + ImRect splitter_bb; + const float avail = axis == ImGuiAxis_X ? ImGui::GetContentRegionAvail().x - style.ItemSpacing.x : ImGui::GetContentRegionAvail().y - style.ItemSpacing.y; + if (anchor < 0) + { + v_2 = ImMax(avail - v_1, min_size_1); // First split is constant size. + } + else if (anchor > 0) + { + v_1 = ImMax(avail - v_2, min_size_0); // Second split is constant size. + } + else + { + float r = v_1 / (v_1 + v_2); // Both splits maintain same relative size to parent. + v_1 = IM_ROUND(avail * r) - 1; + v_2 = IM_ROUND(avail * (1.0f - r)) - 1; + } + if (axis == ImGuiAxis_X) + { + float x = window->DC.CursorPos.x + v_1 + IM_ROUND(style.ItemSpacing.x * 0.5f); + splitter_bb = ImRect(x - 1, window->WorkRect.Min.y, x + 1, window->WorkRect.Max.y); + } + else if (axis == ImGuiAxis_Y) + { + float y = window->DC.CursorPos.y + v_1 + IM_ROUND(style.ItemSpacing.y * 0.5f); + splitter_bb = ImRect(window->WorkRect.Min.x, y - 1, window->WorkRect.Max.x, y + 1); + } + return ImGui::SplitterBehavior(splitter_bb, ImGui::GetID(id), (ImGuiAxis)axis, &v_1, &v_2, min_size_0, min_size_1, 3.0f); +} + +// FIXME-TESTS: Should eventually remove. +ImFont* ImGui::FindFontByPrefix(const char* prefix) +{ + ImGuiContext& g = *GImGui; + for (ImFont* font : g.IO.Fonts->Fonts) + if (strncmp(font->ConfigData->Name, prefix, strlen(prefix)) == 0) + return font; + return NULL; +} + +// Legacy version support +#if IMGUI_VERSION_NUM < 18924 +const char* ImGui::TabBarGetTabName(ImGuiTabBar* tab_bar, ImGuiTabItem* tab) +{ + return tab_bar->GetTabName(tab); +} +#endif + +#if IMGUI_VERSION_NUM < 18927 +ImGuiID ImGui::TableGetInstanceID(ImGuiTable* table, int instance_no) +{ + // Changed in #6140 + return table->ID + instance_no; +} +#endif + +ImGuiID TableGetHeaderID(ImGuiTable* table, const char* column, int instance_no) +{ + IM_ASSERT(table != NULL); + int column_n = -1; + for (int n = 0; n < table->Columns.size() && column_n < 0; n++) + if (strcmp(ImGui::TableGetColumnName(table, n), column) == 0) + column_n = n; + IM_ASSERT(column_n != -1); + return TableGetHeaderID(table, column_n, instance_no); +} + +ImGuiID TableGetHeaderID(ImGuiTable* table, int column_n, int instance_no) +{ + IM_ASSERT(column_n >= 0 && column_n < table->ColumnsCount); + const ImGuiID table_instance_id = ImGui::TableGetInstanceID(table, instance_no); + const char* column_name = ImGui::TableGetColumnName(table, column_n); +#if IMGUI_VERSION_NUM >= 18927 + const int column_id_differencier = column_n; +#else + const int column_id_differencier = instance_no * table->ColumnsCount + column_n; +#endif + const int column_id = ImHashData(&column_id_differencier, sizeof(column_id_differencier), table_instance_id); + return ImHashData(column_name, strlen(column_name), column_id); +} + +// FIXME: Could be moved to core as an internal function? +void TableDiscardInstanceAndSettings(ImGuiID table_id) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.CurrentTable == NULL); + if (ImGuiTableSettings* settings = ImGui::TableSettingsFindByID(table_id)) + settings->ID = 0; + + if (ImGuiTable* table = ImGui::TableFindByID(table_id)) + ImGui::TableRemove(table); + // FIXME-TABLE: We should be able to use TableResetSettings() instead of TableRemove()! Maybe less of a clean slate but would be good to check that it does the job + //ImGui::TableResetSettings(table); +} + +// Helper to verify ImDrawData integrity of buffer count (broke before e.g. #6716) +void DrawDataVerifyMatchingBufferCount(ImDrawData* draw_data) +{ + int total_vtx_count = 0; + int total_idx_count = 0; + for (ImDrawList* draw_list : draw_data->CmdLists) + { + total_vtx_count += draw_list->VtxBuffer.Size; + total_idx_count += draw_list->IdxBuffer.Size; + } + IM_UNUSED(total_vtx_count); + IM_UNUSED(total_idx_count); + IM_ASSERT(total_vtx_count == draw_data->TotalVtxCount); + IM_ASSERT(total_idx_count == draw_data->TotalIdxCount); +} + +//----------------------------------------------------------------------------- +// Simple CSV parser +//----------------------------------------------------------------------------- + +void ImGuiCsvParser::Clear() +{ + Rows = Columns = 0; + if (_Data != NULL) + IM_FREE(_Data); + _Data = NULL; + _Index.clear(); +} + +bool ImGuiCsvParser::Load(const char* filename) +{ + size_t len = 0; + _Data = (char*)ImFileLoadToMemory(filename, "rb", &len, 1); + if (_Data == NULL) + return false; + + int columns = 1; + if (Columns > 0) + { + columns = Columns; // User-provided expected column count. + } + else + { + for (const char* c = _Data; *c != '\n' && *c != '\0'; c++) // Count columns. Quoted columns with commas are not supported. + if (*c == ',') + columns++; + } + + // Count rows. Extra new lines anywhere in the file are ignored. + int max_rows = 0; + for (const char* c = _Data, *end = c + len; c < end; c++) + if ((*c == '\n' && c[1] != '\r' && c[1] != '\n') || *c == '\0') + max_rows++; + + if (columns == 0 || max_rows == 0) + return false; + + // Create index + _Index.resize(columns * max_rows); + + int col = 0; + char* col_data = _Data; + for (char* c = _Data; *c != '\0'; c++) + { + const bool is_comma = (*c == ','); + const bool is_eol = (*c == '\n' || *c == '\r'); + const bool is_eof = (*c == '\0'); + if (is_comma || is_eol || is_eof) + { + _Index[Rows * columns + col] = col_data; + col_data = c + 1; + if (is_comma) + { + col++; + } + else + { + if (col + 1 == columns) + Rows++; + else + fprintf(stderr, "%s: Unexpected number of columns on line %d, ignoring.\n", filename, Rows + 1); // FIXME + col = 0; + } + *c = 0; + if (is_eol) + while (c[1] == '\r' || c[1] == '\n') + c++; + } + } + + Columns = columns; + return true; +} + +//----------------------------------------------------------------------------- diff --git a/libs/imgui_test_engine/imgui_te_utils.h b/libs/imgui_test_engine/imgui_te_utils.h new file mode 100644 index 0000000..1eef55c --- /dev/null +++ b/libs/imgui_test_engine/imgui_te_utils.h @@ -0,0 +1,221 @@ +// dear imgui test engine +// (helpers/utilities. do NOT use this as a general purpose library) + +#pragma once + +//----------------------------------------------------------------------------- +// Includes +//----------------------------------------------------------------------------- + +#include // fabsf +#include // uint64_t +#include // FILE* +#include "imgui.h" // ImGuiID, ImGuiKey +class Str; // Str<> from thirdparty/Str/Str.h + +//----------------------------------------------------------------------------- +// Function Pointers +//----------------------------------------------------------------------------- + +#if IMGUI_TEST_ENGINE_ENABLE_STD_FUNCTION +#include +#define ImFuncPtr(FUNC_TYPE) std::function +#else +#define ImFuncPtr(FUNC_TYPE) FUNC_TYPE* +#endif + +//----------------------------------------------------------------------------- +// Hashing Helpers +//----------------------------------------------------------------------------- + +ImGuiID ImHashDecoratedPath(const char* str, const char* str_end = NULL, ImGuiID seed = 0); +const char* ImFindNextDecoratedPartInPath(const char* str, const char* str_end = NULL); + +//----------------------------------------------------------------------------- +// File/Directory Helpers +//----------------------------------------------------------------------------- + +bool ImFileExist(const char* filename); +bool ImFileDelete(const char* filename); +bool ImFileCreateDirectoryChain(const char* path, const char* path_end = NULL); +bool ImFileFindInParents(const char* sub_path, int max_parent_count, Str* output); +bool ImFileLoadSourceBlurb(const char* filename, int line_no_start, int line_no_end, ImGuiTextBuffer* out_buf); + +//----------------------------------------------------------------------------- +// Path Helpers +//----------------------------------------------------------------------------- + +// Those are strictly string manipulation functions +const char* ImPathFindFilename(const char* path, const char* path_end = NULL); // Return value always between path and path_end +const char* ImPathFindExtension(const char* path, const char* path_end = NULL); // Return value always between path and path_end +void ImPathFixSeparatorsForCurrentOS(char* buf); + +//----------------------------------------------------------------------------- +// String Helpers +//----------------------------------------------------------------------------- + +void ImStrReplace(Str* s, const char* find, const char* repl); +const char* ImStrchrRangeWithEscaping(const char* str, const char* str_end, char find_c); +void ImStrXmlEscape(Str* s); +int ImStrBase64Encode(const unsigned char* src, char* dst, int length); + +//----------------------------------------------------------------------------- +// Parsing Helpers +//----------------------------------------------------------------------------- + +void ImParseExtractArgcArgvFromCommandLine(int* out_argc, char const*** out_argv, const char* cmd_line); +bool ImParseFindIniSection(const char* ini_config, const char* header, ImVector* result); + +//----------------------------------------------------------------------------- +// Time Helpers +//----------------------------------------------------------------------------- + +uint64_t ImTimeGetInMicroseconds(); +void ImTimestampToISO8601(uint64_t timestamp, Str* out_date); + +//----------------------------------------------------------------------------- +// Threading Helpers +//----------------------------------------------------------------------------- + +void ImThreadSleepInMilliseconds(int ms); +void ImThreadSetCurrentThreadDescription(const char* description); + +//----------------------------------------------------------------------------- +// Build Info helpers +//----------------------------------------------------------------------------- + +// All the pointers are expect to be literals/persistent +struct ImBuildInfo +{ + const char* Type = ""; + const char* Cpu = ""; + const char* OS = ""; + const char* Compiler = ""; + char Date[32]; // "YYYY-MM-DD" + const char* Time = ""; +}; + +const ImBuildInfo* ImBuildGetCompilationInfo(); +bool ImBuildFindGitBranchName(const char* git_repo_path, Str* branch_name); + +//----------------------------------------------------------------------------- +// Operating System Helpers +//----------------------------------------------------------------------------- + +enum ImOsConsoleStream +{ + ImOsConsoleStream_StandardOutput, + ImOsConsoleStream_StandardError, +}; + +enum ImOsConsoleTextColor +{ + ImOsConsoleTextColor_Black, + ImOsConsoleTextColor_White, + ImOsConsoleTextColor_BrightWhite, + ImOsConsoleTextColor_BrightRed, + ImOsConsoleTextColor_BrightGreen, + ImOsConsoleTextColor_BrightBlue, + ImOsConsoleTextColor_BrightYellow, +}; + +bool ImOsCreateProcess(const char* cmd_line); +FILE* ImOsPOpen(const char* cmd_line, const char* mode); +void ImOsPClose(FILE* fp); +void ImOsOpenInShell(const char* path); +bool ImOsIsDebuggerPresent(); +void ImOsOutputDebugString(const char* message); +void ImOsConsoleSetTextColor(ImOsConsoleStream stream, ImOsConsoleTextColor color); + +//----------------------------------------------------------------------------- +// Miscellaneous functions +//----------------------------------------------------------------------------- + +// Tables functions +struct ImGuiTable; +ImGuiID TableGetHeaderID(ImGuiTable* table, const char* column, int instance_no = 0); +ImGuiID TableGetHeaderID(ImGuiTable* table, int column_n, int instance_no = 0); +void TableDiscardInstanceAndSettings(ImGuiID table_id); + +// DrawData functions +void DrawDataVerifyMatchingBufferCount(ImDrawData* draw_data); + +//----------------------------------------------------------------------------- +// Helper: maintain/calculate moving average +//----------------------------------------------------------------------------- + +template +struct ImMovingAverage +{ + // Internal Fields + ImVector Samples; + TYPE Accum; + int Idx; + int FillAmount; + + // Functions + ImMovingAverage() { Accum = (TYPE)0; Idx = FillAmount = 0; } + void Init(int count) { Samples.resize(count); memset(Samples.Data, 0, (size_t)Samples.Size * sizeof(TYPE)); Accum = (TYPE)0; Idx = FillAmount = 0; } + void AddSample(TYPE v) { Accum += v - Samples[Idx]; Samples[Idx] = v; if (++Idx == Samples.Size) Idx = 0; if (FillAmount < Samples.Size) FillAmount++; } + TYPE GetAverage() const { return Accum / (TYPE)FillAmount; } + int GetSampleCount() const { return Samples.Size; } + bool IsFull() const { return FillAmount == Samples.Size; } +}; + +//----------------------------------------------------------------------------- +// Helper: Simple/dumb CSV parser +//----------------------------------------------------------------------------- + +struct ImGuiCsvParser +{ + // Public fields + int Columns = 0; // Number of columns in CSV file. + int Rows = 0; // Number of rows in CSV file. + + // Internal fields + char* _Data = NULL; // CSV file data. + ImVector _Index; // CSV table: _Index[row * _Columns + col]. + + // Functions + ImGuiCsvParser(int columns = -1) { Columns = columns; } + ~ImGuiCsvParser() { Clear(); } + bool Load(const char* file_name); // Open and parse a CSV file. + void Clear(); // Free allocated buffers. + const char* GetCell(int row, int col) { IM_ASSERT(0 <= row && row < Rows && 0 <= col && col < Columns); return _Index[row * Columns + col]; } +}; + +//----------------------------------------------------------------------------- +// Misc Dear ImGui extensions +//----------------------------------------------------------------------------- + +#if IMGUI_VERSION_NUM < 18924 +struct ImGuiTabBar; +struct ImGuiTabItem; +#endif + +namespace ImGui +{ + +IMGUI_API void ItemErrorFrame(ImU32 col); + +#if IMGUI_VERSION_NUM < 18927 +ImGuiID TableGetInstanceID(ImGuiTable* table, int instance_no = 0); +#endif + +// Str support for InputText() +IMGUI_API bool InputText(const char* label, Str* str, ImGuiInputTextFlags flags = 0, ImGuiInputTextCallback callback = NULL, void* user_data = NULL); +IMGUI_API bool InputTextWithHint(const char* label, const char* hint, Str* str, ImGuiInputTextFlags flags = 0, ImGuiInputTextCallback callback = NULL, void* user_data = NULL); +IMGUI_API bool InputTextMultiline(const char* label, Str* str, const ImVec2& size = ImVec2(0, 0), ImGuiInputTextFlags flags = 0, ImGuiInputTextCallback callback = NULL, void* user_data = NULL); + +// Splitter +IMGUI_API bool Splitter(const char* id, float* value_1, float* value_2, int axis, int anchor = 0, float min_size_0 = -1.0f, float min_size_1 = -1.0f); + +// Misc +IMGUI_API ImFont* FindFontByPrefix(const char* name); + +// Legacy version support +#if IMGUI_VERSION_NUM < 18924 +IMGUI_API const char* TabBarGetTabName(ImGuiTabBar* tab_bar, ImGuiTabItem* tab); +#endif + +} diff --git a/libs/imgui_test_engine/thirdparty/README.md b/libs/imgui_test_engine/thirdparty/README.md new file mode 100644 index 0000000..b9faa39 --- /dev/null +++ b/libs/imgui_test_engine/thirdparty/README.md @@ -0,0 +1,7 @@ +## Third party libraries used by Test Engine + +Always used: +- `Str/Str.h` simple string type, used by `imgui_test_engine` (Public Domain) + +Used if `IMGUI_TEST_ENGINE_ENABLE_CAPTURE` is defined to 1 (default: 1) +- `stb/imstb_image_write.h` image writer, used by `imgui_capture_tool` (MIT Licence OR Public Domain) diff --git a/libs/imgui_test_engine/thirdparty/Str/README.md b/libs/imgui_test_engine/thirdparty/Str/README.md new file mode 100644 index 0000000..3573ea9 --- /dev/null +++ b/libs/imgui_test_engine/thirdparty/Str/README.md @@ -0,0 +1,71 @@ +``` +Str +Simple C++ string type with an optional local buffer, by Omar Cornut +https://github.com/ocornut/str + +LICENSE +This software is in the public domain. Where that dedication is not +recognized, you are granted a perpetual, irrevocable license to copy, +distribute, and modify this file as you see fit. + +USAGE +Include Str.h in whatever places need to refer to it. +In ONE .cpp file, write '#define STR_IMPLEMENTATION' before the #include. +This expands out the actual implementation into that C/C++ file. + +NOTES +- This isn't a fully featured string class. +- It is a simple, bearable replacement to std::string that isn't heap abusive nor bloated (can actually be debugged by humans!). +- String are mutable. We don't maintain size so length() is not-constant time. +- Maximum string size currently limited to 2 MB (we allocate 21 bits to hold capacity). +- Local buffer size is currently limited to 1023 bytes (we allocate 10 bits to hold local buffer size). +- We could easily raise those limits if we are ok to increase the structure overhead in 32-bits mode. +- In "non-owned" mode for literals/reference we don't do any tracking/counting of references. +- Overhead is 8-bytes in 32-bits, 16-bytes in 64-bits (12 + alignment). +- I'm using this code but it hasn't been tested thoroughly. + +The idea is that you can provide an arbitrary sized local buffer if you expect string to fit +most of the time, and then you avoid using costly heap. + +No local buffer, always use heap, sizeof()==8~16 (depends if your pointers are 32-bits or 64-bits) + + Str s = "hey"; // use heap + +With a local buffer of 16 bytes, sizeof() == 8~16 + 16 bytes. + + Str16 s = "filename.h"; // copy into local buffer + Str16 s = "long_filename_not_very_long_but_longer_than_expected.h"; // use heap + +With a local buffer of 256 bytes, sizeof() == 8~16 + 256 bytes. + + Str256 s = "long_filename_not_very_long_but_longer_than_expected.h"; // copy into local buffer + +Common sizes are defined at the bottom of Str.h, you may define your own. + +Functions: + + Str256 s; + s.set("hello sailor"); // set (copy) + s.setf("%s/%s.tmp", folder, filename); // set (w/format) + s.append("hello"); // append. cost a length() calculation! + s.appendf("hello %d", 42); // append (w/format). cost a length() calculation! + s.set_ref("Hey!"); // set (literal/reference, just copy pointer, no tracking) + +Constructor helper for format string: add a trailing 'f' to the type. Underlying type is the same. + + Str256f filename("%s/%s.tmp", folder, filename); // construct (w/format) + fopen(Str256f("%s/%s.tmp, folder, filename).c_str(), "rb"); // construct (w/format), use as function param, destruct + +Constructor helper for reference/literal: + + StrRef ref("literal"); // copy pointer, no allocation, no string copy + StrRef ref2(GetDebugName()); // copy pointer. no tracking of anything whatsoever, know what you are doing! + +All StrXXX types derives from Str and instance hold the local buffer capacity. +So you can pass e.g. Str256* to a function taking base type Str* and it will be functional! + + void MyFunc(Str& s) { s = "Hello"; } // will use local buffer if available in Str instance + +(Using a template e.g. Str we could remove the LocalBufSize storage but it would make passing typed Str<> to functions tricky. + Instead we don't use template so you can pass them around as the base type Str*. Also, templates are ugly.) +``` diff --git a/libs/imgui_test_engine/thirdparty/Str/Str.h b/libs/imgui_test_engine/thirdparty/Str/Str.h new file mode 100644 index 0000000..ff197d3 --- /dev/null +++ b/libs/imgui_test_engine/thirdparty/Str/Str.h @@ -0,0 +1,658 @@ +// Str v0.32 +// Simple C++ string type with an optional local buffer, by Omar Cornut +// https://github.com/ocornut/str + +// LICENSE +// This software is in the public domain. Where that dedication is not +// recognized, you are granted a perpetual, irrevocable license to copy, +// distribute, and modify this file as you see fit. + +// USAGE +// Include this file in whatever places need to refer to it. +// In ONE .cpp file, write '#define STR_IMPLEMENTATION' before the #include of this file. +// This expands out the actual implementation into that C/C++ file. + + +/* +- This isn't a fully featured string class. +- It is a simple, bearable replacement to std::string that isn't heap abusive nor bloated (can actually be debugged by humans). +- String are mutable. We don't maintain size so length() is not-constant time. +- Maximum string size currently limited to 2 MB (we allocate 21 bits to hold capacity). +- Local buffer size is currently limited to 1023 bytes (we allocate 10 bits to hold local buffer size). +- In "non-owned" mode for literals/reference we don't do any tracking/counting of references. +- Overhead is 8-bytes in 32-bits, 16-bytes in 64-bits (12 + alignment). +- This code hasn't been tested very much. it is probably incomplete or broken. Made it for my own use. + +The idea is that you can provide an arbitrary sized local buffer if you expect string to fit +most of the time, and then you avoid using costly heap. + +No local buffer, always use heap, sizeof()==8~16 (depends if your pointers are 32-bits or 64-bits) + + Str s = "hey"; + +With a local buffer of 16 bytes, sizeof() == 8~16 + 16 bytes. + + Str16 s = "filename.h"; // copy into local buffer + Str16 s = "long_filename_not_very_long_but_longer_than_expected.h"; // use heap + +With a local buffer of 256 bytes, sizeof() == 8~16 + 256 bytes. + + Str256 s = "long_filename_not_very_long_but_longer_than_expected.h"; // copy into local buffer + +Common sizes are defined at the bottom of Str.h, you may define your own. + +Functions: + + Str256 s; + s.set("hello sailor"); // set (copy) + s.setf("%s/%s.tmp", folder, filename); // set (w/format) + s.append("hello"); // append. cost a length() calculation! + s.appendf("hello %d", 42); // append (w/format). cost a length() calculation! + s.set_ref("Hey!"); // set (literal/reference, just copy pointer, no tracking) + +Constructor helper for format string: add a trailing 'f' to the type. Underlying type is the same. + + Str256f filename("%s/%s.tmp", folder, filename); // construct (w/format) + fopen(Str256f("%s/%s.tmp, folder, filename).c_str(), "rb"); // construct (w/format), use as function param, destruct + +Constructor helper for reference/literal: + + StrRef ref("literal"); // copy pointer, no allocation, no string copy + StrRef ref2(GetDebugName()); // copy pointer. no tracking of anything whatsoever, know what you are doing! + +All StrXXX types derives from Str and instance hold the local buffer capacity. So you can pass e.g. Str256* to a function taking base type Str* and it will be functional. + + void MyFunc(Str& s) { s = "Hello"; } // will use local buffer if available in Str instance + +(Using a template e.g. Str we could remove the LocalBufSize storage but it would make passing typed Str<> to functions tricky. + Instead we don't use template so you can pass them around as the base type Str*. Also, templates are ugly.) +*/ + +/* + CHANGELOG + 0.32 - added owned() accessor. + 0.31 - fixed various warnings. + 0.30 - turned into a single header file, removed Str.cpp. + 0.29 - fixed bug when calling reserve on non-owned strings (ie. when using StrRef or set_ref), and fixed include. + 0.28 - breaking change: replaced Str32 by Str30 to avoid collision with Str32 from MacTypes.h . + 0.27 - added STR_API and basic .natvis file. + 0.26 - fixed set(cont char* src, const char* src_end) writing null terminator to the wrong position. + 0.25 - allow set(const char* NULL) or operator= NULL to clear the string. note that set() from range or other types are not allowed. + 0.24 - allow set_ref(const char* NULL) to clear the string. include fixes for linux. + 0.23 - added append(char). added append_from(int idx, XXX) functions. fixed some compilers warnings. + 0.22 - documentation improvements, comments. fixes for some compilers. + 0.21 - added StrXXXf() constructor to construct directly from a format string. +*/ + +/* +TODO +- Since we lose 4-bytes of padding on 64-bits architecture, perhaps just spread the header to 8-bytes and lift size limits? +- More functions/helpers. +*/ + +#ifndef STR_INCLUDED +#define STR_INCLUDED + +//------------------------------------------------------------------------- +// CONFIGURATION +//------------------------------------------------------------------------- + +#ifndef STR_MEMALLOC +#define STR_MEMALLOC malloc +#include +#endif +#ifndef STR_MEMFREE +#define STR_MEMFREE free +#include +#endif +#ifndef STR_ASSERT +#define STR_ASSERT assert +#include +#endif +#ifndef STR_API +#define STR_API +#endif +#include // for va_list +#include // for strlen, strcmp, memcpy, etc. + +// Configuration: #define STR_SUPPORT_STD_STRING 0 to disable setters variants using const std::string& (on by default) +#ifndef STR_SUPPORT_STD_STRING +#define STR_SUPPORT_STD_STRING 1 +#endif + +// Configuration: #define STR_DEFINE_STR32 1 to keep defining Str32/Str32f, but be warned: on macOS/iOS, MacTypes.h also defines a type named Str32. +#ifndef STR_DEFINE_STR32 +#define STR_DEFINE_STR32 0 +#endif + +#if STR_SUPPORT_STD_STRING +#include +#endif + +//------------------------------------------------------------------------- +// HEADERS +//------------------------------------------------------------------------- + +// This is the base class that you can pass around +// Footprint is 8-bytes (32-bits arch) or 16-bytes (64-bits arch) +class STR_API Str +{ + char* Data; // Point to LocalBuf() or heap allocated + int Capacity : 21; // Max 2 MB + int LocalBufSize : 10; // Max 1023 bytes + unsigned int Owned : 1; // Set when we have ownership of the pointed data (most common, unless using set_ref() method or StrRef constructor) + +public: + inline char* c_str() { return Data; } + inline const char* c_str() const { return Data; } + inline bool empty() const { return Data[0] == 0; } + inline int length() const { return (int)strlen(Data); } // by design, allow user to write into the buffer at any time + inline int capacity() const { return Capacity; } + inline bool owned() const { return Owned ? true : false; } + + inline void set_ref(const char* src); + int setf(const char* fmt, ...); + int setfv(const char* fmt, va_list args); + int setf_nogrow(const char* fmt, ...); + int setfv_nogrow(const char* fmt, va_list args); + int append(char c); + int append(const char* s, const char* s_end = NULL); + int appendf(const char* fmt, ...); + int appendfv(const char* fmt, va_list args); + int append_from(int idx, char c); + int append_from(int idx, const char* s, const char* s_end = NULL); // If you know the string length or want to append from a certain point + int appendf_from(int idx, const char* fmt, ...); + int appendfv_from(int idx, const char* fmt, va_list args); + + void clear(); + void reserve(int cap); + void reserve_discard(int cap); + void shrink_to_fit(); + + inline char& operator[](size_t i) { return Data[i]; } + inline char operator[](size_t i) const { return Data[i]; } + //explicit operator const char*() const{ return Data; } + + inline Str(); + inline Str(const char* rhs); + inline void set(const char* src); + inline void set(const char* src, const char* src_end); + inline Str& operator=(const char* rhs) { set(rhs); return *this; } + inline bool operator==(const char* rhs) const { return strcmp(c_str(), rhs) == 0; } + + inline Str(const Str& rhs); + inline void set(const Str& src); + inline Str& operator=(const Str& rhs) { set(rhs); return *this; } + inline bool operator==(const Str& rhs) const { return strcmp(c_str(), rhs.c_str()) == 0; } + +#if STR_SUPPORT_STD_STRING + inline Str(const std::string& rhs); + inline void set(const std::string& src); + inline Str& operator=(const std::string& rhs) { set(rhs); return *this; } + inline bool operator==(const std::string& rhs)const { return strcmp(c_str(), rhs.c_str()) == 0; } +#endif + + // Destructor for all variants + inline ~Str() + { + if (Owned && !is_using_local_buf()) + STR_MEMFREE(Data); + } + + static char* EmptyBuffer; + +protected: + inline char* local_buf() { return (char*)this + sizeof(Str); } + inline const char* local_buf() const { return (char*)this + sizeof(Str); } + inline bool is_using_local_buf() const { return Data == local_buf() && LocalBufSize != 0; } + + // Constructor for StrXXX variants with local buffer + Str(unsigned short local_buf_size) + { + STR_ASSERT(local_buf_size < 1024); + Data = local_buf(); + Data[0] = '\0'; + Capacity = local_buf_size; + LocalBufSize = local_buf_size; + Owned = 1; + } +}; + +void Str::set(const char* src) +{ + // We allow set(NULL) or via = operator to clear the string. + if (src == NULL) + { + clear(); + return; + } + int buf_len = (int)strlen(src)+1; + if (Capacity < buf_len) + reserve_discard(buf_len); + memcpy(Data, src, (size_t)buf_len); + Owned = 1; +} + +void Str::set(const char* src, const char* src_end) +{ + STR_ASSERT(src != NULL && src_end >= src); + int buf_len = (int)(src_end-src)+1; + if ((int)Capacity < buf_len) + reserve_discard(buf_len); + memcpy(Data, src, (size_t)(buf_len - 1)); + Data[buf_len-1] = 0; + Owned = 1; +} + +void Str::set(const Str& src) +{ + int buf_len = (int)strlen(src.c_str())+1; + if ((int)Capacity < buf_len) + reserve_discard(buf_len); + memcpy(Data, src.c_str(), (size_t)buf_len); + Owned = 1; +} + +#if STR_SUPPORT_STD_STRING +void Str::set(const std::string& src) +{ + int buf_len = (int)src.length()+1; + if ((int)Capacity < buf_len) + reserve_discard(buf_len); + memcpy(Data, src.c_str(), (size_t)buf_len); + Owned = 1; +} +#endif + +inline void Str::set_ref(const char* src) +{ + if (Owned && !is_using_local_buf()) + STR_MEMFREE(Data); + Data = src ? (char*)src : EmptyBuffer; + Capacity = 0; + Owned = 0; +} + +Str::Str() +{ + Data = EmptyBuffer; // Shared READ-ONLY initial buffer for 0 capacity + Capacity = 0; + LocalBufSize = 0; + Owned = 0; +} + +Str::Str(const Str& rhs) : Str() +{ + set(rhs); +} + +Str::Str(const char* rhs) : Str() +{ + set(rhs); +} + +#if STR_SUPPORT_STD_STRING +Str::Str(const std::string& rhs) : Str() +{ + set(rhs); +} +#endif + +// Literal/reference string +class StrRef : public Str +{ +public: + StrRef(const char* s) : Str() { set_ref(s); } +}; + +// Types embedding a local buffer +// NB: we need to override the constructor and = operator for both Str& and TYPENAME (without the later compiler will call a default copy operator) +#if STR_SUPPORT_STD_STRING + +#define STR_DEFINETYPE(TYPENAME, LOCALBUFSIZE) \ +class TYPENAME : public Str \ +{ \ + char local_buf[LOCALBUFSIZE]; \ +public: \ + TYPENAME() : Str(LOCALBUFSIZE) {} \ + TYPENAME(const Str& rhs) : Str(LOCALBUFSIZE) { set(rhs); } \ + TYPENAME(const char* rhs) : Str(LOCALBUFSIZE) { set(rhs); } \ + TYPENAME(const TYPENAME& rhs) : Str(LOCALBUFSIZE) { set(rhs); } \ + TYPENAME(const std::string& rhs) : Str(LOCALBUFSIZE) { set(rhs); } \ + TYPENAME& operator=(const char* rhs) { set(rhs); return *this; } \ + TYPENAME& operator=(const Str& rhs) { set(rhs); return *this; } \ + TYPENAME& operator=(const TYPENAME& rhs) { set(rhs); return *this; } \ + TYPENAME& operator=(const std::string& rhs) { set(rhs); return *this; } \ +}; + +#else + +#define STR_DEFINETYPE(TYPENAME, LOCALBUFSIZE) \ +class TYPENAME : public Str \ +{ \ + char local_buf[LOCALBUFSIZE]; \ +public: \ + TYPENAME() : Str(LOCALBUFSIZE) {} \ + TYPENAME(const Str& rhs) : Str(LOCALBUFSIZE) { set(rhs); } \ + TYPENAME(const char* rhs) : Str(LOCALBUFSIZE) { set(rhs); } \ + TYPENAME(const TYPENAME& rhs) : Str(LOCALBUFSIZE) { set(rhs); } \ + TYPENAME& operator=(const char* rhs) { set(rhs); return *this; } \ + TYPENAME& operator=(const Str& rhs) { set(rhs); return *this; } \ + TYPENAME& operator=(const TYPENAME& rhs) { set(rhs); return *this; } \ +}; + +#endif + +// Disable PVS-Studio warning V730: Not all members of a class are initialized inside the constructor (local_buf is not initialized and that is fine) +// -V:STR_DEFINETYPE:730 + +// Helper to define StrXXXf constructors +#define STR_DEFINETYPE_F(TYPENAME, TYPENAME_F) \ +class TYPENAME_F : public TYPENAME \ +{ \ +public: \ + TYPENAME_F(const char* fmt, ...) : TYPENAME() { va_list args; va_start(args, fmt); setfv(fmt, args); va_end(args); } \ +}; + +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunused-private-field" // warning : private field 'local_buf' is not used +#endif + +// Declaring types for common sizes here +STR_DEFINETYPE(Str16, 16) +STR_DEFINETYPE(Str30, 30) +STR_DEFINETYPE(Str64, 64) +STR_DEFINETYPE(Str128, 128) +STR_DEFINETYPE(Str256, 256) +STR_DEFINETYPE(Str512, 512) + +// Declaring helper constructors to pass in format strings in one statement +STR_DEFINETYPE_F(Str16, Str16f) +STR_DEFINETYPE_F(Str30, Str30f) +STR_DEFINETYPE_F(Str64, Str64f) +STR_DEFINETYPE_F(Str128, Str128f) +STR_DEFINETYPE_F(Str256, Str256f) +STR_DEFINETYPE_F(Str512, Str512f) + +#if STR_DEFINE_STR32 +STR_DEFINETYPE(Str32, 32) +STR_DEFINETYPE_F(Str32, Str32f) +#endif + +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + +#endif // #ifndef STR_INCLUDED + +//------------------------------------------------------------------------- +// IMPLEMENTATION +//------------------------------------------------------------------------- + +#ifdef STR_IMPLEMENTATION + +#include // for vsnprintf + +// On some platform vsnprintf() takes va_list by reference and modifies it. +// va_copy is the 'correct' way to copy a va_list but Visual Studio prior to 2013 doesn't have it. +#ifndef va_copy +#define va_copy(dest, src) (dest = src) +#endif + +// Static empty buffer we can point to for empty strings +// Pointing to a literal increases the like-hood of getting a crash if someone attempts to write in the empty string buffer. +char* Str::EmptyBuffer = (char*)"\0NULL"; + +// Clear +void Str::clear() +{ + if (Owned && !is_using_local_buf()) + STR_MEMFREE(Data); + if (LocalBufSize) + { + Data = local_buf(); + Data[0] = '\0'; + Capacity = LocalBufSize; + Owned = 1; + } + else + { + Data = EmptyBuffer; + Capacity = 0; + Owned = 0; + } +} + +// Reserve memory, preserving the current of the buffer +void Str::reserve(int new_capacity) +{ + if (new_capacity <= Capacity) + return; + + char* new_data; + if (new_capacity < LocalBufSize) + { + // Disowned -> LocalBuf + new_data = local_buf(); + new_capacity = LocalBufSize; + } + else + { + // Disowned or LocalBuf -> Heap + new_data = (char*)STR_MEMALLOC((size_t)new_capacity * sizeof(char)); + } + + // string in Data might be longer than new_capacity if it wasn't owned, don't copy too much +#ifdef _MSC_VER + strncpy_s(new_data, (size_t)new_capacity, Data, (size_t)new_capacity - 1); +#else + strncpy(new_data, Data, (size_t)new_capacity - 1); +#endif + new_data[new_capacity - 1] = 0; + + if (Owned && !is_using_local_buf()) + STR_MEMFREE(Data); + + Data = new_data; + Capacity = new_capacity; + Owned = 1; +} + +// Reserve memory, discarding the current of the buffer (if we expect to be fully rewritten) +void Str::reserve_discard(int new_capacity) +{ + if (new_capacity <= Capacity) + return; + + if (Owned && !is_using_local_buf()) + STR_MEMFREE(Data); + + if (new_capacity < LocalBufSize) + { + // Disowned -> LocalBuf + Data = local_buf(); + Capacity = LocalBufSize; + } + else + { + // Disowned or LocalBuf -> Heap + Data = (char*)STR_MEMALLOC((size_t)new_capacity * sizeof(char)); + Capacity = new_capacity; + } + Owned = 1; +} + +void Str::shrink_to_fit() +{ + if (!Owned || is_using_local_buf()) + return; + int new_capacity = length() + 1; + if (Capacity <= new_capacity) + return; + + char* new_data = (char*)STR_MEMALLOC((size_t)new_capacity * sizeof(char)); + memcpy(new_data, Data, (size_t)new_capacity); + STR_MEMFREE(Data); + Data = new_data; + Capacity = new_capacity; +} + +// FIXME: merge setfv() and appendfv()? +int Str::setfv(const char* fmt, va_list args) +{ + // Needed for portability on platforms where va_list are passed by reference and modified by functions + va_list args2; + va_copy(args2, args); + + // MSVC returns -1 on overflow when writing, which forces us to do two passes + // FIXME-OPT: Find a way around that. +#ifdef _MSC_VER + int len = vsnprintf(NULL, 0, fmt, args); + STR_ASSERT(len >= 0); + + if (Capacity < len + 1) + reserve_discard(len + 1); + len = vsnprintf(Data, len + 1, fmt, args2); +#else + // First try + int len = vsnprintf(Owned ? Data : NULL, Owned ? (size_t)Capacity : 0, fmt, args); + STR_ASSERT(len >= 0); + + if (Capacity < len + 1) + { + reserve_discard(len + 1); + len = vsnprintf(Data, (size_t)len + 1, fmt, args2); + } +#endif + + STR_ASSERT(Owned); + return len; +} + +int Str::setf(const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + int len = setfv(fmt, args); + va_end(args); + return len; +} + +int Str::setfv_nogrow(const char* fmt, va_list args) +{ + STR_ASSERT(Owned); + + if (Capacity == 0) + return 0; + + int w = vsnprintf(Data, (size_t)Capacity, fmt, args); + Data[Capacity - 1] = 0; + Owned = 1; + return (w == -1) ? Capacity - 1 : w; +} + +int Str::setf_nogrow(const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + int len = setfv_nogrow(fmt, args); + va_end(args); + return len; +} + +int Str::append_from(int idx, char c) +{ + int add_len = 1; + if (Capacity < idx + add_len + 1) + reserve(idx + add_len + 1); + Data[idx] = c; + Data[idx + add_len] = 0; + STR_ASSERT(Owned); + return add_len; +} + +int Str::append_from(int idx, const char* s, const char* s_end) +{ + if (!s_end) + s_end = s + strlen(s); + int add_len = (int)(s_end - s); + if (Capacity < idx + add_len + 1) + reserve(idx + add_len + 1); + memcpy(Data + idx, (const void*)s, (size_t)add_len); + Data[idx + add_len] = 0; // Our source data isn't necessarily zero-terminated + STR_ASSERT(Owned); + return add_len; +} + +// FIXME: merge setfv() and appendfv()? +int Str::appendfv_from(int idx, const char* fmt, va_list args) +{ + // Needed for portability on platforms where va_list are passed by reference and modified by functions + va_list args2; + va_copy(args2, args); + + // MSVC returns -1 on overflow when writing, which forces us to do two passes + // FIXME-OPT: Find a way around that. +#ifdef _MSC_VER + int add_len = vsnprintf(NULL, 0, fmt, args); + STR_ASSERT(add_len >= 0); + + if (Capacity < idx + add_len + 1) + reserve(idx + add_len + 1); + add_len = vsnprintf(Data + idx, add_len + 1, fmt, args2); +#else + // First try + int add_len = vsnprintf(Owned ? Data + idx : NULL, Owned ? (size_t)(Capacity - idx) : 0, fmt, args); + STR_ASSERT(add_len >= 0); + + if (Capacity < idx + add_len + 1) + { + reserve(idx + add_len + 1); + add_len = vsnprintf(Data + idx, (size_t)add_len + 1, fmt, args2); + } +#endif + + STR_ASSERT(Owned); + return add_len; +} + +int Str::appendf_from(int idx, const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + int len = appendfv_from(idx, fmt, args); + va_end(args); + return len; +} + +int Str::append(char c) +{ + int cur_len = length(); + return append_from(cur_len, c); +} + +int Str::append(const char* s, const char* s_end) +{ + int cur_len = length(); + return append_from(cur_len, s, s_end); +} + +int Str::appendfv(const char* fmt, va_list args) +{ + int cur_len = length(); + return appendfv_from(cur_len, fmt, args); +} + +int Str::appendf(const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + int len = appendfv(fmt, args); + va_end(args); + return len; +} + +#endif // #define STR_IMPLEMENTATION + +//------------------------------------------------------------------------- diff --git a/libs/imgui_test_engine/thirdparty/stb/imstb_image_write.h b/libs/imgui_test_engine/thirdparty/stb/imstb_image_write.h new file mode 100644 index 0000000..082eb69 --- /dev/null +++ b/libs/imgui_test_engine/thirdparty/stb/imstb_image_write.h @@ -0,0 +1,1617 @@ +/* stb_image_write - v1.13 - public domain - http://nothings.org/stb/stb_image_write.h + writes out PNG/BMP/TGA/JPEG/HDR images to C stdio - Sean Barrett 2010-2015 + no warranty implied; use at your own risk + + Before #including, + + #define STB_IMAGE_WRITE_IMPLEMENTATION + + in the file that you want to have the implementation. + + Will probably not work correctly with strict-aliasing optimizations. + +ABOUT: + + This header file is a library for writing images to C stdio or a callback. + + The PNG output is not optimal; it is 20-50% larger than the file + written by a decent optimizing implementation; though providing a custom + zlib compress function (see STBIW_ZLIB_COMPRESS) can mitigate that. + This library is designed for source code compactness and simplicity, + not optimal image file size or run-time performance. + +BUILDING: + + You can #define STBIW_ASSERT(x) before the #include to avoid using assert.h. + You can #define STBIW_MALLOC(), STBIW_REALLOC(), and STBIW_FREE() to replace + malloc,realloc,free. + You can #define STBIW_MEMMOVE() to replace memmove() + You can #define STBIW_ZLIB_COMPRESS to use a custom zlib-style compress function + for PNG compression (instead of the builtin one), it must have the following signature: + unsigned char * my_compress(unsigned char *data, int data_len, int *out_len, int quality); + The returned data will be freed with STBIW_FREE() (free() by default), + so it must be heap allocated with STBIW_MALLOC() (malloc() by default), + +UNICODE: + + If compiling for Windows and you wish to use Unicode filenames, compile + with + #define STBIW_WINDOWS_UTF8 + and pass utf8-encoded filenames. Call stbiw_convert_wchar_to_utf8 to convert + Windows wchar_t filenames to utf8. + +USAGE: + + There are five functions, one for each image file format: + + int stbi_write_png(char const *filename, int w, int h, int comp, const void *data, int stride_in_bytes); + int stbi_write_bmp(char const *filename, int w, int h, int comp, const void *data); + int stbi_write_tga(char const *filename, int w, int h, int comp, const void *data); + int stbi_write_jpg(char const *filename, int w, int h, int comp, const void *data, int quality); + int stbi_write_hdr(char const *filename, int w, int h, int comp, const float *data); + + void stbi_flip_vertically_on_write(int flag); // flag is non-zero to flip data vertically + + There are also five equivalent functions that use an arbitrary write function. You are + expected to open/close your file-equivalent before and after calling these: + + int stbi_write_png_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void *data, int stride_in_bytes); + int stbi_write_bmp_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void *data); + int stbi_write_tga_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void *data); + int stbi_write_hdr_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const float *data); + int stbi_write_jpg_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data, int quality); + + where the callback is: + void stbi_write_func(void *context, void *data, int size); + + You can configure it with these global variables: + int stbi_write_tga_with_rle; // defaults to true; set to 0 to disable RLE + int stbi_write_png_compression_level; // defaults to 8; set to higher for more compression + int stbi_write_force_png_filter; // defaults to -1; set to 0..5 to force a filter mode + + + You can define STBI_WRITE_NO_STDIO to disable the file variant of these + functions, so the library will not use stdio.h at all. However, this will + also disable HDR writing, because it requires stdio for formatted output. + + Each function returns 0 on failure and non-0 on success. + + The functions create an image file defined by the parameters. The image + is a rectangle of pixels stored from left-to-right, top-to-bottom. + Each pixel contains 'comp' channels of data stored interleaved with 8-bits + per channel, in the following order: 1=Y, 2=YA, 3=RGB, 4=RGBA. (Y is + monochrome color.) The rectangle is 'w' pixels wide and 'h' pixels tall. + The *data pointer points to the first byte of the top-left-most pixel. + For PNG, "stride_in_bytes" is the distance in bytes from the first byte of + a row of pixels to the first byte of the next row of pixels. + + PNG creates output files with the same number of components as the input. + The BMP format expands Y to RGB in the file format and does not + output alpha. + + PNG supports writing rectangles of data even when the bytes storing rows of + data are not consecutive in memory (e.g. sub-rectangles of a larger image), + by supplying the stride between the beginning of adjacent rows. The other + formats do not. (Thus you cannot write a native-format BMP through the BMP + writer, both because it is in BGR order and because it may have padding + at the end of the line.) + + PNG allows you to set the deflate compression level by setting the global + variable 'stbi_write_png_compression_level' (it defaults to 8). + + HDR expects linear float data. Since the format is always 32-bit rgb(e) + data, alpha (if provided) is discarded, and for monochrome data it is + replicated across all three channels. + + TGA supports RLE or non-RLE compressed data. To use non-RLE-compressed + data, set the global variable 'stbi_write_tga_with_rle' to 0. + + JPEG does ignore alpha channels in input data; quality is between 1 and 100. + Higher quality looks better but results in a bigger image. + JPEG baseline (no JPEG progressive). + +CREDITS: + + + Sean Barrett - PNG/BMP/TGA + Baldur Karlsson - HDR + Jean-Sebastien Guay - TGA monochrome + Tim Kelsey - misc enhancements + Alan Hickman - TGA RLE + Emmanuel Julien - initial file IO callback implementation + Jon Olick - original jo_jpeg.cpp code + Daniel Gibson - integrate JPEG, allow external zlib + Aarni Koskela - allow choosing PNG filter + + bugfixes: + github:Chribba + Guillaume Chereau + github:jry2 + github:romigrou + Sergio Gonzalez + Jonas Karlsson + Filip Wasil + Thatcher Ulrich + github:poppolopoppo + Patrick Boettcher + github:xeekworx + Cap Petschulat + Simon Rodriguez + Ivan Tikhonov + github:ignotion + Adam Schackart + +LICENSE + + See end of file for license information. + +*/ + +#ifndef INCLUDE_STB_IMAGE_WRITE_H +#define INCLUDE_STB_IMAGE_WRITE_H + +#include + +// if STB_IMAGE_WRITE_STATIC causes problems, try defining STBIWDEF to 'inline' or 'static inline' +#ifndef STBIWDEF +#ifdef STB_IMAGE_WRITE_STATIC +#define STBIWDEF static +#else +#ifdef __cplusplus +#define STBIWDEF extern "C" +#else +#define STBIWDEF extern +#endif +#endif +#endif + +#ifndef STB_IMAGE_WRITE_STATIC // C++ forbids static forward declarations +extern int stbi_write_tga_with_rle; +extern int stbi_write_png_compression_level; +extern int stbi_write_force_png_filter; +#endif + +#ifndef STBI_WRITE_NO_STDIO +STBIWDEF int stbi_write_png(char const *filename, int w, int h, int comp, const void *data, int stride_in_bytes); +STBIWDEF int stbi_write_bmp(char const *filename, int w, int h, int comp, const void *data); +STBIWDEF int stbi_write_tga(char const *filename, int w, int h, int comp, const void *data); +STBIWDEF int stbi_write_hdr(char const *filename, int w, int h, int comp, const float *data); +STBIWDEF int stbi_write_jpg(char const *filename, int x, int y, int comp, const void *data, int quality); + +#ifdef STBI_WINDOWS_UTF8 +STBIWDEF int stbiw_convert_wchar_to_utf8(char *buffer, size_t bufferlen, const wchar_t* input); +#endif +#endif + +typedef void stbi_write_func(void *context, void *data, int size); + +STBIWDEF int stbi_write_png_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void *data, int stride_in_bytes); +STBIWDEF int stbi_write_bmp_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void *data); +STBIWDEF int stbi_write_tga_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void *data); +STBIWDEF int stbi_write_hdr_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const float *data); +STBIWDEF int stbi_write_jpg_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data, int quality); + +STBIWDEF void stbi_flip_vertically_on_write(int flip_boolean); + +#endif//INCLUDE_STB_IMAGE_WRITE_H + +#ifdef STB_IMAGE_WRITE_IMPLEMENTATION + +#ifdef _WIN32 + #ifndef _CRT_SECURE_NO_WARNINGS + #define _CRT_SECURE_NO_WARNINGS + #endif + #ifndef _CRT_NONSTDC_NO_DEPRECATE + #define _CRT_NONSTDC_NO_DEPRECATE + #endif +#endif + +#ifndef STBI_WRITE_NO_STDIO +#include +#endif // STBI_WRITE_NO_STDIO + +#include +#include +#include +#include + +#if defined(STBIW_MALLOC) && defined(STBIW_FREE) && (defined(STBIW_REALLOC) || defined(STBIW_REALLOC_SIZED)) +// ok +#elif !defined(STBIW_MALLOC) && !defined(STBIW_FREE) && !defined(STBIW_REALLOC) && !defined(STBIW_REALLOC_SIZED) +// ok +#else +#error "Must define all or none of STBIW_MALLOC, STBIW_FREE, and STBIW_REALLOC (or STBIW_REALLOC_SIZED)." +#endif + +#ifndef STBIW_MALLOC +#define STBIW_MALLOC(sz) malloc(sz) +#define STBIW_REALLOC(p,newsz) realloc(p,newsz) +#define STBIW_FREE(p) free(p) +#endif + +#ifndef STBIW_REALLOC_SIZED +#define STBIW_REALLOC_SIZED(p,oldsz,newsz) STBIW_REALLOC(p,newsz) +#endif + + +#ifndef STBIW_MEMMOVE +#define STBIW_MEMMOVE(a,b,sz) memmove(a,b,sz) +#endif + + +#ifndef STBIW_ASSERT +#include +#define STBIW_ASSERT(x) assert(x) +#endif + +#define STBIW_UCHAR(x) (unsigned char) ((x) & 0xff) + +#ifdef STB_IMAGE_WRITE_STATIC +static int stbi__flip_vertically_on_write=0; +static int stbi_write_png_compression_level = 8; +static int stbi_write_tga_with_rle = 1; +static int stbi_write_force_png_filter = -1; +#else +int stbi_write_png_compression_level = 8; +int stbi__flip_vertically_on_write=0; +int stbi_write_tga_with_rle = 1; +int stbi_write_force_png_filter = -1; +#endif + +STBIWDEF void stbi_flip_vertically_on_write(int flag) +{ + stbi__flip_vertically_on_write = flag; +} + +typedef struct +{ + stbi_write_func *func; + void *context; +} stbi__write_context; + +// initialize a callback-based context +static void stbi__start_write_callbacks(stbi__write_context *s, stbi_write_func *c, void *context) +{ + s->func = c; + s->context = context; +} + +#ifndef STBI_WRITE_NO_STDIO + +static void stbi__stdio_write(void *context, void *data, int size) +{ + fwrite(data,1,size,(FILE*) context); +} + +#if defined(_MSC_VER) && defined(STBI_WINDOWS_UTF8) +#ifdef __cplusplus +#define STBIW_EXTERN extern "C" +#else +#define STBIW_EXTERN extern +#endif +STBIW_EXTERN __declspec(dllimport) int __stdcall MultiByteToWideChar(unsigned int cp, unsigned long flags, const char *str, int cbmb, wchar_t *widestr, int cchwide); +STBIW_EXTERN __declspec(dllimport) int __stdcall WideCharToMultiByte(unsigned int cp, unsigned long flags, const wchar_t *widestr, int cchwide, char *str, int cbmb, const char *defchar, int *used_default); + +STBIWDEF int stbiw_convert_wchar_to_utf8(char *buffer, size_t bufferlen, const wchar_t* input) +{ + return WideCharToMultiByte(65001 /* UTF8 */, 0, input, -1, buffer, (int) bufferlen, NULL, NULL); +} +#endif + +static FILE *stbiw__fopen(char const *filename, char const *mode) +{ + FILE *f; +#if defined(_MSC_VER) && defined(STBI_WINDOWS_UTF8) + wchar_t wMode[64]; + wchar_t wFilename[1024]; + if (0 == MultiByteToWideChar(65001 /* UTF8 */, 0, filename, -1, wFilename, sizeof(wFilename))) + return 0; + + if (0 == MultiByteToWideChar(65001 /* UTF8 */, 0, mode, -1, wMode, sizeof(wMode))) + return 0; + +#if _MSC_VER >= 1400 + if (0 != _wfopen_s(&f, wFilename, wMode)) + f = 0; +#else + f = _wfopen(wFilename, wMode); +#endif + +#elif defined(_MSC_VER) && _MSC_VER >= 1400 + if (0 != fopen_s(&f, filename, mode)) + f=0; +#else + f = fopen(filename, mode); +#endif + return f; +} + +static int stbi__start_write_file(stbi__write_context *s, const char *filename) +{ + FILE *f = stbiw__fopen(filename, "wb"); + stbi__start_write_callbacks(s, stbi__stdio_write, (void *) f); + return f != NULL; +} + +static void stbi__end_write_file(stbi__write_context *s) +{ + fclose((FILE *)s->context); +} + +#endif // !STBI_WRITE_NO_STDIO + +typedef unsigned int stbiw_uint32; +typedef int stb_image_write_test[sizeof(stbiw_uint32)==4 ? 1 : -1]; + +static void stbiw__writefv(stbi__write_context *s, const char *fmt, va_list v) +{ + while (*fmt) { + switch (*fmt++) { + case ' ': break; + case '1': { unsigned char x = STBIW_UCHAR(va_arg(v, int)); + s->func(s->context,&x,1); + break; } + case '2': { int x = va_arg(v,int); + unsigned char b[2]; + b[0] = STBIW_UCHAR(x); + b[1] = STBIW_UCHAR(x>>8); + s->func(s->context,b,2); + break; } + case '4': { stbiw_uint32 x = va_arg(v,int); + unsigned char b[4]; + b[0]=STBIW_UCHAR(x); + b[1]=STBIW_UCHAR(x>>8); + b[2]=STBIW_UCHAR(x>>16); + b[3]=STBIW_UCHAR(x>>24); + s->func(s->context,b,4); + break; } + default: + STBIW_ASSERT(0); + return; + } + } +} + +static void stbiw__writef(stbi__write_context *s, const char *fmt, ...) +{ + va_list v; + va_start(v, fmt); + stbiw__writefv(s, fmt, v); + va_end(v); +} + +static void stbiw__putc(stbi__write_context *s, unsigned char c) +{ + s->func(s->context, &c, 1); +} + +static void stbiw__write3(stbi__write_context *s, unsigned char a, unsigned char b, unsigned char c) +{ + unsigned char arr[3]; + arr[0] = a; arr[1] = b; arr[2] = c; + s->func(s->context, arr, 3); +} + +static void stbiw__write_pixel(stbi__write_context *s, int rgb_dir, int comp, int write_alpha, int expand_mono, unsigned char *d) +{ + unsigned char bg[3] = { 255, 0, 255}, px[3]; + int k; + + if (write_alpha < 0) + s->func(s->context, &d[comp - 1], 1); + + switch (comp) { + case 2: // 2 pixels = mono + alpha, alpha is written separately, so same as 1-channel case + case 1: + if (expand_mono) + stbiw__write3(s, d[0], d[0], d[0]); // monochrome bmp + else + s->func(s->context, d, 1); // monochrome TGA + break; + case 4: + if (!write_alpha) { + // composite against pink background + for (k = 0; k < 3; ++k) + px[k] = bg[k] + ((d[k] - bg[k]) * d[3]) / 255; + stbiw__write3(s, px[1 - rgb_dir], px[1], px[1 + rgb_dir]); + break; + } + /* FALLTHROUGH */ + case 3: + stbiw__write3(s, d[1 - rgb_dir], d[1], d[1 + rgb_dir]); + break; + } + if (write_alpha > 0) + s->func(s->context, &d[comp - 1], 1); +} + +static void stbiw__write_pixels(stbi__write_context *s, int rgb_dir, int vdir, int x, int y, int comp, void *data, int write_alpha, int scanline_pad, int expand_mono) +{ + stbiw_uint32 zero = 0; + int i,j, j_end; + + if (y <= 0) + return; + + if (stbi__flip_vertically_on_write) + vdir *= -1; + + if (vdir < 0) { + j_end = -1; j = y-1; + } else { + j_end = y; j = 0; + } + + for (; j != j_end; j += vdir) { + for (i=0; i < x; ++i) { + unsigned char *d = (unsigned char *) data + (j*x+i)*comp; + stbiw__write_pixel(s, rgb_dir, comp, write_alpha, expand_mono, d); + } + s->func(s->context, &zero, scanline_pad); + } +} + +static int stbiw__outfile(stbi__write_context *s, int rgb_dir, int vdir, int x, int y, int comp, int expand_mono, void *data, int alpha, int pad, const char *fmt, ...) +{ + if (y < 0 || x < 0) { + return 0; + } else { + va_list v; + va_start(v, fmt); + stbiw__writefv(s, fmt, v); + va_end(v); + stbiw__write_pixels(s,rgb_dir,vdir,x,y,comp,data,alpha,pad, expand_mono); + return 1; + } +} + +static int stbi_write_bmp_core(stbi__write_context *s, int x, int y, int comp, const void *data) +{ + int pad = (-x*3) & 3; + return stbiw__outfile(s,-1,-1,x,y,comp,1,(void *) data,0,pad, + "11 4 22 4" "4 44 22 444444", + 'B', 'M', 14+40+(x*3+pad)*y, 0,0, 14+40, // file header + 40, x,y, 1,24, 0,0,0,0,0,0); // bitmap header +} + +STBIWDEF int stbi_write_bmp_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data) +{ + stbi__write_context s; + stbi__start_write_callbacks(&s, func, context); + return stbi_write_bmp_core(&s, x, y, comp, data); +} + +#ifndef STBI_WRITE_NO_STDIO +STBIWDEF int stbi_write_bmp(char const *filename, int x, int y, int comp, const void *data) +{ + stbi__write_context s; + if (stbi__start_write_file(&s,filename)) { + int r = stbi_write_bmp_core(&s, x, y, comp, data); + stbi__end_write_file(&s); + return r; + } else + return 0; +} +#endif //!STBI_WRITE_NO_STDIO + +static int stbi_write_tga_core(stbi__write_context *s, int x, int y, int comp, void *data) +{ + int has_alpha = (comp == 2 || comp == 4); + int colorbytes = has_alpha ? comp-1 : comp; + int format = colorbytes < 2 ? 3 : 2; // 3 color channels (RGB/RGBA) = 2, 1 color channel (Y/YA) = 3 + + if (y < 0 || x < 0) + return 0; + + if (!stbi_write_tga_with_rle) { + return stbiw__outfile(s, -1, -1, x, y, comp, 0, (void *) data, has_alpha, 0, + "111 221 2222 11", 0, 0, format, 0, 0, 0, 0, 0, x, y, (colorbytes + has_alpha) * 8, has_alpha * 8); + } else { + int i,j,k; + int jend, jdir; + + stbiw__writef(s, "111 221 2222 11", 0,0,format+8, 0,0,0, 0,0,x,y, (colorbytes + has_alpha) * 8, has_alpha * 8); + + if (stbi__flip_vertically_on_write) { + j = 0; + jend = y; + jdir = 1; + } else { + j = y-1; + jend = -1; + jdir = -1; + } + for (; j != jend; j += jdir) { + unsigned char *row = (unsigned char *) data + j * x * comp; + int len; + + for (i = 0; i < x; i += len) { + unsigned char *begin = row + i * comp; + int diff = 1; + len = 1; + + if (i < x - 1) { + ++len; + diff = memcmp(begin, row + (i + 1) * comp, comp); + if (diff) { + const unsigned char *prev = begin; + for (k = i + 2; k < x && len < 128; ++k) { + if (memcmp(prev, row + k * comp, comp)) { + prev += comp; + ++len; + } else { + --len; + break; + } + } + } else { + for (k = i + 2; k < x && len < 128; ++k) { + if (!memcmp(begin, row + k * comp, comp)) { + ++len; + } else { + break; + } + } + } + } + + if (diff) { + unsigned char header = STBIW_UCHAR(len - 1); + s->func(s->context, &header, 1); + for (k = 0; k < len; ++k) { + stbiw__write_pixel(s, -1, comp, has_alpha, 0, begin + k * comp); + } + } else { + unsigned char header = STBIW_UCHAR(len - 129); + s->func(s->context, &header, 1); + stbiw__write_pixel(s, -1, comp, has_alpha, 0, begin); + } + } + } + } + return 1; +} + +STBIWDEF int stbi_write_tga_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data) +{ + stbi__write_context s; + stbi__start_write_callbacks(&s, func, context); + return stbi_write_tga_core(&s, x, y, comp, (void *) data); +} + +#ifndef STBI_WRITE_NO_STDIO +STBIWDEF int stbi_write_tga(char const *filename, int x, int y, int comp, const void *data) +{ + stbi__write_context s; + if (stbi__start_write_file(&s,filename)) { + int r = stbi_write_tga_core(&s, x, y, comp, (void *) data); + stbi__end_write_file(&s); + return r; + } else + return 0; +} +#endif + +// ************************************************************************************************* +// Radiance RGBE HDR writer +// by Baldur Karlsson + +#define stbiw__max(a, b) ((a) > (b) ? (a) : (b)) + +static void stbiw__linear_to_rgbe(unsigned char *rgbe, float *linear) +{ + int exponent; + float maxcomp = stbiw__max(linear[0], stbiw__max(linear[1], linear[2])); + + if (maxcomp < 1e-32f) { + rgbe[0] = rgbe[1] = rgbe[2] = rgbe[3] = 0; + } else { + float normalize = (float) frexp(maxcomp, &exponent) * 256.0f/maxcomp; + + rgbe[0] = (unsigned char)(linear[0] * normalize); + rgbe[1] = (unsigned char)(linear[1] * normalize); + rgbe[2] = (unsigned char)(linear[2] * normalize); + rgbe[3] = (unsigned char)(exponent + 128); + } +} + +static void stbiw__write_run_data(stbi__write_context *s, int length, unsigned char databyte) +{ + unsigned char lengthbyte = STBIW_UCHAR(length+128); + STBIW_ASSERT(length+128 <= 255); + s->func(s->context, &lengthbyte, 1); + s->func(s->context, &databyte, 1); +} + +static void stbiw__write_dump_data(stbi__write_context *s, int length, unsigned char *data) +{ + unsigned char lengthbyte = STBIW_UCHAR(length); + STBIW_ASSERT(length <= 128); // inconsistent with spec but consistent with official code + s->func(s->context, &lengthbyte, 1); + s->func(s->context, data, length); +} + +static void stbiw__write_hdr_scanline(stbi__write_context *s, int width, int ncomp, unsigned char *scratch, float *scanline) +{ + unsigned char scanlineheader[4] = { 2, 2, 0, 0 }; + unsigned char rgbe[4]; + float linear[3]; + int x; + + scanlineheader[2] = (width&0xff00)>>8; + scanlineheader[3] = (width&0x00ff); + + /* skip RLE for images too small or large */ + if (width < 8 || width >= 32768) { + for (x=0; x < width; x++) { + switch (ncomp) { + case 4: /* fallthrough */ + case 3: linear[2] = scanline[x*ncomp + 2]; + linear[1] = scanline[x*ncomp + 1]; + linear[0] = scanline[x*ncomp + 0]; + break; + default: + linear[0] = linear[1] = linear[2] = scanline[x*ncomp + 0]; + break; + } + stbiw__linear_to_rgbe(rgbe, linear); + s->func(s->context, rgbe, 4); + } + } else { + int c,r; + /* encode into scratch buffer */ + for (x=0; x < width; x++) { + switch(ncomp) { + case 4: /* fallthrough */ + case 3: linear[2] = scanline[x*ncomp + 2]; + linear[1] = scanline[x*ncomp + 1]; + linear[0] = scanline[x*ncomp + 0]; + break; + default: + linear[0] = linear[1] = linear[2] = scanline[x*ncomp + 0]; + break; + } + stbiw__linear_to_rgbe(rgbe, linear); + scratch[x + width*0] = rgbe[0]; + scratch[x + width*1] = rgbe[1]; + scratch[x + width*2] = rgbe[2]; + scratch[x + width*3] = rgbe[3]; + } + + s->func(s->context, scanlineheader, 4); + + /* RLE each component separately */ + for (c=0; c < 4; c++) { + unsigned char *comp = &scratch[width*c]; + + x = 0; + while (x < width) { + // find first run + r = x; + while (r+2 < width) { + if (comp[r] == comp[r+1] && comp[r] == comp[r+2]) + break; + ++r; + } + if (r+2 >= width) + r = width; + // dump up to first run + while (x < r) { + int len = r-x; + if (len > 128) len = 128; + stbiw__write_dump_data(s, len, &comp[x]); + x += len; + } + // if there's a run, output it + if (r+2 < width) { // same test as what we break out of in search loop, so only true if we break'd + // find next byte after run + while (r < width && comp[r] == comp[x]) + ++r; + // output run up to r + while (x < r) { + int len = r-x; + if (len > 127) len = 127; + stbiw__write_run_data(s, len, comp[x]); + x += len; + } + } + } + } + } +} + +static int stbi_write_hdr_core(stbi__write_context *s, int x, int y, int comp, float *data) +{ + if (y <= 0 || x <= 0 || data == NULL) + return 0; + else { + // Each component is stored separately. Allocate scratch space for full output scanline. + unsigned char *scratch = (unsigned char *) STBIW_MALLOC(x*4); + int i, len; + char buffer[128]; + char header[] = "#?RADIANCE\n# Written by stb_image_write.h\nFORMAT=32-bit_rle_rgbe\n"; + s->func(s->context, header, sizeof(header)-1); + +#ifdef __STDC_WANT_SECURE_LIB__ + len = sprintf_s(buffer, sizeof(buffer), "EXPOSURE= 1.0000000000000\n\n-Y %d +X %d\n", y, x); +#else + len = sprintf(buffer, "EXPOSURE= 1.0000000000000\n\n-Y %d +X %d\n", y, x); +#endif + s->func(s->context, buffer, len); + + for(i=0; i < y; i++) + stbiw__write_hdr_scanline(s, x, comp, scratch, data + comp*x*(stbi__flip_vertically_on_write ? y-1-i : i)); + STBIW_FREE(scratch); + return 1; + } +} + +STBIWDEF int stbi_write_hdr_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const float *data) +{ + stbi__write_context s; + stbi__start_write_callbacks(&s, func, context); + return stbi_write_hdr_core(&s, x, y, comp, (float *) data); +} + +#ifndef STBI_WRITE_NO_STDIO +STBIWDEF int stbi_write_hdr(char const *filename, int x, int y, int comp, const float *data) +{ + stbi__write_context s; + if (stbi__start_write_file(&s,filename)) { + int r = stbi_write_hdr_core(&s, x, y, comp, (float *) data); + stbi__end_write_file(&s); + return r; + } else + return 0; +} +#endif // STBI_WRITE_NO_STDIO + + +////////////////////////////////////////////////////////////////////////////// +// +// PNG writer +// + +#ifndef STBIW_ZLIB_COMPRESS +// stretchy buffer; stbiw__sbpush() == vector<>::push_back() -- stbiw__sbcount() == vector<>::size() +#define stbiw__sbraw(a) ((int *) (a) - 2) +#define stbiw__sbm(a) stbiw__sbraw(a)[0] +#define stbiw__sbn(a) stbiw__sbraw(a)[1] + +#define stbiw__sbneedgrow(a,n) ((a)==0 || stbiw__sbn(a)+n >= stbiw__sbm(a)) +#define stbiw__sbmaybegrow(a,n) (stbiw__sbneedgrow(a,(n)) ? stbiw__sbgrow(a,n) : 0) +#define stbiw__sbgrow(a,n) stbiw__sbgrowf((void **) &(a), (n), sizeof(*(a))) + +#define stbiw__sbpush(a, v) (stbiw__sbmaybegrow(a,1), (a)[stbiw__sbn(a)++] = (v)) +#define stbiw__sbcount(a) ((a) ? stbiw__sbn(a) : 0) +#define stbiw__sbfree(a) ((a) ? STBIW_FREE(stbiw__sbraw(a)),0 : 0) + +static void *stbiw__sbgrowf(void **arr, int increment, int itemsize) +{ + int m = *arr ? 2*stbiw__sbm(*arr)+increment : increment+1; + void *p = STBIW_REALLOC_SIZED(*arr ? stbiw__sbraw(*arr) : 0, *arr ? (stbiw__sbm(*arr)*itemsize + sizeof(int)*2) : 0, itemsize * m + sizeof(int)*2); + STBIW_ASSERT(p); + if (p) { + if (!*arr) ((int *) p)[1] = 0; + *arr = (void *) ((int *) p + 2); + stbiw__sbm(*arr) = m; + } + return *arr; +} + +static unsigned char *stbiw__zlib_flushf(unsigned char *data, unsigned int *bitbuffer, int *bitcount) +{ + while (*bitcount >= 8) { + stbiw__sbpush(data, STBIW_UCHAR(*bitbuffer)); + *bitbuffer >>= 8; + *bitcount -= 8; + } + return data; +} + +static int stbiw__zlib_bitrev(int code, int codebits) +{ + int res=0; + while (codebits--) { + res = (res << 1) | (code & 1); + code >>= 1; + } + return res; +} + +static unsigned int stbiw__zlib_countm(unsigned char *a, unsigned char *b, int limit) +{ + int i; + for (i=0; i < limit && i < 258; ++i) + if (a[i] != b[i]) break; + return i; +} + +static unsigned int stbiw__zhash(unsigned char *data) +{ + stbiw_uint32 hash = data[0] + (data[1] << 8) + (data[2] << 16); + hash ^= hash << 3; + hash += hash >> 5; + hash ^= hash << 4; + hash += hash >> 17; + hash ^= hash << 25; + hash += hash >> 6; + return hash; +} + +#define stbiw__zlib_flush() (out = stbiw__zlib_flushf(out, &bitbuf, &bitcount)) +#define stbiw__zlib_add(code,codebits) \ + (bitbuf |= (code) << bitcount, bitcount += (codebits), stbiw__zlib_flush()) +#define stbiw__zlib_huffa(b,c) stbiw__zlib_add(stbiw__zlib_bitrev(b,c),c) +// default huffman tables +#define stbiw__zlib_huff1(n) stbiw__zlib_huffa(0x30 + (n), 8) +#define stbiw__zlib_huff2(n) stbiw__zlib_huffa(0x190 + (n)-144, 9) +#define stbiw__zlib_huff3(n) stbiw__zlib_huffa(0 + (n)-256,7) +#define stbiw__zlib_huff4(n) stbiw__zlib_huffa(0xc0 + (n)-280,8) +#define stbiw__zlib_huff(n) ((n) <= 143 ? stbiw__zlib_huff1(n) : (n) <= 255 ? stbiw__zlib_huff2(n) : (n) <= 279 ? stbiw__zlib_huff3(n) : stbiw__zlib_huff4(n)) +#define stbiw__zlib_huffb(n) ((n) <= 143 ? stbiw__zlib_huff1(n) : stbiw__zlib_huff2(n)) + +#define stbiw__ZHASH 16384 + +#endif // STBIW_ZLIB_COMPRESS + +STBIWDEF unsigned char * stbi_zlib_compress(unsigned char *data, int data_len, int *out_len, int quality) +{ +#ifdef STBIW_ZLIB_COMPRESS + // user provided a zlib compress implementation, use that + return STBIW_ZLIB_COMPRESS(data, data_len, out_len, quality); +#else // use builtin + static unsigned short lengthc[] = { 3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258, 259 }; + static unsigned char lengtheb[]= { 0,0,0,0,0,0,0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0 }; + static unsigned short distc[] = { 1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577, 32768 }; + static unsigned char disteb[] = { 0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13 }; + unsigned int bitbuf=0; + int i,j, bitcount=0; + unsigned char *out = NULL; + unsigned char ***hash_table = (unsigned char***) STBIW_MALLOC(stbiw__ZHASH * sizeof(char**)); + if (hash_table == NULL) + return NULL; + if (quality < 5) quality = 5; + + stbiw__sbpush(out, 0x78); // DEFLATE 32K window + stbiw__sbpush(out, 0x5e); // FLEVEL = 1 + stbiw__zlib_add(1,1); // BFINAL = 1 + stbiw__zlib_add(1,2); // BTYPE = 1 -- fixed huffman + + for (i=0; i < stbiw__ZHASH; ++i) + hash_table[i] = NULL; + + i=0; + while (i < data_len-3) { + // hash next 3 bytes of data to be compressed + int h = stbiw__zhash(data+i)&(stbiw__ZHASH-1), best=3; + unsigned char *bestloc = 0; + unsigned char **hlist = hash_table[h]; + int n = stbiw__sbcount(hlist); + for (j=0; j < n; ++j) { + if (hlist[j]-data > i-32768) { // if entry lies within window + int d = stbiw__zlib_countm(hlist[j], data+i, data_len-i); + if (d >= best) { best=d; bestloc=hlist[j]; } + } + } + // when hash table entry is too long, delete half the entries + if (hash_table[h] && stbiw__sbn(hash_table[h]) == 2*quality) { + STBIW_MEMMOVE(hash_table[h], hash_table[h]+quality, sizeof(hash_table[h][0])*quality); + stbiw__sbn(hash_table[h]) = quality; + } + stbiw__sbpush(hash_table[h],data+i); + + if (bestloc) { + // "lazy matching" - check match at *next* byte, and if it's better, do cur byte as literal + h = stbiw__zhash(data+i+1)&(stbiw__ZHASH-1); + hlist = hash_table[h]; + n = stbiw__sbcount(hlist); + for (j=0; j < n; ++j) { + if (hlist[j]-data > i-32767) { + int e = stbiw__zlib_countm(hlist[j], data+i+1, data_len-i-1); + if (e > best) { // if next match is better, bail on current match + bestloc = NULL; + break; + } + } + } + } + + if (bestloc) { + int d = (int) (data+i - bestloc); // distance back + STBIW_ASSERT(d <= 32767 && best <= 258); + for (j=0; best > lengthc[j+1]-1; ++j); + stbiw__zlib_huff(j+257); + if (lengtheb[j]) stbiw__zlib_add(best - lengthc[j], lengtheb[j]); + for (j=0; d > distc[j+1]-1; ++j); + stbiw__zlib_add(stbiw__zlib_bitrev(j,5),5); + if (disteb[j]) stbiw__zlib_add(d - distc[j], disteb[j]); + i += best; + } else { + stbiw__zlib_huffb(data[i]); + ++i; + } + } + // write out final bytes + for (;i < data_len; ++i) + stbiw__zlib_huffb(data[i]); + stbiw__zlib_huff(256); // end of block + // pad with 0 bits to byte boundary + while (bitcount) + stbiw__zlib_add(0,1); + + for (i=0; i < stbiw__ZHASH; ++i) + (void) stbiw__sbfree(hash_table[i]); + STBIW_FREE(hash_table); + + { + // compute adler32 on input + unsigned int s1=1, s2=0; + int blocklen = (int) (data_len % 5552); + j=0; + while (j < data_len) { + for (i=0; i < blocklen; ++i) { s1 += data[j+i]; s2 += s1; } + s1 %= 65521; s2 %= 65521; + j += blocklen; + blocklen = 5552; + } + stbiw__sbpush(out, STBIW_UCHAR(s2 >> 8)); + stbiw__sbpush(out, STBIW_UCHAR(s2)); + stbiw__sbpush(out, STBIW_UCHAR(s1 >> 8)); + stbiw__sbpush(out, STBIW_UCHAR(s1)); + } + *out_len = stbiw__sbn(out); + // make returned pointer freeable + STBIW_MEMMOVE(stbiw__sbraw(out), out, *out_len); + return (unsigned char *) stbiw__sbraw(out); +#endif // STBIW_ZLIB_COMPRESS +} + +static unsigned int stbiw__crc32(unsigned char *buffer, int len) +{ +#ifdef STBIW_CRC32 + return STBIW_CRC32(buffer, len); +#else + static unsigned int crc_table[256] = + { + 0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, 0x706AF48F, 0xE963A535, 0x9E6495A3, + 0x0eDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988, 0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91, + 0x1DB71064, 0x6AB020F2, 0xF3B97148, 0x84BE41DE, 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7, + 0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, 0x14015C4F, 0x63066CD9, 0xFA0F3D63, 0x8D080DF5, + 0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172, 0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, + 0x35B5A8FA, 0x42B2986C, 0xDBBBC9D6, 0xACBCF940, 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59, + 0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116, 0x21B4F4B5, 0x56B3C423, 0xCFBA9599, 0xB8BDA50F, + 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, 0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D, + 0x76DC4190, 0x01DB7106, 0x98D220BC, 0xEFD5102A, 0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433, + 0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, 0x7F6A0DBB, 0x086D3D2D, 0x91646C97, 0xE6635C01, + 0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E, 0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457, + 0x65B0D9C6, 0x12B7E950, 0x8BBEB8EA, 0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65, + 0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, 0x4ADFA541, 0x3DD895D7, 0xA4D1C46D, 0xD3D6F4FB, + 0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0, 0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9, + 0x5005713C, 0x270241AA, 0xBE0B1010, 0xC90C2086, 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F, + 0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, 0x2EB40D81, 0xB7BD5C3B, 0xC0BA6CAD, + 0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A, 0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683, + 0xE3630B12, 0x94643B84, 0x0D6D6A3E, 0x7A6A5AA8, 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1, + 0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE, 0xF762575D, 0x806567CB, 0x196C3671, 0x6E6B06E7, + 0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC, 0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, + 0xD6D6A3E8, 0xA1D1937E, 0x38D8C2C4, 0x4FDFF252, 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B, + 0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60, 0xDF60EFC3, 0xA867DF55, 0x316E8EEF, 0x4669BE79, + 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236, 0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F, + 0xC5BA3BBE, 0xB2BD0B28, 0x2BB45A92, 0x5CB36A04, 0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D, + 0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A, 0x9C0906A9, 0xEB0E363F, 0x72076785, 0x05005713, + 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38, 0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21, + 0x86D3D2D4, 0xF1D4E242, 0x68DDB3F8, 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777, + 0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, 0x8F659EFF, 0xF862AE69, 0x616BFFD3, 0x166CCF45, + 0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2, 0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB, + 0xAED16A4A, 0xD9D65ADC, 0x40DF0B66, 0x37D83BF0, 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9, + 0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, 0xBAD03605, 0xCDD70693, 0x54DE5729, 0x23D967BF, + 0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94, 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D + }; + + unsigned int crc = ~0u; + int i; + for (i=0; i < len; ++i) + crc = (crc >> 8) ^ crc_table[buffer[i] ^ (crc & 0xff)]; + return ~crc; +#endif +} + +#define stbiw__wpng4(o,a,b,c,d) ((o)[0]=STBIW_UCHAR(a),(o)[1]=STBIW_UCHAR(b),(o)[2]=STBIW_UCHAR(c),(o)[3]=STBIW_UCHAR(d),(o)+=4) +#define stbiw__wp32(data,v) stbiw__wpng4(data, (v)>>24,(v)>>16,(v)>>8,(v)); +#define stbiw__wptag(data,s) stbiw__wpng4(data, s[0],s[1],s[2],s[3]) + +static void stbiw__wpcrc(unsigned char **data, int len) +{ + unsigned int crc = stbiw__crc32(*data - len - 4, len+4); + stbiw__wp32(*data, crc); +} + +static unsigned char stbiw__paeth(int a, int b, int c) +{ + int p = a + b - c, pa = abs(p-a), pb = abs(p-b), pc = abs(p-c); + if (pa <= pb && pa <= pc) return STBIW_UCHAR(a); + if (pb <= pc) return STBIW_UCHAR(b); + return STBIW_UCHAR(c); +} + +// @OPTIMIZE: provide an option that always forces left-predict or paeth predict +static void stbiw__encode_png_line(unsigned char *pixels, int stride_bytes, int width, int height, int y, int n, int filter_type, signed char *line_buffer) +{ + static int mapping[] = { 0,1,2,3,4 }; + static int firstmap[] = { 0,1,0,5,6 }; + int *mymap = (y != 0) ? mapping : firstmap; + int i; + int type = mymap[filter_type]; + unsigned char *z = pixels + stride_bytes * (stbi__flip_vertically_on_write ? height-1-y : y); + int signed_stride = stbi__flip_vertically_on_write ? -stride_bytes : stride_bytes; + + if (type==0) { + memcpy(line_buffer, z, width*n); + return; + } + + // first loop isn't optimized since it's just one pixel + for (i = 0; i < n; ++i) { + switch (type) { + case 1: line_buffer[i] = z[i]; break; + case 2: line_buffer[i] = z[i] - z[i-signed_stride]; break; + case 3: line_buffer[i] = z[i] - (z[i-signed_stride]>>1); break; + case 4: line_buffer[i] = (signed char) (z[i] - stbiw__paeth(0,z[i-signed_stride],0)); break; + case 5: line_buffer[i] = z[i]; break; + case 6: line_buffer[i] = z[i]; break; + } + } + switch (type) { + case 1: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - z[i-n]; break; + case 2: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - z[i-signed_stride]; break; + case 3: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - ((z[i-n] + z[i-signed_stride])>>1); break; + case 4: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - stbiw__paeth(z[i-n], z[i-signed_stride], z[i-signed_stride-n]); break; + case 5: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - (z[i-n]>>1); break; + case 6: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - stbiw__paeth(z[i-n], 0,0); break; + } +} + +STBIWDEF unsigned char *stbi_write_png_to_mem(const unsigned char *pixels, int stride_bytes, int x, int y, int n, int *out_len) +{ + int force_filter = stbi_write_force_png_filter; + int ctype[5] = { -1, 0, 4, 2, 6 }; + unsigned char sig[8] = { 137,80,78,71,13,10,26,10 }; + unsigned char *out,*o, *filt, *zlib; + signed char *line_buffer; + int j,zlen; + + if (stride_bytes == 0) + stride_bytes = x * n; + + if (force_filter >= 5) { + force_filter = -1; + } + + filt = (unsigned char *) STBIW_MALLOC((x*n+1) * y); if (!filt) return 0; + line_buffer = (signed char *) STBIW_MALLOC(x * n); if (!line_buffer) { STBIW_FREE(filt); return 0; } + for (j=0; j < y; ++j) { + int filter_type; + if (force_filter > -1) { + filter_type = force_filter; + stbiw__encode_png_line((unsigned char*)(pixels), stride_bytes, x, y, j, n, force_filter, line_buffer); + } else { // Estimate the best filter by running through all of them: + int best_filter = 0, best_filter_val = 0x7fffffff, est, i; + for (filter_type = 0; filter_type < 5; filter_type++) { + stbiw__encode_png_line((unsigned char*)(pixels), stride_bytes, x, y, j, n, filter_type, line_buffer); + + // Estimate the entropy of the line using this filter; the less, the better. + est = 0; + for (i = 0; i < x*n; ++i) { + est += abs((signed char) line_buffer[i]); + } + if (est < best_filter_val) { + best_filter_val = est; + best_filter = filter_type; + } + } + if (filter_type != best_filter) { // If the last iteration already got us the best filter, don't redo it + stbiw__encode_png_line((unsigned char*)(pixels), stride_bytes, x, y, j, n, best_filter, line_buffer); + filter_type = best_filter; + } + } + // when we get here, filter_type contains the filter type, and line_buffer contains the data + filt[j*(x*n+1)] = (unsigned char) filter_type; + STBIW_MEMMOVE(filt+j*(x*n+1)+1, line_buffer, x*n); + } + STBIW_FREE(line_buffer); + zlib = stbi_zlib_compress(filt, y*( x*n+1), &zlen, stbi_write_png_compression_level); + STBIW_FREE(filt); + if (!zlib) return 0; + + // each tag requires 12 bytes of overhead + out = (unsigned char *) STBIW_MALLOC(8 + 12+13 + 12+zlen + 12); + if (!out) return 0; + *out_len = 8 + 12+13 + 12+zlen + 12; + + o=out; + STBIW_MEMMOVE(o,sig,8); o+= 8; + stbiw__wp32(o, 13); // header length + stbiw__wptag(o, "IHDR"); + stbiw__wp32(o, x); + stbiw__wp32(o, y); + *o++ = 8; + *o++ = STBIW_UCHAR(ctype[n]); + *o++ = 0; + *o++ = 0; + *o++ = 0; + stbiw__wpcrc(&o,13); + + stbiw__wp32(o, zlen); + stbiw__wptag(o, "IDAT"); + STBIW_MEMMOVE(o, zlib, zlen); + o += zlen; + STBIW_FREE(zlib); + stbiw__wpcrc(&o, zlen); + + stbiw__wp32(o,0); + stbiw__wptag(o, "IEND"); + stbiw__wpcrc(&o,0); + + STBIW_ASSERT(o == out + *out_len); + + return out; +} + +#ifndef STBI_WRITE_NO_STDIO +STBIWDEF int stbi_write_png(char const *filename, int x, int y, int comp, const void *data, int stride_bytes) +{ + FILE *f; + int len; + unsigned char *png = stbi_write_png_to_mem((const unsigned char *) data, stride_bytes, x, y, comp, &len); + if (png == NULL) return 0; + + f = stbiw__fopen(filename, "wb"); + if (!f) { STBIW_FREE(png); return 0; } + fwrite(png, 1, len, f); + fclose(f); + STBIW_FREE(png); + return 1; +} +#endif + +STBIWDEF int stbi_write_png_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data, int stride_bytes) +{ + int len; + unsigned char *png = stbi_write_png_to_mem((const unsigned char *) data, stride_bytes, x, y, comp, &len); + if (png == NULL) return 0; + func(context, png, len); + STBIW_FREE(png); + return 1; +} + + +/* *************************************************************************** + * + * JPEG writer + * + * This is based on Jon Olick's jo_jpeg.cpp: + * public domain Simple, Minimalistic JPEG writer - http://www.jonolick.com/code.html + */ + +static const unsigned char stbiw__jpg_ZigZag[] = { 0,1,5,6,14,15,27,28,2,4,7,13,16,26,29,42,3,8,12,17,25,30,41,43,9,11,18, + 24,31,40,44,53,10,19,23,32,39,45,52,54,20,22,33,38,46,51,55,60,21,34,37,47,50,56,59,61,35,36,48,49,57,58,62,63 }; + +static void stbiw__jpg_writeBits(stbi__write_context *s, int *bitBufP, int *bitCntP, const unsigned short *bs) { + int bitBuf = *bitBufP, bitCnt = *bitCntP; + bitCnt += bs[1]; + bitBuf |= bs[0] << (24 - bitCnt); + while(bitCnt >= 8) { + unsigned char c = (bitBuf >> 16) & 255; + stbiw__putc(s, c); + if(c == 255) { + stbiw__putc(s, 0); + } + bitBuf <<= 8; + bitCnt -= 8; + } + *bitBufP = bitBuf; + *bitCntP = bitCnt; +} + +static void stbiw__jpg_DCT(float *d0p, float *d1p, float *d2p, float *d3p, float *d4p, float *d5p, float *d6p, float *d7p) { + float d0 = *d0p, d1 = *d1p, d2 = *d2p, d3 = *d3p, d4 = *d4p, d5 = *d5p, d6 = *d6p, d7 = *d7p; + float z1, z2, z3, z4, z5, z11, z13; + + float tmp0 = d0 + d7; + float tmp7 = d0 - d7; + float tmp1 = d1 + d6; + float tmp6 = d1 - d6; + float tmp2 = d2 + d5; + float tmp5 = d2 - d5; + float tmp3 = d3 + d4; + float tmp4 = d3 - d4; + + // Even part + float tmp10 = tmp0 + tmp3; // phase 2 + float tmp13 = tmp0 - tmp3; + float tmp11 = tmp1 + tmp2; + float tmp12 = tmp1 - tmp2; + + d0 = tmp10 + tmp11; // phase 3 + d4 = tmp10 - tmp11; + + z1 = (tmp12 + tmp13) * 0.707106781f; // c4 + d2 = tmp13 + z1; // phase 5 + d6 = tmp13 - z1; + + // Odd part + tmp10 = tmp4 + tmp5; // phase 2 + tmp11 = tmp5 + tmp6; + tmp12 = tmp6 + tmp7; + + // The rotator is modified from fig 4-8 to avoid extra negations. + z5 = (tmp10 - tmp12) * 0.382683433f; // c6 + z2 = tmp10 * 0.541196100f + z5; // c2-c6 + z4 = tmp12 * 1.306562965f + z5; // c2+c6 + z3 = tmp11 * 0.707106781f; // c4 + + z11 = tmp7 + z3; // phase 5 + z13 = tmp7 - z3; + + *d5p = z13 + z2; // phase 6 + *d3p = z13 - z2; + *d1p = z11 + z4; + *d7p = z11 - z4; + + *d0p = d0; *d2p = d2; *d4p = d4; *d6p = d6; +} + +static void stbiw__jpg_calcBits(int val, unsigned short bits[2]) { + int tmp1 = val < 0 ? -val : val; + val = val < 0 ? val-1 : val; + bits[1] = 1; + while(tmp1 >>= 1) { + ++bits[1]; + } + bits[0] = val & ((1<0)&&(DU[end0pos]==0); --end0pos) { + } + // end0pos = first element in reverse order !=0 + if(end0pos == 0) { + stbiw__jpg_writeBits(s, bitBuf, bitCnt, EOB); + return DU[0]; + } + for(i = 1; i <= end0pos; ++i) { + int startpos = i; + int nrzeroes; + unsigned short bits[2]; + for (; DU[i]==0 && i<=end0pos; ++i) { + } + nrzeroes = i-startpos; + if ( nrzeroes >= 16 ) { + int lng = nrzeroes>>4; + int nrmarker; + for (nrmarker=1; nrmarker <= lng; ++nrmarker) + stbiw__jpg_writeBits(s, bitBuf, bitCnt, M16zeroes); + nrzeroes &= 15; + } + stbiw__jpg_calcBits(DU[i], bits); + stbiw__jpg_writeBits(s, bitBuf, bitCnt, HTAC[(nrzeroes<<4)+bits[1]]); + stbiw__jpg_writeBits(s, bitBuf, bitCnt, bits); + } + if(end0pos != 63) { + stbiw__jpg_writeBits(s, bitBuf, bitCnt, EOB); + } + return DU[0]; +} + +static int stbi_write_jpg_core(stbi__write_context *s, int width, int height, int comp, const void* data, int quality) { + // Constants that don't pollute global namespace + static const unsigned char std_dc_luminance_nrcodes[] = {0,0,1,5,1,1,1,1,1,1,0,0,0,0,0,0,0}; + static const unsigned char std_dc_luminance_values[] = {0,1,2,3,4,5,6,7,8,9,10,11}; + static const unsigned char std_ac_luminance_nrcodes[] = {0,0,2,1,3,3,2,4,3,5,5,4,4,0,0,1,0x7d}; + static const unsigned char std_ac_luminance_values[] = { + 0x01,0x02,0x03,0x00,0x04,0x11,0x05,0x12,0x21,0x31,0x41,0x06,0x13,0x51,0x61,0x07,0x22,0x71,0x14,0x32,0x81,0x91,0xa1,0x08, + 0x23,0x42,0xb1,0xc1,0x15,0x52,0xd1,0xf0,0x24,0x33,0x62,0x72,0x82,0x09,0x0a,0x16,0x17,0x18,0x19,0x1a,0x25,0x26,0x27,0x28, + 0x29,0x2a,0x34,0x35,0x36,0x37,0x38,0x39,0x3a,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4a,0x53,0x54,0x55,0x56,0x57,0x58,0x59, + 0x5a,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x83,0x84,0x85,0x86,0x87,0x88,0x89, + 0x8a,0x92,0x93,0x94,0x95,0x96,0x97,0x98,0x99,0x9a,0xa2,0xa3,0xa4,0xa5,0xa6,0xa7,0xa8,0xa9,0xaa,0xb2,0xb3,0xb4,0xb5,0xb6, + 0xb7,0xb8,0xb9,0xba,0xc2,0xc3,0xc4,0xc5,0xc6,0xc7,0xc8,0xc9,0xca,0xd2,0xd3,0xd4,0xd5,0xd6,0xd7,0xd8,0xd9,0xda,0xe1,0xe2, + 0xe3,0xe4,0xe5,0xe6,0xe7,0xe8,0xe9,0xea,0xf1,0xf2,0xf3,0xf4,0xf5,0xf6,0xf7,0xf8,0xf9,0xfa + }; + static const unsigned char std_dc_chrominance_nrcodes[] = {0,0,3,1,1,1,1,1,1,1,1,1,0,0,0,0,0}; + static const unsigned char std_dc_chrominance_values[] = {0,1,2,3,4,5,6,7,8,9,10,11}; + static const unsigned char std_ac_chrominance_nrcodes[] = {0,0,2,1,2,4,4,3,4,7,5,4,4,0,1,2,0x77}; + static const unsigned char std_ac_chrominance_values[] = { + 0x00,0x01,0x02,0x03,0x11,0x04,0x05,0x21,0x31,0x06,0x12,0x41,0x51,0x07,0x61,0x71,0x13,0x22,0x32,0x81,0x08,0x14,0x42,0x91, + 0xa1,0xb1,0xc1,0x09,0x23,0x33,0x52,0xf0,0x15,0x62,0x72,0xd1,0x0a,0x16,0x24,0x34,0xe1,0x25,0xf1,0x17,0x18,0x19,0x1a,0x26, + 0x27,0x28,0x29,0x2a,0x35,0x36,0x37,0x38,0x39,0x3a,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4a,0x53,0x54,0x55,0x56,0x57,0x58, + 0x59,0x5a,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x82,0x83,0x84,0x85,0x86,0x87, + 0x88,0x89,0x8a,0x92,0x93,0x94,0x95,0x96,0x97,0x98,0x99,0x9a,0xa2,0xa3,0xa4,0xa5,0xa6,0xa7,0xa8,0xa9,0xaa,0xb2,0xb3,0xb4, + 0xb5,0xb6,0xb7,0xb8,0xb9,0xba,0xc2,0xc3,0xc4,0xc5,0xc6,0xc7,0xc8,0xc9,0xca,0xd2,0xd3,0xd4,0xd5,0xd6,0xd7,0xd8,0xd9,0xda, + 0xe2,0xe3,0xe4,0xe5,0xe6,0xe7,0xe8,0xe9,0xea,0xf2,0xf3,0xf4,0xf5,0xf6,0xf7,0xf8,0xf9,0xfa + }; + // Huffman tables + static const unsigned short YDC_HT[256][2] = { {0,2},{2,3},{3,3},{4,3},{5,3},{6,3},{14,4},{30,5},{62,6},{126,7},{254,8},{510,9}}; + static const unsigned short UVDC_HT[256][2] = { {0,2},{1,2},{2,2},{6,3},{14,4},{30,5},{62,6},{126,7},{254,8},{510,9},{1022,10},{2046,11}}; + static const unsigned short YAC_HT[256][2] = { + {10,4},{0,2},{1,2},{4,3},{11,4},{26,5},{120,7},{248,8},{1014,10},{65410,16},{65411,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {12,4},{27,5},{121,7},{502,9},{2038,11},{65412,16},{65413,16},{65414,16},{65415,16},{65416,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {28,5},{249,8},{1015,10},{4084,12},{65417,16},{65418,16},{65419,16},{65420,16},{65421,16},{65422,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {58,6},{503,9},{4085,12},{65423,16},{65424,16},{65425,16},{65426,16},{65427,16},{65428,16},{65429,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {59,6},{1016,10},{65430,16},{65431,16},{65432,16},{65433,16},{65434,16},{65435,16},{65436,16},{65437,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {122,7},{2039,11},{65438,16},{65439,16},{65440,16},{65441,16},{65442,16},{65443,16},{65444,16},{65445,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {123,7},{4086,12},{65446,16},{65447,16},{65448,16},{65449,16},{65450,16},{65451,16},{65452,16},{65453,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {250,8},{4087,12},{65454,16},{65455,16},{65456,16},{65457,16},{65458,16},{65459,16},{65460,16},{65461,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {504,9},{32704,15},{65462,16},{65463,16},{65464,16},{65465,16},{65466,16},{65467,16},{65468,16},{65469,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {505,9},{65470,16},{65471,16},{65472,16},{65473,16},{65474,16},{65475,16},{65476,16},{65477,16},{65478,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {506,9},{65479,16},{65480,16},{65481,16},{65482,16},{65483,16},{65484,16},{65485,16},{65486,16},{65487,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {1017,10},{65488,16},{65489,16},{65490,16},{65491,16},{65492,16},{65493,16},{65494,16},{65495,16},{65496,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {1018,10},{65497,16},{65498,16},{65499,16},{65500,16},{65501,16},{65502,16},{65503,16},{65504,16},{65505,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {2040,11},{65506,16},{65507,16},{65508,16},{65509,16},{65510,16},{65511,16},{65512,16},{65513,16},{65514,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {65515,16},{65516,16},{65517,16},{65518,16},{65519,16},{65520,16},{65521,16},{65522,16},{65523,16},{65524,16},{0,0},{0,0},{0,0},{0,0},{0,0}, + {2041,11},{65525,16},{65526,16},{65527,16},{65528,16},{65529,16},{65530,16},{65531,16},{65532,16},{65533,16},{65534,16},{0,0},{0,0},{0,0},{0,0},{0,0} + }; + static const unsigned short UVAC_HT[256][2] = { + {0,2},{1,2},{4,3},{10,4},{24,5},{25,5},{56,6},{120,7},{500,9},{1014,10},{4084,12},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {11,4},{57,6},{246,8},{501,9},{2038,11},{4085,12},{65416,16},{65417,16},{65418,16},{65419,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {26,5},{247,8},{1015,10},{4086,12},{32706,15},{65420,16},{65421,16},{65422,16},{65423,16},{65424,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {27,5},{248,8},{1016,10},{4087,12},{65425,16},{65426,16},{65427,16},{65428,16},{65429,16},{65430,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {58,6},{502,9},{65431,16},{65432,16},{65433,16},{65434,16},{65435,16},{65436,16},{65437,16},{65438,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {59,6},{1017,10},{65439,16},{65440,16},{65441,16},{65442,16},{65443,16},{65444,16},{65445,16},{65446,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {121,7},{2039,11},{65447,16},{65448,16},{65449,16},{65450,16},{65451,16},{65452,16},{65453,16},{65454,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {122,7},{2040,11},{65455,16},{65456,16},{65457,16},{65458,16},{65459,16},{65460,16},{65461,16},{65462,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {249,8},{65463,16},{65464,16},{65465,16},{65466,16},{65467,16},{65468,16},{65469,16},{65470,16},{65471,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {503,9},{65472,16},{65473,16},{65474,16},{65475,16},{65476,16},{65477,16},{65478,16},{65479,16},{65480,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {504,9},{65481,16},{65482,16},{65483,16},{65484,16},{65485,16},{65486,16},{65487,16},{65488,16},{65489,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {505,9},{65490,16},{65491,16},{65492,16},{65493,16},{65494,16},{65495,16},{65496,16},{65497,16},{65498,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {506,9},{65499,16},{65500,16},{65501,16},{65502,16},{65503,16},{65504,16},{65505,16},{65506,16},{65507,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {2041,11},{65508,16},{65509,16},{65510,16},{65511,16},{65512,16},{65513,16},{65514,16},{65515,16},{65516,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {16352,14},{65517,16},{65518,16},{65519,16},{65520,16},{65521,16},{65522,16},{65523,16},{65524,16},{65525,16},{0,0},{0,0},{0,0},{0,0},{0,0}, + {1018,10},{32707,15},{65526,16},{65527,16},{65528,16},{65529,16},{65530,16},{65531,16},{65532,16},{65533,16},{65534,16},{0,0},{0,0},{0,0},{0,0},{0,0} + }; + static const int YQT[] = {16,11,10,16,24,40,51,61,12,12,14,19,26,58,60,55,14,13,16,24,40,57,69,56,14,17,22,29,51,87,80,62,18,22, + 37,56,68,109,103,77,24,35,55,64,81,104,113,92,49,64,78,87,103,121,120,101,72,92,95,98,112,100,103,99}; + static const int UVQT[] = {17,18,24,47,99,99,99,99,18,21,26,66,99,99,99,99,24,26,56,99,99,99,99,99,47,66,99,99,99,99,99,99, + 99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99}; + static const float aasf[] = { 1.0f * 2.828427125f, 1.387039845f * 2.828427125f, 1.306562965f * 2.828427125f, 1.175875602f * 2.828427125f, + 1.0f * 2.828427125f, 0.785694958f * 2.828427125f, 0.541196100f * 2.828427125f, 0.275899379f * 2.828427125f }; + + int row, col, i, k; + float fdtbl_Y[64], fdtbl_UV[64]; + unsigned char YTable[64], UVTable[64]; + + if(!data || !width || !height || comp > 4 || comp < 1) { + return 0; + } + + quality = quality ? quality : 90; + quality = quality < 1 ? 1 : quality > 100 ? 100 : quality; + quality = quality < 50 ? 5000 / quality : 200 - quality * 2; + + for(i = 0; i < 64; ++i) { + int uvti, yti = (YQT[i]*quality+50)/100; + YTable[stbiw__jpg_ZigZag[i]] = (unsigned char) (yti < 1 ? 1 : yti > 255 ? 255 : yti); + uvti = (UVQT[i]*quality+50)/100; + UVTable[stbiw__jpg_ZigZag[i]] = (unsigned char) (uvti < 1 ? 1 : uvti > 255 ? 255 : uvti); + } + + for(row = 0, k = 0; row < 8; ++row) { + for(col = 0; col < 8; ++col, ++k) { + fdtbl_Y[k] = 1 / (YTable [stbiw__jpg_ZigZag[k]] * aasf[row] * aasf[col]); + fdtbl_UV[k] = 1 / (UVTable[stbiw__jpg_ZigZag[k]] * aasf[row] * aasf[col]); + } + } + + // Write Headers + { + static const unsigned char head0[] = { 0xFF,0xD8,0xFF,0xE0,0,0x10,'J','F','I','F',0,1,1,0,0,1,0,1,0,0,0xFF,0xDB,0,0x84,0 }; + static const unsigned char head2[] = { 0xFF,0xDA,0,0xC,3,1,0,2,0x11,3,0x11,0,0x3F,0 }; + const unsigned char head1[] = { 0xFF,0xC0,0,0x11,8,(unsigned char)(height>>8),STBIW_UCHAR(height),(unsigned char)(width>>8),STBIW_UCHAR(width), + 3,1,0x11,0,2,0x11,1,3,0x11,1,0xFF,0xC4,0x01,0xA2,0 }; + s->func(s->context, (void*)head0, sizeof(head0)); + s->func(s->context, (void*)YTable, sizeof(YTable)); + stbiw__putc(s, 1); + s->func(s->context, UVTable, sizeof(UVTable)); + s->func(s->context, (void*)head1, sizeof(head1)); + s->func(s->context, (void*)(std_dc_luminance_nrcodes+1), sizeof(std_dc_luminance_nrcodes)-1); + s->func(s->context, (void*)std_dc_luminance_values, sizeof(std_dc_luminance_values)); + stbiw__putc(s, 0x10); // HTYACinfo + s->func(s->context, (void*)(std_ac_luminance_nrcodes+1), sizeof(std_ac_luminance_nrcodes)-1); + s->func(s->context, (void*)std_ac_luminance_values, sizeof(std_ac_luminance_values)); + stbiw__putc(s, 1); // HTUDCinfo + s->func(s->context, (void*)(std_dc_chrominance_nrcodes+1), sizeof(std_dc_chrominance_nrcodes)-1); + s->func(s->context, (void*)std_dc_chrominance_values, sizeof(std_dc_chrominance_values)); + stbiw__putc(s, 0x11); // HTUACinfo + s->func(s->context, (void*)(std_ac_chrominance_nrcodes+1), sizeof(std_ac_chrominance_nrcodes)-1); + s->func(s->context, (void*)std_ac_chrominance_values, sizeof(std_ac_chrominance_values)); + s->func(s->context, (void*)head2, sizeof(head2)); + } + + // Encode 8x8 macroblocks + { + static const unsigned short fillBits[] = {0x7F, 7}; + const unsigned char *imageData = (const unsigned char *)data; + int DCY=0, DCU=0, DCV=0; + int bitBuf=0, bitCnt=0; + // comp == 2 is grey+alpha (alpha is ignored) + int ofsG = comp > 2 ? 1 : 0, ofsB = comp > 2 ? 2 : 0; + int x, y, pos; + for(y = 0; y < height; y += 8) { + for(x = 0; x < width; x += 8) { + float YDU[64], UDU[64], VDU[64]; + for(row = y, pos = 0; row < y+8; ++row) { + // row >= height => use last input row + int clamped_row = (row < height) ? row : height - 1; + int base_p = (stbi__flip_vertically_on_write ? (height-1-clamped_row) : clamped_row)*width*comp; + for(col = x; col < x+8; ++col, ++pos) { + float r, g, b; + // if col >= width => use pixel from last input column + int p = base_p + ((col < width) ? col : (width-1))*comp; + + r = imageData[p+0]; + g = imageData[p+ofsG]; + b = imageData[p+ofsB]; + YDU[pos]=+0.29900f*r+0.58700f*g+0.11400f*b-128; + UDU[pos]=-0.16874f*r-0.33126f*g+0.50000f*b; + VDU[pos]=+0.50000f*r-0.41869f*g-0.08131f*b; + } + } + + DCY = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, YDU, fdtbl_Y, DCY, YDC_HT, YAC_HT); + DCU = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, UDU, fdtbl_UV, DCU, UVDC_HT, UVAC_HT); + DCV = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, VDU, fdtbl_UV, DCV, UVDC_HT, UVAC_HT); + } + } + + // Do the bit alignment of the EOI marker + stbiw__jpg_writeBits(s, &bitBuf, &bitCnt, fillBits); + } + + // EOI + stbiw__putc(s, 0xFF); + stbiw__putc(s, 0xD9); + + return 1; +} + +STBIWDEF int stbi_write_jpg_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data, int quality) +{ + stbi__write_context s; + stbi__start_write_callbacks(&s, func, context); + return stbi_write_jpg_core(&s, x, y, comp, (void *) data, quality); +} + + +#ifndef STBI_WRITE_NO_STDIO +STBIWDEF int stbi_write_jpg(char const *filename, int x, int y, int comp, const void *data, int quality) +{ + stbi__write_context s; + if (stbi__start_write_file(&s,filename)) { + int r = stbi_write_jpg_core(&s, x, y, comp, data, quality); + stbi__end_write_file(&s); + return r; + } else + return 0; +} +#endif + +#endif // STB_IMAGE_WRITE_IMPLEMENTATION + +/* Revision history + 1.10 (2019-02-07) + support utf8 filenames in Windows; fix warnings and platform ifdefs + 1.09 (2018-02-11) + fix typo in zlib quality API, improve STB_I_W_STATIC in C++ + 1.08 (2018-01-29) + add stbi__flip_vertically_on_write, external zlib, zlib quality, choose PNG filter + 1.07 (2017-07-24) + doc fix + 1.06 (2017-07-23) + writing JPEG (using Jon Olick's code) + 1.05 ??? + 1.04 (2017-03-03) + monochrome BMP expansion + 1.03 ??? + 1.02 (2016-04-02) + avoid allocating large structures on the stack + 1.01 (2016-01-16) + STBIW_REALLOC_SIZED: support allocators with no realloc support + avoid race-condition in crc initialization + minor compile issues + 1.00 (2015-09-14) + installable file IO function + 0.99 (2015-09-13) + warning fixes; TGA rle support + 0.98 (2015-04-08) + added STBIW_MALLOC, STBIW_ASSERT etc + 0.97 (2015-01-18) + fixed HDR asserts, rewrote HDR rle logic + 0.96 (2015-01-17) + add HDR output + fix monochrome BMP + 0.95 (2014-08-17) + add monochrome TGA output + 0.94 (2014-05-31) + rename private functions to avoid conflicts with stb_image.h + 0.93 (2014-05-27) + warning fixes + 0.92 (2010-08-01) + casts to unsigned char to fix warnings + 0.91 (2010-07-17) + first public release + 0.90 first internal release +*/ + +/* +------------------------------------------------------------------------------ +This software is available under 2 licenses -- choose whichever you prefer. +------------------------------------------------------------------------------ +ALTERNATIVE A - MIT License +Copyright (c) 2017 Sean Barrett +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +------------------------------------------------------------------------------ +ALTERNATIVE B - Public Domain (www.unlicense.org) +This is free and unencumbered software released into the public domain. +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, +commercial or non-commercial, and by any means. +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +------------------------------------------------------------------------------ +*/ diff --git a/libs/winpthreads/include/pthread.h b/libs/winpthreads/include/pthread.h new file mode 100644 index 0000000..93d7c11 --- /dev/null +++ b/libs/winpthreads/include/pthread.h @@ -0,0 +1,692 @@ +/* + Copyright (c) 2011-2016 mingw-w64 project + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +/* + * Parts of this library are derived by: + * + * Posix Threads library for Microsoft Windows + * + * Use at own risk, there is no implied warranty to this code. + * It uses undocumented features of Microsoft Windows that can change + * at any time in the future. + * + * (C) 2010 Lockless Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Lockless Inc. nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AN + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + */ +#ifndef WIN_PTHREADS_H +#define WIN_PTHREADS_H + +#include +#include +#include + +#include +#include +#include +#include + +#include + +#include "pthread_compat.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define __WINPTHREADS_VERSION_MAJOR 0 +#define __WINPTHREADS_VERSION_MINOR 5 +#define __WINPTHREADS_VERSION_PATCHLEVEL 0 + +/* MSB 8-bit major version, 8-bit minor version, 16-bit patch level. */ +#define __WINPTHREADS_VERSION 0x00050000 + +#if defined(IN_WINPTHREAD) +# if defined(DLL_EXPORT) +# define WINPTHREAD_API __declspec(dllexport) /* building the DLL */ +# else +# define WINPTHREAD_API /* building the static library */ +# endif +#else +# if defined(WINPTHREADS_USE_DLLIMPORT) +# define WINPTHREAD_API __declspec(dllimport) /* user wants explicit `dllimport` */ +# else +# define WINPTHREAD_API /* the default; auto imported in case of DLL */ +# endif +#endif + +/* #define WINPTHREAD_DBG 1 */ + +/* Compatibility stuff: */ +#define RWLS_PER_THREAD 8 + +/* Error-codes. */ +#ifndef ETIMEDOUT +#define ETIMEDOUT 138 +#endif +#ifndef ENOTSUP +#define ENOTSUP 129 +#endif +#ifndef EWOULDBLOCK +#define EWOULDBLOCK 140 +#endif + +/* pthread specific defines. */ + +#define PTHREAD_CANCEL_DISABLE 0 +#define PTHREAD_CANCEL_ENABLE 0x01 + +#define PTHREAD_CANCEL_DEFERRED 0 +#define PTHREAD_CANCEL_ASYNCHRONOUS 0x02 + +#define PTHREAD_CREATE_JOINABLE 0 +#define PTHREAD_CREATE_DETACHED 0x04 + +#define PTHREAD_EXPLICIT_SCHED 0 +#define PTHREAD_INHERIT_SCHED 0x08 + +#define PTHREAD_SCOPE_PROCESS 0 +#define PTHREAD_SCOPE_SYSTEM 0x10 + +#define PTHREAD_DEFAULT_ATTR (PTHREAD_CANCEL_ENABLE) + +#define PTHREAD_CANCELED ((void *) (intptr_t) 0xDEADBEEF) + +#define _PTHREAD_NULL_THREAD ((pthread_t) 0) + +#define PTHREAD_ONCE_INIT 0 + +#define PTHREAD_DESTRUCTOR_ITERATIONS 256 +#define PTHREAD_KEYS_MAX (1<<20) + +#define PTHREAD_MUTEX_NORMAL 0 +#define PTHREAD_MUTEX_ERRORCHECK 1 +#define PTHREAD_MUTEX_RECURSIVE 2 +#define PTHREAD_MUTEX_DEFAULT PTHREAD_MUTEX_NORMAL + +#define PTHREAD_MUTEX_SHARED 1 +#define PTHREAD_MUTEX_PRIVATE 0 + +#define PTHREAD_PRIO_NONE 0 +#define PTHREAD_PRIO_INHERIT 8 +#define PTHREAD_PRIO_PROTECT 16 +#define PTHREAD_PRIO_MULT 32 +#define PTHREAD_PROCESS_SHARED 1 +#define PTHREAD_PROCESS_PRIVATE 0 + +#define PTHREAD_MUTEX_FAST_NP PTHREAD_MUTEX_NORMAL +#define PTHREAD_MUTEX_TIMED_NP PTHREAD_MUTEX_FAST_NP +#define PTHREAD_MUTEX_ADAPTIVE_NP PTHREAD_MUTEX_FAST_NP +#define PTHREAD_MUTEX_ERRORCHECK_NP PTHREAD_MUTEX_ERRORCHECK +#define PTHREAD_MUTEX_RECURSIVE_NP PTHREAD_MUTEX_RECURSIVE + +WINPTHREAD_API void * pthread_timechange_handler_np(void * dummy); +WINPTHREAD_API int pthread_delay_np (const struct timespec *interval); +WINPTHREAD_API int pthread_num_processors_np(void); +WINPTHREAD_API int pthread_set_num_processors_np(int n); + +#define PTHREAD_BARRIER_SERIAL_THREAD 1 + +/* maximum number of times a read lock may be obtained */ +#define MAX_READ_LOCKS (INT_MAX - 1) + +/* No fork() in windows - so ignore this */ +#define pthread_atfork(F1,F2,F3) 0 + +/* unsupported stuff: */ +#define pthread_mutex_getprioceiling(M, P) ENOTSUP +#define pthread_mutex_setprioceiling(M, P) ENOTSUP +#define pthread_getcpuclockid(T, C) ENOTSUP +#define pthread_attr_getguardsize(A, S) ENOTSUP +#define pthread_attr_setgaurdsize(A, S) ENOTSUP + +typedef long pthread_once_t; +typedef unsigned pthread_mutexattr_t; +typedef unsigned pthread_key_t; +typedef void *pthread_barrierattr_t; +typedef int pthread_condattr_t; +typedef int pthread_rwlockattr_t; + +/* +struct _pthread_v; + +typedef struct pthread_t { + struct _pthread_v *p; + int x; +} pthread_t; +*/ + +typedef uintptr_t pthread_t; + +typedef struct _pthread_cleanup _pthread_cleanup; +struct _pthread_cleanup +{ + void (*func)(void *); + void *arg; + _pthread_cleanup *next; +}; + +#define pthread_cleanup_push(F, A) \ + do { \ + const _pthread_cleanup _pthread_cup = \ + { (F), (A), *pthread_getclean() }; \ + MemoryBarrier(); \ + *pthread_getclean() = (_pthread_cleanup *) &_pthread_cup; \ + MemoryBarrier(); \ + do { \ + do {} while (0) + +/* Note that if async cancelling is used, then there is a race here */ +#define pthread_cleanup_pop(E) \ + } while (0); \ + *pthread_getclean() = _pthread_cup.next; \ + if ((E)) _pthread_cup.func((pthread_once_t *)_pthread_cup.arg); \ + } while (0) + +#ifndef SCHED_OTHER +/* Some POSIX realtime extensions, mostly stubbed */ +#define SCHED_OTHER 0 +#define SCHED_FIFO 1 +#define SCHED_RR 2 +#define SCHED_MIN SCHED_OTHER +#define SCHED_MAX SCHED_RR + +struct sched_param { + int sched_priority; +}; + +WINPTHREAD_API int sched_yield(void); +WINPTHREAD_API int sched_get_priority_min(int pol); +WINPTHREAD_API int sched_get_priority_max(int pol); +WINPTHREAD_API int sched_getscheduler(pid_t pid); +WINPTHREAD_API int sched_setscheduler(pid_t pid, int pol, const struct sched_param *param); + +#endif + +typedef struct pthread_attr_t pthread_attr_t; +struct pthread_attr_t +{ + unsigned p_state; + void *stack; + size_t s_size; + struct sched_param param; +}; + +WINPTHREAD_API int pthread_attr_setschedparam(pthread_attr_t *attr, const struct sched_param *param); +WINPTHREAD_API int pthread_attr_getschedparam(const pthread_attr_t *attr, struct sched_param *param); +WINPTHREAD_API int pthread_getschedparam(pthread_t thread, int *pol, struct sched_param *param); +WINPTHREAD_API int pthread_setschedparam(pthread_t thread, int pol, const struct sched_param *param); +WINPTHREAD_API int pthread_attr_setschedpolicy (pthread_attr_t *attr, int pol); +WINPTHREAD_API int pthread_attr_getschedpolicy (const pthread_attr_t *attr, int *pol); + +/* synchronization objects */ +typedef intptr_t pthread_spinlock_t; +typedef intptr_t pthread_mutex_t; +typedef intptr_t pthread_cond_t; +typedef intptr_t pthread_rwlock_t; +typedef void *pthread_barrier_t; + +#define PTHREAD_MUTEX_NORMAL 0 +#define PTHREAD_MUTEX_ERRORCHECK 1 +#define PTHREAD_MUTEX_RECURSIVE 2 + +#define GENERIC_INITIALIZER -1 +#define GENERIC_ERRORCHECK_INITIALIZER -2 +#define GENERIC_RECURSIVE_INITIALIZER -3 +#define GENERIC_NORMAL_INITIALIZER -1 +#define PTHREAD_MUTEX_INITIALIZER (pthread_mutex_t)GENERIC_INITIALIZER +#define PTHREAD_RECURSIVE_MUTEX_INITIALIZER (pthread_mutex_t)GENERIC_RECURSIVE_INITIALIZER +#define PTHREAD_ERRORCHECK_MUTEX_INITIALIZER (pthread_mutex_t)GENERIC_ERRORCHECK_INITIALIZER +#define PTHREAD_NORMAL_MUTEX_INITIALIZER (pthread_mutex_t)GENERIC_NORMAL_INITIALIZER +#define PTHREAD_DEFAULT_MUTEX_INITIALIZER PTHREAD_NORMAL_MUTEX_INITIALIZER +#define PTHREAD_COND_INITIALIZER (pthread_cond_t)GENERIC_INITIALIZER +#define PTHREAD_RWLOCK_INITIALIZER (pthread_rwlock_t)GENERIC_INITIALIZER +#define PTHREAD_SPINLOCK_INITIALIZER (pthread_spinlock_t)GENERIC_INITIALIZER + +WINPTHREAD_API extern void (**_pthread_key_dest)(void *); +WINPTHREAD_API int pthread_key_create(pthread_key_t *key, void (* dest)(void *)); +WINPTHREAD_API int pthread_key_delete(pthread_key_t key); +WINPTHREAD_API void * pthread_getspecific(pthread_key_t key); +WINPTHREAD_API int pthread_setspecific(pthread_key_t key, const void *value); + +WINPTHREAD_API pthread_t pthread_self(void); +WINPTHREAD_API int pthread_once(pthread_once_t *o, void (*func)(void)); +WINPTHREAD_API void pthread_testcancel(void); +WINPTHREAD_API int pthread_equal(pthread_t t1, pthread_t t2); +WINPTHREAD_API void pthread_tls_init(void); +WINPTHREAD_API void _pthread_cleanup_dest(pthread_t t); +WINPTHREAD_API int pthread_get_concurrency(int *val); +WINPTHREAD_API int pthread_set_concurrency(int val); +WINPTHREAD_API void pthread_exit(void *res); +WINPTHREAD_API void _pthread_invoke_cancel(void); +WINPTHREAD_API int pthread_cancel(pthread_t t); +WINPTHREAD_API int pthread_kill(pthread_t t, int sig); +WINPTHREAD_API unsigned _pthread_get_state(const pthread_attr_t *attr, unsigned flag); +WINPTHREAD_API int _pthread_set_state(pthread_attr_t *attr, unsigned flag, unsigned val); +WINPTHREAD_API int pthread_setcancelstate(int state, int *oldstate); +WINPTHREAD_API int pthread_setcanceltype(int type, int *oldtype); +WINPTHREAD_API unsigned __stdcall pthread_create_wrapper(void *args); +WINPTHREAD_API int pthread_create(pthread_t *th, const pthread_attr_t *attr, void *(* func)(void *), void *arg); +WINPTHREAD_API int pthread_join(pthread_t t, void **res); +WINPTHREAD_API int pthread_detach(pthread_t t); +WINPTHREAD_API int pthread_setname_np(pthread_t thread, const char *name); +WINPTHREAD_API int pthread_getname_np(pthread_t thread, char *name, size_t len); + + +WINPTHREAD_API int pthread_rwlock_init(pthread_rwlock_t *rwlock_, const pthread_rwlockattr_t *attr); +WINPTHREAD_API int pthread_rwlock_wrlock(pthread_rwlock_t *l); +WINPTHREAD_API int pthread_rwlock_timedwrlock(pthread_rwlock_t *rwlock, const struct timespec *ts); +WINPTHREAD_API int pthread_rwlock_rdlock(pthread_rwlock_t *l); +WINPTHREAD_API int pthread_rwlock_timedrdlock(pthread_rwlock_t *l, const struct timespec *ts); +WINPTHREAD_API int pthread_rwlock_unlock(pthread_rwlock_t *l); +WINPTHREAD_API int pthread_rwlock_tryrdlock(pthread_rwlock_t *l); +WINPTHREAD_API int pthread_rwlock_trywrlock(pthread_rwlock_t *l); +WINPTHREAD_API int pthread_rwlock_destroy (pthread_rwlock_t *l); + +WINPTHREAD_API int pthread_cond_init(pthread_cond_t *cv, const pthread_condattr_t *a); +WINPTHREAD_API int pthread_cond_destroy(pthread_cond_t *cv); +WINPTHREAD_API int pthread_cond_signal (pthread_cond_t *cv); +WINPTHREAD_API int pthread_cond_broadcast (pthread_cond_t *cv); +WINPTHREAD_API int pthread_cond_wait (pthread_cond_t *cv, pthread_mutex_t *external_mutex); +WINPTHREAD_API int pthread_cond_timedwait(pthread_cond_t *cv, pthread_mutex_t *external_mutex, const struct timespec *t); +WINPTHREAD_API int pthread_cond_timedwait_relative_np(pthread_cond_t *cv, pthread_mutex_t *external_mutex, const struct timespec *t); + +WINPTHREAD_API int pthread_mutex_lock(pthread_mutex_t *m); +WINPTHREAD_API int pthread_mutex_timedlock(pthread_mutex_t *m, const struct timespec *ts); +WINPTHREAD_API int pthread_mutex_unlock(pthread_mutex_t *m); +WINPTHREAD_API int pthread_mutex_trylock(pthread_mutex_t *m); +WINPTHREAD_API int pthread_mutex_init(pthread_mutex_t *m, const pthread_mutexattr_t *a); +WINPTHREAD_API int pthread_mutex_destroy(pthread_mutex_t *m); + +WINPTHREAD_API int pthread_barrier_destroy(pthread_barrier_t *b); +WINPTHREAD_API int pthread_barrier_init(pthread_barrier_t *b, const void *attr, unsigned int count); +WINPTHREAD_API int pthread_barrier_wait(pthread_barrier_t *b); + +WINPTHREAD_API int pthread_spin_init(pthread_spinlock_t *l, int pshared); +WINPTHREAD_API int pthread_spin_destroy(pthread_spinlock_t *l); +/* No-fair spinlock due to lack of knowledge of thread number. */ +WINPTHREAD_API int pthread_spin_lock(pthread_spinlock_t *l); +WINPTHREAD_API int pthread_spin_trylock(pthread_spinlock_t *l); +WINPTHREAD_API int pthread_spin_unlock(pthread_spinlock_t *l); + +WINPTHREAD_API int pthread_attr_init(pthread_attr_t *attr); +WINPTHREAD_API int pthread_attr_destroy(pthread_attr_t *attr); +WINPTHREAD_API int pthread_attr_setdetachstate(pthread_attr_t *a, int flag); +WINPTHREAD_API int pthread_attr_getdetachstate(const pthread_attr_t *a, int *flag); +WINPTHREAD_API int pthread_attr_setinheritsched(pthread_attr_t *a, int flag); +WINPTHREAD_API int pthread_attr_getinheritsched(const pthread_attr_t *a, int *flag); +WINPTHREAD_API int pthread_attr_setscope(pthread_attr_t *a, int flag); +WINPTHREAD_API int pthread_attr_getscope(const pthread_attr_t *a, int *flag); +WINPTHREAD_API int pthread_attr_getstack(const pthread_attr_t *attr, void **stack, size_t *size); +WINPTHREAD_API int pthread_attr_setstack(pthread_attr_t *attr, void *stack, size_t size); +WINPTHREAD_API int pthread_attr_getstackaddr(const pthread_attr_t *attr, void **stack); +WINPTHREAD_API int pthread_attr_setstackaddr(pthread_attr_t *attr, void *stack); +WINPTHREAD_API int pthread_attr_getstacksize(const pthread_attr_t *attr, size_t *size); +WINPTHREAD_API int pthread_attr_setstacksize(pthread_attr_t *attr, size_t size); + +WINPTHREAD_API int pthread_mutexattr_init(pthread_mutexattr_t *a); +WINPTHREAD_API int pthread_mutexattr_destroy(pthread_mutexattr_t *a); +WINPTHREAD_API int pthread_mutexattr_gettype(const pthread_mutexattr_t *a, int *type); +WINPTHREAD_API int pthread_mutexattr_settype(pthread_mutexattr_t *a, int type); +WINPTHREAD_API int pthread_mutexattr_getpshared(const pthread_mutexattr_t *a, int *type); +WINPTHREAD_API int pthread_mutexattr_setpshared(pthread_mutexattr_t * a, int type); +WINPTHREAD_API int pthread_mutexattr_getprotocol(const pthread_mutexattr_t *a, int *type); +WINPTHREAD_API int pthread_mutexattr_setprotocol(pthread_mutexattr_t *a, int type); +WINPTHREAD_API int pthread_mutexattr_getprioceiling(const pthread_mutexattr_t *a, int * prio); +WINPTHREAD_API int pthread_mutexattr_setprioceiling(pthread_mutexattr_t *a, int prio); +WINPTHREAD_API int pthread_getconcurrency(void); +WINPTHREAD_API int pthread_setconcurrency(int new_level); + +WINPTHREAD_API int pthread_condattr_destroy(pthread_condattr_t *a); +WINPTHREAD_API int pthread_condattr_init(pthread_condattr_t *a); +WINPTHREAD_API int pthread_condattr_getpshared(const pthread_condattr_t *a, int *s); +WINPTHREAD_API int pthread_condattr_setpshared(pthread_condattr_t *a, int s); + +#ifndef __clockid_t_defined +typedef int clockid_t; +#define __clockid_t_defined 1 +#endif /* __clockid_t_defined */ + +WINPTHREAD_API int pthread_condattr_getclock (const pthread_condattr_t *attr, + clockid_t *clock_id); +WINPTHREAD_API int pthread_condattr_setclock(pthread_condattr_t *attr, + clockid_t clock_id); +WINPTHREAD_API int __pthread_clock_nanosleep(clockid_t clock_id, int flags, const struct timespec *rqtp, struct timespec *rmtp); + +WINPTHREAD_API int pthread_barrierattr_init(void **attr); +WINPTHREAD_API int pthread_barrierattr_destroy(void **attr); +WINPTHREAD_API int pthread_barrierattr_setpshared(void **attr, int s); +WINPTHREAD_API int pthread_barrierattr_getpshared(void **attr, int *s); + +/* Private extensions for analysis and internal use. */ +WINPTHREAD_API struct _pthread_cleanup ** pthread_getclean (void); +WINPTHREAD_API void * pthread_gethandle (pthread_t t); +WINPTHREAD_API void * pthread_getevent (void); + +WINPTHREAD_API unsigned long long _pthread_rel_time_in_ms(const struct timespec *ts); +WINPTHREAD_API unsigned long long _pthread_time_in_ms(void); +WINPTHREAD_API unsigned long long _pthread_time_in_ms_from_timespec(const struct timespec *ts); +WINPTHREAD_API int _pthread_tryjoin (pthread_t t, void **res); +WINPTHREAD_API int pthread_rwlockattr_destroy(pthread_rwlockattr_t *a); +WINPTHREAD_API int pthread_rwlockattr_getpshared(pthread_rwlockattr_t *a, int *s); +WINPTHREAD_API int pthread_rwlockattr_init(pthread_rwlockattr_t *a); +WINPTHREAD_API int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *a, int s); + +#ifndef SIG_BLOCK +#define SIG_BLOCK 0 +#endif +#ifndef SIG_UNBLOCK +#define SIG_UNBLOCK 1 +#endif +#ifndef SIG_SETMASK +#define SIG_SETMASK 2 +#endif + +#include + +#undef _POSIX_THREAD_DESTRUCTOR_ITERATIONS +#define _POSIX_THREAD_DESTRUCTOR_ITERATIONS PTHREAD_DESTRUCTOR_ITERATIONS + +#undef _POSIX_THREAD_KEYS_MAX +#define _POSIX_THREAD_KEYS_MAX PTHREAD_KEYS_MAX + +#undef PTHREAD_THREADS_MAX +#define PTHREAD_THREADS_MAX 2019 + +#undef _POSIX_SEM_NSEMS_MAX +#define _POSIX_SEM_NSEMS_MAX 256 + +#undef SEM_NSEMS_MAX +#define SEM_NSEMS_MAX 1024 + +/* Wrap cancellation points. */ +#if defined(__WINPTHREAD_ENABLE_WRAP_API) \ + || defined(__WINPTRHEAD_ENABLE_WRAP_API) /* historical typo */ +#define accept(...) (pthread_testcancel(), accept(__VA_ARGS__)) +#define aio_suspend(...) (pthread_testcancel(), aio_suspend(__VA_ARGS__)) +#define clock_nanosleep(...) (pthread_testcancel(), clock_nanosleep(__VA_ARGS__)) +#define close(...) (pthread_testcancel(), close(__VA_ARGS__)) +#define connect(...) (pthread_testcancel(), connect(__VA_ARGS__)) +#define creat(...) (pthread_testcancel(), creat(__VA_ARGS__)) +#define fcntl(...) (pthread_testcancel(), fcntl(__VA_ARGS__)) +#define fdatasync(...) (pthread_testcancel(), fdatasync(__VA_ARGS__)) +#define fsync(...) (pthread_testcancel(), fsync(__VA_ARGS__)) +#define getmsg(...) (pthread_testcancel(), getmsg(__VA_ARGS__)) +#define getpmsg(...) (pthread_testcancel(), getpmsg(__VA_ARGS__)) +#define lockf(...) (pthread_testcancel(), lockf(__VA_ARGS__)) +#define mg_receive(...) (pthread_testcancel(), mg_receive(__VA_ARGS__)) +#define mg_send(...) (pthread_testcancel(), mg_send(__VA_ARGS__)) +#define mg_timedreceive(...) (pthread_testcancel(), mg_timedreceive(__VA_ARGS__)) +#define mg_timessend(...) (pthread_testcancel(), mg_timedsend(__VA_ARGS__)) +#define msgrcv(...) (pthread_testcancel(), msgrecv(__VA_ARGS__)) +#define msgsnd(...) (pthread_testcancel(), msgsnd(__VA_ARGS__)) +#define msync(...) (pthread_testcancel(), msync(__VA_ARGS__)) +#define nanosleep(...) (pthread_testcancel(), nanosleep(__VA_ARGS__)) +#define open(...) (pthread_testcancel(), open(__VA_ARGS__)) +#define pause(...) (pthread_testcancel(), pause(__VA_ARGS__)) +#define poll(...) (pthread_testcancel(), poll(__VA_ARGS__)) +#define pread(...) (pthread_testcancel(), pread(__VA_ARGS__)) +#define pselect(...) (pthread_testcancel(), pselect(__VA_ARGS__)) +#define putmsg(...) (pthread_testcancel(), putmsg(__VA_ARGS__)) +#define putpmsg(...) (pthread_testcancel(), putpmsg(__VA_ARGS__)) +#define pwrite(...) (pthread_testcancel(), pwrite(__VA_ARGS__)) +#define read(...) (pthread_testcancel(), read(__VA_ARGS__)) +#define readv(...) (pthread_testcancel(), readv(__VA_ARGS__)) +#define recv(...) (pthread_testcancel(), recv(__VA_ARGS__)) +#define recvfrom(...) (pthread_testcancel(), recvfrom(__VA_ARGS__)) +#define recvmsg(...) (pthread_testcancel(), recvmsg(__VA_ARGS__)) +#define select(...) (pthread_testcancel(), select(__VA_ARGS__)) +#define sem_timedwait(...) (pthread_testcancel(), sem_timedwait(__VA_ARGS__)) +#define sem_wait(...) (pthread_testcancel(), sem_wait(__VA_ARGS__)) +#define send(...) (pthread_testcancel(), send(__VA_ARGS__)) +#define sendmsg(...) (pthread_testcancel(), sendmsg(__VA_ARGS__)) +#define sendto(...) (pthread_testcancel(), sendto(__VA_ARGS__)) +#define sigpause(...) (pthread_testcancel(), sigpause(__VA_ARGS__)) +#define sigsuspend(...) (pthread_testcancel(), sigsuspend(__VA_ARGS__)) +#define sigwait(...) (pthread_testcancel(), sigwait(__VA_ARGS__)) +#define sigwaitinfo(...) (pthread_testcancel(), sigwaitinfo(__VA_ARGS__)) +#define sleep(...) (pthread_testcancel(), sleep(__VA_ARGS__)) +//#define Sleep(...) (pthread_testcancel(), Sleep(__VA_ARGS__)) +#define system(...) (pthread_testcancel(), system(__VA_ARGS__)) +#define access(...) (pthread_testcancel(), access(__VA_ARGS__)) +#define asctime(...) (pthread_testcancel(), asctime(__VA_ARGS__)) +#define catclose(...) (pthread_testcancel(), catclose(__VA_ARGS__)) +#define catgets(...) (pthread_testcancel(), catgets(__VA_ARGS__)) +#define catopen(...) (pthread_testcancel(), catopen(__VA_ARGS__)) +#define closedir(...) (pthread_testcancel(), closedir(__VA_ARGS__)) +#define closelog(...) (pthread_testcancel(), closelog(__VA_ARGS__)) +#define ctermid(...) (pthread_testcancel(), ctermid(__VA_ARGS__)) +#define ctime(...) (pthread_testcancel(), ctime(__VA_ARGS__)) +#define dbm_close(...) (pthread_testcancel(), dbm_close(__VA_ARGS__)) +#define dbm_delete(...) (pthread_testcancel(), dbm_delete(__VA_ARGS__)) +#define dbm_fetch(...) (pthread_testcancel(), dbm_fetch(__VA_ARGS__)) +#define dbm_nextkey(...) (pthread_testcancel(), dbm_nextkey(__VA_ARGS__)) +#define dbm_open(...) (pthread_testcancel(), dbm_open(__VA_ARGS__)) +#define dbm_store(...) (pthread_testcancel(), dbm_store(__VA_ARGS__)) +#define dlclose(...) (pthread_testcancel(), dlclose(__VA_ARGS__)) +#define dlopen(...) (pthread_testcancel(), dlopen(__VA_ARGS__)) +#define endgrent(...) (pthread_testcancel(), endgrent(__VA_ARGS__)) +#define endhostent(...) (pthread_testcancel(), endhostent(__VA_ARGS__)) +#define endnetent(...) (pthread_testcancel(), endnetent(__VA_ARGS__)) +#define endprotoent(...) (pthread_testcancel(), endprotoend(__VA_ARGS__)) +#define endpwent(...) (pthread_testcancel(), endpwent(__VA_ARGS__)) +#define endservent(...) (pthread_testcancel(), endservent(__VA_ARGS__)) +#define endutxent(...) (pthread_testcancel(), endutxent(__VA_ARGS__)) +#define fclose(...) (pthread_testcancel(), fclose(__VA_ARGS__)) +#define fflush(...) (pthread_testcancel(), fflush(__VA_ARGS__)) +#define fgetc(...) (pthread_testcancel(), fgetc(__VA_ARGS__)) +#define fgetpos(...) (pthread_testcancel(), fgetpos(__VA_ARGS__)) +#define fgets(...) (pthread_testcancel(), fgets(__VA_ARGS__)) +#define fgetwc(...) (pthread_testcancel(), fgetwc(__VA_ARGS__)) +#define fgetws(...) (pthread_testcancel(), fgetws(__VA_ARGS__)) +#define fmtmsg(...) (pthread_testcancel(), fmtmsg(__VA_ARGS__)) +#define fopen(...) (pthread_testcancel(), fopen(__VA_ARGS__)) +#define fpathconf(...) (pthread_testcancel(), fpathconf(__VA_ARGS__)) +#define fprintf(...) (pthread_testcancel(), fprintf(__VA_ARGS__)) +#define fputc(...) (pthread_testcancel(), fputc(__VA_ARGS__)) +#define fputs(...) (pthread_testcancel(), fputs(__VA_ARGS__)) +#define fputwc(...) (pthread_testcancel(), fputwc(__VA_ARGS__)) +#define fputws(...) (pthread_testcancel(), fputws(__VA_ARGS__)) +#define fread(...) (pthread_testcancel(), fread(__VA_ARGS__)) +#define freopen(...) (pthread_testcancel(), freopen(__VA_ARGS__)) +#define fscanf(...) (pthread_testcancel(), fscanf(__VA_ARGS__)) +#define fseek(...) (pthread_testcancel(), fseek(__VA_ARGS__)) +#define fseeko(...) (pthread_testcancel(), fseeko(__VA_ARGS__)) +#define fsetpos(...) (pthread_testcancel(), fsetpos(__VA_ARGS__)) +#define fstat(...) (pthread_testcancel(), fstat(__VA_ARGS__)) +#define ftell(...) (pthread_testcancel(), ftell(__VA_ARGS__)) +#define ftello(...) (pthread_testcancel(), ftello(__VA_ARGS__)) +#define ftw(...) (pthread_testcancel(), ftw(__VA_ARGS__)) +#define fwprintf(...) (pthread_testcancel(), fwprintf(__VA_ARGS__)) +#define fwrite(...) (pthread_testcancel(), fwrite(__VA_ARGS__)) +#define fwscanf(...) (pthread_testcancel(), fwscanf(__VA_ARGS__)) +#define getaddrinfo(...) (pthread_testcancel(), getaddrinfo(__VA_ARGS__)) +#define getc(...) (pthread_testcancel(), getc(__VA_ARGS__)) +#define getc_unlocked(...) (pthread_testcancel(), getc_unlocked(__VA_ARGS__)) +#define getchar(...) (pthread_testcancel(), getchar(__VA_ARGS__)) +#define getchar_unlocked(...) (pthread_testcancel(), getchar_unlocked(__VA_ARGS__)) +#define getcwd(...) (pthread_testcancel(), getcwd(__VA_ARGS__)) +#define getdate(...) (pthread_testcancel(), getdate(__VA_ARGS__)) +#define getgrent(...) (pthread_testcancel(), getgrent(__VA_ARGS__)) +#define getgrgid(...) (pthread_testcancel(), getgrgid(__VA_ARGS__)) +#define getgrgid_r(...) (pthread_testcancel(), getgrgid_r(__VA_ARGS__)) +#define gergrnam(...) (pthread_testcancel(), getgrnam(__VA_ARGS__)) +#define getgrnam_r(...) (pthread_testcancel(), getgrnam_r(__VA_ARGS__)) +#define gethostbyaddr(...) (pthread_testcancel(), gethostbyaddr(__VA_ARGS__)) +#define gethostbyname(...) (pthread_testcancel(), gethostbyname(__VA_ARGS__)) +#define gethostent(...) (pthread_testcancel(), gethostent(__VA_ARGS__)) +#define gethostid(...) (pthread_testcancel(), gethostid(__VA_ARGS__)) +#define gethostname(...) (pthread_testcancel(), gethostname(__VA_ARGS__)) +#define getlogin(...) (pthread_testcancel(), getlogin(__VA_ARGS__)) +#define getlogin_r(...) (pthread_testcancel(), getlogin_r(__VA_ARGS__)) +#define getnameinfo(...) (pthread_testcancel(), getnameinfo(__VA_ARGS__)) +#define getnetbyaddr(...) (pthread_testcancel(), getnetbyaddr(__VA_ARGS__)) +#define getnetbyname(...) (pthread_testcancel(), getnetbyname(__VA_ARGS__)) +#define getnetent(...) (pthread_testcancel(), getnetent(__VA_ARGS__)) +#define getopt(...) (pthread_testcancel(), getopt(__VA_ARGS__)) +#define getprotobyname(...) (pthread_testcancel(), getprotobyname(__VA_ARGS__)) +#define getprotobynumber(...) (pthread_testcancel(), getprotobynumber(__VA_ARGS__)) +#define getprotoent(...) (pthread_testcancel(), getprotoent(__VA_ARGS__)) +#define getpwent(...) (pthread_testcancel(), getpwent(__VA_ARGS__)) +#define getpwnam(...) (pthread_testcancel(), getpwnam(__VA_ARGS__)) +#define getpwnam_r(...) (pthread_testcancel(), getpwnam_r(__VA_ARGS__)) +#define getpwuid(...) (pthread_testcancel(), getpwuid(__VA_ARGS__)) +#define getpwuid_r(...) (pthread_testcancel(), getpwuid_r(__VA_ARGS__)) +#define gets(...) (pthread_testcancel(), gets(__VA_ARGS__)) +#define getservbyname(...) (pthread_testcancel(), getservbyname(__VA_ARGS__)) +#define getservbyport(...) (pthread_testcancel(), getservbyport(__VA_ARGS__)) +#define getservent(...) (pthread_testcancel(), getservent(__VA_ARGS__)) +#define getutxent(...) (pthread_testcancel(), getutxent(__VA_ARGS__)) +#define getutxid(...) (pthread_testcancel(), getutxid(__VA_ARGS__)) +#define getutxline(...) (pthread_testcancel(), getutxline(__VA_ARGS__)) +#undef getwc +#define getwc(...) (pthread_testcancel(), getwc(__VA_ARGS__)) +#undef getwchar +#define getwchar(...) (pthread_testcancel(), getwchar(__VA_ARGS__)) +#define getwd(...) (pthread_testcancel(), getwd(__VA_ARGS__)) +#define glob(...) (pthread_testcancel(), glob(__VA_ARGS__)) +#define iconv_close(...) (pthread_testcancel(), iconv_close(__VA_ARGS__)) +#define iconv_open(...) (pthread_testcancel(), iconv_open(__VA_ARGS__)) +#define ioctl(...) (pthread_testcancel(), ioctl(__VA_ARGS__)) +#define link(...) (pthread_testcancel(), link(__VA_ARGS__)) +#define localtime(...) (pthread_testcancel(), localtime(__VA_ARGS__)) +#define lseek(...) (pthread_testcancel(), lseek(__VA_ARGS__)) +#define lstat(...) (pthread_testcancel(), lstat(__VA_ARGS__)) +#define mkstemp(...) (pthread_testcancel(), mkstemp(__VA_ARGS__)) +#define nftw(...) (pthread_testcancel(), nftw(__VA_ARGS__)) +#define opendir(...) (pthread_testcancel(), opendir(__VA_ARGS__)) +#define openlog(...) (pthread_testcancel(), openlog(__VA_ARGS__)) +#define pathconf(...) (pthread_testcancel(), pathconf(__VA_ARGS__)) +#define pclose(...) (pthread_testcancel(), pclose(__VA_ARGS__)) +#define perror(...) (pthread_testcancel(), perror(__VA_ARGS__)) +#define popen(...) (pthread_testcancel(), popen(__VA_ARGS__)) +#define posix_fadvise(...) (pthread_testcancel(), posix_fadvise(__VA_ARGS__)) +#define posix_fallocate(...) (pthread_testcancel(), posix_fallocate(__VA_ARGS__)) +#define posix_madvise(...) (pthread_testcancel(), posix_madvise(__VA_ARGS__)) +#define posix_openpt(...) (pthread_testcancel(), posix_openpt(__VA_ARGS__)) +#define posix_spawn(...) (pthread_testcancel(), posix_spawn(__VA_ARGS__)) +#define posix_spawnp(...) (pthread_testcancel(), posix_spawnp(__VA_ARGS__)) +#define posix_trace_clear(...) (pthread_testcancel(), posix_trace_clear(__VA_ARGS__)) +#define posix_trace_close(...) (pthread_testcancel(), posix_trace_close(__VA_ARGS__)) +#define posix_trace_create(...) (pthread_testcancel(), posix_trace_create(__VA_ARGS__)) +#define posix_trace_create_withlog(...) (pthread_testcancel(), posix_trace_create_withlog(__VA_ARGS__)) +#define posix_trace_eventtypelist_getne(...) (pthread_testcancel(), posix_trace_eventtypelist_getne(__VA_ARGS__)) +#define posix_trace_eventtypelist_rewin(...) (pthread_testcancel(), posix_trace_eventtypelist_rewin(__VA_ARGS__)) +#define posix_trace_flush(...) (pthread_testcancel(), posix_trace_flush(__VA_ARGS__)) +#define posix_trace_get_attr(...) (pthread_testcancel(), posix_trace_get_attr(__VA_ARGS__)) +#define posix_trace_get_filter(...) (pthread_testcancel(), posix_trace_get_filter(__VA_ARGS__)) +#define posix_trace_get_status(...) (pthread_testcancel(), posix_trace_get_status(__VA_ARGS__)) +#define posix_trace_getnext_event(...) (pthread_testcancel(), posix_trace_getnext_event(__VA_ARGS__)) +#define posix_trace_open(...) (pthread_testcancel(), posix_trace_open(__VA_ARGS__)) +#define posix_trace_rewind(...) (pthread_testcancel(), posix_trace_rewind(__VA_ARGS__)) +#define posix_trace_setfilter(...) (pthread_testcancel(), posix_trace_setfilter(__VA_ARGS__)) +#define posix_trace_shutdown(...) (pthread_testcancel(), posix_trace_shutdown(__VA_ARGS__)) +#define posix_trace_timedgetnext_event(...) (pthread_testcancel(), posix_trace_timedgetnext_event(__VA_ARGS__)) +#define posix_typed_mem_open(...) (pthread_testcancel(), posix_typed_mem_open(__VA_ARGS__)) +#define printf(...) (pthread_testcancel(), printf(__VA_ARGS__)) +#define putc(...) (pthread_testcancel(), putc(__VA_ARGS__)) +#define putc_unlocked(...) (pthread_testcancel(), putc_unlocked(__VA_ARGS__)) +#define putchar(...) (pthread_testcancel(), putchar(__VA_ARGS__)) +#define putchar_unlocked(...) (pthread_testcancel(), putchar_unlocked(__VA_ARGS__)) +#define puts(...) (pthread_testcancel(), puts(__VA_ARGS__)) +#define pututxline(...) (pthread_testcancel(), pututxline(__VA_ARGS__)) +#undef putwc +#define putwc(...) (pthread_testcancel(), putwc(__VA_ARGS__)) +#undef putwchar +#define putwchar(...) (pthread_testcancel(), putwchar(__VA_ARGS__)) +#define readdir(...) (pthread_testcancel(), readdir(__VA_ARSG__)) +#define readdir_r(...) (pthread_testcancel(), readdir_r(__VA_ARGS__)) +#define remove(...) (pthread_testcancel(), remove(__VA_ARGS__)) +#define rename(...) (pthread_testcancel(), rename(__VA_ARGS__)) +#define rewind(...) (pthread_testcancel(), rewind(__VA_ARGS__)) +#define rewinddir(...) (pthread_testcancel(), rewinddir(__VA_ARGS__)) +#define scanf(...) (pthread_testcancel(), scanf(__VA_ARGS__)) +#define seekdir(...) (pthread_testcancel(), seekdir(__VA_ARGS__)) +#define semop(...) (pthread_testcancel(), semop(__VA_ARGS__)) +#define setgrent(...) (pthread_testcancel(), setgrent(__VA_ARGS__)) +#define sethostent(...) (pthread_testcancel(), sethostemt(__VA_ARGS__)) +#define setnetent(...) (pthread_testcancel(), setnetent(__VA_ARGS__)) +#define setprotoent(...) (pthread_testcancel(), setprotoent(__VA_ARGS__)) +#define setpwent(...) (pthread_testcancel(), setpwent(__VA_ARGS__)) +#define setservent(...) (pthread_testcancel(), setservent(__VA_ARGS__)) +#define setutxent(...) (pthread_testcancel(), setutxent(__VA_ARGS__)) +#define stat(...) (pthread_testcancel(), stat(__VA_ARGS__)) +#define strerror(...) (pthread_testcancel(), strerror(__VA_ARGS__)) +#define strerror_r(...) (pthread_testcancel(), strerror_r(__VA_ARGS__)) +#define strftime(...) (pthread_testcancel(), strftime(__VA_ARGS__)) +#define symlink(...) (pthread_testcancel(), symlink(__VA_ARGS__)) +#define sync(...) (pthread_testcancel(), sync(__VA_ARGS__)) +#define syslog(...) (pthread_testcancel(), syslog(__VA_ARGS__)) +#define tmpfile(...) (pthread_testcancel(), tmpfile(__VA_ARGS__)) +#define tmpnam(...) (pthread_testcancel(), tmpnam(__VA_ARGS__)) +#define ttyname(...) (pthread_testcancel(), ttyname(__VA_ARGS__)) +#define ttyname_r(...) (pthread_testcancel(), ttyname_r(__VA_ARGS__)) +#define tzset(...) (pthread_testcancel(), tzset(__VA_ARGS__)) +#define ungetc(...) (pthread_testcancel(), ungetc(__VA_ARGS__)) +#define ungetwc(...) (pthread_testcancel(), ungetwc(__VA_ARGS__)) +#define unlink(...) (pthread_testcancel(), unlink(__VA_ARGS__)) +#define vfprintf(...) (pthread_testcancel(), vfprintf(__VA_ARGS__)) +#define vfwprintf(...) (pthread_testcancel(), vfwprintf(__VA_ARGS__)) +#define vprintf(...) (pthread_testcancel(), vprintf(__VA_ARGS__)) +#define vwprintf(...) (pthread_testcancel(), vwprintf(__VA_ARGS__)) +#define wcsftime(...) (pthread_testcancel(), wcsftime(__VA_ARGS__)) +#define wordexp(...) (pthread_testcancel(), wordexp(__VA_ARGS__)) +#define wprintf(...) (pthread_testcancel(), wprintf(__VA_ARGS__)) +#define wscanf(...) (pthread_testcancel(), wscanf(__VA_ARGS__)) +#endif + +/* We deal here with a gcc issue for posix threading on Windows. + We would need to change here gcc's gthr-posix.h header, but this + got rejected. So we deal it within this header. */ +#ifdef _GTHREAD_USE_MUTEX_INIT_FUNC +#undef _GTHREAD_USE_MUTEX_INIT_FUNC +#endif +#define _GTHREAD_USE_MUTEX_INIT_FUNC 1 + +#ifdef __cplusplus +} +#endif + +#endif /* WIN_PTHREADS_H */ diff --git a/libs/winpthreads/include/pthread_compat.h b/libs/winpthreads/include/pthread_compat.h new file mode 100644 index 0000000..63f5f49 --- /dev/null +++ b/libs/winpthreads/include/pthread_compat.h @@ -0,0 +1,86 @@ +/* + Copyright (c) 2011-2016 mingw-w64 project + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +/* + * Parts of this library are derived by: + * + * Posix Threads library for Microsoft Windows + * + * Use at own risk, there is no implied warranty to this code. + * It uses undocumented features of Microsoft Windows that can change + * at any time in the future. + * + * (C) 2010 Lockless Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Lockless Inc. nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AN + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef WIN_PTHREADS_PTHREAD_COMPAT_H +#define WIN_PTHREADS_PTHREAD_COMPAT_H + +#ifdef __GNUC__ + +#define WINPTHREADS_INLINE inline +#define WINPTHREADS_ATTRIBUTE(X) __attribute__(X) +#define WINPTHREADS_SECTION(X) __section__(X) + +#elif _MSC_VER + +#include "pthread_time.h" + +#ifdef _WIN64 +typedef __int64 pid_t; +#else +typedef int pid_t; +#endif +typedef int clockid_t; + +#define WINPTHREADS_INLINE __inline +#define WINPTHREADS_ATTRIBUTE(X) __declspec X +#define WINPTHREADS_SECTION(X) allocate(X) + +#endif + +#endif diff --git a/libs/winpthreads/include/pthread_signal.h b/libs/winpthreads/include/pthread_signal.h new file mode 100644 index 0000000..42a50ae --- /dev/null +++ b/libs/winpthreads/include/pthread_signal.h @@ -0,0 +1,29 @@ +/* + Copyright (c) 2013-2016 mingw-w64 project + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#ifndef WIN_PTHREADS_SIGNAL_H +#define WIN_PTHREADS_SIGNAL_H + +/* Windows has rudimentary signals support. */ +#define pthread_sigmask(H, S1, S2) 0 + +#endif /* WIN_PTHREADS_SIGNAL_H */ diff --git a/libs/winpthreads/include/pthread_time.h b/libs/winpthreads/include/pthread_time.h new file mode 100644 index 0000000..eec4168 --- /dev/null +++ b/libs/winpthreads/include/pthread_time.h @@ -0,0 +1,102 @@ +/* + Copyright (c) 2011-2016 mingw-w64 project + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include + +#ifndef WIN_PTHREADS_TIME_H +#define WIN_PTHREADS_TIME_H + +/* Posix timers are supported */ +#ifndef _POSIX_TIMERS +#define _POSIX_TIMERS 200809L +#endif + +/* Monotonic clocks are available. */ +#ifndef _POSIX_MONOTONIC_CLOCK +#define _POSIX_MONOTONIC_CLOCK 200809L +#endif + +/* CPU-time clocks are available. */ +#ifndef _POSIX_CPUTIME +#define _POSIX_CPUTIME 200809L +#endif + +/* Clock support in threads are available. */ +#ifndef _POSIX_THREAD_CPUTIME +#define _POSIX_THREAD_CPUTIME 200809L +#endif + +#ifndef __clockid_t_defined +typedef int clockid_t; +#define __clockid_t_defined 1 +#endif /* __clockid_t_defined */ + +#ifndef TIMER_ABSTIME +#define TIMER_ABSTIME 1 +#endif + +#ifndef CLOCK_REALTIME +#define CLOCK_REALTIME 0 +#endif + +#ifndef CLOCK_MONOTONIC +#define CLOCK_MONOTONIC 1 +#endif + +#ifndef CLOCK_PROCESS_CPUTIME_ID +#define CLOCK_PROCESS_CPUTIME_ID 2 +#endif + +#ifndef CLOCK_THREAD_CPUTIME_ID +#define CLOCK_THREAD_CPUTIME_ID 3 +#endif + +#ifndef CLOCK_REALTIME_COARSE +#define CLOCK_REALTIME_COARSE 4 +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/* Make sure we provide default for WINPTHREAD_API, if not defined. */ +#pragma push_macro("WINPTHREAD_API") +#ifndef WINPTHREAD_API +#define WINPTHREAD_API +#endif + +/* These should really be dllimport'ed if using winpthread dll */ +WINPTHREAD_API int __cdecl nanosleep(const struct timespec *request, struct timespec *remain); + +WINPTHREAD_API int __cdecl clock_nanosleep(clockid_t clock_id, int flags, const struct timespec *request, struct timespec *remain); +WINPTHREAD_API int __cdecl clock_getres(clockid_t clock_id, struct timespec *res); +WINPTHREAD_API int __cdecl clock_gettime(clockid_t clock_id, struct timespec *tp); +WINPTHREAD_API int __cdecl clock_settime(clockid_t clock_id, const struct timespec *tp); + +#pragma pop_macro("WINPTHREAD_API") + +#ifdef __cplusplus +} +#endif + +#endif /* WIN_PTHREADS_TIME_H */ + diff --git a/libs/winpthreads/include/pthread_unistd.h b/libs/winpthreads/include/pthread_unistd.h new file mode 100644 index 0000000..6469353 --- /dev/null +++ b/libs/winpthreads/include/pthread_unistd.h @@ -0,0 +1,192 @@ +/* + Copyright (c) 2011-2016 mingw-w64 project + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#ifndef WIN_PTHREADS_UNISTD_H +#define WIN_PTHREADS_UNISTD_H + +/* Set defines described by the POSIX Threads Extension (1003.1c-1995) */ +/* _SC_THREADS + Basic support for POSIX threads is available. The functions + + pthread_atfork(), + pthread_attr_destroy(), + pthread_attr_getdetachstate(), + pthread_attr_getschedparam(), + pthread_attr_init(), + pthread_attr_setdetachstate(), + pthread_attr_setschedparam(), + pthread_cancel(), + pthread_cleanup_push(), + pthread_cleanup_pop(), + pthread_cond_broadcast(), + pthread_cond_destroy(), + pthread_cond_init(), + pthread_cond_signal(), + pthread_cond_timedwait(), + pthread_cond_wait(), + pthread_condattr_destroy(), + pthread_condattr_init(), + pthread_create(), + pthread_detach(), + pthread_equal(), + pthread_exit(), + pthread_getspecific(), + pthread_join(, + pthread_key_create(), + pthread_key_delete(), + pthread_mutex_destroy(), + pthread_mutex_init(), + pthread_mutex_lock(), + pthread_mutex_trylock(), + pthread_mutex_unlock(), + pthread_mutexattr_destroy(), + pthread_mutexattr_init(), + pthread_once(), + pthread_rwlock_destroy(), + pthread_rwlock_init(), + pthread_rwlock_rdlock(), + pthread_rwlock_tryrdlock(), + pthread_rwlock_trywrlock(), + pthread_rwlock_unlock(), + pthread_rwlock_wrlock(), + pthread_rwlockattr_destroy(), + pthread_rwlockattr_init(), + pthread_self(), + pthread_setcancelstate(), + pthread_setcanceltype(), + pthread_setspecific(), + pthread_testcancel() + + are present. */ +#undef _POSIX_THREADS +#define _POSIX_THREADS 200112L + +/* _SC_READER_WRITER_LOCKS + This option implies the _POSIX_THREADS option. Conversely, under + POSIX 1003.1-2001 the _POSIX_THREADS option implies this option. + + The functions + pthread_rwlock_destroy(), + pthread_rwlock_init(), + pthread_rwlock_rdlock(), + pthread_rwlock_tryrdlock(), + pthread_rwlock_trywrlock(), + pthread_rwlock_unlock(), + pthread_rwlock_wrlock(), + pthread_rwlockattr_destroy(), + pthread_rwlockattr_init() + + are present. +*/ +#undef _POSIX_READER_WRITER_LOCKS +#define _POSIX_READER_WRITER_LOCKS 200112L + +/* _SC_SPIN_LOCKS + This option implies the _POSIX_THREADS and _POSIX_THREAD_SAFE_FUNCTIONS + options. The functions + + pthread_spin_destroy(), + pthread_spin_init(), + pthread_spin_lock(), + pthread_spin_trylock(), + pthread_spin_unlock() + + are present. */ +#undef _POSIX_SPIN_LOCKS +#define _POSIX_SPIN_LOCKS 200112L + +/* _SC_BARRIERS + This option implies the _POSIX_THREADS and _POSIX_THREAD_SAFE_FUNCTIONS + options. The functions + + pthread_barrier_destroy(), + pthread_barrier_init(), + pthread_barrier_wait(), + pthread_barrierattr_destroy(), + pthread_barrierattr_init() + + are present. +*/ +#undef _POSIX_BARRIERS +#define _POSIX_BARRIERS 200112L + +/* _SC_TIMEOUTS + The functions + + mq_timedreceive(), - not supported + mq_timedsend(), - not supported + posix_trace_timedgetnext_event(), - not supported + pthread_mutex_timedlock(), + pthread_rwlock_timedrdlock(), + pthread_rwlock_timedwrlock(), + sem_timedwait(), + + are present. */ +#undef _POSIX_TIMEOUTS +#define _POSIX_TIMEOUTS 200112L + +/* _SC_TIMERS - not supported + The functions + + clock_getres(), + clock_gettime(), + clock_settime(), + nanosleep(), + timer_create(), + timer_delete(), + timer_gettime(), + timer_getoverrun(), + timer_settime() + + are present. */ +/* #undef _POSIX_TIMERS */ + +/* _SC_CLOCK_SELECTION + This option implies the _POSIX_TIMERS option. The functions + + pthread_condattr_getclock(), + pthread_condattr_setclock(), + clock_nanosleep() + + are present. +*/ +#undef _POSIX_CLOCK_SELECTION +#define _POSIX_CLOCK_SELECTION 200112 + +/* _SC_SEMAPHORES + The include file is present. The functions + + sem_close(), + sem_destroy(), + sem_getvalue(), + sem_init(), + sem_open(), + sem_post(), + sem_trywait(), + sem_unlink(), + sem_wait() + + are present. */ +#undef _POSIX_SEMAPHORES +#define _POSIX_SEMAPHORES 200112 + +#endif /* WIN_PTHREADS_UNISTD_H */ diff --git a/libs/winpthreads/include/sched.h b/libs/winpthreads/include/sched.h new file mode 100644 index 0000000..bf28487 --- /dev/null +++ b/libs/winpthreads/include/sched.h @@ -0,0 +1,83 @@ +/* + Copyright (c) 2011-2016 mingw-w64 project + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include +#include +#include + +#include +#include +#include + +#include + +#ifndef WIN_PTHREADS_SCHED_H +#define WIN_PTHREADS_SCHED_H + +#ifndef SCHED_OTHER +/* Some POSIX realtime extensions, mostly stubbed */ +#define SCHED_OTHER 0 +#define SCHED_FIFO 1 +#define SCHED_RR 2 +#define SCHED_MIN SCHED_OTHER +#define SCHED_MAX SCHED_RR + +struct sched_param { + int sched_priority; +}; + +#ifdef __cplusplus +extern "C" { +#endif + +#if defined(IN_WINPTHREAD) +# if defined(DLL_EXPORT) && !defined(WINPTHREAD_EXPORT_ALL_DEBUG) +# define WINPTHREAD_SCHED_API __declspec(dllexport) /* building the DLL */ +# else +# define WINPTHREAD_SCHED_API /* building the static library */ +# endif +#else +# if defined(WINPTHREADS_USE_DLLIMPORT) +# define WINPTHREAD_SCHED_API __declspec(dllimport) /* user wants explicit `dllimport` */ +# else +# define WINPTHREAD_SCHED_API /* the default; auto imported in case of DLL */ +# endif +#endif + +WINPTHREAD_SCHED_API int sched_yield(void); +WINPTHREAD_SCHED_API int sched_get_priority_min(int pol); +WINPTHREAD_SCHED_API int sched_get_priority_max(int pol); +WINPTHREAD_SCHED_API int sched_getscheduler(pid_t pid); +WINPTHREAD_SCHED_API int sched_setscheduler(pid_t pid, int pol, const struct sched_param *param); + +#ifdef __cplusplus +} +#endif + +#endif + +#ifndef sched_rr_get_interval +#define sched_rr_get_interval(_p, _i) \ + ( errno = ENOTSUP, (int) -1 ) +#endif + +#endif /* WIN_PTHREADS_SCHED_H */ diff --git a/libs/winpthreads/include/semaphore.h b/libs/winpthreads/include/semaphore.h new file mode 100644 index 0000000..8e3fa6f --- /dev/null +++ b/libs/winpthreads/include/semaphore.h @@ -0,0 +1,85 @@ +/* + Copyright (c) 2011-2016 mingw-w64 project + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#ifndef WIN_PTHREADS_SEMAPHORE_H +#define WIN_PTHREADS_SEMAPHORE_H + +#ifdef __cplusplus +extern "C" { +#endif + +#if defined(IN_WINPTHREAD) +# if defined(DLL_EXPORT) && !defined(WINPTHREAD_EXPORT_ALL_DEBUG) +# define WINPTHREAD_SEMA_API __declspec(dllexport) /* building the DLL */ +# else +# define WINPTHREAD_SEMA_API /* building the static library */ +# endif +#else +# if defined(WINPTHREADS_USE_DLLIMPORT) +# define WINPTHREAD_SEMA_API __declspec(dllimport) /* user wants explicit `dllimport` */ +# else +# define WINPTHREAD_SEMA_API /* the default; auto imported in case of DLL */ +# endif +#endif + +/* Set this to 0 to disable it */ +#define USE_SEM_CriticalSection_SpinCount 100 + +#define SEM_VALUE_MAX INT_MAX + +#ifndef _MODE_T_ +#define _MODE_T_ +typedef unsigned short mode_t; +#endif + +typedef void *sem_t; + +#define SEM_FAILED NULL + +WINPTHREAD_SEMA_API int sem_init(sem_t * sem, int pshared, unsigned int value); + +WINPTHREAD_SEMA_API int sem_destroy(sem_t *sem); + +WINPTHREAD_SEMA_API int sem_trywait(sem_t *sem); + +WINPTHREAD_SEMA_API int sem_wait(sem_t *sem); + +WINPTHREAD_SEMA_API int sem_timedwait(sem_t * sem, const struct timespec *t); + +WINPTHREAD_SEMA_API int sem_post(sem_t *sem); + +WINPTHREAD_SEMA_API int sem_post_multiple(sem_t *sem, int count); + +/* yes, it returns a semaphore (or SEM_FAILED) */ +WINPTHREAD_SEMA_API sem_t * sem_open(const char * name, int oflag, mode_t mode, unsigned int value); + +WINPTHREAD_SEMA_API int sem_close(sem_t * sem); + +WINPTHREAD_SEMA_API int sem_unlink(const char * name); + +WINPTHREAD_SEMA_API int sem_getvalue(sem_t * sem, int * sval); + +#ifdef __cplusplus +} +#endif + +#endif /* WIN_PTHREADS_SEMAPHORE_H */ diff --git a/libs/winpthreads/src/barrier.c b/libs/winpthreads/src/barrier.c new file mode 100644 index 0000000..e973aaa --- /dev/null +++ b/libs/winpthreads/src/barrier.c @@ -0,0 +1,246 @@ +/* + Copyright (c) 2011-2016 mingw-w64 project + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include +#include +#include +#include "pthread.h" +#include "barrier.h" +#include "ref.h" +#include "misc.h" + +static pthread_spinlock_t barrier_global = PTHREAD_SPINLOCK_INITIALIZER; + +static WINPTHREADS_ATTRIBUTE((noinline)) int +barrier_unref(volatile pthread_barrier_t *barrier, int res) +{ + pthread_spin_lock(&barrier_global); +#ifdef WINPTHREAD_DBG + assert((((barrier_t *)*barrier)->valid == LIFE_BARRIER) && (((barrier_t *)*barrier)->busy > 0)); +#endif + ((barrier_t *)*barrier)->busy -= 1; + pthread_spin_unlock(&barrier_global); + return res; +} + +static WINPTHREADS_ATTRIBUTE((noinline)) int barrier_ref(volatile pthread_barrier_t *barrier) +{ + int r = 0; + pthread_spin_lock(&barrier_global); + + if (!barrier || !*barrier || ((barrier_t *)*barrier)->valid != LIFE_BARRIER) r = EINVAL; + else { + ((barrier_t *)*barrier)->busy += 1; + } + + pthread_spin_unlock(&barrier_global); + + return r; +} + +static WINPTHREADS_ATTRIBUTE((noinline)) int +barrier_ref_destroy(volatile pthread_barrier_t *barrier, pthread_barrier_t *bDestroy) +{ + int r = 0; + + *bDestroy = NULL; + pthread_spin_lock(&barrier_global); + + if (!barrier || !*barrier || ((barrier_t *)*barrier)->valid != LIFE_BARRIER) r = EINVAL; + else { + barrier_t *b_ = (barrier_t *)*barrier; + if (b_->busy) r = EBUSY; + else { + *bDestroy = *barrier; + *barrier = NULL; + } + } + + pthread_spin_unlock(&barrier_global); + return r; +} + +static WINPTHREADS_ATTRIBUTE((noinline)) void +barrier_ref_set (volatile pthread_barrier_t *barrier, void *v) +{ + pthread_spin_lock(&barrier_global); + *barrier = v; + pthread_spin_unlock(&barrier_global); +} + +int pthread_barrier_destroy(pthread_barrier_t *b_) +{ + pthread_barrier_t bDestroy; + barrier_t *b; + int r; + + while ((r = barrier_ref_destroy(b_,&bDestroy)) == EBUSY) + Sleep(0); + + if (r) + return r; + + b = (barrier_t *)bDestroy; + + pthread_mutex_lock(&b->m); + + if (sem_destroy(&b->sems[0]) != 0) + { + /* Could this happen? */ + *b_ = bDestroy; + pthread_mutex_unlock (&b->m); + return EBUSY; + } + if (sem_destroy(&b->sems[1]) != 0) + { + sem_init (&b->sems[0], b->share, 0); + *b_ = bDestroy; + pthread_mutex_unlock (&b->m); + return -1; + } + pthread_mutex_unlock(&b->m); + if(pthread_mutex_destroy(&b->m) != 0) { + sem_init (&b->sems[0], b->share, 0); + sem_init (&b->sems[1], b->share, 0); + *b_ = bDestroy; + return -1; + } + b->valid = DEAD_BARRIER; + free(bDestroy); + return 0; + +} + +int +pthread_barrier_init (pthread_barrier_t *b_, const void *attr, + unsigned int count) +{ + barrier_t *b; + + if (!count || !b_) + return EINVAL; + + if ((b = (pthread_barrier_t)calloc(1,sizeof(*b))) == NULL) + return ENOMEM; + if (!attr || *((int **)attr) == NULL) + b->share = PTHREAD_PROCESS_PRIVATE; + else + memcpy (&b->share, *((void **) attr), sizeof (int)); + b->total = count; + b->count = count; + b->valid = LIFE_BARRIER; + b->sel = 0; + + if (pthread_mutex_init(&b->m, NULL) != 0) + { + free (b); + return ENOMEM; + } + + if (sem_init(&b->sems[0], b->share, 0) != 0) + { + pthread_mutex_destroy(&b->m); + free (b); + return ENOMEM; + } + if (sem_init(&b->sems[1], b->share, 0) != 0) + { + pthread_mutex_destroy(&b->m); + sem_destroy(&b->sems[0]); + free (b); + return ENOMEM; + } + barrier_ref_set (b_,b); + + return 0; +} + +int pthread_barrier_wait(pthread_barrier_t *b_) +{ + long sel; + int r, e, rslt; + barrier_t *b; + + r = barrier_ref(b_); + if(r) return r; + + b = (barrier_t *)*b_; + + if ((r = pthread_mutex_lock(&b->m)) != 0) return barrier_unref(b_,EINVAL); + sel = b->sel; + InterlockedDecrement((long*)&b->total); + if (b->total == 0) + { + b->total = b->count; + b->sel = (sel != 0 ? 0 : 1); + e = 1; + rslt = PTHREAD_BARRIER_SERIAL_THREAD; + r = (b->count > 1 ? sem_post_multiple (&b->sems[sel], b->count - 1) : 0); + } + else { e = 0; rslt= 0; } + pthread_mutex_unlock(&b->m); + if (!e) + r = sem_wait(&b->sems[sel]); + + if (!r) r = rslt; + return barrier_unref(b_,r); +} + +int pthread_barrierattr_init(void **attr) +{ + int *p; + + if ((p = (int *) calloc (1, sizeof (int))) == NULL) + return ENOMEM; + + *p = PTHREAD_PROCESS_PRIVATE; + *attr = p; + + return 0; +} + +int pthread_barrierattr_destroy(void **attr) +{ + void *p; + if (!attr || (p = *attr) == NULL) + return EINVAL; + *attr = NULL; + free (p); + return 0; +} + +int pthread_barrierattr_setpshared(void **attr, int s) +{ + if (!attr || *attr == NULL + || (s != PTHREAD_PROCESS_SHARED && s != PTHREAD_PROCESS_PRIVATE)) + return EINVAL; + memcpy (*attr, &s, sizeof (int)); + return 0; +} + +int pthread_barrierattr_getpshared(void **attr, int *s) +{ + if (!attr || !s || *attr == NULL) + return EINVAL; + memcpy (s, *attr, sizeof (int)); + return 0; +} diff --git a/libs/winpthreads/src/barrier.h b/libs/winpthreads/src/barrier.h new file mode 100644 index 0000000..5509678 --- /dev/null +++ b/libs/winpthreads/src/barrier.h @@ -0,0 +1,52 @@ +/* + Copyright (c) 2011-2016 mingw-w64 project + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#ifndef WIN_PTHREADS_BARRIER_H +#define WIN_PTHREADS_BARRIER_H + +#define LIFE_BARRIER 0xBAB1FEED +#define DEAD_BARRIER 0xDEADB00F + +#define _PTHREAD_BARRIER_FLAG (1<<30) + +#define CHECK_BARRIER(b) \ + do { \ + if (!(b) || ( ((barrier_t *)(*b))->valid != (unsigned int)LIFE_BARRIER ) ) \ + return EINVAL; \ + } while (0) + +#include "semaphore.h" + +typedef struct barrier_t barrier_t; +struct barrier_t +{ + int valid; + int busy; + int count; + int total; + int share; + long sel; + pthread_mutex_t m; + sem_t sems[2]; +}; + +#endif diff --git a/libs/winpthreads/src/clock.c b/libs/winpthreads/src/clock.c new file mode 100644 index 0000000..954d845 --- /dev/null +++ b/libs/winpthreads/src/clock.c @@ -0,0 +1,257 @@ +/** + * This file has no copyright assigned and is placed in the Public Domain. + * This file is part of the w64 mingw-runtime package. + * No warranty is given; refer to the file DISCLAIMER.PD within this package. + */ + +#include +#include +#include +#include +#ifndef IN_WINPTHREAD +#define IN_WINPTHREAD 1 +#endif +#include "pthread.h" +#include "pthread_time.h" +#include "misc.h" + +#define POW10_7 10000000 +#define POW10_9 1000000000 + +/* Number of 100ns-seconds between the beginning of the Windows epoch + * (Jan. 1, 1601) and the Unix epoch (Jan. 1, 1970) + */ +#define DELTA_EPOCH_IN_100NS INT64_C(116444736000000000) + +static WINPTHREADS_INLINE int lc_set_errno(int result) +{ + if (result != 0) { + errno = result; + return -1; + } + return 0; +} + +/** + * Get the resolution of the specified clock clock_id and + * stores it in the struct timespec pointed to by res. + * @param clock_id The clock_id argument is the identifier of the particular + * clock on which to act. The following clocks are supported: + *
+ *     CLOCK_REALTIME  System-wide real-time clock. Setting this clock
+ *                 requires appropriate privileges.
+ *     CLOCK_MONOTONIC Clock that cannot be set and represents monotonic
+ *                 time since some unspecified starting point.
+ *     CLOCK_PROCESS_CPUTIME_ID High-resolution per-process timer from the CPU.
+ *     CLOCK_THREAD_CPUTIME_ID  Thread-specific CPU-time clock.
+ * 
+ * @param res The pointer to a timespec structure to receive the time + * resolution. + * @return If the function succeeds, the return value is 0. + * If the function fails, the return value is -1, + * with errno set to indicate the error. + */ +int clock_getres(clockid_t clock_id, struct timespec *res) +{ + clockid_t id = clock_id; + + if (id == CLOCK_REALTIME && _pthread_get_system_time_best_as_file_time == GetSystemTimeAsFileTime) + id = CLOCK_REALTIME_COARSE; /* GetSystemTimePreciseAsFileTime() not available */ + + switch(id) { + case CLOCK_REALTIME: + case CLOCK_MONOTONIC: + { + LARGE_INTEGER pf; + + if (QueryPerformanceFrequency(&pf) == 0) + return lc_set_errno(EINVAL); + + res->tv_sec = 0; + res->tv_nsec = (int) ((POW10_9 + (pf.QuadPart >> 1)) / pf.QuadPart); + if (res->tv_nsec < 1) + res->tv_nsec = 1; + + return 0; + } + + case CLOCK_REALTIME_COARSE: + case CLOCK_PROCESS_CPUTIME_ID: + case CLOCK_THREAD_CPUTIME_ID: + { + DWORD timeAdjustment, timeIncrement; + BOOL isTimeAdjustmentDisabled; + + (void) GetSystemTimeAdjustment(&timeAdjustment, &timeIncrement, &isTimeAdjustmentDisabled); + res->tv_sec = 0; + res->tv_nsec = timeIncrement * 100; + + return 0; + } + default: + break; + } + + return lc_set_errno(EINVAL); +} + +/** + * Get the time of the specified clock clock_id and stores it in the struct + * timespec pointed to by tp. + * @param clock_id The clock_id argument is the identifier of the particular + * clock on which to act. The following clocks are supported: + *
+ *     CLOCK_REALTIME  System-wide real-time clock. Setting this clock
+ *                 requires appropriate privileges.
+ *     CLOCK_MONOTONIC Clock that cannot be set and represents monotonic
+ *                 time since some unspecified starting point.
+ *     CLOCK_PROCESS_CPUTIME_ID High-resolution per-process timer from the CPU.
+ *     CLOCK_THREAD_CPUTIME_ID  Thread-specific CPU-time clock.
+ * 
+ * @param tp The pointer to a timespec structure to receive the time. + * @return If the function succeeds, the return value is 0. + * If the function fails, the return value is -1, + * with errno set to indicate the error. + */ +int clock_gettime(clockid_t clock_id, struct timespec *tp) +{ + unsigned __int64 t; + LARGE_INTEGER pf, pc; + union { + unsigned __int64 u64; + FILETIME ft; + } ct, et, kt, ut; + + switch(clock_id) { + case CLOCK_REALTIME: + { + _pthread_get_system_time_best_as_file_time(&ct.ft); + t = ct.u64 - DELTA_EPOCH_IN_100NS; + tp->tv_sec = t / POW10_7; + tp->tv_nsec = ((int) (t % POW10_7)) * 100; + + return 0; + } + + case CLOCK_REALTIME_COARSE: + { + GetSystemTimeAsFileTime(&ct.ft); + t = ct.u64 - DELTA_EPOCH_IN_100NS; + tp->tv_sec = t / POW10_7; + tp->tv_nsec = ((int) (t % POW10_7)) * 100; + + return 0; + } + + case CLOCK_MONOTONIC: + { + if (QueryPerformanceFrequency(&pf) == 0) + return lc_set_errno(EINVAL); + + if (QueryPerformanceCounter(&pc) == 0) + return lc_set_errno(EINVAL); + + tp->tv_sec = pc.QuadPart / pf.QuadPart; + tp->tv_nsec = (int) (((pc.QuadPart % pf.QuadPart) * POW10_9 + (pf.QuadPart >> 1)) / pf.QuadPart); + if (tp->tv_nsec >= POW10_9) { + tp->tv_sec ++; + tp->tv_nsec -= POW10_9; + } + + return 0; + } + + case CLOCK_PROCESS_CPUTIME_ID: + { + if(0 == GetProcessTimes(GetCurrentProcess(), &ct.ft, &et.ft, &kt.ft, &ut.ft)) + return lc_set_errno(EINVAL); + t = kt.u64 + ut.u64; + tp->tv_sec = t / POW10_7; + tp->tv_nsec = ((int) (t % POW10_7)) * 100; + + return 0; + } + + case CLOCK_THREAD_CPUTIME_ID: + { + if(0 == GetThreadTimes(GetCurrentThread(), &ct.ft, &et.ft, &kt.ft, &ut.ft)) + return lc_set_errno(EINVAL); + t = kt.u64 + ut.u64; + tp->tv_sec = t / POW10_7; + tp->tv_nsec = ((int) (t % POW10_7)) * 100; + + return 0; + } + + default: + break; + } + + return lc_set_errno(EINVAL); +} + +/** + * Sleep for the specified time. + * @param clock_id This argument should always be CLOCK_REALTIME (0). + * @param flags 0 for relative sleep interval, others for absolute waking up. + * @param request The desired sleep interval or absolute waking up time. + * @param remain The remain amount of time to sleep. + * The current implemention just ignore it. + * @return If the function succeeds, the return value is 0. + * If the function fails, the return value is -1, + * with errno set to indicate the error. + */ +int clock_nanosleep(clockid_t clock_id, int flags, + const struct timespec *request, + struct timespec *remain) +{ + struct timespec tp; + + if (clock_id != CLOCK_REALTIME) + return lc_set_errno(EINVAL); + + if (flags == 0) + return nanosleep(request, remain); + + /* TIMER_ABSTIME = 1 */ + clock_gettime(CLOCK_REALTIME, &tp); + + tp.tv_sec = request->tv_sec - tp.tv_sec; + tp.tv_nsec = request->tv_nsec - tp.tv_nsec; + if (tp.tv_nsec < 0) { + tp.tv_nsec += POW10_9; + tp.tv_sec --; + } + + return nanosleep(&tp, remain); +} + +/** + * Set the time of the specified clock clock_id. + * @param clock_id This argument should always be CLOCK_REALTIME (0). + * @param tp The requested time. + * @return If the function succeeds, the return value is 0. + * If the function fails, the return value is -1, + * with errno set to indicate the error. + */ +int clock_settime(clockid_t clock_id, const struct timespec *tp) +{ + SYSTEMTIME st; + + union { + unsigned __int64 u64; + FILETIME ft; + } t; + + if (clock_id != CLOCK_REALTIME) + return lc_set_errno(EINVAL); + + t.u64 = tp->tv_sec * (__int64) POW10_7 + tp->tv_nsec / 100 + DELTA_EPOCH_IN_100NS; + if (FileTimeToSystemTime(&t.ft, &st) == 0) + return lc_set_errno(EINVAL); + + if (SetSystemTime(&st) == 0) + return lc_set_errno(EPERM); + + return 0; +} diff --git a/libs/winpthreads/src/cond.c b/libs/winpthreads/src/cond.c new file mode 100644 index 0000000..813648f --- /dev/null +++ b/libs/winpthreads/src/cond.c @@ -0,0 +1,755 @@ +/* + Copyright (c) 2011-2016 mingw-w64 project + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +/* + * Posix Condition Variables for Microsoft Windows. + * 22-9-2010 Partly based on the ACE framework implementation. + */ +#include +#include +#include +#include +#include "pthread.h" +#include "pthread_time.h" +#include "ref.h" +#include "cond.h" +#include "thread.h" +#include "misc.h" +#include "winpthread_internal.h" + +#include "pthread_compat.h" + +int __pthread_shallcancel (void); + +static int do_sema_b_wait (HANDLE sema, int nointerrupt, DWORD timeout,CRITICAL_SECTION *cs, LONG *val); +static int do_sema_b_release(HANDLE sema, LONG count,CRITICAL_SECTION *cs, LONG *val); +static void cleanup_wait(void *arg); + +typedef struct sCondWaitHelper { + cond_t *c; + pthread_mutex_t *external_mutex; + int *r; +} sCondWaitHelper; + +int do_sema_b_wait_intern (HANDLE sema, int nointerrupt, DWORD timeout); + +#ifdef WINPTHREAD_DBG +static int print_state = 0; +static FILE *fo; +void cond_print_set(int state, FILE *f) +{ + if (f) fo = f; + if (!fo) fo = stdout; + print_state = state; +} + +void cond_print(volatile pthread_cond_t *c, char *txt) +{ + if (!print_state) return; + cond_t *c_ = (cond_t *)*c; + if (c_ == NULL) { + fprintf(fo,"C%p %lu %s\n",(void *)*c,GetCurrentThreadId(),txt); + } else { + fprintf(fo,"C%p %lu V=%0X w=%ld %s\n", + (void *)*c, + GetCurrentThreadId(), + (int)c_->valid, + c_->waiters_count_, + txt + ); + } +} +#endif + +static pthread_spinlock_t cond_locked = PTHREAD_SPINLOCK_INITIALIZER; + +static int +cond_static_init (pthread_cond_t *c) +{ + int r = 0; + + pthread_spin_lock (&cond_locked); + if (c == NULL) + r = EINVAL; + else if (*c == PTHREAD_COND_INITIALIZER) + r = pthread_cond_init (c, NULL); + else + /* We assume someone was faster ... */ + r = 0; + pthread_spin_unlock (&cond_locked); + return r; +} + +int +pthread_condattr_destroy (pthread_condattr_t *a) +{ + if (!a) + return EINVAL; + *a = 0; + return 0; +} + +int +pthread_condattr_init (pthread_condattr_t *a) +{ + if (!a) + return EINVAL; + *a = 0; + return 0; +} + +int +pthread_condattr_getpshared (const pthread_condattr_t *a, int *s) +{ + if (!a || !s) + return EINVAL; + *s = *a; + return 0; +} + +int +pthread_condattr_getclock (const pthread_condattr_t *a, clockid_t *clock_id) +{ + if (!a || !clock_id) + return EINVAL; + *clock_id = 0; + return 0; +} + +int +pthread_condattr_setclock(pthread_condattr_t *a, clockid_t clock_id) +{ + if (!a || clock_id != 0) + return EINVAL; + return 0; +} + +int +__pthread_clock_nanosleep (clockid_t clock_id, int flags, const struct timespec *rqtp, + struct timespec *rmtp) +{ + unsigned long long tick, tick2; + unsigned long long delay; + DWORD dw; + + if (clock_id != CLOCK_REALTIME + && clock_id != CLOCK_MONOTONIC + && clock_id != CLOCK_PROCESS_CPUTIME_ID) + return EINVAL; + if ((flags & TIMER_ABSTIME) != 0) + delay = _pthread_rel_time_in_ms (rqtp); + else + delay = _pthread_time_in_ms_from_timespec (rqtp); + do + { + dw = (DWORD) (delay >= 99999ULL ? 99999ULL : delay); + tick = _pthread_time_in_ms (); + pthread_delay_np_ms (dw); + tick2 = _pthread_time_in_ms (); + tick2 -= tick; + if (tick2 >= delay) + delay = 0; + else + delay -= tick2; + } + while (delay != 0ULL); + if (rmtp) + memset (rmtp, 0, sizeof (*rmtp)); + return 0; +} + +int +pthread_condattr_setpshared (pthread_condattr_t *a, int s) +{ + if (!a || (s != PTHREAD_PROCESS_SHARED && s != PTHREAD_PROCESS_PRIVATE)) + return EINVAL; + if (s == PTHREAD_PROCESS_SHARED) + { + *a = PTHREAD_PROCESS_PRIVATE; + return ENOSYS; + } + *a = s; + return 0; +} + +int +pthread_cond_init (pthread_cond_t *c, const pthread_condattr_t *a) +{ + cond_t *_c; + int r = 0; + + if (!c) + return EINVAL; + if (a && *a == PTHREAD_PROCESS_SHARED) + return ENOSYS; + + if ((_c = calloc(1, sizeof(*_c))) == NULL) + return ENOMEM; + + _c->valid = DEAD_COND; + _c->busy = 0; + _c->waiters_count_ = 0; + _c->waiters_count_gone_ = 0; + _c->waiters_count_unblock_ = 0; + + _c->sema_q = CreateSemaphore (NULL, /* no security */ + 0, /* initially 0 */ + 0x7fffffff, /* max count */ + NULL); /* unnamed */ + _c->sema_b = CreateSemaphore (NULL, /* no security */ + 0, /* initially 0 */ + 0x7fffffff, /* max count */ + NULL); + if (_c->sema_q == NULL || _c->sema_b == NULL) { + if (_c->sema_q != NULL) + CloseHandle (_c->sema_q); + if (_c->sema_b != NULL) + CloseHandle (_c->sema_b); + free (_c); + r = EAGAIN; + } else { + InitializeCriticalSection(&_c->waiters_count_lock_); + InitializeCriticalSection(&_c->waiters_b_lock_); + InitializeCriticalSection(&_c->waiters_q_lock_); + _c->value_q = 0; + _c->value_b = 1; + } + if (!r) + { + _c->valid = LIFE_COND; + *c = (pthread_cond_t)_c; + } + else + *c = (pthread_cond_t)NULL; + return r; +} + +int +pthread_cond_destroy (pthread_cond_t *c) +{ + cond_t *_c; + int r; + if (!c || !*c) + return EINVAL; + if (*c == PTHREAD_COND_INITIALIZER) + { + pthread_spin_lock (&cond_locked); + if (*c == PTHREAD_COND_INITIALIZER) + { + *c = (pthread_cond_t)NULL; + r = 0; + } + else + r = EBUSY; + pthread_spin_unlock (&cond_locked); + return r; + } + _c = (cond_t *) *c; + r = do_sema_b_wait(_c->sema_b, 0, INFINITE,&_c->waiters_b_lock_,&_c->value_b); + if (r != 0) + return r; + if (!TryEnterCriticalSection (&_c->waiters_count_lock_)) + { + do_sema_b_release (_c->sema_b, 1,&_c->waiters_b_lock_,&_c->value_b); + return EBUSY; + } + if (_c->waiters_count_ > _c->waiters_count_gone_) + { + r = do_sema_b_release (_c->sema_b, 1,&_c->waiters_b_lock_,&_c->value_b); + if (!r) r = EBUSY; + LeaveCriticalSection(&_c->waiters_count_lock_); + return r; + } + *c = (pthread_cond_t)NULL; + do_sema_b_release (_c->sema_b, 1,&_c->waiters_b_lock_,&_c->value_b); + + if (!CloseHandle (_c->sema_q) && !r) + r = EINVAL; + if (!CloseHandle (_c->sema_b) && !r) + r = EINVAL; + LeaveCriticalSection (&_c->waiters_count_lock_); + DeleteCriticalSection(&_c->waiters_count_lock_); + DeleteCriticalSection(&_c->waiters_b_lock_); + DeleteCriticalSection(&_c->waiters_q_lock_); + _c->valid = DEAD_COND; + free(_c); + return 0; +} + +int +pthread_cond_signal (pthread_cond_t *c) +{ + cond_t *_c; + int r; + + if (!c || !*c) + return EINVAL; + _c = (cond_t *)*c; + if (_c == (cond_t *)PTHREAD_COND_INITIALIZER) + return 0; + else if (_c->valid != (unsigned int)LIFE_COND) + return EINVAL; + + EnterCriticalSection (&_c->waiters_count_lock_); + /* If there aren't any waiters, then this is a no-op. */ + if (_c->waiters_count_unblock_ != 0) + { + if (_c->waiters_count_ == 0) + { + LeaveCriticalSection (&_c->waiters_count_lock_); + /* pthread_testcancel(); */ + return 0; + } + _c->waiters_count_ -= 1; + _c->waiters_count_unblock_ += 1; + } + else if (_c->waiters_count_ > _c->waiters_count_gone_) + { + r = do_sema_b_wait (_c->sema_b, 1, INFINITE,&_c->waiters_b_lock_,&_c->value_b); + if (r != 0) + { + LeaveCriticalSection (&_c->waiters_count_lock_); + /* pthread_testcancel(); */ + return r; + } + if (_c->waiters_count_gone_ != 0) + { + _c->waiters_count_ -= _c->waiters_count_gone_; + _c->waiters_count_gone_ = 0; + } + _c->waiters_count_ -= 1; + _c->waiters_count_unblock_ = 1; + } + else + { + LeaveCriticalSection (&_c->waiters_count_lock_); + /* pthread_testcancel(); */ + return 0; + } + LeaveCriticalSection (&_c->waiters_count_lock_); + r = do_sema_b_release(_c->sema_q, 1,&_c->waiters_q_lock_,&_c->value_q); + /* pthread_testcancel(); */ + return r; +} + +int +pthread_cond_broadcast (pthread_cond_t *c) +{ + cond_t *_c; + int r; + int relCnt = 0; + + if (!c || !*c) + return EINVAL; + _c = (cond_t *)*c; + if (_c == (cond_t*)PTHREAD_COND_INITIALIZER) + return 0; + else if (_c->valid != (unsigned int)LIFE_COND) + return EINVAL; + + EnterCriticalSection (&_c->waiters_count_lock_); + /* If there aren't any waiters, then this is a no-op. */ + if (_c->waiters_count_unblock_ != 0) + { + if (_c->waiters_count_ == 0) + { + LeaveCriticalSection (&_c->waiters_count_lock_); + /* pthread_testcancel(); */ + return 0; + } + relCnt = _c->waiters_count_; + _c->waiters_count_ = 0; + _c->waiters_count_unblock_ += relCnt; + } + else if (_c->waiters_count_ > _c->waiters_count_gone_) + { + r = do_sema_b_wait (_c->sema_b, 1, INFINITE,&_c->waiters_b_lock_,&_c->value_b); + if (r != 0) + { + LeaveCriticalSection (&_c->waiters_count_lock_); + /* pthread_testcancel(); */ + return r; + } + if (_c->waiters_count_gone_ != 0) + { + _c->waiters_count_ -= _c->waiters_count_gone_; + _c->waiters_count_gone_ = 0; + } + relCnt = _c->waiters_count_; + _c->waiters_count_ = 0; + _c->waiters_count_unblock_ = relCnt; + } + else + { + LeaveCriticalSection (&_c->waiters_count_lock_); + /* pthread_testcancel(); */ + return 0; + } + LeaveCriticalSection (&_c->waiters_count_lock_); + r = do_sema_b_release(_c->sema_q, relCnt,&_c->waiters_q_lock_,&_c->value_q); + /* pthread_testcancel(); */ + return r; +} + +int +pthread_cond_wait (pthread_cond_t *c, pthread_mutex_t *external_mutex) +{ + sCondWaitHelper ch; + cond_t *_c; + int r; + + /* pthread_testcancel(); */ + + if (!c || *c == (pthread_cond_t)NULL) + return EINVAL; + _c = (cond_t *)*c; + if (*c == PTHREAD_COND_INITIALIZER) + { + r = cond_static_init(c); + if (r != 0 && r != EBUSY) + return r; + _c = (cond_t *) *c; + } else if (_c->valid != (unsigned int)LIFE_COND) + return EINVAL; + +tryagain: + r = do_sema_b_wait (_c->sema_b, 0, INFINITE,&_c->waiters_b_lock_,&_c->value_b); + if (r != 0) + return r; + + if (!TryEnterCriticalSection (&_c->waiters_count_lock_)) + { + r = do_sema_b_release (_c->sema_b, 1,&_c->waiters_b_lock_,&_c->value_b); + if (r != 0) + return r; + sched_yield(); + goto tryagain; + } + + _c->waiters_count_++; + LeaveCriticalSection(&_c->waiters_count_lock_); + r = do_sema_b_release (_c->sema_b, 1,&_c->waiters_b_lock_,&_c->value_b); + if (r != 0) + return r; + + ch.c = _c; + ch.r = &r; + ch.external_mutex = external_mutex; + + pthread_cleanup_push(cleanup_wait, (void *) &ch); + r = pthread_mutex_unlock(external_mutex); + if (!r) + r = do_sema_b_wait (_c->sema_q, 0, INFINITE,&_c->waiters_q_lock_,&_c->value_q); + + pthread_cleanup_pop(1); + return r; +} + +static int +pthread_cond_timedwait_impl (pthread_cond_t *c, pthread_mutex_t *external_mutex, const struct timespec *t, int rel) +{ + sCondWaitHelper ch; + DWORD dwr; + int r; + cond_t *_c; + + /* pthread_testcancel(); */ + + if (!c || !*c) + return EINVAL; + _c = (cond_t *)*c; + if (_c == (cond_t *)PTHREAD_COND_INITIALIZER) + { + r = cond_static_init(c); + if (r && r != EBUSY) + return r; + _c = (cond_t *) *c; + } else if ((_c)->valid != (unsigned int)LIFE_COND) + return EINVAL; + + if (rel == 0) + { + dwr = dwMilliSecs(_pthread_rel_time_in_ms(t)); + } + else + { + dwr = dwMilliSecs(_pthread_time_in_ms_from_timespec(t)); + } + +tryagain: + r = do_sema_b_wait (_c->sema_b, 0, INFINITE,&_c->waiters_b_lock_,&_c->value_b); + if (r != 0) + return r; + + if (!TryEnterCriticalSection (&_c->waiters_count_lock_)) + { + r = do_sema_b_release (_c->sema_b, 1,&_c->waiters_b_lock_,&_c->value_b); + if (r != 0) + return r; + sched_yield(); + goto tryagain; + } + + _c->waiters_count_++; + LeaveCriticalSection(&_c->waiters_count_lock_); + r = do_sema_b_release (_c->sema_b, 1,&_c->waiters_b_lock_,&_c->value_b); + if (r != 0) + return r; + + ch.c = _c; + ch.r = &r; + ch.external_mutex = external_mutex; + { + pthread_cleanup_push(cleanup_wait, (void *) &ch); + + r = pthread_mutex_unlock(external_mutex); + if (!r) + r = do_sema_b_wait (_c->sema_q, 0, dwr,&_c->waiters_q_lock_,&_c->value_q); + + pthread_cleanup_pop(1); + } + return r; +} + +int +pthread_cond_timedwait(pthread_cond_t *c, pthread_mutex_t *m, const struct timespec *t) +{ + return pthread_cond_timedwait_impl(c, m, t, 0); +} + +int +pthread_cond_timedwait_relative_np(pthread_cond_t *c, pthread_mutex_t *m, const struct timespec *t) +{ + return pthread_cond_timedwait_impl(c, m, t, 1); +} + +static void +cleanup_wait (void *arg) +{ + int n, r; + sCondWaitHelper *ch = (sCondWaitHelper *) arg; + cond_t *_c; + + _c = ch->c; + EnterCriticalSection (&_c->waiters_count_lock_); + n = _c->waiters_count_unblock_; + if (n != 0) + _c->waiters_count_unblock_ -= 1; + else if ((INT_MAX/2) - 1 == _c->waiters_count_gone_) + { + _c->waiters_count_gone_ += 1; + r = do_sema_b_wait (_c->sema_b, 1, INFINITE,&_c->waiters_b_lock_,&_c->value_b); + if (r != 0) + { + LeaveCriticalSection(&_c->waiters_count_lock_); + ch->r[0] = r; + return; + } + _c->waiters_count_ -= _c->waiters_count_gone_; + r = do_sema_b_release (_c->sema_b, 1,&_c->waiters_b_lock_,&_c->value_b); + if (r != 0) + { + LeaveCriticalSection(&_c->waiters_count_lock_); + ch->r[0] = r; + return; + } + _c->waiters_count_gone_ = 0; + } + else + _c->waiters_count_gone_ += 1; + LeaveCriticalSection (&_c->waiters_count_lock_); + + if (n == 1) + { + r = do_sema_b_release (_c->sema_b, 1,&_c->waiters_b_lock_,&_c->value_b); + if (r != 0) + { + ch->r[0] = r; + return; + } + } + r = pthread_mutex_lock(ch->external_mutex); + if (r != 0) + ch->r[0] = r; +} + +static int +do_sema_b_wait (HANDLE sema, int nointerrupt, DWORD timeout,CRITICAL_SECTION *cs, LONG *val) +{ + int r; + LONG v; + EnterCriticalSection(cs); + InterlockedDecrement(val); + v = val[0]; + LeaveCriticalSection(cs); + if (v >= 0) + return 0; + r = do_sema_b_wait_intern (sema, nointerrupt, timeout); + EnterCriticalSection(cs); + if (r != 0) + InterlockedIncrement(val); + LeaveCriticalSection(cs); + return r; +} + +int +do_sema_b_wait_intern (HANDLE sema, int nointerrupt, DWORD timeout) +{ + HANDLE arr[2]; + DWORD maxH = 1; + int r = 0; + DWORD res, dt; + if (nointerrupt == 1) + { + res = _pthread_wait_for_single_object(sema, timeout); + switch (res) { + case WAIT_TIMEOUT: + r = ETIMEDOUT; + break; + case WAIT_ABANDONED: + r = EPERM; + break; + case WAIT_OBJECT_0: + break; + default: + /*We can only return EINVAL though it might not be posix compliant */ + r = EINVAL; + } + if (r != 0 && r != EINVAL && WaitForSingleObject(sema, 0) == WAIT_OBJECT_0) + r = 0; + return r; + } + arr[0] = sema; + arr[1] = (HANDLE) pthread_getevent (); + if (arr[1] != NULL) maxH += 1; + if (maxH == 2) + { +redo: + res = _pthread_wait_for_multiple_objects(maxH, arr, 0, timeout); + switch (res) { + case WAIT_TIMEOUT: + r = ETIMEDOUT; + break; + case (WAIT_OBJECT_0 + 1): + ResetEvent(arr[1]); + if (nointerrupt != 2) + { + pthread_testcancel(); + return EINVAL; + } + pthread_testcancel (); + goto redo; + case WAIT_ABANDONED: + r = EPERM; + break; + case WAIT_OBJECT_0: + r = 0; + break; + default: + /*We can only return EINVAL though it might not be posix compliant */ + r = EINVAL; + } + if (r != 0 && r != EINVAL && WaitForSingleObject(arr[0], 0) == WAIT_OBJECT_0) + r = 0; + if (r != 0 && nointerrupt != 2 && __pthread_shallcancel ()) + return EINVAL; + return r; + } + if (timeout == INFINITE) + { + do { + res = _pthread_wait_for_single_object(sema, 40); + switch (res) { + case WAIT_TIMEOUT: + r = ETIMEDOUT; + break; + case WAIT_ABANDONED: + r = EPERM; + break; + case WAIT_OBJECT_0: + r = 0; + break; + default: + /*We can only return EINVAL though it might not be posix compliant */ + r = EINVAL; + } + if (r != 0 && __pthread_shallcancel ()) + { + if (nointerrupt != 2) + pthread_testcancel(); + return EINVAL; + } + } while (r == ETIMEDOUT); + if (r != 0 && r != EINVAL && WaitForSingleObject(sema, 0) == WAIT_OBJECT_0) + r = 0; + return r; + } + dt = 20; + do { + if (dt > timeout) dt = timeout; + res = _pthread_wait_for_single_object(sema, dt); + switch (res) { + case WAIT_TIMEOUT: + r = ETIMEDOUT; + break; + case WAIT_ABANDONED: + r = EPERM; + break; + case WAIT_OBJECT_0: + r = 0; + break; + default: + /*We can only return EINVAL though it might not be posix compliant */ + r = EINVAL; + } + timeout -= dt; + if (timeout != 0 && r != 0 && __pthread_shallcancel ()) + return EINVAL; + } while (r == ETIMEDOUT && timeout != 0); + if (r != 0 && r == ETIMEDOUT && WaitForSingleObject(sema, 0) == WAIT_OBJECT_0) + r = 0; + if (r != 0 && nointerrupt != 2) + pthread_testcancel(); + return r; +} + +static int +do_sema_b_release(HANDLE sema, LONG count,CRITICAL_SECTION *cs, LONG *val) +{ + int wc; + EnterCriticalSection(cs); + if (((long long) val[0] + (long long) count) > (long long) 0x7fffffffLL) + { + LeaveCriticalSection(cs); + return ERANGE; + } + wc = -val[0]; + InterlockedExchangeAdd(val, count); + if (wc <= 0 || ReleaseSemaphore(sema, (wc < count ? wc : count), NULL)) + { + LeaveCriticalSection(cs); + return 0; + } + InterlockedExchangeAdd(val, -count); + LeaveCriticalSection(cs); + return EINVAL; +} diff --git a/libs/winpthreads/src/cond.h b/libs/winpthreads/src/cond.h new file mode 100644 index 0000000..5e869e9 --- /dev/null +++ b/libs/winpthreads/src/cond.h @@ -0,0 +1,63 @@ +/* + Copyright (c) 2011-2016 mingw-w64 project + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#ifndef WIN_PTHREADS_COND_H +#define WIN_PTHREADS_COND_H + +#include + +#define CHECK_COND(c) \ + do { \ + if (!(c) || !*c || (*c == PTHREAD_COND_INITIALIZER) \ + || ( ((cond_t *)(*c))->valid != (unsigned int)LIFE_COND ) ) \ + return EINVAL; \ + } while (0) + +#define LIFE_COND 0xC0BAB1FD +#define DEAD_COND 0xC0DEADBF + +#define STATIC_COND_INITIALIZER(x) ((pthread_cond_t)(x) == ((pthread_cond_t)PTHREAD_COND_INITIALIZER)) + +typedef struct cond_t cond_t; +struct cond_t +{ + unsigned int valid; + int busy; + LONG waiters_count_; /* Number of waiting threads. */ + LONG waiters_count_unblock_; /* Number of waiting threads whitch can be unblocked. */ + LONG waiters_count_gone_; /* Number of waiters which are gone. */ + CRITICAL_SECTION waiters_count_lock_; /* Serialize access to . */ + CRITICAL_SECTION waiters_q_lock_; /* Serialize access to sema_q. */ + LONG value_q; + CRITICAL_SECTION waiters_b_lock_; /* Serialize access to sema_b. */ + LONG value_b; + HANDLE sema_q; /* Semaphore used to queue up threads waiting for the condition to + become signaled. */ + HANDLE sema_b; /* Semaphore used to queue up threads waiting for the condition which + became signaled. */ +}; + +void cond_print_set(int state, FILE *f); + +void cond_print(volatile pthread_cond_t *c, char *txt); + +#endif diff --git a/libs/winpthreads/src/libgcc/dll_math.c b/libs/winpthreads/src/libgcc/dll_math.c new file mode 100644 index 0000000..77bb1fe --- /dev/null +++ b/libs/winpthreads/src/libgcc/dll_math.c @@ -0,0 +1,586 @@ +/*- + * Copyright (c) 1992, 1993 + * The Regents of the University of California. All rights reserved. + * + * This software was developed by the Computer Systems Engineering group + * at Lawrence Berkeley Laboratory under DARPA contract BG 91-66 and + * contributed to Berkeley. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 4. Neither the name of the University nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#ifndef _LIBKERN_QUAD_H_ +#define _LIBKERN_QUAD_H_ + +/* + * Quad arithmetic. + * + * This library makes the following assumptions: + * + * - The type long long (aka quad_t) exists. + * + * - A quad variable is exactly twice as long as `long'. + * + * - The machine's arithmetic is two's complement. + * + * This library can provide 128-bit arithmetic on a machine with 128-bit + * quads and 64-bit longs, for instance, or 96-bit arithmetic on machines + * with 48-bit longs. + */ +/* +#include +#include +#include +#include +*/ + +#include +typedef long long quad_t; +typedef unsigned long long u_quad_t; +typedef unsigned long u_long; +#define CHAR_BIT __CHAR_BIT__ + +/* + * Define the order of 32-bit words in 64-bit words. + * For little endian only. + */ +#define _QUAD_HIGHWORD 1 +#define _QUAD_LOWWORD 0 + +/* + * Depending on the desired operation, we view a `long long' (aka quad_t) in + * one or more of the following formats. + */ +union uu { + quad_t q; /* as a (signed) quad */ + quad_t uq; /* as an unsigned quad */ + long sl[2]; /* as two signed longs */ + u_long ul[2]; /* as two unsigned longs */ +}; + +/* + * Define high and low longwords. + */ +#define H _QUAD_HIGHWORD +#define L _QUAD_LOWWORD + +/* + * Total number of bits in a quad_t and in the pieces that make it up. + * These are used for shifting, and also below for halfword extraction + * and assembly. + */ +#define QUAD_BITS (sizeof(quad_t) * CHAR_BIT) +#define LONG_BITS (sizeof(long) * CHAR_BIT) +#define HALF_BITS (sizeof(long) * CHAR_BIT / 2) + +/* + * Extract high and low shortwords from longword, and move low shortword of + * longword to upper half of long, i.e., produce the upper longword of + * ((quad_t)(x) << (number_of_bits_in_long/2)). (`x' must actually be u_long.) + * + * These are used in the multiply code, to split a longword into upper + * and lower halves, and to reassemble a product as a quad_t, shifted left + * (sizeof(long)*CHAR_BIT/2). + */ +#define HHALF(x) ((x) >> HALF_BITS) +#define LHALF(x) ((x) & ((1 << HALF_BITS) - 1)) +#define LHUP(x) ((x) << HALF_BITS) + +typedef unsigned int qshift_t; + +quad_t __ashldi3(quad_t, qshift_t); +quad_t __ashrdi3(quad_t, qshift_t); +int __cmpdi2(quad_t a, quad_t b); +quad_t __divdi3(quad_t a, quad_t b); +quad_t __lshrdi3(quad_t, qshift_t); +quad_t __moddi3(quad_t a, quad_t b); +u_quad_t __qdivrem(u_quad_t u, u_quad_t v, u_quad_t *rem); +u_quad_t __udivdi3(u_quad_t a, u_quad_t b); +u_quad_t __umoddi3(u_quad_t a, u_quad_t b); +int __ucmpdi2(u_quad_t a, u_quad_t b); +quad_t __divmoddi4(quad_t a, quad_t b, quad_t *rem); +u_quad_t __udivmoddi4(u_quad_t a, u_quad_t b, u_quad_t *rem); + +#endif /* !_LIBKERN_QUAD_H_ */ + +#if defined (_X86_) && !defined (__x86_64__) +/* + * Shift a (signed) quad value left (arithmetic shift left). + * This is the same as logical shift left! + */ +quad_t +__ashldi3(a, shift) + quad_t a; + qshift_t shift; +{ + union uu aa; + + aa.q = a; + if (shift >= LONG_BITS) { + aa.ul[H] = shift >= QUAD_BITS ? 0 : + aa.ul[L] << (shift - LONG_BITS); + aa.ul[L] = 0; + } else if (shift > 0) { + aa.ul[H] = (aa.ul[H] << shift) | + (aa.ul[L] >> (LONG_BITS - shift)); + aa.ul[L] <<= shift; + } + return (aa.q); +} + +/* + * Shift a (signed) quad value right (arithmetic shift right). + */ +quad_t +__ashrdi3(a, shift) + quad_t a; + qshift_t shift; +{ + union uu aa; + + aa.q = a; + if (shift >= LONG_BITS) { + long s; + + /* + * Smear bits rightward using the machine's right-shift + * method, whether that is sign extension or zero fill, + * to get the `sign word' s. Note that shifting by + * LONG_BITS is undefined, so we shift (LONG_BITS-1), + * then 1 more, to get our answer. + */ + s = (aa.sl[H] >> (LONG_BITS - 1)) >> 1; + aa.ul[L] = shift >= QUAD_BITS ? s : + aa.sl[H] >> (shift - LONG_BITS); + aa.ul[H] = s; + } else if (shift > 0) { + aa.ul[L] = (aa.ul[L] >> shift) | + (aa.ul[H] << (LONG_BITS - shift)); + aa.sl[H] >>= shift; + } + return (aa.q); +} + +/* + * Return 0, 1, or 2 as a <, =, > b respectively. + * Both a and b are considered signed---which means only the high word is + * signed. + */ +int +__cmpdi2(a, b) + quad_t a, b; +{ + union uu aa, bb; + + aa.q = a; + bb.q = b; + return (aa.sl[H] < bb.sl[H] ? 0 : aa.sl[H] > bb.sl[H] ? 2 : + aa.ul[L] < bb.ul[L] ? 0 : aa.ul[L] > bb.ul[L] ? 2 : 1); +} + +/* + * Divide two signed quads. + * ??? if -1/2 should produce -1 on this machine, this code is wrong + */ +quad_t +__divdi3(a, b) + quad_t a, b; +{ + u_quad_t ua, ub, uq; + int neg; + + if (a < 0) + ua = -(u_quad_t)a, neg = 1; + else + ua = a, neg = 0; + if (b < 0) + ub = -(u_quad_t)b, neg ^= 1; + else + ub = b; + uq = __qdivrem(ua, ub, (u_quad_t *)0); + return (neg ? -uq : uq); +} + +/* + * Shift an (unsigned) quad value right (logical shift right). + */ +quad_t +__lshrdi3(a, shift) + quad_t a; + qshift_t shift; +{ + union uu aa; + + aa.q = a; + if (shift >= LONG_BITS) { + aa.ul[L] = shift >= QUAD_BITS ? 0 : + aa.ul[H] >> (shift - LONG_BITS); + aa.ul[H] = 0; + } else if (shift > 0) { + aa.ul[L] = (aa.ul[L] >> shift) | + (aa.ul[H] << (LONG_BITS - shift)); + aa.ul[H] >>= shift; + } + return (aa.q); +} + +/* + * Return remainder after dividing two signed quads. + * + * XXX + * If -1/2 should produce -1 on this machine, this code is wrong. + */ +quad_t +__moddi3(a, b) + quad_t a, b; +{ + u_quad_t ua, ub, ur; + int neg; + + if (a < 0) + ua = -(u_quad_t)a, neg = 1; + else + ua = a, neg = 0; + if (b < 0) + ub = -(u_quad_t)b; + else + ub = b; + (void)__qdivrem(ua, ub, &ur); + return (neg ? -ur : ur); +} + + +/* + * Multiprecision divide. This algorithm is from Knuth vol. 2 (2nd ed), + * section 4.3.1, pp. 257--259. + */ + +#define B (1 << HALF_BITS) /* digit base */ + +/* Combine two `digits' to make a single two-digit number. */ +#define COMBINE(a, b) (((u_long)(a) << HALF_BITS) | (b)) + +/* select a type for digits in base B: use unsigned short if they fit */ +#if ULONG_MAX == 0xffffffff && USHRT_MAX >= 0xffff +typedef unsigned short digit; +#else +typedef u_long digit; +#endif + +/* + * Shift p[0]..p[len] left `sh' bits, ignoring any bits that + * `fall out' the left (there never will be any such anyway). + * We may assume len >= 0. NOTE THAT THIS WRITES len+1 DIGITS. + */ +static void +__shl(register digit *p, register int len, register int sh) +{ + register int i; + + for (i = 0; i < len; i++) + p[i] = LHALF(p[i] << sh) | (p[i + 1] >> (HALF_BITS - sh)); + p[i] = LHALF(p[i] << sh); +} + +/* + * __qdivrem(u, v, rem) returns u/v and, optionally, sets *rem to u%v. + * + * We do this in base 2-sup-HALF_BITS, so that all intermediate products + * fit within u_long. As a consequence, the maximum length dividend and + * divisor are 4 `digits' in this base (they are shorter if they have + * leading zeros). + */ +u_quad_t +__qdivrem(uq, vq, arq) + u_quad_t uq, vq, *arq; +{ + union uu tmp; + digit *u, *v, *q; + register digit v1, v2; + u_long qhat, rhat, t; + int m, n, d, j, i; + digit uspace[5], vspace[5], qspace[5]; + + /* + * Take care of special cases: divide by zero, and u < v. + */ + if (vq == 0) { + /* divide by zero. */ + static volatile const unsigned int zero = 0; + + tmp.ul[H] = tmp.ul[L] = 1 / zero; + if (arq) + *arq = uq; + return (tmp.q); + } + if (uq < vq) { + if (arq) + *arq = uq; + return (0); + } + u = &uspace[0]; + v = &vspace[0]; + q = &qspace[0]; + + /* + * Break dividend and divisor into digits in base B, then + * count leading zeros to determine m and n. When done, we + * will have: + * u = (u[1]u[2]...u[m+n]) sub B + * v = (v[1]v[2]...v[n]) sub B + * v[1] != 0 + * 1 < n <= 4 (if n = 1, we use a different division algorithm) + * m >= 0 (otherwise u < v, which we already checked) + * m + n = 4 + * and thus + * m = 4 - n <= 2 + */ + tmp.uq = uq; + u[0] = 0; + u[1] = HHALF(tmp.ul[H]); + u[2] = LHALF(tmp.ul[H]); + u[3] = HHALF(tmp.ul[L]); + u[4] = LHALF(tmp.ul[L]); + tmp.uq = vq; + v[1] = HHALF(tmp.ul[H]); + v[2] = LHALF(tmp.ul[H]); + v[3] = HHALF(tmp.ul[L]); + v[4] = LHALF(tmp.ul[L]); + for (n = 4; v[1] == 0; v++) { + if (--n == 1) { + u_long rbj; /* r*B+u[j] (not root boy jim) */ + digit q1, q2, q3, q4; + + /* + * Change of plan, per exercise 16. + * r = 0; + * for j = 1..4: + * q[j] = floor((r*B + u[j]) / v), + * r = (r*B + u[j]) % v; + * We unroll this completely here. + */ + t = v[2]; /* nonzero, by definition */ + q1 = u[1] / t; + rbj = COMBINE(u[1] % t, u[2]); + q2 = rbj / t; + rbj = COMBINE(rbj % t, u[3]); + q3 = rbj / t; + rbj = COMBINE(rbj % t, u[4]); + q4 = rbj / t; + if (arq) + *arq = rbj % t; + tmp.ul[H] = COMBINE(q1, q2); + tmp.ul[L] = COMBINE(q3, q4); + return (tmp.q); + } + } + + /* + * By adjusting q once we determine m, we can guarantee that + * there is a complete four-digit quotient at &qspace[1] when + * we finally stop. + */ + for (m = 4 - n; u[1] == 0; u++) + m--; + for (i = 4 - m; --i >= 0;) + q[i] = 0; + q += 4 - m; + + /* + * Here we run Program D, translated from MIX to C and acquiring + * a few minor changes. + * + * D1: choose multiplier 1 << d to ensure v[1] >= B/2. + */ + d = 0; + for (t = v[1]; t < B / 2; t <<= 1) + d++; + if (d > 0) { + __shl(&u[0], m + n, d); /* u <<= d */ + __shl(&v[1], n - 1, d); /* v <<= d */ + } + /* + * D2: j = 0. + */ + j = 0; + v1 = v[1]; /* for D3 -- note that v[1..n] are constant */ + v2 = v[2]; /* for D3 */ + do { + register digit uj0, uj1, uj2; + + /* + * D3: Calculate qhat (\^q, in TeX notation). + * Let qhat = min((u[j]*B + u[j+1])/v[1], B-1), and + * let rhat = (u[j]*B + u[j+1]) mod v[1]. + * While rhat < B and v[2]*qhat > rhat*B+u[j+2], + * decrement qhat and increase rhat correspondingly. + * Note that if rhat >= B, v[2]*qhat < rhat*B. + */ + uj0 = u[j + 0]; /* for D3 only -- note that u[j+...] change */ + uj1 = u[j + 1]; /* for D3 only */ + uj2 = u[j + 2]; /* for D3 only */ + if (uj0 == v1) { + qhat = B; + rhat = uj1; + goto qhat_too_big; + } else { + u_long nn = COMBINE(uj0, uj1); + qhat = nn / v1; + rhat = nn % v1; + } + while (v2 * qhat > COMBINE(rhat, uj2)) { + qhat_too_big: + qhat--; + if ((rhat += v1) >= B) + break; + } + /* + * D4: Multiply and subtract. + * The variable `t' holds any borrows across the loop. + * We split this up so that we do not require v[0] = 0, + * and to eliminate a final special case. + */ + for (t = 0, i = n; i > 0; i--) { + t = u[i + j] - v[i] * qhat - t; + u[i + j] = LHALF(t); + t = (B - HHALF(t)) & (B - 1); + } + t = u[j] - t; + u[j] = LHALF(t); + /* + * D5: test remainder. + * There is a borrow if and only if HHALF(t) is nonzero; + * in that (rare) case, qhat was too large (by exactly 1). + * Fix it by adding v[1..n] to u[j..j+n]. + */ + if (HHALF(t)) { + qhat--; + for (t = 0, i = n; i > 0; i--) { /* D6: add back. */ + t += u[i + j] + v[i]; + u[i + j] = LHALF(t); + t = HHALF(t); + } + u[j] = LHALF(u[j] + t); + } + q[j] = qhat; + } while (++j <= m); /* D7: loop on j. */ + + /* + * If caller wants the remainder, we have to calculate it as + * u[m..m+n] >> d (this is at most n digits and thus fits in + * u[m+1..m+n], but we may need more source digits). + */ + if (arq) { + if (d) { + for (i = m + n; i > m; --i) + u[i] = (u[i] >> d) | + LHALF(u[i - 1] << (HALF_BITS - d)); + u[i] = 0; + } + tmp.ul[H] = COMBINE(uspace[1], uspace[2]); + tmp.ul[L] = COMBINE(uspace[3], uspace[4]); + *arq = tmp.q; + } + + tmp.ul[H] = COMBINE(qspace[1], qspace[2]); + tmp.ul[L] = COMBINE(qspace[3], qspace[4]); + return (tmp.q); +} + +/* + * Return 0, 1, or 2 as a <, =, > b respectively. + * Neither a nor b are considered signed. + */ +int +__ucmpdi2(a, b) + u_quad_t a, b; +{ + union uu aa, bb; + + aa.uq = a; + bb.uq = b; + return (aa.ul[H] < bb.ul[H] ? 0 : aa.ul[H] > bb.ul[H] ? 2 : + aa.ul[L] < bb.ul[L] ? 0 : aa.ul[L] > bb.ul[L] ? 2 : 1); +} + +/* + * Divide two unsigned quads. + */ +u_quad_t +__udivdi3(a, b) + u_quad_t a, b; +{ + + return (__qdivrem(a, b, (u_quad_t *)0)); +} + +/* + * Return remainder after dividing two unsigned quads. + */ +u_quad_t +__umoddi3(a, b) + u_quad_t a, b; +{ + u_quad_t r; + + (void)__qdivrem(a, b, &r); + return (r); +} + +/* + * Divide two signed quads. + * This function is new in GCC 7. + */ +quad_t +__divmoddi4(a, b, rem) + quad_t a, b, *rem; +{ + u_quad_t ua, ub, uq, ur; + int negq, negr; + + if (a < 0) + ua = -(u_quad_t)a, negq = 1, negr = 1; + else + ua = a, negq = 0, negr = 0; + if (b < 0) + ub = -(u_quad_t)b, negq ^= 1; + else + ub = b; + uq = __qdivrem(ua, ub, &ur); + if (rem) + *rem = (negr ? -ur : ur); + return (negq ? -uq : uq); +} + +u_quad_t +__udivmoddi4(u_quad_t a, u_quad_t b, u_quad_t *rem) +{ + return __qdivrem(a, b, rem); +} + +#else +static int __attribute__((unused)) dummy; +#endif /*deined (_X86_) && !defined (__x86_64__)*/ + diff --git a/libs/winpthreads/src/misc.c b/libs/winpthreads/src/misc.c new file mode 100644 index 0000000..457bc86 --- /dev/null +++ b/libs/winpthreads/src/misc.c @@ -0,0 +1,197 @@ +/* + Copyright (c) 2011-2016 mingw-w64 project + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include +#include "pthread.h" +#include "misc.h" + +void (WINAPI *_pthread_get_system_time_best_as_file_time) (LPFILETIME) = NULL; +static ULONGLONG (WINAPI *_pthread_get_tick_count_64) (VOID); + +#if defined(__GNUC__) || defined(__clang__) +__attribute__((constructor)) +#endif +static void winpthreads_init(void) +{ + HMODULE mod = GetModuleHandleA("kernel32.dll"); + if (mod) + { + _pthread_get_tick_count_64 = + (ULONGLONG (WINAPI *)(VOID))(void*) GetProcAddress(mod, "GetTickCount64"); + + /* <1us precision on Windows 10 */ + _pthread_get_system_time_best_as_file_time = + (void (WINAPI *)(LPFILETIME))(void*) GetProcAddress(mod, "GetSystemTimePreciseAsFileTime"); + } + + if (!_pthread_get_system_time_best_as_file_time) + /* >15ms precision on Windows 10 */ + _pthread_get_system_time_best_as_file_time = GetSystemTimeAsFileTime; +} + +#if defined(_MSC_VER) && !defined(__clang__) +/* Force a reference to __xc_t to prevent whole program optimization + * from discarding the variable. */ + +/* On x86, symbols are prefixed with an underscore. */ +# if defined(_M_IX86) +# pragma comment(linker, "/include:___xc_t") +# else +# pragma comment(linker, "/include:__xc_t") +# endif + +#pragma section(".CRT$XCT", long, read) +__declspec(allocate(".CRT$XCT")) +extern const _PVFV __xc_t; +const _PVFV __xc_t = winpthreads_init; +#endif + +unsigned long long _pthread_time_in_ms(void) +{ + FILETIME ft; + + GetSystemTimeAsFileTime(&ft); + return (((unsigned long long)ft.dwHighDateTime << 32) + ft.dwLowDateTime + - 0x19DB1DED53E8000ULL) / 10000ULL; +} + +unsigned long long _pthread_time_in_ms_from_timespec(const struct timespec *ts) +{ + unsigned long long t = (unsigned long long) ts->tv_sec * 1000LL; + /* The +999999 is here to ensure that the division always rounds up */ + t += (unsigned long long) (ts->tv_nsec + 999999) / 1000000; + + return t; +} + +unsigned long long _pthread_rel_time_in_ms(const struct timespec *ts) +{ + unsigned long long t1 = _pthread_time_in_ms_from_timespec(ts); + unsigned long long t2 = _pthread_time_in_ms(); + + /* Prevent underflow */ + if (t1 < t2) return 0; + return t1 - t2; +} + +static unsigned long long +_pthread_get_tick_count (long long *frequency) +{ + if (_pthread_get_tick_count_64 != NULL) + return _pthread_get_tick_count_64 (); + + LARGE_INTEGER freq, timestamp; + + if (*frequency == 0) + { + if (QueryPerformanceFrequency (&freq)) + *frequency = freq.QuadPart; + else + *frequency = -1; + } + + if (*frequency > 0 && QueryPerformanceCounter (×tamp)) + return timestamp.QuadPart / (*frequency / 1000); + + /* Fallback */ + return GetTickCount (); +} + +/* A wrapper around WaitForSingleObject() that ensures that + * the wait function does not time out before the time + * actually runs out. This is needed because WaitForSingleObject() + * might have poor accuracy, returning earlier than expected. + * On the other hand, returning a bit *later* than expected + * is acceptable in a preemptive multitasking environment. + */ +unsigned long +_pthread_wait_for_single_object (void *handle, unsigned long timeout) +{ + DWORD result; + unsigned long long start_time, end_time; + unsigned long wait_time; + long long frequency = 0; + + if (timeout == INFINITE || timeout == 0) + return WaitForSingleObject ((HANDLE) handle, (DWORD) timeout); + + start_time = _pthread_get_tick_count (&frequency); + end_time = start_time + timeout; + wait_time = timeout; + + do + { + unsigned long long current_time; + + result = WaitForSingleObject ((HANDLE) handle, (DWORD) wait_time); + if (result != WAIT_TIMEOUT) + break; + + current_time = _pthread_get_tick_count (&frequency); + if (current_time >= end_time) + break; + + wait_time = (DWORD) (end_time - current_time); + } while (TRUE); + + return result; +} + +/* A wrapper around WaitForMultipleObjects() that ensures that + * the wait function does not time out before the time + * actually runs out. This is needed because WaitForMultipleObjects() + * might have poor accuracy, returning earlier than expected. + * On the other hand, returning a bit *later* than expected + * is acceptable in a preemptive multitasking environment. + */ +unsigned long +_pthread_wait_for_multiple_objects (unsigned long count, void **handles, unsigned int all, unsigned long timeout) +{ + DWORD result; + unsigned long long start_time, end_time; + unsigned long wait_time; + long long frequency = 0; + + if (timeout == INFINITE || timeout == 0) + return WaitForMultipleObjects ((DWORD) count, (HANDLE *) handles, all, (DWORD) timeout); + + start_time = _pthread_get_tick_count (&frequency); + end_time = start_time + timeout; + wait_time = timeout; + + do + { + unsigned long long current_time; + + result = WaitForMultipleObjects ((DWORD) count, (HANDLE *) handles, all, (DWORD) wait_time); + if (result != WAIT_TIMEOUT) + break; + + current_time = _pthread_get_tick_count (&frequency); + if (current_time >= end_time) + break; + + wait_time = (DWORD) (end_time - current_time); + } while (TRUE); + + return result; +} diff --git a/libs/winpthreads/src/misc.h b/libs/winpthreads/src/misc.h new file mode 100644 index 0000000..edefb0d --- /dev/null +++ b/libs/winpthreads/src/misc.h @@ -0,0 +1,126 @@ +/* + Copyright (c) 2011-2016 mingw-w64 project + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#ifndef WIN_PTHREADS_MISC_H +#define WIN_PTHREADS_MISC_H + +#include "pthread_compat.h" + +#ifndef assert + +#ifndef ASSERT_TRACE +# define ASSERT_TRACE 0 +#else +# undef ASSERT_TRACE +# define ASSERT_TRACE 0 +#endif + +# define assert(e) \ + ((e) ? ((ASSERT_TRACE) ? fprintf(stderr, \ + "Assertion succeeded: (%s), file %s, line %d\n", \ + #e, __FILE__, (int) __LINE__), \ + fflush(stderr) : \ + 0) : \ + (fprintf(stderr, "Assertion failed: (%s), file %s, line %d\n", \ + #e, __FILE__, (int) __LINE__), exit(1), 0)) + +# define fixme(e) \ + ((e) ? ((ASSERT_TRACE) ? fprintf(stderr, \ + "Assertion succeeded: (%s), file %s, line %d\n", \ + #e, __FILE__, (int) __LINE__), \ + fflush(stderr) : \ + 0) : \ + (fprintf(stderr, "FIXME: (%s), file %s, line %d\n", \ + #e, __FILE__, (int) __LINE__), 0, 0)) + +#endif + +#define PTR2INT(x) ((int)(uintptr_t)(x)) + +#if SIZE_MAX>UINT_MAX +typedef long long LONGBAG; +#else +typedef long LONGBAG; +#endif + +#if !WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP) +#undef GetHandleInformation +#define GetHandleInformation(h,f) (1) +#endif + +#define CHECK_HANDLE(h) \ + do { \ + DWORD dwFlags; \ + if (!(h) || ((h) == INVALID_HANDLE_VALUE) || !GetHandleInformation((h), &dwFlags)) \ + return EINVAL; \ + } while (0) + +#define CHECK_PTR(p) do { if (!(p)) return EINVAL; } while (0) + +#define UPD_RESULT(x,r) do { int _r = (x); (r) = (r) ? (r) : _r; } while (0) + +#define CHECK_THREAD(t) \ + do { \ + CHECK_PTR(t); \ + CHECK_HANDLE((t)->h); \ + } while (0) + +#define CHECK_OBJECT(o, e) \ + do { \ + DWORD dwFlags; \ + if (!(o)) return e; \ + if (!((o)->h) || (((o)->h) == INVALID_HANDLE_VALUE) || !GetHandleInformation(((o)->h), &dwFlags)) \ + return e; \ + } while (0) + +#define VALID(x) if (!(p)) return EINVAL; + +/* ms can be 64 bit, solve wrap-around issues: */ +static WINPTHREADS_INLINE unsigned long dwMilliSecs(unsigned long long ms) +{ + if (ms >= 0xffffffffULL) return 0xfffffffful; + return (unsigned long) ms; +} + +unsigned long long _pthread_time_in_ms(void); +unsigned long long _pthread_time_in_ms_from_timespec(const struct timespec *ts); +unsigned long long _pthread_rel_time_in_ms(const struct timespec *ts); +unsigned long _pthread_wait_for_single_object (void *handle, unsigned long timeout); +unsigned long _pthread_wait_for_multiple_objects (unsigned long count, void **handles, unsigned int all, unsigned long timeout); + +extern void (WINAPI *_pthread_get_system_time_best_as_file_time) (LPFILETIME); + +#if defined(__GNUC__) || defined(__clang__) +#define likely(cond) __builtin_expect((cond) != 0, 1) +#define unlikely(cond) __builtin_expect((cond) != 0, 0) +#else +#define likely(cond) (cond) +#define unlikely(cond) (cond) +#endif + +#if defined(__GNUC__) || defined(__clang__) +#define UNREACHABLE() __builtin_unreachable() +#elif defined(_MSC_VER) +#define UNREACHABLE() __assume(0) +#endif + +#endif diff --git a/libs/winpthreads/src/mutex.c b/libs/winpthreads/src/mutex.c new file mode 100644 index 0000000..866e18d --- /dev/null +++ b/libs/winpthreads/src/mutex.c @@ -0,0 +1,381 @@ +/* + Copyright (c) 2011, 2014 mingw-w64 project + Copyright (c) 2015 Intel Corporation + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include +#include +#include +#include +#include "pthread.h" +#include "misc.h" + +typedef enum { + Unlocked, /* Not locked. */ + Locked, /* Locked but without waiters. */ + Waiting, /* Locked, may have waiters. */ +} mutex_state_t; + +typedef enum { + Normal, + Errorcheck, + Recursive, +} mutex_type_t; + +/* The heap-allocated part of a mutex. */ +typedef struct { + mutex_state_t state; + mutex_type_t type; + HANDLE event; /* Auto-reset event, or NULL if not yet allocated. */ + unsigned rec_lock; /* For recursive mutexes, the number of times the + mutex has been locked in excess by the same thread. */ + volatile DWORD owner; /* For recursive and error-checking mutexes, the + ID of the owning thread if the mutex is locked. */ +} mutex_impl_t; + +/* Whether a mutex is still a static initializer (not a pointer to + a mutex_impl_t). */ +static bool +is_static_initializer(pthread_mutex_t m) +{ + /* Treat 0 as a static initializer as well (for normal mutexes), + to tolerate sloppy code in libgomp. (We should rather fix that code!) */ + intptr_t v = (intptr_t)m; + return v >= -3 && v <= 0; +/* Should be simple: + return (uintptr_t)m >= (uintptr_t)-3; */ +} + +/* Create and return the implementation part of a mutex from a static + initialiser. Return NULL on out-of-memory error. */ +static WINPTHREADS_ATTRIBUTE((noinline)) mutex_impl_t * +mutex_impl_init(pthread_mutex_t *m, mutex_impl_t *mi) +{ + mutex_impl_t *new_mi = malloc(sizeof(mutex_impl_t)); + if (new_mi == NULL) + return NULL; + new_mi->state = Unlocked; + new_mi->type = (mi == (void *)PTHREAD_RECURSIVE_MUTEX_INITIALIZER ? Recursive + : mi == (void *)PTHREAD_ERRORCHECK_MUTEX_INITIALIZER ? Errorcheck + : Normal); + new_mi->event = NULL; + new_mi->rec_lock = 0; + new_mi->owner = (DWORD)-1; + if (InterlockedCompareExchangePointer((PVOID volatile *)m, new_mi, mi) == mi) { + return new_mi; + } else { + /* Someone created the struct before us. */ + free(new_mi); + return (mutex_impl_t *)*m; + } +} + +/* Return the implementation part of a mutex, creating it if necessary. + Return NULL on out-of-memory error. */ +static inline mutex_impl_t * +mutex_impl(pthread_mutex_t *m) +{ + mutex_impl_t *mi = (mutex_impl_t *)*m; + if (is_static_initializer((pthread_mutex_t)mi)) { + return mutex_impl_init(m, mi); + } else { + /* mi cannot be null here; avoid a test in the fast path. */ + if (mi == NULL) + UNREACHABLE(); + return mi; + } +} + +/* Lock a mutex. Give up after 'timeout' ms (with ETIMEDOUT), + or never if timeout=INFINITE. */ +static inline int +pthread_mutex_lock_intern (pthread_mutex_t *m, DWORD timeout) +{ + mutex_impl_t *mi = mutex_impl(m); + if (mi == NULL) + return ENOMEM; + mutex_state_t old_state = InterlockedExchange((long *)&mi->state, Locked); + if (unlikely(old_state != Unlocked)) { + /* The mutex is already locked. */ + + if (mi->type != Normal) { + /* Recursive or Errorcheck */ + if (mi->owner == GetCurrentThreadId()) { + /* FIXME: A recursive mutex should not need two atomic ops when locking + recursively. We could rewrite by doing compare-and-swap instead of + test-and-set the first time, but it would lead to more code + duplication and add a conditional branch to the critical path. */ + InterlockedCompareExchange((long *)&mi->state, old_state, Locked); + if (mi->type == Recursive) { + mi->rec_lock++; + return 0; + } else { + /* type == Errorcheck */ + return EDEADLK; + } + } + } + + /* Make sure there is an event object on which to wait. */ + if (mi->event == NULL) { + /* Make an auto-reset event object. */ + HANDLE ev = CreateEvent(NULL, false, false, NULL); + if (ev == NULL) { + switch (GetLastError()) { + case ERROR_ACCESS_DENIED: + return EPERM; + default: + return ENOMEM; /* Probably accurate enough. */ + } + } + if (InterlockedCompareExchangePointer(&mi->event, ev, NULL) != NULL) { + /* Someone created the event before us. */ + CloseHandle(ev); + } + } + + /* At this point, mi->event is non-NULL. */ + + while (InterlockedExchange((long *)&mi->state, Waiting) != Unlocked) { + /* For timed locking attempts, it is possible (although unlikely) + that we are woken up but someone else grabs the lock before us, + and we have to go back to sleep again. In that case, the total + wait may be longer than expected. */ + + unsigned r = _pthread_wait_for_single_object(mi->event, timeout); + switch (r) { + case WAIT_TIMEOUT: + return ETIMEDOUT; + case WAIT_OBJECT_0: + break; + default: + return EINVAL; + } + } + } + + if (mi->type != Normal) + mi->owner = GetCurrentThreadId(); + + return 0; +} + +int +pthread_mutex_lock (pthread_mutex_t *m) +{ + return pthread_mutex_lock_intern (m, INFINITE); +} + +int pthread_mutex_timedlock(pthread_mutex_t *m, const struct timespec *ts) +{ + unsigned long long patience; + if (ts != NULL) { + unsigned long long end = _pthread_time_in_ms_from_timespec(ts); + unsigned long long now = _pthread_time_in_ms(); + patience = end > now ? end - now : 0; + if (patience > 0xffffffff) + patience = INFINITE; + } else { + patience = INFINITE; + } + return pthread_mutex_lock_intern(m, patience); +} + +int pthread_mutex_unlock(pthread_mutex_t *m) +{ + /* Here m might an initialiser of an error-checking or recursive mutex, in + which case the behaviour is well-defined, so we can't skip this check. */ + mutex_impl_t *mi = mutex_impl(m); + if (mi == NULL) + return ENOMEM; + + if (unlikely(mi->type != Normal)) { + if (mi->state == Unlocked) + return EINVAL; + if (mi->owner != GetCurrentThreadId()) + return EPERM; + if (mi->rec_lock > 0) { + mi->rec_lock--; + return 0; + } + mi->owner = (DWORD)-1; + } + if (unlikely(InterlockedExchange((long *)&mi->state, Unlocked) == Waiting)) { + if (!SetEvent(mi->event)) + return EPERM; + } + return 0; +} + +int pthread_mutex_trylock(pthread_mutex_t *m) +{ + mutex_impl_t *mi = mutex_impl(m); + if (mi == NULL) + return ENOMEM; + + if (InterlockedCompareExchange((long *)&mi->state, Locked, Unlocked) == Unlocked) { + if (mi->type != Normal) + mi->owner = GetCurrentThreadId(); + return 0; + } else { + if (mi->type == Recursive && mi->owner == GetCurrentThreadId()) { + mi->rec_lock++; + return 0; + } + return EBUSY; + } +} + +int +pthread_mutex_init (pthread_mutex_t *m, const pthread_mutexattr_t *a) +{ + pthread_mutex_t init = PTHREAD_MUTEX_INITIALIZER; + if (a != NULL) { + int pshared; + if (pthread_mutexattr_getpshared(a, &pshared) == 0 + && pshared == PTHREAD_PROCESS_SHARED) + return ENOSYS; + + int type; + if (pthread_mutexattr_gettype(a, &type) == 0) { + switch (type) { + case PTHREAD_MUTEX_ERRORCHECK: + init = PTHREAD_ERRORCHECK_MUTEX_INITIALIZER; + break; + case PTHREAD_MUTEX_RECURSIVE: + init = PTHREAD_RECURSIVE_MUTEX_INITIALIZER; + break; + default: + init = PTHREAD_MUTEX_INITIALIZER; + break; + } + } + } + *m = init; + return 0; +} + +int pthread_mutex_destroy (pthread_mutex_t *m) +{ + mutex_impl_t *mi = (mutex_impl_t *)*m; + if (!is_static_initializer((pthread_mutex_t)mi)) { + if (mi->event != NULL) + CloseHandle(mi->event); + free(mi); + /* Sabotage attempts to re-use the mutex before initialising it again. */ + *m = (pthread_mutex_t)NULL; + } + + return 0; +} + +int pthread_mutexattr_init(pthread_mutexattr_t *a) +{ + *a = PTHREAD_MUTEX_NORMAL | (PTHREAD_PROCESS_PRIVATE << 3); + return 0; +} + +int pthread_mutexattr_destroy(pthread_mutexattr_t *a) +{ + if (!a) + return EINVAL; + + return 0; +} + +int pthread_mutexattr_gettype(const pthread_mutexattr_t *a, int *type) +{ + if (!a || !type) + return EINVAL; + + *type = *a & 3; + + return 0; +} + +int pthread_mutexattr_settype(pthread_mutexattr_t *a, int type) +{ + if (!a || (type != PTHREAD_MUTEX_NORMAL && type != PTHREAD_MUTEX_RECURSIVE && type != PTHREAD_MUTEX_ERRORCHECK)) + return EINVAL; + *a &= ~3; + *a |= type; + + return 0; +} + +int pthread_mutexattr_getpshared(const pthread_mutexattr_t *a, int *type) +{ + if (!a || !type) + return EINVAL; + *type = (*a & 4 ? PTHREAD_PROCESS_SHARED : PTHREAD_PROCESS_PRIVATE); + + return 0; +} + +int pthread_mutexattr_setpshared(pthread_mutexattr_t * a, int type) +{ + int r = 0; + if (!a || (type != PTHREAD_PROCESS_SHARED + && type != PTHREAD_PROCESS_PRIVATE)) + return EINVAL; + if (type == PTHREAD_PROCESS_SHARED) + { + type = PTHREAD_PROCESS_PRIVATE; + r = ENOSYS; + } + type = (type == PTHREAD_PROCESS_SHARED ? 4 : 0); + + *a &= ~4; + *a |= type; + + return r; +} + +int pthread_mutexattr_getprotocol(const pthread_mutexattr_t *a, int *type) +{ + *type = *a & (8 + 16); + + return 0; +} + +int pthread_mutexattr_setprotocol(pthread_mutexattr_t *a, int type) +{ + if ((type & (8 + 16)) != 8 + 16) return EINVAL; + + *a &= ~(8 + 16); + *a |= type; + + return 0; +} + +int pthread_mutexattr_getprioceiling(const pthread_mutexattr_t *a, int * prio) +{ + *prio = *a / PTHREAD_PRIO_MULT; + return 0; +} + +int pthread_mutexattr_setprioceiling(pthread_mutexattr_t *a, int prio) +{ + *a &= (PTHREAD_PRIO_MULT - 1); + *a += prio * PTHREAD_PRIO_MULT; + + return 0; +} diff --git a/libs/winpthreads/src/nanosleep.c b/libs/winpthreads/src/nanosleep.c new file mode 100644 index 0000000..0cce449 --- /dev/null +++ b/libs/winpthreads/src/nanosleep.c @@ -0,0 +1,71 @@ +/** + * This file has no copyright assigned and is placed in the Public Domain. + * This file is part of the w64 mingw-runtime package. + * No warranty is given; refer to the file DISCLAIMER.PD within this package. + */ + +#include +#include +#include +#include "pthread.h" +#include "pthread_time.h" +#include "winpthread_internal.h" + +#define POW10_3 1000 +#define POW10_4 10000 +#define POW10_6 1000000 +#define POW10_9 1000000000 +#define MAX_SLEEP_IN_MS 4294967294UL + +/** + * Sleep for the specified time. + * @param request The desired amount of time to sleep. + * @param remain The remain amount of time to sleep. + * @return If the function succeeds, the return value is 0. + * If the function fails, the return value is -1, + * with errno set to indicate the error. + */ +int nanosleep(const struct timespec *request, struct timespec *remain) +{ + unsigned long ms, rc = 0; + unsigned __int64 u64, want, real; + + union { + unsigned __int64 ns100; + FILETIME ft; + } _start, _end; + + if (request->tv_sec < 0 || request->tv_nsec < 0 || request->tv_nsec >= POW10_9) { + errno = EINVAL; + return -1; + } + + if (remain != NULL) GetSystemTimeAsFileTime(&_start.ft); + + want = u64 = request->tv_sec * POW10_3 + request->tv_nsec / POW10_6; + while (u64 > 0 && rc == 0) { + if (u64 >= MAX_SLEEP_IN_MS) ms = MAX_SLEEP_IN_MS; + else ms = (unsigned long) u64; + + u64 -= ms; + rc = pthread_delay_np_ms(ms); + } + + if (rc != 0) { /* WAIT_IO_COMPLETION (192) */ + if (remain != NULL) { + GetSystemTimeAsFileTime(&_end.ft); + real = (_end.ns100 - _start.ns100) / POW10_4; + + if (real >= want) u64 = 0; + else u64 = want - real; + + remain->tv_sec = u64 / POW10_3; + remain->tv_nsec = (long) (u64 % POW10_3) * POW10_6; + } + + errno = EINTR; + return -1; + } + + return 0; +} diff --git a/libs/winpthreads/src/ref.c b/libs/winpthreads/src/ref.c new file mode 100644 index 0000000..0344d45 --- /dev/null +++ b/libs/winpthreads/src/ref.c @@ -0,0 +1,34 @@ +/* + Copyright (c) 2011-2016 mingw-w64 project + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include +#include +#include +#include "pthread.h" +#include "semaphore.h" +#include "rwlock.h" +#include "cond.h" +#include "barrier.h" +#include "sem.h" +#include "ref.h" +#include "misc.h" + diff --git a/libs/winpthreads/src/ref.h b/libs/winpthreads/src/ref.h new file mode 100644 index 0000000..5ca6750 --- /dev/null +++ b/libs/winpthreads/src/ref.h @@ -0,0 +1,29 @@ +/* + Copyright (c) 2011-2016 mingw-w64 project + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#ifndef WIN_PTHREADS_REF_H +#define WIN_PTHREADS_REF_H +#include "pthread.h" +#include "semaphore.h" + +#endif + diff --git a/libs/winpthreads/src/rwlock.c b/libs/winpthreads/src/rwlock.c new file mode 100644 index 0000000..76669df --- /dev/null +++ b/libs/winpthreads/src/rwlock.c @@ -0,0 +1,537 @@ +/* + Copyright (c) 2011-2016 mingw-w64 project + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include +#include +#include +#include "pthread.h" +#include "thread.h" +#include "ref.h" +#include "rwlock.h" +#include "misc.h" + +static pthread_spinlock_t rwl_global = PTHREAD_SPINLOCK_INITIALIZER; + +static WINPTHREADS_ATTRIBUTE((noinline)) int rwlock_static_init(pthread_rwlock_t *rw); + +static WINPTHREADS_ATTRIBUTE((noinline)) int rwl_unref(volatile pthread_rwlock_t *rwl, int res) +{ + pthread_spin_lock(&rwl_global); +#ifdef WINPTHREAD_DBG + assert((((rwlock_t *)*rwl)->valid == LIFE_RWLOCK) && (((rwlock_t *)*rwl)->busy > 0)); +#endif + ((rwlock_t *)*rwl)->busy--; + pthread_spin_unlock(&rwl_global); + return res; +} + +static WINPTHREADS_ATTRIBUTE((noinline)) int rwl_ref(pthread_rwlock_t *rwl, int f ) +{ + int r = 0; + if (STATIC_RWL_INITIALIZER(*rwl)) { + r = rwlock_static_init(rwl); + if (r != 0 && r != EBUSY) + return r; + } + pthread_spin_lock(&rwl_global); + + if (!rwl || !*rwl || ((rwlock_t *)*rwl)->valid != LIFE_RWLOCK) r = EINVAL; + else { + ((rwlock_t *)*rwl)->busy ++; + } + + pthread_spin_unlock(&rwl_global); + + return r; +} + +static WINPTHREADS_ATTRIBUTE((noinline)) int rwl_ref_unlock(pthread_rwlock_t *rwl ) +{ + int r = 0; + + pthread_spin_lock(&rwl_global); + + if (!rwl || !*rwl || ((rwlock_t *)*rwl)->valid != LIFE_RWLOCK) r = EINVAL; + else if (STATIC_RWL_INITIALIZER(*rwl)) r= EPERM; + else { + ((rwlock_t *)*rwl)->busy ++; + } + + pthread_spin_unlock(&rwl_global); + + return r; +} + +static WINPTHREADS_ATTRIBUTE((noinline)) int rwl_ref_destroy(pthread_rwlock_t *rwl, pthread_rwlock_t *rDestroy ) +{ + int r = 0; + + *rDestroy = (pthread_rwlock_t)NULL; + pthread_spin_lock(&rwl_global); + + if (!rwl || !*rwl) r = EINVAL; + else { + rwlock_t *r_ = (rwlock_t *)*rwl; + if (STATIC_RWL_INITIALIZER(*rwl)) *rwl = (pthread_rwlock_t)NULL; + else if (r_->valid != LIFE_RWLOCK) r = EINVAL; + else if (r_->busy) r = EBUSY; + else { + *rDestroy = *rwl; + *rwl = (pthread_rwlock_t)NULL; + } + } + + pthread_spin_unlock(&rwl_global); + return r; +} + +static int rwlock_gain_both_locks(rwlock_t *rwlock) +{ + int ret; + ret = pthread_mutex_lock(&rwlock->mex); + if (ret != 0) + return ret; + ret = pthread_mutex_lock(&rwlock->mcomplete); + if (ret != 0) + pthread_mutex_unlock(&rwlock->mex); + return ret; +} + +static int rwlock_free_both_locks(rwlock_t *rwlock, int last_fail) +{ + int ret, ret2; + ret = pthread_mutex_unlock(&rwlock->mcomplete); + ret2 = pthread_mutex_unlock(&rwlock->mex); + if (last_fail && ret2 != 0) + ret = ret2; + else if (!last_fail && !ret) + ret = ret2; + return ret; +} + +#ifdef WINPTHREAD_DBG +static int print_state = 0; +void rwl_print_set(int state) +{ + print_state = state; +} + +void rwl_print(volatile pthread_rwlock_t *rwl, char *txt) +{ + if (!print_state) return; + rwlock_t *r = (rwlock_t *)*rwl; + if (r == NULL) { + printf("RWL%p %lu %s\n",(void *)*rwl,GetCurrentThreadId(),txt); + } else { + printf("RWL%p %lu V=%0X B=%d r=%ld w=%ld L=%p %s\n", + (void *)*rwl, + GetCurrentThreadId(), + (int)r->valid, + (int)r->busy, + 0L,0L,NULL,txt); + } +} +#endif + +static pthread_spinlock_t cond_locked = PTHREAD_SPINLOCK_INITIALIZER; + +static WINPTHREADS_ATTRIBUTE((noinline)) int rwlock_static_init(pthread_rwlock_t *rw) +{ + int r; + pthread_spin_lock(&cond_locked); + if (*rw != PTHREAD_RWLOCK_INITIALIZER) + { + pthread_spin_unlock(&cond_locked); + return EINVAL; + } + r = pthread_rwlock_init (rw, NULL); + pthread_spin_unlock(&cond_locked); + + return r; +} + +int pthread_rwlock_init (pthread_rwlock_t *rwlock_, const pthread_rwlockattr_t *attr) +{ + rwlock_t *rwlock; + int r; + + if(!rwlock_) + return EINVAL; + *rwlock_ = (pthread_rwlock_t)NULL; + if ((rwlock = calloc(1, sizeof(*rwlock))) == NULL) + return ENOMEM; + rwlock->valid = DEAD_RWLOCK; + + rwlock->nex_count = rwlock->nsh_count = rwlock->ncomplete = 0; + if ((r = pthread_mutex_init (&rwlock->mex, NULL)) != 0) + { + free(rwlock); + return r; + } + if ((r = pthread_mutex_init (&rwlock->mcomplete, NULL)) != 0) + { + pthread_mutex_destroy(&rwlock->mex); + free(rwlock); + return r; + } + if ((r = pthread_cond_init (&rwlock->ccomplete, NULL)) != 0) + { + pthread_mutex_destroy(&rwlock->mex); + pthread_mutex_destroy (&rwlock->mcomplete); + free(rwlock); + return r; + } + rwlock->valid = LIFE_RWLOCK; + *rwlock_ = (pthread_rwlock_t)rwlock; + return r; +} + +int pthread_rwlock_destroy (pthread_rwlock_t *rwlock_) +{ + rwlock_t *rwlock; + pthread_rwlock_t rDestroy; + int r, r2; + + pthread_spin_lock(&cond_locked); + r = rwl_ref_destroy(rwlock_,&rDestroy); + pthread_spin_unlock(&cond_locked); + + if(r) return r; + if(!rDestroy) return 0; /* destroyed a (still) static initialized rwl */ + + rwlock = (rwlock_t *)rDestroy; + r = rwlock_gain_both_locks (rwlock); + if (r != 0) + { + *rwlock_ = rDestroy; + return r; + } + if (rwlock->nsh_count > rwlock->ncomplete || rwlock->nex_count > 0) + { + *rwlock_ = rDestroy; + r = rwlock_free_both_locks(rwlock, 1); + if (!r) + r = EBUSY; + return r; + } + rwlock->valid = DEAD_RWLOCK; + r = rwlock_free_both_locks(rwlock, 0); + if (r != 0) { *rwlock_ = rDestroy; return r; } + + r = pthread_cond_destroy(&rwlock->ccomplete); + r2 = pthread_mutex_destroy(&rwlock->mex); + if (!r) r = r2; + r2 = pthread_mutex_destroy(&rwlock->mcomplete); + if (!r) r = r2; + rwlock->valid = DEAD_RWLOCK; + free((void *)rDestroy); + return 0; +} + +int pthread_rwlock_rdlock (pthread_rwlock_t *rwlock_) +{ + rwlock_t *rwlock; + int ret; + + /* pthread_testcancel(); */ + + ret = rwl_ref(rwlock_,0); + if(ret != 0) return ret; + + rwlock = (rwlock_t *)*rwlock_; + + ret = pthread_mutex_lock(&rwlock->mex); + if (ret != 0) return rwl_unref(rwlock_, ret); + InterlockedIncrement((long*)&rwlock->nsh_count); + if (rwlock->nsh_count == INT_MAX) + { + ret = pthread_mutex_lock(&rwlock->mcomplete); + if (ret != 0) + { + pthread_mutex_unlock(&rwlock->mex); + return rwl_unref(rwlock_,ret); + } + rwlock->nsh_count -= rwlock->ncomplete; + rwlock->ncomplete = 0; + ret = rwlock_free_both_locks(rwlock, 0); + return rwl_unref(rwlock_, ret); + } + ret = pthread_mutex_unlock(&rwlock->mex); + return rwl_unref(rwlock_, ret); +} + +int pthread_rwlock_timedrdlock (pthread_rwlock_t *rwlock_, const struct timespec *ts) +{ + rwlock_t *rwlock; + int ret; + + /* pthread_testcancel(); */ + + ret = rwl_ref(rwlock_,0); + if(ret != 0) return ret; + + rwlock = (rwlock_t *)*rwlock_; + if ((ret = pthread_mutex_timedlock (&rwlock->mex, ts)) != 0) + return rwl_unref(rwlock_, ret); + InterlockedIncrement(&rwlock->nsh_count); + if (rwlock->nsh_count == INT_MAX) + { + ret = pthread_mutex_timedlock(&rwlock->mcomplete, ts); + if (ret != 0) + { + if (ret == ETIMEDOUT) + InterlockedIncrement(&rwlock->ncomplete); + pthread_mutex_unlock(&rwlock->mex); + return rwl_unref(rwlock_, ret); + } + rwlock->nsh_count -= rwlock->ncomplete; + rwlock->ncomplete = 0; + ret = rwlock_free_both_locks(rwlock, 0); + return rwl_unref(rwlock_, ret); + } + ret = pthread_mutex_unlock(&rwlock->mex); + return rwl_unref(rwlock_, ret); +} + +int pthread_rwlock_tryrdlock (pthread_rwlock_t *rwlock_) +{ + rwlock_t *rwlock; + int ret; + + ret = rwl_ref(rwlock_,RWL_TRY); + if(ret != 0) return ret; + + rwlock = (rwlock_t *)*rwlock_; + ret = pthread_mutex_trylock(&rwlock->mex); + if (ret != 0) + return rwl_unref(rwlock_, ret); + InterlockedIncrement(&rwlock->nsh_count); + if (rwlock->nsh_count == INT_MAX) + { + ret = pthread_mutex_lock(&rwlock->mcomplete); + if (ret != 0) + { + pthread_mutex_unlock(&rwlock->mex); + return rwl_unref(rwlock_, ret); + } + rwlock->nsh_count -= rwlock->ncomplete; + rwlock->ncomplete = 0; + ret = rwlock_free_both_locks(rwlock, 0); + return rwl_unref(rwlock_, ret); + } + ret = pthread_mutex_unlock(&rwlock->mex); + return rwl_unref(rwlock_,ret); +} + +int pthread_rwlock_trywrlock (pthread_rwlock_t *rwlock_) +{ + rwlock_t *rwlock; + int ret; + + ret = rwl_ref(rwlock_,RWL_TRY); + if(ret != 0) return ret; + + rwlock = (rwlock_t *)*rwlock_; + ret = pthread_mutex_trylock (&rwlock->mex); + if (ret != 0) + return rwl_unref(rwlock_, ret); + ret = pthread_mutex_trylock(&rwlock->mcomplete); + if (ret != 0) + { + int r1 = pthread_mutex_unlock(&rwlock->mex); + if (r1 != 0) + ret = r1; + return rwl_unref(rwlock_, ret); + } + if (rwlock->nex_count != 0) + return rwl_unref(rwlock_, EBUSY); + if (rwlock->ncomplete > 0) + { + rwlock->nsh_count -= rwlock->ncomplete; + rwlock->ncomplete = 0; + } + if (rwlock->nsh_count > 0) + { + ret = rwlock_free_both_locks(rwlock, 0); + if (!ret) + ret = EBUSY; + return rwl_unref(rwlock_, ret); + } + rwlock->nex_count = 1; + return rwl_unref(rwlock_, 0); +} + +int pthread_rwlock_unlock (pthread_rwlock_t *rwlock_) +{ + rwlock_t *rwlock; + int ret; + + ret = rwl_ref_unlock(rwlock_); + if(ret != 0) return ret; + + rwlock = (rwlock_t *)*rwlock_; + if (rwlock->nex_count == 0) + { + ret = pthread_mutex_lock(&rwlock->mcomplete); + if (!ret) + { + int r1; + InterlockedIncrement(&rwlock->ncomplete); + if (rwlock->ncomplete == 0) + ret = pthread_cond_signal(&rwlock->ccomplete); + r1 = pthread_mutex_unlock(&rwlock->mcomplete); + if (!ret) + ret = r1; + } + } + else + { + InterlockedDecrement(&rwlock->nex_count); + ret = rwlock_free_both_locks(rwlock, 0); + } + return rwl_unref(rwlock_, ret); +} + +static void st_cancelwrite (void *arg) +{ + rwlock_t *rwlock = (rwlock_t *)arg; + + rwlock->nsh_count = - rwlock->ncomplete; + rwlock->ncomplete = 0; + rwlock_free_both_locks(rwlock, 0); +} + +int pthread_rwlock_wrlock (pthread_rwlock_t *rwlock_) +{ + rwlock_t *rwlock; + int ret; + + /* pthread_testcancel(); */ + ret = rwl_ref(rwlock_,0); + if(ret != 0) return ret; + + rwlock = (rwlock_t *)*rwlock_; + ret = rwlock_gain_both_locks(rwlock); + if (ret != 0) + return rwl_unref(rwlock_,ret); + + if (rwlock->nex_count == 0) + { + if (rwlock->ncomplete > 0) + { + rwlock->nsh_count -= rwlock->ncomplete; + rwlock->ncomplete = 0; + } + if (rwlock->nsh_count > 0) + { + rwlock->ncomplete = -rwlock->nsh_count; + pthread_cleanup_push(st_cancelwrite, (void *) rwlock); + do { + ret = pthread_cond_wait(&rwlock->ccomplete, &rwlock->mcomplete); + } while (!ret && rwlock->ncomplete < 0); + + pthread_cleanup_pop(!ret ? 0 : 1); + if (!ret) + rwlock->nsh_count = 0; + } + } + if(!ret) + InterlockedIncrement((long*)&rwlock->nex_count); + return rwl_unref(rwlock_,ret); +} + +int pthread_rwlock_timedwrlock (pthread_rwlock_t *rwlock_, const struct timespec *ts) +{ + int ret; + rwlock_t *rwlock; + + /* pthread_testcancel(); */ + if (!rwlock_ || !ts) + return EINVAL; + if ((ret = rwl_ref(rwlock_,0)) != 0) + return ret; + rwlock = (rwlock_t *)*rwlock_; + + ret = pthread_mutex_timedlock(&rwlock->mex, ts); + if (ret != 0) + return rwl_unref(rwlock_,ret); + ret = pthread_mutex_timedlock (&rwlock->mcomplete, ts); + if (ret != 0) + { + pthread_mutex_unlock(&rwlock->mex); + return rwl_unref(rwlock_,ret); + } + if (rwlock->nex_count == 0) + { + if (rwlock->ncomplete > 0) + { + rwlock->nsh_count -= rwlock->ncomplete; + rwlock->ncomplete = 0; + } + if (rwlock->nsh_count > 0) + { + rwlock->ncomplete = -rwlock->nsh_count; + pthread_cleanup_push(st_cancelwrite, (void *) rwlock); + do { + ret = pthread_cond_timedwait(&rwlock->ccomplete, &rwlock->mcomplete, ts); + } while (rwlock->ncomplete < 0 && !ret); + pthread_cleanup_pop(!ret ? 0 : 1); + + if (!ret) + rwlock->nsh_count = 0; + } + } + if(!ret) + InterlockedIncrement((long*)&rwlock->nex_count); + return rwl_unref(rwlock_,ret); +} + +int pthread_rwlockattr_destroy(pthread_rwlockattr_t *a) +{ + if (!a) + return EINVAL; + return 0; +} + +int pthread_rwlockattr_init(pthread_rwlockattr_t *a) +{ + if (!a) + return EINVAL; + *a = PTHREAD_PROCESS_PRIVATE; + return 0; +} + +int pthread_rwlockattr_getpshared(pthread_rwlockattr_t *a, int *s) +{ + if (!a || !s) + return EINVAL; + *s = *a; + return 0; +} + +int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *a, int s) +{ + if (!a || (s != PTHREAD_PROCESS_SHARED && s != PTHREAD_PROCESS_PRIVATE)) + return EINVAL; + *a = s; + return 0; +} diff --git a/libs/winpthreads/src/rwlock.h b/libs/winpthreads/src/rwlock.h new file mode 100644 index 0000000..2d640fa --- /dev/null +++ b/libs/winpthreads/src/rwlock.h @@ -0,0 +1,49 @@ +/* + Copyright (c) 2011-2016 mingw-w64 project + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#ifndef WIN_PTHREADS_RWLOCK_H +#define WIN_PTHREADS_RWLOCK_H + +#define LIFE_RWLOCK 0xBAB1F0ED +#define DEAD_RWLOCK 0xDEADB0EF + +#define STATIC_RWL_INITIALIZER(x) ((pthread_rwlock_t)(x) == ((pthread_rwlock_t)PTHREAD_RWLOCK_INITIALIZER)) + +typedef struct rwlock_t rwlock_t; +struct rwlock_t { + unsigned int valid; + int busy; + LONG nex_count; /* Exclusive access counter. */ + LONG nsh_count; /* Shared access counter. */ + LONG ncomplete; /* Shared completed counter. */ + pthread_mutex_t mex; /* Exclusive access protection. */ + pthread_mutex_t mcomplete; /* Shared completed protection. */ + pthread_cond_t ccomplete; /* Shared access completed queue. */ +}; + +#define RWL_SET 0x01 +#define RWL_TRY 0x02 + +void rwl_print(volatile pthread_rwlock_t *rwl, char *txt); +void rwl_print_set(int state); + +#endif diff --git a/libs/winpthreads/src/sched.c b/libs/winpthreads/src/sched.c new file mode 100644 index 0000000..976bcc1 --- /dev/null +++ b/libs/winpthreads/src/sched.c @@ -0,0 +1,218 @@ +/* + Copyright (c) 2011-2016 mingw-w64 project + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include +#include +#include "pthread.h" +#include "thread.h" + +#include "misc.h" + +int sched_get_priority_min(int pol) +{ + if (pol < SCHED_MIN || pol > SCHED_MAX) { + errno = EINVAL; + return -1; + } + + return THREAD_PRIORITY_IDLE; +} + +int sched_get_priority_max(int pol) +{ + if (pol < SCHED_MIN || pol > SCHED_MAX) { + errno = EINVAL; + return -1; + } + + return THREAD_PRIORITY_TIME_CRITICAL; +} + +int pthread_attr_setschedparam(pthread_attr_t *attr, const struct sched_param *p) +{ + int r = 0; + + if (attr == NULL || p == NULL) { + return EINVAL; + } + memcpy(&attr->param, p, sizeof (*p)); + return r; +} + +int pthread_attr_getschedparam(const pthread_attr_t *attr, struct sched_param *p) +{ + int r = 0; + + if (attr == NULL || p == NULL) { + return EINVAL; + } + memcpy(p, &attr->param, sizeof (*p)); + return r; +} + +int pthread_attr_setschedpolicy (pthread_attr_t *attr, int pol) +{ + if (!attr || pol < SCHED_MIN || pol > SCHED_MAX) + return EINVAL; + if (pol != SCHED_OTHER) + return ENOTSUP; + return 0; +} + +int pthread_attr_getschedpolicy (const pthread_attr_t *attr, int *pol) +{ + if (!attr || !pol) + return EINVAL; + *pol = SCHED_OTHER; + return 0; +} + +static int pthread_check(pthread_t t) +{ + struct _pthread_v *pv; + + if (!t) + return ESRCH; + pv = __pth_gpointer_locked (t); + if (pv->ended == 0) + return 0; + CHECK_OBJECT(pv, ESRCH); + return 0; +} + +int pthread_getschedparam(pthread_t t, int *pol, struct sched_param *p) +{ + int r; + //if (!t) + // t = pthread_self(); + + if ((r = pthread_check(t)) != 0) + { + return r; + } + + if (!p || !pol) + { + return EINVAL; + } + *pol = __pth_gpointer_locked (t)->sched_pol; + p->sched_priority = __pth_gpointer_locked (t)->sched.sched_priority; + + return 0; +} + +int pthread_setschedparam(pthread_t t, int pol, const struct sched_param *p) +{ + struct _pthread_v *pv; + int r, pr = 0; + //if (!t.p) t = pthread_self(); + + if ((r = pthread_check(t)) != 0) + return r; + + if (pol < SCHED_MIN || pol > SCHED_MAX || p == NULL) + return EINVAL; + if (pol != SCHED_OTHER) + return ENOTSUP; + pr = p->sched_priority; + if (pr < sched_get_priority_min(pol) || pr > sched_get_priority_max(pol)) + return EINVAL; + + /* See msdn: there are actually 7 priorities: + THREAD_PRIORITY_IDLE - -15 + THREAD_PRIORITY_LOWEST -2 + THREAD_PRIORITY_BELOW_NORMAL -1 + THREAD_PRIORITY_NORMAL 0 + THREAD_PRIORITY_ABOVE_NORMAL 1 + THREAD_PRIORITY_HIGHEST 2 + THREAD_PRIORITY_TIME_CRITICAL 15 + */ + if (pr <= THREAD_PRIORITY_IDLE) { + pr = THREAD_PRIORITY_IDLE; + } else if (pr <= THREAD_PRIORITY_LOWEST) { + pr = THREAD_PRIORITY_LOWEST; + } else if (pr >= THREAD_PRIORITY_TIME_CRITICAL) { + pr = THREAD_PRIORITY_TIME_CRITICAL; + } else if (pr >= THREAD_PRIORITY_HIGHEST) { + pr = THREAD_PRIORITY_HIGHEST; + } + pv = __pth_gpointer_locked (t); + if (SetThreadPriority(pv->h, pr)) { + pv->sched_pol = pol; + pv->sched.sched_priority = p->sched_priority; + } else + r = EINVAL; + return r; +} + +int sched_getscheduler(pid_t pid) +{ + if (pid != 0) + { + HANDLE h = NULL; + int selfPid = (int) GetCurrentProcessId (); + + if (pid != (pid_t) selfPid && (h = OpenProcess (PROCESS_QUERY_INFORMATION, 0, (DWORD) pid)) == NULL) + { + errno = (GetLastError () == (0xFF & ERROR_ACCESS_DENIED)) ? EPERM : ESRCH; + return -1; + } + if (h) + CloseHandle (h); + } + return SCHED_OTHER; +} + +int sched_setscheduler(pid_t pid, int pol, const struct sched_param *param) +{ + if (!param) + { + errno = EINVAL; + return -1; + } + if (pid != 0) + { + HANDLE h = NULL; + int selfPid = (int) GetCurrentProcessId (); + + if (pid != (pid_t) selfPid && (h = OpenProcess (PROCESS_SET_INFORMATION, 0, (DWORD) pid)) == NULL) + { + errno = (GetLastError () == (0xFF & ERROR_ACCESS_DENIED)) ? EPERM : ESRCH; + return -1; + } + if (h) + CloseHandle (h); + } + + if (pol != SCHED_OTHER) + { + errno = ENOSYS; + return -1; + } + return SCHED_OTHER; +} + +int sched_yield(void) +{ + Sleep(0); + return 0; +} diff --git a/libs/winpthreads/src/sem.c b/libs/winpthreads/src/sem.c new file mode 100644 index 0000000..340ff69 --- /dev/null +++ b/libs/winpthreads/src/sem.c @@ -0,0 +1,354 @@ +/* + Copyright (c) 2011-2016 mingw-w64 project + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include +#include +#include +#include "pthread.h" +#include "thread.h" +#include "misc.h" +#include "semaphore.h" +#include "sem.h" +#include "ref.h" + +int do_sema_b_wait_intern (HANDLE sema, int nointerrupt, DWORD timeout); + +static int +sem_result (int res) +{ + if (res != 0) { + errno = res; + return -1; + } + return 0; +} + +int +sem_init (sem_t *sem, int pshared, unsigned int value) +{ + _sem_t *sv; + + if (!sem || value > (unsigned int)SEM_VALUE_MAX) + return sem_result (EINVAL); + if (pshared != PTHREAD_PROCESS_PRIVATE) + return sem_result (EPERM); + + if ((sv = (sem_t) calloc (1,sizeof (*sv))) == NULL) + return sem_result (ENOMEM); + + sv->value = value; + if (pthread_mutex_init (&sv->vlock, NULL) != 0) + { + free (sv); + return sem_result (ENOSPC); + } + if ((sv->s = CreateSemaphore (NULL, 0, SEM_VALUE_MAX, NULL)) == NULL) + { + pthread_mutex_destroy (&sv->vlock); + free (sv); + return sem_result (ENOSPC); + } + + sv->valid = LIFE_SEM; + *sem = sv; + return 0; +} + +int +sem_destroy (sem_t *sem) +{ + int r; + _sem_t *sv = NULL; + + if (!sem || (sv = *sem) == NULL) + return sem_result (EINVAL); + if ((r = pthread_mutex_lock (&sv->vlock)) != 0) + return sem_result (r); + +#if 0 + /* We don't wait for destroying a semaphore ... + or? */ + if (sv->value < 0) + { + pthread_mutex_unlock (&sv->vlock); + return sem_result (EBUSY); + } +#endif + + if (!CloseHandle (sv->s)) + { + pthread_mutex_unlock (&sv->vlock); + return sem_result (EINVAL); + } + *sem = NULL; + sv->value = SEM_VALUE_MAX; + pthread_mutex_unlock(&sv->vlock); + Sleep (0); + while (pthread_mutex_destroy (&sv->vlock) == EBUSY) + Sleep (0); + sv->valid = DEAD_SEM; + free (sv); + return 0; +} + +static int +sem_std_enter (sem_t *sem,_sem_t **svp, int do_test) +{ + int r; + _sem_t *sv; + + if (do_test) + pthread_testcancel (); + if (!sem) + return sem_result (EINVAL); + sv = *sem; + if (sv == NULL) + return sem_result (EINVAL); + + if ((r = pthread_mutex_lock (&sv->vlock)) != 0) + return sem_result (r); + + if (*sem == NULL) + { + pthread_mutex_unlock(&sv->vlock); + return sem_result (EINVAL); + } + *svp = sv; + return 0; +} + +int +sem_trywait (sem_t *sem) +{ + _sem_t *sv; + + if (sem_std_enter (sem, &sv, 0) != 0) + return -1; + if (sv->value <= 0) + { + pthread_mutex_unlock (&sv->vlock); + return sem_result (EAGAIN); + } + sv->value--; + pthread_mutex_unlock (&sv->vlock); + + return 0; +} + +struct sSemTimedWait +{ + sem_t *p; + int *ret; +}; + +static void +clean_wait_sem (void *s) +{ + struct sSemTimedWait *p = (struct sSemTimedWait *) s; + _sem_t *sv = NULL; + + if (sem_std_enter (p->p, &sv, 0) != 0) + return; + + if (WaitForSingleObject (sv->s, 0) != WAIT_OBJECT_0) + InterlockedIncrement (&sv->value); + else if (p->ret) + p->ret[0] = 0; + pthread_mutex_unlock (&sv->vlock); +} + +int +sem_wait (sem_t *sem) +{ + long cur_v; + int ret = 0; + _sem_t *sv; + HANDLE semh; + struct sSemTimedWait arg; + + if (sem_std_enter (sem, &sv, 1) != 0) + return -1; + + arg.ret = &ret; + arg.p = sem; + InterlockedDecrement (&sv->value); + cur_v = sv->value; + semh = sv->s; + pthread_mutex_unlock (&sv->vlock); + + if (cur_v >= 0) + return 0; + else + { + pthread_cleanup_push (clean_wait_sem, (void *) &arg); + ret = do_sema_b_wait_intern (semh, 2, INFINITE); + pthread_cleanup_pop (ret); + if (ret == EINVAL) + return 0; + } + + if (!ret) + return 0; + + return sem_result (ret); +} + +int +sem_timedwait (sem_t *sem, const struct timespec *t) +{ + int cur_v, ret = 0; + DWORD dwr; + HANDLE semh; + _sem_t *sv; + struct sSemTimedWait arg; + + if (!t) + return sem_wait (sem); + dwr = dwMilliSecs(_pthread_rel_time_in_ms (t)); + + if (sem_std_enter (sem, &sv, 1) != 0) + return -1; + + arg.ret = &ret; + arg.p = sem; + InterlockedDecrement (&sv->value); + cur_v = sv->value; + semh = sv->s; + pthread_mutex_unlock(&sv->vlock); + + if (cur_v >= 0) + return 0; + else + { + pthread_cleanup_push (clean_wait_sem, (void *) &arg); + ret = do_sema_b_wait_intern (semh, 2, dwr); + pthread_cleanup_pop (ret); + if (ret == EINVAL) + return 0; + } + + if (!ret) + return 0; + return sem_result (ret); +} + +int +sem_post (sem_t *sem) +{ + _sem_t *sv; + + if (sem_std_enter (sem, &sv, 0) != 0) + return -1; + + if (sv->value >= SEM_VALUE_MAX) + { + pthread_mutex_unlock (&sv->vlock); + return sem_result (ERANGE); + } + InterlockedIncrement (&sv->value); + if (sv->value > 0 || ReleaseSemaphore (sv->s, 1, NULL)) + { + pthread_mutex_unlock (&sv->vlock); + return 0; + } + InterlockedDecrement (&sv->value); + pthread_mutex_unlock (&sv->vlock); + + return sem_result (EINVAL); +} + +int +sem_post_multiple (sem_t *sem, int count) +{ + int waiters_count; + _sem_t *sv; + + if (count <= 0) + return sem_result (EINVAL); + if (sem_std_enter (sem, &sv, 0) != 0) + return -1; + + if (sv->value > (SEM_VALUE_MAX - count)) + { + pthread_mutex_unlock (&sv->vlock); + return sem_result (ERANGE); + } + waiters_count = -sv->value; + sv->value += count; + /*InterlockedExchangeAdd((long*)&sv->value, (long) count);*/ + if (waiters_count <= 0 + || ReleaseSemaphore (sv->s, + (waiters_count < count ? waiters_count + : count), NULL)) + { + pthread_mutex_unlock(&sv->vlock); + return 0; + } + /*InterlockedExchangeAdd((long*)&sv->value, -((long) count));*/ + sv->value -= count; + pthread_mutex_unlock(&sv->vlock); + return sem_result (EINVAL); +} + +sem_t * +sem_open (const char *name, int oflag, mode_t mode, unsigned int value) +{ + sem_result (ENOSYS); + return NULL; +} + +int +sem_close (sem_t *sem) +{ + return sem_result (ENOSYS); +} + +int +sem_unlink (const char *name) +{ + return sem_result (ENOSYS); +} + +int +sem_getvalue (sem_t *sem, int *sval) +{ + _sem_t *sv; + int r; + + if (!sval) + return sem_result (EINVAL); + + if (!sem || (sv = *sem) == NULL) + return sem_result (EINVAL); + + if ((r = pthread_mutex_lock (&sv->vlock)) != 0) + return sem_result (r); + if (*sem == NULL) + { + pthread_mutex_unlock (&sv->vlock); + return sem_result (EINVAL); + } + + *sval = (int) sv->value; + pthread_mutex_unlock (&sv->vlock); + return 0; +} diff --git a/libs/winpthreads/src/sem.h b/libs/winpthreads/src/sem.h new file mode 100644 index 0000000..3b3ace7 --- /dev/null +++ b/libs/winpthreads/src/sem.h @@ -0,0 +1,40 @@ +/* + Copyright (c) 2011-2016 mingw-w64 project + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#ifndef WIN_SEM +#define WIN_SEM + +#include + +#define LIFE_SEM 0xBAB1F00D +#define DEAD_SEM 0xDEADBEEF + +typedef struct _sem_t _sem_t; +struct _sem_t +{ + unsigned int valid; + HANDLE s; + volatile long value; + pthread_mutex_t vlock; +}; + +#endif /* WIN_SEM */ diff --git a/libs/winpthreads/src/spinlock.c b/libs/winpthreads/src/spinlock.c new file mode 100644 index 0000000..224c5a0 --- /dev/null +++ b/libs/winpthreads/src/spinlock.c @@ -0,0 +1,74 @@ +/* + Copyright (c) 2013 mingw-w64 project + Copyright (c) 2015 Intel Corporation + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include +#include "pthread.h" +#include "misc.h" + +/* We use the pthread_spinlock_t itself as a lock: + -1 is free, 0 is locked. + (This is dictated by PTHREAD_SPINLOCK_INITIALIZER, which we can't change + without breaking binary compatibility.) */ +typedef intptr_t spinlock_word_t; + +int +pthread_spin_init (pthread_spinlock_t *lock, int pshared) +{ + spinlock_word_t *lk = (spinlock_word_t *)lock; + *lk = -1; + return 0; +} + + +int +pthread_spin_destroy (pthread_spinlock_t *lock) +{ + return 0; +} + +int +pthread_spin_lock (pthread_spinlock_t *lock) +{ + volatile spinlock_word_t *lk = (volatile spinlock_word_t *)lock; + while (unlikely(InterlockedExchangePointer((PVOID volatile *)lk, 0) == 0)) + do { + YieldProcessor(); + } while (*lk == 0); + return 0; +} + +int +pthread_spin_trylock (pthread_spinlock_t *lock) +{ + spinlock_word_t *lk = (spinlock_word_t *)lock; + return InterlockedExchangePointer((PVOID volatile *)lk, 0) == 0 ? EBUSY : 0; +} + + +int +pthread_spin_unlock (pthread_spinlock_t *lock) +{ + volatile spinlock_word_t *lk = (volatile spinlock_word_t *)lock; + *lk = -1; + return 0; +} diff --git a/libs/winpthreads/src/thread.c b/libs/winpthreads/src/thread.c new file mode 100644 index 0000000..36ee665 --- /dev/null +++ b/libs/winpthreads/src/thread.c @@ -0,0 +1,1914 @@ +/* + Copyright (c) 2011-2016 mingw-w64 project + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include +#include +#include +#include +#include +#include +#include "pthread.h" +#include "thread.h" +#include "misc.h" +#include "winpthread_internal.h" + +static _pthread_v *__pthread_self_lite (void); + +void (**_pthread_key_dest)(void *) = NULL; + +static volatile long _pthread_cancelling; +static int _pthread_concur; + +/* FIXME Will default to zero as needed */ +static pthread_once_t _pthread_tls_once; +static DWORD _pthread_tls = 0xffffffff; + +static pthread_rwlock_t _pthread_key_lock = PTHREAD_RWLOCK_INITIALIZER; +static unsigned long _pthread_key_max=0L; +static unsigned long _pthread_key_sch=0L; + +static _pthread_v *pthr_root = NULL, *pthr_last = NULL; +static pthread_mutex_t mtx_pthr_locked = PTHREAD_RECURSIVE_MUTEX_INITIALIZER; + +static __pthread_idlist *idList = NULL; +static size_t idListCnt = 0; +static size_t idListMax = 0; +static pthread_t idListNextId = 0; + +#if !defined(_MSC_VER) +#define USE_VEH_FOR_MSC_SETTHREADNAME +#endif +#if !WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP) +/* forbidden RemoveVectoredExceptionHandler/AddVectoredExceptionHandler APIs */ +#undef USE_VEH_FOR_MSC_SETTHREADNAME +#endif + +#if defined(USE_VEH_FOR_MSC_SETTHREADNAME) +static void *SetThreadName_VEH_handle = NULL; + +static LONG __stdcall +SetThreadName_VEH (PEXCEPTION_POINTERS ExceptionInfo) +{ + if (ExceptionInfo->ExceptionRecord != NULL && + ExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_SET_THREAD_NAME) + return EXCEPTION_CONTINUE_EXECUTION; + + return EXCEPTION_CONTINUE_SEARCH; +} + +static PVOID (*AddVectoredExceptionHandlerFuncPtr) (ULONG, PVECTORED_EXCEPTION_HANDLER); +static ULONG (*RemoveVectoredExceptionHandlerFuncPtr) (PVOID); + +static void __attribute__((constructor)) +ctor (void) +{ + HMODULE module = GetModuleHandleA("kernel32.dll"); + if (module) { + AddVectoredExceptionHandlerFuncPtr = (__typeof__(AddVectoredExceptionHandlerFuncPtr)) GetProcAddress(module, "AddVectoredExceptionHandler"); + RemoveVectoredExceptionHandlerFuncPtr = (__typeof__(RemoveVectoredExceptionHandlerFuncPtr)) GetProcAddress(module, "RemoveVectoredExceptionHandler"); + } +} +#endif + +typedef struct _THREADNAME_INFO +{ + DWORD dwType; /* must be 0x1000 */ + LPCSTR szName; /* pointer to name (in user addr space) */ + DWORD dwThreadID; /* thread ID (-1=caller thread) */ + DWORD dwFlags; /* reserved for future use, must be zero */ +} THREADNAME_INFO; + +static void +SetThreadName (DWORD dwThreadID, LPCSTR szThreadName) +{ + THREADNAME_INFO info; + DWORD infosize; + + info.dwType = 0x1000; + info.szName = szThreadName; + info.dwThreadID = dwThreadID; + info.dwFlags = 0; + + infosize = sizeof (info) / sizeof (ULONG_PTR); + +#if defined(_MSC_VER) && !defined (USE_VEH_FOR_MSC_SETTHREADNAME) + __try + { + RaiseException (EXCEPTION_SET_THREAD_NAME, 0, infosize, (ULONG_PTR *)&info); + } + __except (EXCEPTION_EXECUTE_HANDLER) + { + } +#else + /* Without a debugger we *must* have an exception handler, + * otherwise raising an exception will crash the process. + */ +#if WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP) + if ((!IsDebuggerPresent ()) && (SetThreadName_VEH_handle == NULL)) +#else + if (!IsDebuggerPresent ()) +#endif + return; + + RaiseException (EXCEPTION_SET_THREAD_NAME, 0, infosize, (ULONG_PTR *) &info); +#endif +} + +/* Search the list idList for an element with identifier ID. If + found, its associated _pthread_v pointer is returned, otherwise + NULL. + NOTE: This method is not locked. */ +static struct _pthread_v * +__pthread_get_pointer (pthread_t id) +{ + size_t l, r, p; + if (!idListCnt) + return NULL; + if (idListCnt == 1) + return (idList[0].id == id ? idList[0].ptr : NULL); + l = 0; r = idListCnt - 1; + while (l <= r) + { + p = (l + r) >> 1; + if (idList[p].id == id) + return idList[p].ptr; + else if (idList[p].id > id) + { + if (p == l) + return NULL; + r = p - 1; + } + else + { + l = p + 1; + } + } + + return NULL; +} + +static void +__pth_remove_use_for_key (pthread_key_t key) +{ + int i; + + pthread_mutex_lock (&mtx_pthr_locked); + for (i = 0; i < idListCnt; i++) + { + if (idList[i].ptr != NULL + && idList[i].ptr->keyval != NULL + && key < idList[i].ptr->keymax) + { + idList[i].ptr->keyval[key] = NULL; + idList[i].ptr->keyval_set[key] = 0; + } + } + pthread_mutex_unlock (&mtx_pthr_locked); +} + +/* Search the list idList for an element with identifier ID. If + found, its associated _pthread_v pointer is returned, otherwise + NULL. + NOTE: This method uses lock mtx_pthr_locked. */ +struct _pthread_v * +__pth_gpointer_locked (pthread_t id) +{ + struct _pthread_v *ret; + if (!id) + return NULL; + pthread_mutex_lock (&mtx_pthr_locked); + ret = __pthread_get_pointer (id); + pthread_mutex_unlock (&mtx_pthr_locked); + return ret; +} + +/* Registers in the list idList an element with _pthread_v pointer + and creates and unique identifier ID. If successful created the + ID of this element is returned, otherwise on failure zero ID gets + returned. + NOTE: This method is not locked. */ +static pthread_t +__pthread_register_pointer (struct _pthread_v *ptr) +{ + __pthread_idlist *e; + size_t i; + + if (!ptr) + return 0; + /* Check if a resize of list is necessary. */ + if (idListCnt >= idListMax) + { + if (!idListCnt) + { + e = (__pthread_idlist *) malloc (sizeof (__pthread_idlist) * 16); + if (!e) + return 0; + idListMax = 16; + idList = e; + } + else + { + e = (__pthread_idlist *) realloc (idList, sizeof (__pthread_idlist) * (idListMax + 16)); + if (!e) + return 0; + idListMax += 16; + idList = e; + } + } + do + { + ++idListNextId; + /* If two MSB are set we reset to id 1. We need to check here bits + to avoid gcc's no-overflow issue on increment. Additionally we + need to handle different size of pthread_t on 32-bit/64-bit. */ + if ((idListNextId & ( ((pthread_t) 1) << ((sizeof (pthread_t) * 8) - 2))) != 0) + idListNextId = 1; + } + while (idListNextId == 0 || __pthread_get_pointer (idListNextId)); + /* We assume insert at end of list. */ + i = idListCnt; + if (i != 0) + { + /* Find position we can actual insert sorted. */ + while (i > 0 && idList[i - 1].id > idListNextId) + --i; + if (i != idListCnt) + memmove (&idList[i + 1], &idList[i], sizeof (__pthread_idlist) * (idListCnt - i)); + } + idList[i].id = idListNextId; + idList[i].ptr = ptr; + ++idListCnt; + return idListNextId; +} + +/* Deregisters in the list idList an element with identifier ID and + returns its _pthread_v pointer on success. Otherwise NULL is returned. + NOTE: This method is not locked. */ +static struct _pthread_v * +__pthread_deregister_pointer (pthread_t id) +{ + size_t l, r, p; + if (!idListCnt) + return NULL; + l = 0; r = idListCnt - 1; + while (l <= r) + { + p = (l + r) >> 1; + if (idList[p].id == id) + { + struct _pthread_v *ret = idList[p].ptr; + p++; + if (p < idListCnt) + memmove (&idList[p - 1], &idList[p], sizeof (__pthread_idlist) * (idListCnt - p)); + --idListCnt; + /* Is this last element in list then free list. */ + if (idListCnt == 0) + { + free (idList); + idListCnt = idListMax = 0; + } + return ret; + } + else if (idList[p].id > id) + { + if (p == l) + return NULL; + r = p - 1; + } + else + { + l = p + 1; + } + } + return NULL; +} + +/* Save a _pthread_v element for reuse in pool. */ +static void +push_pthread_mem (_pthread_v *sv) +{ + if (!sv || sv->next != NULL) + return; + pthread_mutex_lock (&mtx_pthr_locked); + if (sv->x != 0) + __pthread_deregister_pointer (sv->x); + if (sv->keyval) + free (sv->keyval); + if (sv->keyval_set) + free (sv->keyval_set); + if (sv->thread_name) + free (sv->thread_name); + memset (sv, 0, sizeof(struct _pthread_v)); + if (pthr_last == NULL) + pthr_root = pthr_last = sv; + else + { + pthr_last->next = sv; + pthr_last = sv; + } + pthread_mutex_unlock (&mtx_pthr_locked); +} + +/* Get a _pthread_v element from pool, or allocate it. + Note the unique identifier is created for the element here, too. */ +static _pthread_v * +pop_pthread_mem (void) +{ + _pthread_v *r = NULL; + + pthread_mutex_lock (&mtx_pthr_locked); + if ((r = pthr_root) == NULL) + { + if ((r = (_pthread_v *)calloc (1,sizeof(struct _pthread_v))) != NULL) + { + r->x = __pthread_register_pointer (r); + if (r->x == 0) + { + free (r); + r = NULL; + } + } + pthread_mutex_unlock (&mtx_pthr_locked); + return r; + } + r->x = __pthread_register_pointer (r); + if (r->x == 0) + r = NULL; + else + { + if((pthr_root = r->next) == NULL) + pthr_last = NULL; + + r->next = NULL; + } + pthread_mutex_unlock (&mtx_pthr_locked); + return r; +} + +/* Free memory consumed in _pthread_v pointer pool. */ +static void +free_pthread_mem (void) +{ +#if 0 + _pthread_v *t; + + pthread_mutex_lock (&mtx_pthr_locked); + t = pthr_root; + while (t != NULL) + { + _pthread_v *sv = t; + t = t->next; + if (sv->x != 0 && sv->ended == 0 && sv->valid != DEAD_THREAD) + { + pthread_mutex_unlock (&mtx_pthr_locked); + pthread_cancel (t->x); + Sleep (0); + pthread_mutex_lock (&mtx_pthr_locked); + t = pthr_root; + continue; + } + else if (sv->x != 0 && sv->valid != DEAD_THREAD) + { + pthread_mutex_unlock (&mtx_pthr_locked); + Sleep (0); + pthread_mutex_lock (&mtx_pthr_locked); + continue; + } + if (sv->x != 0) + __pthread_deregister_pointer (sv->x); + sv->x = 0; + free (sv); + pthr_root = t; + } + pthread_mutex_unlock (&mtx_pthr_locked); +#endif + return; +} + +static void +replace_spin_keys (pthread_spinlock_t *old, pthread_spinlock_t new) +{ + if (old == NULL) + return; + + if (EPERM == pthread_spin_destroy (old)) + { +#define THREADERR "Error cleaning up spin_keys for thread %lu.\n" + char threaderr[sizeof(THREADERR) + 8] = { 0 }; + snprintf(threaderr, sizeof(threaderr), THREADERR, GetCurrentThreadId()); +#undef THREADERR + OutputDebugStringA (threaderr); + abort (); + } + + *old = new; +} + +/* Hook for TLS-based deregistration/registration of thread. */ +static void WINAPI +__dyn_tls_pthread (HANDLE hDllHandle, DWORD dwReason, LPVOID lpreserved) +{ + _pthread_v *t = NULL; + pthread_spinlock_t new_spin_keys = PTHREAD_SPINLOCK_INITIALIZER; + + if (dwReason == DLL_PROCESS_DETACH) + { +#if defined(USE_VEH_FOR_MSC_SETTHREADNAME) + if (lpreserved == NULL && SetThreadName_VEH_handle != NULL) + { + if (RemoveVectoredExceptionHandlerFuncPtr != NULL) + RemoveVectoredExceptionHandlerFuncPtr (SetThreadName_VEH_handle); + SetThreadName_VEH_handle = NULL; + } +#endif + free_pthread_mem (); + } + else if (dwReason == DLL_PROCESS_ATTACH) + { +#if defined(USE_VEH_FOR_MSC_SETTHREADNAME) + if (AddVectoredExceptionHandlerFuncPtr != NULL) + SetThreadName_VEH_handle = AddVectoredExceptionHandlerFuncPtr (1, &SetThreadName_VEH); + else + SetThreadName_VEH_handle = NULL; + /* Can't do anything on error anyway, check for NULL later */ +#endif + } + else if (dwReason == DLL_THREAD_DETACH) + { + if (_pthread_tls != 0xffffffff) + t = (_pthread_v *)TlsGetValue(_pthread_tls); + if (t && t->thread_noposix != 0) + { + _pthread_cleanup_dest (t->x); + if (t->h != NULL) + { + CloseHandle (t->h); + if (t->evStart) + CloseHandle (t->evStart); + t->evStart = NULL; + t->h = NULL; + } + pthread_mutex_destroy (&t->p_clock); + replace_spin_keys (&t->spin_keys, new_spin_keys); + push_pthread_mem (t); + t = NULL; + TlsSetValue (_pthread_tls, t); + } + else if (t && t->ended == 0) + { + if (t->evStart) + CloseHandle(t->evStart); + t->evStart = NULL; + t->ended = 1; + _pthread_cleanup_dest (t->x); + if ((t->p_state & PTHREAD_CREATE_DETACHED) == PTHREAD_CREATE_DETACHED) + { + t->valid = DEAD_THREAD; + if (t->h != NULL) + CloseHandle (t->h); + t->h = NULL; + pthread_mutex_destroy(&t->p_clock); + replace_spin_keys (&t->spin_keys, new_spin_keys); + push_pthread_mem (t); + t = NULL; + TlsSetValue (_pthread_tls, t); + return; + } + pthread_mutex_destroy(&t->p_clock); + replace_spin_keys (&t->spin_keys, new_spin_keys); + } + else if (t) + { + if (t->evStart) + CloseHandle (t->evStart); + t->evStart = NULL; + pthread_mutex_destroy (&t->p_clock); + replace_spin_keys (&t->spin_keys, new_spin_keys); + } + } +} + +/* TLS-runtime section variable. */ + +#if defined(_MSC_VER) +/* Force a reference to _tls_used to make the linker create the TLS + * directory if it's not already there. (e.g. if __declspec(thread) + * is not used). + * Force a reference to __xl_f to prevent whole program optimization + * from discarding the variable. */ + +/* On x86, symbols are prefixed with an underscore. */ +# if defined(_M_IX86) +# pragma comment(linker, "/include:__tls_used") +# pragma comment(linker, "/include:___xl_f") +# else +# pragma comment(linker, "/include:_tls_used") +# pragma comment(linker, "/include:__xl_f") +# endif + +/* .CRT$XLA to .CRT$XLZ is an array of PIMAGE_TLS_CALLBACK + * pointers. Pick an arbitrary location for our callback. + * + * See VC\...\crt\src\vcruntime\tlssup.cpp for reference. */ + +# pragma section(".CRT$XLF", long, read) +#endif + +WINPTHREADS_ATTRIBUTE((WINPTHREADS_SECTION(".CRT$XLF"))) +extern const PIMAGE_TLS_CALLBACK __xl_f; +const PIMAGE_TLS_CALLBACK __xl_f = __dyn_tls_pthread; + + +#ifdef WINPTHREAD_DBG +static int print_state = 0; +void thread_print_set (int state) +{ + print_state = state; +} + +void +thread_print (volatile pthread_t t, char *txt) +{ + if (!print_state) + return; + if (!t) + printf("T%p %lu %s\n",NULL,GetCurrentThreadId(),txt); + else + { + printf("T%p %lu V=%0X H=%p %s\n", + (void *) __pth_gpointer_locked (t), + GetCurrentThreadId(), + (__pth_gpointer_locked (t))->valid, + (__pth_gpointer_locked (t))->h, + txt + ); + } +} +#endif + +/* Internal collect-once structure. */ +typedef struct collect_once_t { + pthread_once_t *o; + pthread_mutex_t m; + int count; + struct collect_once_t *next; +} collect_once_t; + +static collect_once_t *once_obj = NULL; + +static pthread_spinlock_t once_global = PTHREAD_SPINLOCK_INITIALIZER; + +static collect_once_t * +enterOnceObject (pthread_once_t *o) +{ + collect_once_t *c, *p = NULL; + pthread_spin_lock (&once_global); + c = once_obj; + while (c != NULL && c->o != o) + { + c = (p = c)->next; + } + if (!c) + { + c = (collect_once_t *) calloc(1,sizeof(collect_once_t)); + c->o = o; + c->count = 1; + if (!p) + once_obj = c; + else + p->next = c; + pthread_mutex_init(&c->m, NULL); + } + else + c->count += 1; + pthread_spin_unlock (&once_global); + return c; +} + +static void +leaveOnceObject (collect_once_t *c) +{ + collect_once_t *h, *p = NULL; + if (!c) + return; + pthread_spin_lock (&once_global); + h = once_obj; + while (h != NULL && c != h) + h = (p = h)->next; + + if (h) + { + c->count -= 1; + if (c->count == 0) + { + pthread_mutex_destroy(&c->m); + if (!p) + once_obj = c->next; + else + p->next = c->next; + free (c); + } + } + else + fprintf(stderr, "%p not found?!?!\n", (void *) c); + pthread_spin_unlock (&once_global); +} + +static void +_pthread_once_cleanup (void *o) +{ + collect_once_t *co = (collect_once_t *) o; + pthread_mutex_unlock (&co->m); + leaveOnceObject (co); +} + +static int +_pthread_once_raw (pthread_once_t *o, void (*func)(void)) +{ + collect_once_t *co; + long state = *o; + + CHECK_PTR(o); + CHECK_PTR(func); + + if (state == 1) + return 0; + co = enterOnceObject(o); + pthread_mutex_lock(&co->m); + if (*o == 0) + { + func(); + *o = 1; + } + else if (*o != 1) + fprintf (stderr," once %p is %ld\n", (void *) o, (long) *o); + pthread_mutex_unlock(&co->m); + leaveOnceObject(co); + + /* Done */ + return 0; +} + +/* Unimplemented. */ +void * +pthread_timechange_handler_np(void *dummy) +{ + return NULL; +} + +/* Compatibility routine for pthread-win32. It waits for ellapse of + interval and additionally checks for possible thread-cancelation. */ +int +pthread_delay_np (const struct timespec *interval) +{ + DWORD to = (!interval ? 0 : dwMilliSecs (_pthread_time_in_ms_from_timespec (interval))); + struct _pthread_v *s = __pthread_self_lite (); + + if (!to) + { + pthread_testcancel (); + Sleep (0); + pthread_testcancel (); + return 0; + } + pthread_testcancel (); + if (s->evStart) + _pthread_wait_for_single_object (s->evStart, to); + else + Sleep (to); + pthread_testcancel (); + return 0; +} + +int pthread_delay_np_ms (DWORD to); + +int +pthread_delay_np_ms (DWORD to) +{ + struct _pthread_v *s = __pthread_self_lite (); + + if (!to) + { + pthread_testcancel (); + Sleep (0); + pthread_testcancel (); + return 0; + } + pthread_testcancel (); + if (s->evStart) + _pthread_wait_for_single_object (s->evStart, to); + else + Sleep (to); + pthread_testcancel (); + return 0; +} + +/* Compatibility routine for pthread-win32. It returns the + amount of available CPUs on system. */ +int +pthread_num_processors_np(void) +{ + int r = 0; + DWORD_PTR ProcessAffinityMask, SystemAffinityMask; + + if (GetProcessAffinityMask(GetCurrentProcess(), &ProcessAffinityMask, &SystemAffinityMask)) + { + for(; ProcessAffinityMask != 0; ProcessAffinityMask >>= 1) + r += (ProcessAffinityMask & 1) != 0; + } + /* assume at least 1 */ + return r ? r : 1; +} + +/* Compatiblity routine for pthread-win32. Allows to set amount of used + CPUs for process. */ +int +pthread_set_num_processors_np(int n) +{ + DWORD_PTR ProcessAffinityMask, ProcessNewAffinityMask = 0, SystemAffinityMask; + int r = 0; + /* need at least 1 */ + n = n ? n : 1; + if (GetProcessAffinityMask (GetCurrentProcess (), &ProcessAffinityMask, &SystemAffinityMask)) + { + for (; ProcessAffinityMask != 0; ProcessAffinityMask >>= 1) + { + ProcessNewAffinityMask <<= 1; + if ((ProcessAffinityMask & 1) != 0 && r < n) + { + ProcessNewAffinityMask |= 1; + r++; + } + } + SetProcessAffinityMask (GetCurrentProcess (),ProcessNewAffinityMask); + } + return r; +} + +int +pthread_once (pthread_once_t *o, void (*func)(void)) +{ + collect_once_t *co; + long state = *o; + + CHECK_PTR(o); + CHECK_PTR(func); + + if (state == 1) + return 0; + co = enterOnceObject(o); + pthread_mutex_lock(&co->m); + if (*o == 0) + { + pthread_cleanup_push(_pthread_once_cleanup, co); + func(); + pthread_cleanup_pop(0); + *o = 1; + } + else if (*o != 1) + fprintf (stderr," once %p is %ld\n", (void *) o, (long) *o); + pthread_mutex_unlock(&co->m); + leaveOnceObject(co); + + return 0; +} + +int +pthread_key_create (pthread_key_t *key, void (* dest)(void *)) +{ + unsigned int i; + long nmax; + void (**d)(void *); + + if (!key) + return EINVAL; + + pthread_rwlock_wrlock (&_pthread_key_lock); + + for (i = _pthread_key_sch; i < _pthread_key_max; i++) + { + if (!_pthread_key_dest[i]) + { + *key = i; + if (dest) + _pthread_key_dest[i] = dest; + else + _pthread_key_dest[i] = (void(*)(void *))1; + pthread_rwlock_unlock (&_pthread_key_lock); + return 0; + } + } + + for (i = 0; i < _pthread_key_sch; i++) + { + if (!_pthread_key_dest[i]) + { + *key = i; + if (dest) + _pthread_key_dest[i] = dest; + else + _pthread_key_dest[i] = (void(*)(void *))1; + pthread_rwlock_unlock (&_pthread_key_lock); + + return 0; + } + } + + if (_pthread_key_max == PTHREAD_KEYS_MAX) + { + pthread_rwlock_unlock(&_pthread_key_lock); + return ENOMEM; + } + + nmax = _pthread_key_max * 2; + if (nmax == 0) + nmax = _pthread_key_max + 1; + if (nmax > PTHREAD_KEYS_MAX) + nmax = PTHREAD_KEYS_MAX; + + /* No spare room anywhere */ + d = (void (__cdecl **)(void *))realloc(_pthread_key_dest, nmax * sizeof(*d)); + if (!d) + { + pthread_rwlock_unlock (&_pthread_key_lock); + return ENOMEM; + } + + /* Clear new region */ + memset ((void *) &d[_pthread_key_max], 0, (nmax-_pthread_key_max)*sizeof(void *)); + + /* Use new region */ + _pthread_key_dest = d; + _pthread_key_sch = _pthread_key_max + 1; + *key = _pthread_key_max; + _pthread_key_max = nmax; + + if (dest) + _pthread_key_dest[*key] = dest; + else + _pthread_key_dest[*key] = (void(*)(void *))1; + + pthread_rwlock_unlock (&_pthread_key_lock); + return 0; +} + +int +pthread_key_delete (pthread_key_t key) +{ + if (key >= _pthread_key_max || !_pthread_key_dest) + return EINVAL; + + pthread_rwlock_wrlock (&_pthread_key_lock); + + _pthread_key_dest[key] = NULL; + + /* Start next search from our location */ + if (_pthread_key_sch > key) + _pthread_key_sch = key; + /* So now we need to walk the complete list of threads + and remove key's reference for it. */ + __pth_remove_use_for_key (key); + + pthread_rwlock_unlock (&_pthread_key_lock); + return 0; +} + +void * +pthread_getspecific (pthread_key_t key) +{ + DWORD lasterr = GetLastError (); + void *r; + _pthread_v *t = __pthread_self_lite (); + pthread_spin_lock (&t->spin_keys); + r = (key >= t->keymax || t->keyval_set[key] == 0 ? NULL : t->keyval[key]); + pthread_spin_unlock (&t->spin_keys); + SetLastError (lasterr); + return r; +} + +int +pthread_setspecific (pthread_key_t key, const void *value) +{ + DWORD lasterr = GetLastError (); + _pthread_v *t = __pthread_self_lite (); + + pthread_spin_lock (&t->spin_keys); + + if (key >= t->keymax) + { + int keymax = (key + 1); + void **kv; + unsigned char *kv_set; + + kv = (void **) realloc (t->keyval, keymax * sizeof (void *)); + + if (!kv) + { + pthread_spin_unlock (&t->spin_keys); + return ENOMEM; + } + kv_set = (unsigned char *) realloc (t->keyval_set, keymax); + if (!kv_set) + { + pthread_spin_unlock (&t->spin_keys); + return ENOMEM; + } + + /* Clear new region */ + memset (&kv[t->keymax], 0, (keymax - t->keymax)*sizeof(void *)); + memset (&kv_set[t->keymax], 0, (keymax - t->keymax)); + + t->keyval = kv; + t->keyval_set = kv_set; + t->keymax = keymax; + } + + t->keyval[key] = (void *) value; + t->keyval_set[key] = 1; + pthread_spin_unlock (&t->spin_keys); + SetLastError (lasterr); + + return 0; +} + +int +pthread_equal (pthread_t t1, pthread_t t2) +{ + return (t1 == t2); +} + +void +pthread_tls_init (void) +{ + _pthread_tls = TlsAlloc(); + + /* Cannot continue if out of indexes */ + if (_pthread_tls == TLS_OUT_OF_INDEXES) + abort(); +} + +void +_pthread_cleanup_dest (pthread_t t) +{ + _pthread_v *tv; + unsigned int i, j; + + if (!t) + return; + tv = __pth_gpointer_locked (t); + if (!tv) + return; + + for (j = 0; j < PTHREAD_DESTRUCTOR_ITERATIONS; j++) + { + int flag = 0; + + pthread_spin_lock (&tv->spin_keys); + for (i = 0; i < tv->keymax; i++) + { + void *val = tv->keyval[i]; + + if (tv->keyval_set[i]) + { + pthread_rwlock_rdlock (&_pthread_key_lock); + if ((uintptr_t) _pthread_key_dest[i] > 1) + { + /* Call destructor */ + tv->keyval[i] = NULL; + tv->keyval_set[i] = 0; + pthread_spin_unlock (&tv->spin_keys); + _pthread_key_dest[i](val); + pthread_spin_lock (&tv->spin_keys); + flag = 1; + } + else + { + tv->keyval[i] = NULL; + tv->keyval_set[i] = 0; + } + pthread_rwlock_unlock(&_pthread_key_lock); + } + } + pthread_spin_unlock (&tv->spin_keys); + /* Nothing to do? */ + if (!flag) + return; + } +} + +static _pthread_v * +__pthread_self_lite (void) +{ + _pthread_v *t; + pthread_spinlock_t new_spin_keys = PTHREAD_SPINLOCK_INITIALIZER; + + _pthread_once_raw (&_pthread_tls_once, pthread_tls_init); + + t = (_pthread_v *) TlsGetValue (_pthread_tls); + if (t) + return t; + /* Main thread? */ + t = (struct _pthread_v *) pop_pthread_mem (); + + /* If cannot initialize main thread, then the only thing we can do is return null pthread_t */ + if (!__xl_f || !t) + return 0; + + t->p_state = PTHREAD_DEFAULT_ATTR /*| PTHREAD_CREATE_DETACHED*/; + t->tid = GetCurrentThreadId(); + t->evStart = CreateEvent (NULL, 1, 0, NULL); + t->p_clock = PTHREAD_MUTEX_INITIALIZER; + replace_spin_keys (&t->spin_keys, new_spin_keys); + t->sched_pol = SCHED_OTHER; + t->h = NULL; //GetCurrentThread(); + if (!DuplicateHandle(GetCurrentProcess(), GetCurrentThread(), GetCurrentProcess(), &t->h, 0, FALSE, DUPLICATE_SAME_ACCESS)) + abort (); + t->sched.sched_priority = GetThreadPriority(t->h); + t->ended = 0; + t->thread_noposix = 1; + + /* Save for later */ + if (!TlsSetValue(_pthread_tls, t)) + abort (); + return t; +} + +pthread_t +pthread_self (void) +{ + _pthread_v *t = __pthread_self_lite (); + + if (!t) + return 0; + return t->x; +} + +/* Internal helper for getting event handle of thread T. */ +void * +pthread_getevent (void) +{ + _pthread_v *t = __pthread_self_lite (); + return (!t ? NULL : t->evStart); +} + +/* Internal helper for getting thread handle of thread T. */ +void * +pthread_gethandle (pthread_t t) +{ + struct _pthread_v *tv = __pth_gpointer_locked (t); + return (!tv ? NULL : tv->h); +} + +/* Internal helper for getting pointer of clean of current thread. */ +struct _pthread_cleanup ** +pthread_getclean (void) +{ + struct _pthread_v *t = __pthread_self_lite (); + if (!t) return NULL; + return &t->clean; +} + +int +pthread_get_concurrency (int *val) +{ + *val = _pthread_concur; + return 0; +} + +int +pthread_set_concurrency (int val) +{ + _pthread_concur = val; + return 0; +} + +void +pthread_exit (void *res) +{ + _pthread_v *t = NULL; + unsigned rslt = (unsigned) ((intptr_t) res); + struct _pthread_v *id = __pthread_self_lite (); + + id->ret_arg = res; + + _pthread_cleanup_dest (id->x); + if (id->thread_noposix == 0) + longjmp(id->jb, 1); + + /* Make sure we free ourselves if we are detached */ + if ((t = (_pthread_v *)TlsGetValue(_pthread_tls)) != NULL) + { + if (!t->h) + { + t->valid = DEAD_THREAD; + if (t->evStart) + CloseHandle (t->evStart); + t->evStart = NULL; + rslt = (unsigned) (size_t) t->ret_arg; + push_pthread_mem(t); + t = NULL; + TlsSetValue (_pthread_tls, t); + } + else + { + rslt = (unsigned) (size_t) t->ret_arg; + t->ended = 1; + if (t->evStart) + CloseHandle (t->evStart); + t->evStart = NULL; + if ((t->p_state & PTHREAD_CREATE_DETACHED) == PTHREAD_CREATE_DETACHED) + { + t->valid = DEAD_THREAD; + CloseHandle (t->h); + t->h = NULL; + push_pthread_mem(t); + t = NULL; + TlsSetValue(_pthread_tls, t); + } + } + } + /* Time to die */ + _endthreadex(rslt); +} + +void +_pthread_invoke_cancel (void) +{ + _pthread_cleanup *pcup; + struct _pthread_v *se = __pthread_self_lite (); + se->in_cancel = 1; + _pthread_setnobreak (1); + InterlockedDecrement(&_pthread_cancelling); + + /* Call cancel queue */ + for (pcup = se->clean; pcup; pcup = pcup->next) + { + pcup->func((pthread_once_t *)pcup->arg); + } + + _pthread_setnobreak (0); + pthread_exit(PTHREAD_CANCELED); +} + +int +__pthread_shallcancel (void) +{ + struct _pthread_v *t; + if (!_pthread_cancelling) + return 0; + t = __pthread_self_lite (); + if (t == NULL) + return 0; + if (t->nobreak <= 0 && t->cancelled && (t->p_state & PTHREAD_CANCEL_ENABLE)) + return 1; + return 0; +} + +void +_pthread_setnobreak (int v) +{ + struct _pthread_v *t = __pthread_self_lite (); + if (t == NULL) + return; + if (v > 0) + InterlockedIncrement ((long*)&t->nobreak); + else + InterlockedDecrement((long*)&t->nobreak); +} + +void +pthread_testcancel (void) +{ + struct _pthread_v *self = __pthread_self_lite (); + + if (!self || self->in_cancel) + return; + if (!_pthread_cancelling) + return; + pthread_mutex_lock (&self->p_clock); + + if (self->cancelled && (self->p_state & PTHREAD_CANCEL_ENABLE) && self->nobreak <= 0) + { + self->in_cancel = 1; + self->p_state &= ~PTHREAD_CANCEL_ENABLE; + if (self->evStart) + ResetEvent (self->evStart); + pthread_mutex_unlock (&self->p_clock); + _pthread_invoke_cancel (); + } + pthread_mutex_unlock (&self->p_clock); +} + +int +pthread_cancel (pthread_t t) +{ + struct _pthread_v *tv = __pth_gpointer_locked (t); + + if (tv == NULL) + return ESRCH; + CHECK_OBJECT(tv, ESRCH); + /*if (tv->ended) return ESRCH;*/ + pthread_mutex_lock(&tv->p_clock); + if (pthread_equal(pthread_self(), t)) + { + if(tv->cancelled) + { + pthread_mutex_unlock(&tv->p_clock); + return (tv->in_cancel ? ESRCH : 0); + } + tv->cancelled = 1; + InterlockedIncrement(&_pthread_cancelling); + if(tv->evStart) SetEvent(tv->evStart); + if ((tv->p_state & PTHREAD_CANCEL_ASYNCHRONOUS) != 0 && (tv->p_state & PTHREAD_CANCEL_ENABLE) != 0) + { + tv->p_state &= ~PTHREAD_CANCEL_ENABLE; + tv->in_cancel = 1; + pthread_mutex_unlock(&tv->p_clock); + _pthread_invoke_cancel(); + } + else + pthread_mutex_unlock(&tv->p_clock); + return 0; + } + + if ((tv->p_state & PTHREAD_CANCEL_ASYNCHRONOUS) != 0 && (tv->p_state & PTHREAD_CANCEL_ENABLE) != 0) + { + /* Dangerous asynchronous cancelling */ + CONTEXT ctxt; + + if(tv->in_cancel) + { + pthread_mutex_unlock(&tv->p_clock); + return (tv->in_cancel ? ESRCH : 0); + } + /* Already done? */ + if(tv->cancelled || tv->in_cancel) + { + /* ??? pthread_mutex_unlock (&tv->p_clock); */ + return ESRCH; + } + + ctxt.ContextFlags = CONTEXT_CONTROL; + + SuspendThread (tv->h); + if (WaitForSingleObject (tv->h, 0) == WAIT_TIMEOUT) + { + GetThreadContext(tv->h, &ctxt); +#ifdef _M_X64 + ctxt.Rip = (uintptr_t) _pthread_invoke_cancel; +#elif defined(_M_IX86) + ctxt.Eip = (uintptr_t) _pthread_invoke_cancel; +#elif defined(_M_ARM) || defined(_M_ARM64) + ctxt.Pc = (uintptr_t) _pthread_invoke_cancel; +#else +#error Unsupported architecture +#endif +#if WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP) + SetThreadContext (tv->h, &ctxt); +#endif + + /* Also try deferred Cancelling */ + tv->cancelled = 1; + tv->p_state &= ~PTHREAD_CANCEL_ENABLE; + tv->in_cancel = 1; + + /* Notify everyone to look */ + InterlockedIncrement (&_pthread_cancelling); + if (tv->evStart) + SetEvent (tv->evStart); + pthread_mutex_unlock (&tv->p_clock); + + ResumeThread (tv->h); + } + } + else + { + if (tv->cancelled == 0) + { + /* Safe deferred Cancelling */ + tv->cancelled = 1; + + /* Notify everyone to look */ + InterlockedIncrement (&_pthread_cancelling); + if (tv->evStart) + SetEvent (tv->evStart); + } + else + { + pthread_mutex_unlock (&tv->p_clock); + return (tv->in_cancel ? ESRCH : 0); + } + } + pthread_mutex_unlock (&tv->p_clock); + return 0; +} + +/* half-stubbed version as we don't really well support signals */ +int +pthread_kill (pthread_t t, int sig) +{ + struct _pthread_v *tv; + + pthread_mutex_lock (&mtx_pthr_locked); + tv = __pthread_get_pointer (t); + if (!tv || t != tv->x || tv->in_cancel || tv->ended || tv->h == NULL + || tv->h == INVALID_HANDLE_VALUE) + { + pthread_mutex_unlock (&mtx_pthr_locked); + return ESRCH; + } + pthread_mutex_unlock (&mtx_pthr_locked); + if (!sig) + return 0; + if (sig < SIGINT || sig > NSIG) + return EINVAL; + return pthread_cancel(t); +} + +unsigned +_pthread_get_state (const pthread_attr_t *attr, unsigned flag) +{ + return (attr->p_state & flag); +} + +int +_pthread_set_state (pthread_attr_t *attr, unsigned flag, unsigned val) +{ + if (~flag & val) + return EINVAL; + attr->p_state &= ~flag; + attr->p_state |= val; + + return 0; +} + +int +pthread_attr_init (pthread_attr_t *attr) +{ + memset (attr, 0, sizeof (pthread_attr_t)); + attr->p_state = PTHREAD_DEFAULT_ATTR; + attr->stack = NULL; + attr->s_size = 0; + return 0; +} + +int +pthread_attr_destroy (pthread_attr_t *attr) +{ + /* No need to do anything */ + memset (attr, 0, sizeof(pthread_attr_t)); + return 0; +} + +int +pthread_attr_setdetachstate (pthread_attr_t *a, int flag) +{ + return _pthread_set_state(a, PTHREAD_CREATE_DETACHED, flag); +} + +int +pthread_attr_getdetachstate (const pthread_attr_t *a, int *flag) +{ + *flag = _pthread_get_state(a, PTHREAD_CREATE_DETACHED); + return 0; +} + +int +pthread_attr_setinheritsched (pthread_attr_t *a, int flag) +{ + if (!a || (flag != PTHREAD_INHERIT_SCHED && flag != PTHREAD_EXPLICIT_SCHED)) + return EINVAL; + return _pthread_set_state(a, PTHREAD_INHERIT_SCHED, flag); +} + +int +pthread_attr_getinheritsched (const pthread_attr_t *a, int *flag) +{ + *flag = _pthread_get_state(a, PTHREAD_INHERIT_SCHED); + return 0; +} + +int +pthread_attr_setscope (pthread_attr_t *a, int flag) +{ + return _pthread_set_state(a, PTHREAD_SCOPE_SYSTEM, flag); +} + +int +pthread_attr_getscope (const pthread_attr_t *a, int *flag) +{ + *flag = _pthread_get_state(a, PTHREAD_SCOPE_SYSTEM); + return 0; +} + +int +pthread_attr_getstack (const pthread_attr_t *attr, void **stack, size_t *size) +{ + *stack = (char *) attr->stack - attr->s_size; + *size = attr->s_size; + return 0; +} + +int +pthread_attr_setstack (pthread_attr_t *attr, void *stack, size_t size) +{ + attr->s_size = size; + attr->stack = (char *) stack + size; + return 0; +} + +int +pthread_attr_getstackaddr (const pthread_attr_t *attr, void **stack) +{ + *stack = attr->stack; + return 0; +} + +int +pthread_attr_setstackaddr (pthread_attr_t *attr, void *stack) +{ + attr->stack = stack; + return 0; +} + +int +pthread_attr_getstacksize (const pthread_attr_t *attr, size_t *size) +{ + *size = attr->s_size; + return 0; +} + +int +pthread_attr_setstacksize (pthread_attr_t *attr, size_t size) +{ + attr->s_size = size; + return 0; +} + +static void +test_cancel_locked (pthread_t t) +{ + struct _pthread_v *tv = __pth_gpointer_locked (t); + + if (!tv || tv->in_cancel || tv->ended != 0 || (tv->p_state & PTHREAD_CANCEL_ENABLE) == 0) + return; + if ((tv->p_state & PTHREAD_CANCEL_ASYNCHRONOUS) == 0) + return; + if (WaitForSingleObject(tv->evStart, 0) != WAIT_OBJECT_0) + return; + pthread_mutex_unlock (&tv->p_clock); + _pthread_invoke_cancel(); +} + +int +pthread_setcancelstate (int state, int *oldstate) +{ + _pthread_v *t = __pthread_self_lite (); + + if (!t || (state & PTHREAD_CANCEL_ENABLE) != state) + return EINVAL; + + pthread_mutex_lock (&t->p_clock); + if (oldstate) + *oldstate = t->p_state & PTHREAD_CANCEL_ENABLE; + t->p_state &= ~PTHREAD_CANCEL_ENABLE; + t->p_state |= state; + test_cancel_locked (t->x); + pthread_mutex_unlock (&t->p_clock); + + return 0; +} + +int +pthread_setcanceltype (int type, int *oldtype) +{ + _pthread_v *t = __pthread_self_lite (); + + if (!t || (type & PTHREAD_CANCEL_ASYNCHRONOUS) != type) + return EINVAL; + + pthread_mutex_lock (&t->p_clock); + if (oldtype) + *oldtype = t->p_state & PTHREAD_CANCEL_ASYNCHRONOUS; + t->p_state &= ~PTHREAD_CANCEL_ASYNCHRONOUS; + t->p_state |= type; + test_cancel_locked (t->x); + pthread_mutex_unlock (&t->p_clock); + + return 0; +} + +void _fpreset (void); + +#if defined(__i386__) +/* Align ESP on 16-byte boundaries. */ +# if __GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 2) +__attribute__((force_align_arg_pointer)) +# endif +#endif +unsigned __stdcall +pthread_create_wrapper (void *args) +{ + unsigned rslt = 0; + struct _pthread_v *tv = (struct _pthread_v *)args; + + _fpreset(); + + pthread_mutex_lock (&mtx_pthr_locked); + pthread_mutex_lock (&tv->p_clock); + _pthread_once_raw(&_pthread_tls_once, pthread_tls_init); + TlsSetValue(_pthread_tls, tv); + tv->tid = GetCurrentThreadId(); + pthread_mutex_unlock (&tv->p_clock); + + + if (!setjmp(tv->jb)) + { + intptr_t trslt = (intptr_t) 128; + /* Provide to this thread a default exception handler. */ + #ifdef __SEH__ + asm ("\t.tl_start:\n"); + #endif /* Call function and save return value */ + pthread_mutex_unlock (&mtx_pthr_locked); + if (tv->func) + trslt = (intptr_t) tv->func(tv->ret_arg); + #ifdef __SEH__ + asm ("\tnop\n\t.tl_end: nop\n" +#ifdef __arm__ + "\t.seh_handler __C_specific_handler, %except\n" +#else + "\t.seh_handler __C_specific_handler, @except\n" +#endif + "\t.seh_handlerdata\n" + "\t.long 1\n" + "\t.rva .tl_start, .tl_end, _gnu_exception_handler ,.tl_end\n" + "\t.text" + ); + #endif + pthread_mutex_lock (&mtx_pthr_locked); + tv->ret_arg = (void*) trslt; + /* Clean up destructors */ + _pthread_cleanup_dest(tv->x); + } + else + pthread_mutex_lock (&mtx_pthr_locked); + + pthread_mutex_lock (&tv->p_clock); + rslt = (unsigned) (size_t) tv->ret_arg; + /* Make sure we free ourselves if we are detached */ + if (tv->evStart) + CloseHandle (tv->evStart); + tv->evStart = NULL; + if (!tv->h) + { + tv->valid = DEAD_THREAD; + pthread_mutex_unlock (&tv->p_clock); + pthread_mutex_destroy (&tv->p_clock); + push_pthread_mem (tv); + tv = NULL; + TlsSetValue (_pthread_tls, tv); + } + else + { + pthread_mutex_unlock (&tv->p_clock); + pthread_mutex_destroy (&tv->p_clock); + /* Reinitialise p_clock, since there may be attempts at + destroying it again in __dyn_tls_thread later on. */ + tv->p_clock = PTHREAD_MUTEX_INITIALIZER; + tv->ended = 1; + } + while (pthread_mutex_unlock (&mtx_pthr_locked) == 0) + Sleep (0); + _endthreadex (rslt); + return rslt; +} + +int +pthread_create (pthread_t *th, const pthread_attr_t *attr, void *(* func)(void *), void *arg) +{ + HANDLE thrd = NULL; + int redo = 0; + struct _pthread_v *tv; + unsigned int ssize = 0; + pthread_spinlock_t new_spin_keys = PTHREAD_SPINLOCK_INITIALIZER; + + if (attr && attr->s_size > UINT_MAX) + return EINVAL; + + if ((tv = pop_pthread_mem ()) == NULL) + return EAGAIN; + + if (th) + *th = tv->x; + + /* Save data in pthread_t */ + tv->ended = 0; + tv->ret_arg = arg; + tv->func = func; + tv->p_state = PTHREAD_DEFAULT_ATTR; + tv->h = INVALID_HANDLE_VALUE; + /* We retry it here a few times, as events are a limited resource ... */ + do + { + tv->evStart = CreateEvent (NULL, 1, 0, NULL); + if (tv->evStart != NULL) + break; + Sleep ((!redo ? 0 : 20)); + } + while (++redo <= 4); + + tv->p_clock = PTHREAD_MUTEX_INITIALIZER; + replace_spin_keys (&tv->spin_keys, new_spin_keys); + tv->valid = LIFE_THREAD; + tv->sched.sched_priority = THREAD_PRIORITY_NORMAL; + tv->sched_pol = SCHED_OTHER; + if (tv->evStart == NULL) + { + if (th) + memset (th, 0, sizeof (pthread_t)); + push_pthread_mem (tv); + return EAGAIN; + } + + if (attr) + { + int inh = 0; + tv->p_state = attr->p_state; + ssize = (unsigned int)attr->s_size; + pthread_attr_getinheritsched (attr, &inh); + if (inh) + { + tv->sched.sched_priority = __pthread_self_lite ()->sched.sched_priority; + } + else + tv->sched.sched_priority = attr->param.sched_priority; + } + + /* Make sure tv->h has value of INVALID_HANDLE_VALUE */ + _ReadWriteBarrier(); + + thrd = (HANDLE) _beginthreadex(NULL, ssize, pthread_create_wrapper, tv, 0x4/*CREATE_SUSPEND*/, NULL); + if (thrd == INVALID_HANDLE_VALUE) + thrd = 0; + /* Failed */ + if (!thrd) + { + if (tv->evStart) + CloseHandle (tv->evStart); + pthread_mutex_destroy (&tv->p_clock); + replace_spin_keys (&tv->spin_keys, new_spin_keys); + tv->evStart = NULL; + tv->h = 0; + if (th) + memset (th, 0, sizeof (pthread_t)); + push_pthread_mem (tv); + return EAGAIN; + } + { + int pr = tv->sched.sched_priority; + if (pr <= THREAD_PRIORITY_IDLE) { + pr = THREAD_PRIORITY_IDLE; + } else if (pr <= THREAD_PRIORITY_LOWEST) { + pr = THREAD_PRIORITY_LOWEST; + } else if (pr >= THREAD_PRIORITY_TIME_CRITICAL) { + pr = THREAD_PRIORITY_TIME_CRITICAL; + } else if (pr >= THREAD_PRIORITY_HIGHEST) { + pr = THREAD_PRIORITY_HIGHEST; + } + SetThreadPriority (thrd, pr); + } + ResetEvent (tv->evStart); + if ((tv->p_state & PTHREAD_CREATE_DETACHED) != 0) + { + tv->h = 0; + ResumeThread (thrd); + CloseHandle (thrd); + } + else + { + tv->h = thrd; + ResumeThread (thrd); + } + Sleep (0); + return 0; +} + +int +pthread_join (pthread_t t, void **res) +{ + DWORD dwFlags; + struct _pthread_v *tv = __pth_gpointer_locked (t); + pthread_spinlock_t new_spin_keys = PTHREAD_SPINLOCK_INITIALIZER; + + if (!tv || tv->h == NULL || !GetHandleInformation(tv->h, &dwFlags)) + return ESRCH; + if ((tv->p_state & PTHREAD_CREATE_DETACHED) != 0) + return EINVAL; + if (pthread_equal(pthread_self(), t)) + return EDEADLK; + + /* pthread_testcancel (); */ + if (tv->ended == 0 || (tv->h != NULL && tv->h != INVALID_HANDLE_VALUE)) + WaitForSingleObject (tv->h, INFINITE); + CloseHandle (tv->h); + if (tv->evStart) + CloseHandle (tv->evStart); + tv->evStart = NULL; + /* Obtain return value */ + if (res) + *res = tv->ret_arg; + pthread_mutex_destroy (&tv->p_clock); + replace_spin_keys (&tv->spin_keys, new_spin_keys); + push_pthread_mem (tv); + + return 0; +} + +int +_pthread_tryjoin (pthread_t t, void **res) +{ + DWORD dwFlags; + struct _pthread_v *tv; + pthread_spinlock_t new_spin_keys = PTHREAD_SPINLOCK_INITIALIZER; + + pthread_mutex_lock (&mtx_pthr_locked); + tv = __pthread_get_pointer (t); + + if (!tv || tv->h == NULL || !GetHandleInformation(tv->h, &dwFlags)) + { + pthread_mutex_unlock (&mtx_pthr_locked); + return ESRCH; + } + + if ((tv->p_state & PTHREAD_CREATE_DETACHED) != 0) + { + pthread_mutex_unlock (&mtx_pthr_locked); + return EINVAL; + } + if (pthread_equal(pthread_self(), t)) + { + pthread_mutex_unlock (&mtx_pthr_locked); + return EDEADLK; + } + if(tv->ended == 0 && WaitForSingleObject(tv->h, 0)) + { + if (tv->ended == 0) + { + pthread_mutex_unlock (&mtx_pthr_locked); + /* pthread_testcancel (); */ + return EBUSY; + } + } + CloseHandle (tv->h); + if (tv->evStart) + CloseHandle (tv->evStart); + tv->evStart = NULL; + + /* Obtain return value */ + if (res) + *res = tv->ret_arg; + pthread_mutex_destroy (&tv->p_clock); + replace_spin_keys (&tv->spin_keys, new_spin_keys); + + push_pthread_mem (tv); + + pthread_mutex_unlock (&mtx_pthr_locked); + /* pthread_testcancel (); */ + return 0; +} + +int +pthread_detach (pthread_t t) +{ + int r = 0; + DWORD dwFlags; + struct _pthread_v *tv = __pth_gpointer_locked (t); + HANDLE dw; + pthread_spinlock_t new_spin_keys = PTHREAD_SPINLOCK_INITIALIZER; + + pthread_mutex_lock (&mtx_pthr_locked); + if (!tv || tv->h == NULL || !GetHandleInformation(tv->h, &dwFlags)) + { + pthread_mutex_unlock (&mtx_pthr_locked); + return ESRCH; + } + if ((tv->p_state & PTHREAD_CREATE_DETACHED) != 0) + { + pthread_mutex_unlock (&mtx_pthr_locked); + return EINVAL; + } + /* if (tv->ended) r = ESRCH; */ + dw = tv->h; + tv->h = 0; + tv->p_state |= PTHREAD_CREATE_DETACHED; + _ReadWriteBarrier(); + if (dw) + { + CloseHandle (dw); + if (tv->ended) + { + if (tv->evStart) + CloseHandle (tv->evStart); + tv->evStart = NULL; + pthread_mutex_destroy (&tv->p_clock); + replace_spin_keys (&tv->spin_keys, new_spin_keys); + push_pthread_mem (tv); + } + } + pthread_mutex_unlock (&mtx_pthr_locked); + + return r; +} + +static int dummy_concurrency_level = 0; + +int +pthread_getconcurrency (void) +{ + return dummy_concurrency_level; +} + +int +pthread_setconcurrency (int new_level) +{ + dummy_concurrency_level = new_level; + return 0; +} + +int +pthread_setname_np (pthread_t thread, const char *name) +{ + struct _pthread_v *tv; + char *stored_name; + + if (name == NULL) + return EINVAL; + + tv = __pth_gpointer_locked (thread); + if (!tv || thread != tv->x || tv->in_cancel || tv->ended || tv->h == NULL + || tv->h == INVALID_HANDLE_VALUE) + return ESRCH; + + stored_name = strdup (name); + if (stored_name == NULL) + return ENOMEM; + + if (tv->thread_name != NULL) + free (tv->thread_name); + + tv->thread_name = stored_name; + SetThreadName (tv->tid, name); + return 0; +} + +int +pthread_getname_np (pthread_t thread, char *name, size_t len) +{ + HRESULT result; + struct _pthread_v *tv; + + if (name == NULL) + return EINVAL; + + tv = __pth_gpointer_locked (thread); + if (!tv || thread != tv->x || tv->in_cancel || tv->ended || tv->h == NULL + || tv->h == INVALID_HANDLE_VALUE) + return ESRCH; + + if (len < 1) + return ERANGE; + + if (tv->thread_name == NULL) + { + name[0] = '\0'; + return 0; + } + + if (strlen (tv->thread_name) >= len) + return ERANGE; + + result = StringCchCopyNA (name, len, tv->thread_name, len - 1); + if (SUCCEEDED (result)) + return 0; + + return ERANGE; +} diff --git a/libs/winpthreads/src/thread.h b/libs/winpthreads/src/thread.h new file mode 100644 index 0000000..5b88226 --- /dev/null +++ b/libs/winpthreads/src/thread.h @@ -0,0 +1,79 @@ +/* + Copyright (c) 2011-2016 mingw-w64 project + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#ifndef WIN_PTHREAD_H +#define WIN_PTHREAD_H + +#include +#include +#include "rwlock.h" + +#define LIFE_THREAD 0xBAB1F00D +#define DEAD_THREAD 0xDEADBEEF +#define EXCEPTION_SET_THREAD_NAME ((DWORD) 0x406D1388) + +typedef struct _pthread_v _pthread_v; +struct _pthread_v +{ + unsigned int valid; + void *ret_arg; + void *(* func)(void *); + _pthread_cleanup *clean; + int nobreak; + HANDLE h; + HANDLE evStart; + pthread_mutex_t p_clock; + int cancelled : 2; + int in_cancel : 2; + int thread_noposix : 2; + unsigned int p_state; + unsigned int keymax; + void **keyval; + unsigned char *keyval_set; + char *thread_name; + pthread_spinlock_t spin_keys; + DWORD tid; + int rwlc; + pthread_rwlock_t rwlq[RWLS_PER_THREAD]; + int sched_pol; + int ended; + struct sched_param sched; + jmp_buf jb; + struct _pthread_v *next; + pthread_t x; /* Internal posix handle. */ +}; + +typedef struct __pthread_idlist { + struct _pthread_v *ptr; + pthread_t id; +} __pthread_idlist; + +int _pthread_tryjoin(pthread_t t, void **res); +void _pthread_setnobreak(int); +#ifdef WINPTHREAD_DBG +void thread_print_set(int state); +void thread_print(volatile pthread_t t, char *txt); +#endif +int __pthread_shallcancel(void); +struct _pthread_v *WINPTHREAD_API __pth_gpointer_locked (pthread_t id); + +#endif diff --git a/libs/winpthreads/src/winpthread_internal.h b/libs/winpthreads/src/winpthread_internal.h new file mode 100644 index 0000000..eb6838c --- /dev/null +++ b/libs/winpthreads/src/winpthread_internal.h @@ -0,0 +1,27 @@ +/* + Copyright (c) 2011-2016 mingw-w64 project + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#ifndef WINPTHREAD_INTERNAL_H +#define WINPTHREAD_INTERNAL_H +WINPTHREAD_API struct _pthread_v * __pth_gpointer_locked (pthread_t id); +int pthread_delay_np_ms (DWORD to); +#endif /*WINPTHREAD_INTERNAL_H*/ diff --git a/libs/winpthreads/src/wpth_ver.h b/libs/winpthreads/src/wpth_ver.h new file mode 100644 index 0000000..1089a61 --- /dev/null +++ b/libs/winpthreads/src/wpth_ver.h @@ -0,0 +1,29 @@ +/* + Copyright (c) 2011-2016 mingw-w64 project + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#ifndef __WPTHREADS_VERSION__ +#define __WPTHREADS_VERSION__ + +#define WPTH_VERSION 1,0,0,0 +#define WPTH_VERSION_STRING "1, 0, 0, 0\0" + +#endif diff --git a/src/backend_dx12.zig b/src/backend_dx12.zig new file mode 100644 index 0000000..22672e7 --- /dev/null +++ b/src/backend_dx12.zig @@ -0,0 +1,59 @@ +pub const D3D12_CPU_DESCRIPTOR_HANDLE = extern struct { + ptr: c_ulonglong, +}; + +pub const D3D12_GPU_DESCRIPTOR_HANDLE = extern struct { + ptr: c_ulonglong, +}; + +pub fn init( + device: *const anyopaque, // ID3D12Device + num_frames_in_flight: u32, + rtv_format: c_uint, // DXGI_FORMAT + cbv_srv_heap: *const anyopaque, // ID3D12DescriptorHeap + font_srv_cpu_desc_handle: D3D12_CPU_DESCRIPTOR_HANDLE, + font_srv_gpu_desc_handle: D3D12_GPU_DESCRIPTOR_HANDLE, +) void { + if (!ImGui_ImplDX12_Init( + device, + num_frames_in_flight, + rtv_format, + cbv_srv_heap, + font_srv_cpu_desc_handle, + font_srv_gpu_desc_handle, + )) { + @panic("failed to init d3d12 for imgui"); + } +} + +pub fn deinit() void { + ImGui_ImplDX12_Shutdown(); +} + +pub fn newFrame() void { + ImGui_ImplDX12_NewFrame(); +} + +pub fn render( + draw_data: *const anyopaque, // *gui.DrawData + gfx_command_list: *const anyopaque, // *ID3D12GraphicsCommandList +) void { + ImGui_ImplDX12_RenderDrawData(draw_data, gfx_command_list); +} + +// Those functions are defined in 'imgui_impl_dx12.cpp` +// (they include few custom changes). +extern fn ImGui_ImplDX12_Init( + device: *const anyopaque, // ID3D12Device + num_frames_in_flight: u32, + rtv_format: u32, // DXGI_FORMAT + cbv_srv_heap: *const anyopaque, // ID3D12DescriptorHeap + font_srv_cpu_desc_handle: D3D12_CPU_DESCRIPTOR_HANDLE, + font_srv_gpu_desc_handle: D3D12_GPU_DESCRIPTOR_HANDLE, +) bool; +extern fn ImGui_ImplDX12_Shutdown() void; +extern fn ImGui_ImplDX12_NewFrame() void; +extern fn ImGui_ImplDX12_RenderDrawData( + draw_data: *const anyopaque, // *ImDrawData + graphics_command_list: *const anyopaque, // *ID3D12GraphicsCommandList +) void; diff --git a/src/backend_glfw.zig b/src/backend_glfw.zig new file mode 100644 index 0000000..83603fe --- /dev/null +++ b/src/backend_glfw.zig @@ -0,0 +1,35 @@ +const gui = @import("gui.zig"); + +// This call will install GLFW callbacks to handle GUI interactions. +// Those callbacks will chain-call user's previously installed callbacks, if any. +// This means that custom user's callbacks need to be installed *before* calling zgpu.gui.init(). +pub fn init( + window: *const anyopaque, // zglfw.Window +) void { + if (!ImGui_ImplGlfw_InitForOther(window, true)) { + unreachable; + } +} + +pub fn initOpenGL( + window: *const anyopaque, // zglfw.Window +) void { + if (!ImGui_ImplGlfw_InitForOpenGL(window, true)) { + unreachable; + } +} + +pub fn deinit() void { + ImGui_ImplGlfw_Shutdown(); +} + +pub fn newFrame() void { + ImGui_ImplGlfw_NewFrame(); +} + +// Those functions are defined in `imgui_impl_glfw.cpp` +// (they include few custom changes). +extern fn ImGui_ImplGlfw_InitForOther(window: *const anyopaque, install_callbacks: bool) bool; +extern fn ImGui_ImplGlfw_InitForOpenGL(window: *const anyopaque, install_callbacks: bool) bool; +extern fn ImGui_ImplGlfw_NewFrame() void; +extern fn ImGui_ImplGlfw_Shutdown() void; diff --git a/src/backend_glfw_dx12.zig b/src/backend_glfw_dx12.zig index ea48e94..48c63cb 100644 --- a/src/backend_glfw_dx12.zig +++ b/src/backend_glfw_dx12.zig @@ -1,4 +1,6 @@ const gui = @import("gui.zig"); +const backend_glfw = @import("backend_glfw.zig"); +const backend_dx12 = @import("backend_dx12.zig"); pub fn init( window: *const anyopaque, // zglfw.Window @@ -6,33 +8,28 @@ pub fn init( num_frames_in_flight: u32, rtv_format: c_uint, // DXGI_FORMAT cbv_srv_heap: *const anyopaque, // ID3D12DescriptorHeap - font_srv_cpu_desc_handle: D3D12_CPU_DESCRIPTOR_HANDLE, - font_srv_gpu_desc_handle: D3D12_GPU_DESCRIPTOR_HANDLE, + font_srv_cpu_desc_handle: backend_dx12.D3D12_CPU_DESCRIPTOR_HANDLE, + font_srv_gpu_desc_handle: backend_dx12.D3D12_GPU_DESCRIPTOR_HANDLE, ) void { - if (!ImGui_ImplGlfw_InitForOther(window, true)) { - @panic("failed to init glfw for imgui"); - } - - if (!ImGui_ImplDX12_Init( + backend_glfw.init(window); + backend_dx12.init( device, num_frames_in_flight, rtv_format, cbv_srv_heap, font_srv_cpu_desc_handle, font_srv_gpu_desc_handle, - )) { - @panic("failed to init d3d12 for imgui"); - } + ); } pub fn deinit() void { - ImGui_ImplGlfw_Shutdown(); - ImGui_ImplDX12_Shutdown(); + backend_dx12.deinit(); + backend_glfw.deinit(); } pub fn newFrame(fb_width: u32, fb_height: u32) void { - ImGui_ImplGlfw_NewFrame(); - ImGui_ImplDX12_NewFrame(); + backend_glfw.newFrame(); + backend_dx12.newFrame(); gui.io.setDisplaySize(@as(f32, @floatFromInt(fb_width)), @as(f32, @floatFromInt(fb_height))); gui.io.setDisplayFramebufferScale(1.0, 1.0); @@ -44,31 +41,5 @@ pub fn draw( graphics_command_list: *const anyopaque, // *ID3D12GraphicsCommandList ) void { gui.render(); - ImGui_ImplDX12_RenderDrawData(gui.getDrawData(), graphics_command_list); + backend_dx12.render(gui.getDrawData(), graphics_command_list); } - -pub const D3D12_CPU_DESCRIPTOR_HANDLE = extern struct { - ptr: c_ulonglong, -}; - -pub const D3D12_GPU_DESCRIPTOR_HANDLE = extern struct { - ptr: c_ulonglong, -}; - -extern fn ImGui_ImplGlfw_InitForOther(window: *const anyopaque, install_callbacks: bool) bool; -extern fn ImGui_ImplGlfw_NewFrame() void; -extern fn ImGui_ImplGlfw_Shutdown() void; -extern fn ImGui_ImplDX12_Init( - device: *const anyopaque, // ID3D12Device - num_frames_in_flight: u32, - rtv_format: u32, // DXGI_FORMAT - cbv_srv_heap: *const anyopaque, // ID3D12DescriptorHeap - font_srv_cpu_desc_handle: D3D12_CPU_DESCRIPTOR_HANDLE, - font_srv_gpu_desc_handle: D3D12_GPU_DESCRIPTOR_HANDLE, -) bool; -extern fn ImGui_ImplDX12_Shutdown() void; -extern fn ImGui_ImplDX12_NewFrame() void; -extern fn ImGui_ImplDX12_RenderDrawData( - draw_data: *const anyopaque, // *ImDrawData - graphics_command_list: *const anyopaque, // *ID3D12GraphicsCommandList -) void; diff --git a/src/backend_glfw_opengl.zig b/src/backend_glfw_opengl.zig index 68a2003..cb94dc3 100644 --- a/src/backend_glfw_opengl.zig +++ b/src/backend_glfw_opengl.zig @@ -1,12 +1,11 @@ const gui = @import("gui.zig"); +const backend_glfw = @import("backend_glfw.zig"); pub fn initWithGlSlVersion( window: *const anyopaque, // zglfw.Window glsl_version: ?[:0]const u8, // e.g. "#version 130" ) void { - if (!ImGui_ImplGlfw_InitForOpenGL(window, true)) { - unreachable; - } + backend_glfw.initOpenGL(window); ImGui_ImplOpenGL3_Init(@ptrCast(glsl_version)); } @@ -18,12 +17,12 @@ pub fn init( } pub fn deinit() void { - ImGui_ImplGlfw_Shutdown(); ImGui_ImplOpenGL3_Shutdown(); + backend_glfw.deinit(); } pub fn newFrame(fb_width: u32, fb_height: u32) void { - ImGui_ImplGlfw_NewFrame(); + backend_glfw.newFrame(); ImGui_ImplOpenGL3_NewFrame(); gui.io.setDisplaySize(@as(f32, @floatFromInt(fb_width)), @as(f32, @floatFromInt(fb_height))); @@ -37,9 +36,8 @@ pub fn draw() void { ImGui_ImplOpenGL3_RenderDrawData(gui.getDrawData()); } -extern fn ImGui_ImplGlfw_InitForOpenGL(window: *const anyopaque, install_callbacks: bool) bool; -extern fn ImGui_ImplGlfw_NewFrame() void; -extern fn ImGui_ImplGlfw_Shutdown() void; +// Those functions are defined in 'imgui_impl_opengl3.cpp` +// (they include few custom changes). extern fn ImGui_ImplOpenGL3_Init(glsl_version: [*c]const u8) void; extern fn ImGui_ImplOpenGL3_Shutdown() void; extern fn ImGui_ImplOpenGL3_NewFrame() void; diff --git a/src/backend_glfw_wgpu.zig b/src/backend_glfw_wgpu.zig index c247df2..e6fa517 100644 --- a/src/backend_glfw_wgpu.zig +++ b/src/backend_glfw_wgpu.zig @@ -1,33 +1,40 @@ const gui = @import("gui.zig"); +const backend_glfw = @import("backend_glfw.zig"); // This call will install GLFW callbacks to handle GUI interactions. // Those callbacks will chain-call user's previously installed callbacks, if any. // This means that custom user's callbacks need to be installed *before* calling zgpu.gui.init(). pub fn init( window: *const anyopaque, // zglfw.Window - wgpu_device: *const anyopaque, // WGPUDevice - wgpu_swap_chain_format: u32, // WGPUTextureFormat - wgpu_depth_format: u32, // WGPUTextureFormat + wgpu_device: *const anyopaque, // wgpu.Device + wgpu_swap_chain_format: u32, // wgpu.TextureFormat + wgpu_depth_format: u32, // wgpu.TextureFormat ) void { - if (!ImGui_ImplGlfw_InitForOther(window, true)) { - unreachable; - } + backend_glfw.init(window); - if (!ImGui_ImplWGPU_Init(wgpu_device, 1, wgpu_swap_chain_format, wgpu_depth_format)) { + var info = ImGui_ImplWGPU_InitInfo{ + .device = wgpu_device, + .num_frames_in_flight = 1, + .rt_format = wgpu_swap_chain_format, + .depth_format = wgpu_depth_format, + .pipeline_multisample_state = .{}, + }; + + if (!ImGui_ImplWGPU_Init(&info)) { unreachable; } } pub fn deinit() void { ImGui_ImplWGPU_Shutdown(); - ImGui_ImplGlfw_Shutdown(); + backend_glfw.deinit(); } pub fn newFrame(fb_width: u32, fb_height: u32) void { ImGui_ImplWGPU_NewFrame(); - ImGui_ImplGlfw_NewFrame(); + backend_glfw.newFrame(); - gui.io.setDisplaySize(@as(f32, @floatFromInt(fb_width)), @as(f32, @floatFromInt(fb_height))); + gui.io.setDisplaySize(@floatFromInt(fb_width), @floatFromInt(fb_height)); gui.io.setDisplayFramebufferScale(1.0, 1.0); gui.newFrame(); @@ -38,17 +45,23 @@ pub fn draw(wgpu_render_pass: *const anyopaque) void { ImGui_ImplWGPU_RenderDrawData(gui.getDrawData(), wgpu_render_pass); } -// Those functions are defined in `imgui_impl_glfw.cpp` and 'imgui_impl_wgpu.cpp` +pub const ImGui_ImplWGPU_InitInfo = extern struct { + device: *const anyopaque, + num_frames_in_flight: u32 = 1, + rt_format: u32, + depth_format: u32, + + pipeline_multisample_state: extern struct { + next_in_chain: ?*const anyopaque = null, + count: u32 = 1, + mask: u32 = @bitCast(@as(i32, -1)), + alpha_to_coverage_enabled: bool = false, + }, +}; + +// Those functions are defined in 'imgui_impl_wgpu.cpp` // (they include few custom changes). -extern fn ImGui_ImplGlfw_InitForOther(window: *const anyopaque, install_callbacks: bool) bool; -extern fn ImGui_ImplGlfw_NewFrame() void; -extern fn ImGui_ImplGlfw_Shutdown() void; -extern fn ImGui_ImplWGPU_Init( - device: *const anyopaque, // WGPUDevice - num_frames_in_flight: u32, - rt_format: u32, // WGPUTextureFormat - wgpu_depth_format: u32, // WGPUTextureFormat -) bool; +extern fn ImGui_ImplWGPU_Init(init_info: *ImGui_ImplWGPU_InitInfo) bool; extern fn ImGui_ImplWGPU_NewFrame() void; extern fn ImGui_ImplWGPU_RenderDrawData(draw_data: *const anyopaque, pass_encoder: *const anyopaque) void; extern fn ImGui_ImplWGPU_Shutdown() void; diff --git a/src/backend_win32_dx12.zig b/src/backend_win32_dx12.zig new file mode 100644 index 0000000..2cc5bf9 --- /dev/null +++ b/src/backend_win32_dx12.zig @@ -0,0 +1,48 @@ +const std = @import("std"); + +const gui = @import("gui.zig"); +const backend_dx12 = @import("backend_dx12.zig"); + +pub fn init( + hwnd: *const anyopaque, // HWND + d3d12_device: *const anyopaque, // ID3D12Device* + num_frames_in_flight: u16, + rtv_format: u32, // DXGI_FORMAT + cbv_srv_heap: *const anyopaque, // ID3D12DescriptorHeap* + font_srv_cpu_desc_handle: backend_dx12.D3D12_CPU_DESCRIPTOR_HANDLE, + font_srv_gpu_desc_handle: backend_dx12.D3D12_GPU_DESCRIPTOR_HANDLE, +) void { + std.debug.assert(ImGui_ImplWin32_Init(hwnd)); + backend_dx12.init( + d3d12_device, + num_frames_in_flight, + rtv_format, + cbv_srv_heap, + font_srv_cpu_desc_handle, + font_srv_gpu_desc_handle, + ); +} + +pub fn deinit() void { + backend_dx12.deinit(); + ImGui_ImplWin32_Shutdown(); +} + +pub fn newFrame(fb_width: u32, fb_height: u32) void { + ImGui_ImplWin32_NewFrame(); + backend_dx12.newFrame(); + + gui.io.setDisplaySize(@as(f32, @floatFromInt(fb_width)), @as(f32, @floatFromInt(fb_height))); + gui.io.setDisplayFramebufferScale(1.0, 1.0); + + gui.newFrame(); +} + +pub fn draw(graphics_command_list: *const anyopaque) void { + gui.render(); + backend_dx12.render(gui.getDrawData(), graphics_command_list); +} + +extern fn ImGui_ImplWin32_Init(hwnd: *const anyopaque) bool; +extern fn ImGui_ImplWin32_Shutdown() void; +extern fn ImGui_ImplWin32_NewFrame() void; diff --git a/src/gui.zig b/src/gui.zig index c50b57f..4bb4eac 100644 --- a/src/gui.zig +++ b/src/gui.zig @@ -5,13 +5,16 @@ // //-------------------------------------------------------------------------------------------------- pub const plot = @import("plot.zig"); +pub const te = @import("te.zig"); pub const backend = switch (@import("zgui_options").backend) { .glfw_wgpu => @import("backend_glfw_wgpu.zig"), .glfw_opengl3 => @import("backend_glfw_opengl.zig"), .glfw_dx12 => @import("backend_glfw_dx12.zig"), - .win32_dx12 => .{}, // TODO: + .glfw => @import("backend_glfw.zig"), + .win32_dx12 => @import("backend_win32_dx12.zig"), .no_backend => .{}, }; +const te_enabled = @import("zgui_options").with_te; //-------------------------------------------------------------------------------------------------- const std = @import("std"); const assert = std.debug.assert; @@ -26,6 +29,7 @@ pub const DrawVert = extern struct { color: u32, }; //-------------------------------------------------------------------------------------------------- + pub fn init(allocator: std.mem.Allocator) void { if (zguiGetCurrentContext() == null) { mem_allocator = allocator; @@ -37,6 +41,10 @@ pub fn init(allocator: std.mem.Allocator) void { temp_buffer = std.ArrayList(u8).init(allocator); temp_buffer.?.resize(3 * 1024 + 1) catch unreachable; + + if (te_enabled) { + te.init(); + } } } pub fn deinit() void { @@ -44,6 +52,12 @@ pub fn deinit() void { temp_buffer.?.deinit(); zguiDestroyContext(null); + // Must be after destroy imgui context. + // And before allocation check + if (te_enabled) { + te.deinit(); + } + if (mem_allocations.?.count() > 0) { var it = mem_allocations.?.iterator(); while (it.next()) |kv| { @@ -124,7 +138,13 @@ pub const ConfigFlags = packed struct(c_int) { nav_no_capture_keyboard: bool = false, no_mouse: bool = false, no_mouse_cursor_change: bool = false, - user_storage: u14 = 0, + dock_enable: bool = false, + _pading0: u3 = 0, + viewport_enable: bool = false, + _pading1: u3 = 0, + dpi_enable_scale_viewport: bool = false, + dpi_enable_scale_fonts: bool = false, + user_storage: u4 = 0, is_srgb: bool = false, is_touch_screen: bool = false, _padding: u10 = 0, @@ -147,6 +167,7 @@ pub const FontConfig = extern struct { merge_mode: bool, font_builder_flags: c_uint, rasterizer_multiply: f32, + rasterizer_density: f32, ellipsis_char: Wchar, name: [40]u8, dst_font: *Font, @@ -326,7 +347,7 @@ pub const DrawData = *extern struct { cmd_lists_count: c_int, total_idx_count: c_int, total_vtx_count: c_int, - cmd_lists: [*]DrawList, + cmd_lists: Vector(DrawList), display_pos: [2]f32, display_size: [2]f32, framebuffer_scale: [2]f32, @@ -334,7 +355,7 @@ pub const DrawData = *extern struct { pub const Font = *opaque {}; pub const Ident = u32; pub const TextureIdent = *anyopaque; -pub const Wchar = u16; +pub const Wchar = if (@import("zgui_options").use_wchar32) u32 else u16; pub const Key = enum(c_int) { none = 0, tab = 512, @@ -409,6 +430,18 @@ pub const Key = enum(c_int) { f10, f11, f12, + f13, + f14, + f15, + f16, + f17, + f18, + f19, + f20, + f21, + f22, + f23, + f24, apostrophe, comma, minus, @@ -443,6 +476,9 @@ pub const Key = enum(c_int) { keypad_enter, keypad_equal, + app_back, + app_forward, + gamepad_start, gamepad_back, gamepad_faceleft, @@ -483,6 +519,7 @@ pub const Key = enum(c_int) { mod_super = 1 << 15, mod_mask_ = 0xf000, }; + //-------------------------------------------------------------------------------------------------- pub const WindowFlags = packed struct(c_int) { no_title_bar: bool = false, @@ -501,12 +538,11 @@ pub const WindowFlags = packed struct(c_int) { no_bring_to_front_on_focus: bool = false, always_vertical_scrollbar: bool = false, always_horizontal_scrollbar: bool = false, - always_use_window_padding: bool = false, - _removed: u1 = 0, no_nav_inputs: bool = false, no_nav_focus: bool = false, unsaved_document: bool = false, - _padding: u11 = 0, + no_docking: bool = false, + _padding: u12 = 0, pub const no_nav = WindowFlags{ .no_nav_inputs = true, .no_nav_focus = true }; pub const no_decoration = WindowFlags{ @@ -521,6 +557,20 @@ pub const WindowFlags = packed struct(c_int) { .no_nav_focus = true, }; }; + +pub const ChildFlags = packed struct(c_int) { + border: bool = false, + no_move: bool = false, + always_use_window_padding: bool = false, + resize_x: bool = false, + resize_y: bool = false, + auto_resize_x: bool = false, + auto_resize_y: bool = false, + always_auto_resize: bool = false, + frame_style: bool = false, + _padding: u23 = 0, +}; + //-------------------------------------------------------------------------------------------------- pub const SliderFlags = packed struct(c_int) { _reserved0: bool = false, @@ -629,7 +679,12 @@ pub fn setNextWindowBgAlpha(args: SetNextWindowBgAlpha) void { zguiSetNextWindowBgAlpha(args.alpha); } extern fn zguiSetNextWindowBgAlpha(alpha: f32) void; - +//-------------------------------------------------------------------------------------------------- +pub fn setWindowFocus(name: ?[:0]const u8) void { + zguiSetWindowFocus(name orelse null); +} +extern fn zguiSetWindowFocus(name: ?[*:0]const u8) void; +//------------------------------------------------------------------------------------------------- pub fn setKeyboardFocusHere(offset: i32) void { zguiSetKeyboardFocusHere(offset); } @@ -651,19 +706,19 @@ extern fn zguiEnd() void; const BeginChild = struct { w: f32 = 0.0, h: f32 = 0.0, - border: bool = false, - flags: WindowFlags = .{}, + child_flags: ChildFlags = .{}, + window_flags: WindowFlags = .{}, }; pub fn beginChild(str_id: [:0]const u8, args: BeginChild) bool { - return zguiBeginChild(str_id, args.w, args.h, args.border, args.flags); + return zguiBeginChild(str_id, args.w, args.h, args.child_flags, args.window_flags); } pub fn beginChildId(id: Ident, args: BeginChild) bool { - return zguiBeginChildId(id, args.w, args.h, args.border, args.flags); + return zguiBeginChildId(id, args.w, args.h, args.child_flags, args.window_flags); } /// `pub fn endChild() void` pub const endChild = zguiEndChild; -extern fn zguiBeginChild(str_id: [*:0]const u8, w: f32, h: f32, border: bool, flags: WindowFlags) bool; -extern fn zguiBeginChildId(id: Ident, w: f32, h: f32, border: bool, flags: WindowFlags) bool; +extern fn zguiBeginChild(str_id: [*:0]const u8, w: f32, h: f32, flags: ChildFlags, window_flags: WindowFlags) bool; +extern fn zguiBeginChildId(id: Ident, w: f32, h: f32, flags: ChildFlags, window_flags: WindowFlags) bool; extern fn zguiEndChild() void; //-------------------------------------------------------------------------------------------------- /// `pub fn zguiGetScrollX() f32` @@ -720,7 +775,8 @@ pub const FocusedFlags = packed struct(c_int) { root_window: bool = false, any_window: bool = false, no_popup_hierarchy: bool = false, - _padding: u28 = 0, + dock_hierarchy: bool = false, + _padding: u27 = 0, pub const root_and_child_windows = FocusedFlags{ .root_window = true, .child_windows = true }; }; @@ -730,22 +786,27 @@ pub const HoveredFlags = packed struct(c_int) { root_window: bool = false, any_window: bool = false, no_popup_hierarchy: bool = false, - _reserved0: bool = false, + dock_hierarchy: bool = false, allow_when_blocked_by_popup: bool = false, _reserved1: bool = false, allow_when_blocked_by_active_item: bool = false, - allow_when_overlapped: bool = false, + allow_when_overlapped_by_item: bool = false, + allow_when_overlapped_by_window: bool = false, allow_when_disabled: bool = false, no_nav_override: bool = false, + for_tooltip: bool = false, + stationary: bool = false, + delay_none: bool = false, delay_normal: bool = false, delay_short: bool = false, no_shared_delay: bool = false, - _padding: u18 = 0, + _padding: u14 = 0, pub const rect_only = HoveredFlags{ .allow_when_blocked_by_popup = true, .allow_when_blocked_by_active_item = true, - .allow_when_overlapped = true, + .allow_when_overlapped_by_item = true, + .allow_when_overlapped_by_window = true, }; pub const root_and_child_windows = HoveredFlags{ .root_window = true, .child_windows = true }; }; @@ -812,6 +873,79 @@ extern fn zguiGetContentRegionAvail(size: *[2]f32) void; extern fn zguiGetContentRegionMax(size: *[2]f32) void; extern fn zguiGetWindowContentRegionMin(size: *[2]f32) void; extern fn zguiGetWindowContentRegionMax(size: *[2]f32) void; +//-------------------------------------------------------------------------------------------------- +// +// Docking +// +//-------------------------------------------------------------------------------------------------- +pub const DockNodeFlags = packed struct(c_int) { + keep_alive_only: bool = false, + _reserved: u1 = 0, + no_docking_over_central_node: bool = false, + passthru_central_node: bool = false, + no_docking_split: bool = false, + no_resize: bool = false, + auto_hide_tab_bar: bool = false, + no_undocking: bool = false, + _padding_0: u2 = 0, + + // Extended enum entries from imgui_internal (unstable, subject to change, use at own risk) + dock_space: bool = false, + central_node: bool = false, + no_tab_bar: bool = false, + hidden_tab_bar: bool = false, + no_window_menu_button: bool = false, + no_close_button: bool = false, + no_resize_x: bool = false, + no_resize_y: bool = false, + docked_windows_in_focus_route: bool = false, + no_docking_split_other: bool = false, + no_docking_over_me: bool = false, + no_docking_over_other: bool = false, + no_docking_over_empty: bool = false, + _padding_1: u9 = 0, +}; +extern fn zguiDockSpace(str_id: [*:0]const u8, size: *const [2]f32, flags: DockNodeFlags) Ident; + +pub fn DockSpace(str_id: [:0]const u8, size: [2]f32, flags: DockNodeFlags) Ident { + return zguiDockSpace(str_id.ptr, &size, flags); +} +extern fn zguiDockSpaceOverViewport(viewport: Viewport, flags: DockNodeFlags) Ident; +pub const DockSpaceOverViewport = zguiDockSpaceOverViewport; + +//-------------------------------------------------------------------------------------------------- +// +// DockBuilder (Unstable internal imgui API, subject to change, use at own risk) +// +//-------------------------------------------------------------------------------------------------- +pub fn dockBuilderDockWindow(window_name: [:0]const u8, node_id: Ident) void { + zguiDockBuilderDockWindow(window_name.ptr, node_id); +} +pub const dockBuilderAddNode = zguiDockBuilderAddNode; +pub const dockBuilderRemoveNode = zguiDockBuilderRemoveNode; +pub fn dockBuilderSetNodePos(node_id: Ident, pos: [2]f32) void { + zguiDockBuilderSetNodePos(node_id, &pos); +} +pub fn dockBuilderSetNodeSize(node_id: Ident, size: [2]f32) void { + zguiDockBuilderSetNodeSize(node_id, &size); +} +pub const dockBuilderSplitNode = zguiDockBuilderSplitNode; +pub const dockBuilderFinish = zguiDockBuilderFinish; + +extern fn zguiDockBuilderDockWindow(window_name: [*:0]const u8, node_id: Ident) void; +extern fn zguiDockBuilderAddNode(node_id: Ident, flags: DockNodeFlags) Ident; +extern fn zguiDockBuilderRemoveNode(node_id: Ident) void; +extern fn zguiDockBuilderSetNodePos(node_id: Ident, pos: *const [2]f32) void; +extern fn zguiDockBuilderSetNodeSize(node_id: Ident, size: *const [2]f32) void; +extern fn zguiDockBuilderSplitNode( + node_id: Ident, + split_dir: Direction, + size_ratio_for_node_at_dir: f32, + out_id_at_dir: ?*Ident, + out_id_at_opposite_dir: ?*Ident, +) Ident; +extern fn zguiDockBuilderFinish(node_id: Ident) void; + //-------------------------------------------------------------------------------------------------- // // Style @@ -847,6 +981,8 @@ pub const Style = extern struct { tab_rounding: f32, tab_border_size: f32, tab_min_width_for_close_button: f32, + tab_bar_border_size: f32, + table_angled_header_angle: f32, color_button_position: Direction, button_text_align: [2]f32, selectable_text_align: [2]f32, @@ -855,6 +991,7 @@ pub const Style = extern struct { separator_text_padding: [2]f32, display_window_padding: [2]f32, display_safe_area_padding: [2]f32, + docking_separator_size: f32, mouse_cursor_scale: f32, anti_aliased_lines: bool, anti_aliased_lines_use_tex: bool, @@ -864,6 +1001,13 @@ pub const Style = extern struct { colors: [@typeInfo(StyleCol).Enum.fields.len][4]f32, + hover_stationary_delay: f32, + hover_delay_short: f32, + hover_delay_normal: f32, + + hover_flags_for_tooltip_mouse: HoveredFlags, + hover_flags_for_tooltip_nav: HoveredFlags, + /// `pub fn init() Style` pub const init = zguiStyle_Init; extern fn zguiStyle_Init() Style; @@ -922,6 +1066,8 @@ pub const StyleCol = enum(c_int) { tab_active, tab_unfocused, tab_unfocused_active, + docking_preview, + docking_empty_bg, plot_lines, plot_lines_hovered, plot_histogram, @@ -996,11 +1142,13 @@ pub const StyleVar = enum(c_int) { grab_min_size, // 1f grab_rounding, // 1f tab_rounding, // 1f + tab_bar_border_size, // 1f button_text_align, // 2f selectable_text_align, // 2f separator_text_border_size, // 1f separator_text_align, // 2f separator_text_padding, // 2f + docking_separator_size, // 1f }; pub fn pushStyleVar1f(args: struct { @@ -1227,8 +1375,14 @@ pub fn getItemRectMin() [2]f32 { zguiGetItemRectMin(&rect); return rect; } +pub fn getItemRectSize() [2]f32 { + var rect: [2]f32 = undefined; + zguiGetItemRectSize(&rect); + return rect; +} extern fn zguiGetItemRectMax(rect: *[2]f32) void; extern fn zguiGetItemRectMin(rect: *[2]f32) void; +extern fn zguiGetItemRectSize(rect: *[2]f32) void; //-------------------------------------------------------------------------------------------------- // // ID stack/scopes @@ -1502,31 +1656,35 @@ pub fn comboFromEnum( /// i32 (the underlying imgui restriction) current_item: anytype, ) bool { - const item_names = comptime lbl: { - const item_type = @typeInfo(@TypeOf(current_item.*)); - switch (item_type) { - .Enum => |e| { - var str: [:0]const u8 = ""; - - for (e.fields) |f| { - str = str ++ f.name ++ "\x00"; - } - break :lbl str; - }, - else => { - @compileError("Error: current_item must be a pointer-to-an-enum, not a " ++ @TypeOf(current_item)); - }, - } + const EnumType = @TypeOf(current_item.*); + const enum_type_info = switch (@typeInfo(EnumType)) { + .Enum => |enum_type_info| enum_type_info, + else => @compileError("Error: current_item must be a pointer-to-an-enum, not a " ++ @TypeOf(current_item)), }; - var item: i32 = @intCast(@intFromEnum(current_item.*)); + const FieldNameIndex = std.meta.Tuple(&.{ []const u8, i32 }); + comptime var item_names: [:0]const u8 = ""; + comptime var field_name_to_index_list: [enum_type_info.fields.len]FieldNameIndex = undefined; + comptime var index_to_enum: [enum_type_info.fields.len]EnumType = undefined; + + comptime { + for (enum_type_info.fields, 0..) |f, i| { + item_names = item_names ++ f.name ++ "\x00"; + const e: EnumType = @enumFromInt(f.value); + field_name_to_index_list[i] = .{ f.name, @intCast(i) }; + index_to_enum[i] = e; + } + } + + const field_name_to_index = std.StaticStringMap(i32).initComptime(&field_name_to_index_list); + var item: i32 = field_name_to_index.get(@tagName(current_item.*)).?; const result = combo(label, .{ .items_separated_by_zeros = item_names, .current_item = &item, }); - current_item.* = @enumFromInt(item); + current_item.* = index_to_enum[@intCast(item)]; return result; } @@ -1545,7 +1703,8 @@ pub const ComboFlags = packed struct(c_int) { height_largest: bool = false, no_arrow_button: bool = false, no_preview: bool = false, - _padding: u25 = 0, + width_fit_preview: bool = false, + _padding: u24 = 0, }; //-------------------------------------------------------------------------------------------------- const BeginCombo = struct { @@ -2235,7 +2394,7 @@ pub const InputTextCallbackData = extern struct { pub const InputTextCallback = *const fn (data: *InputTextCallbackData) i32; //-------------------------------------------------------------------------------------------------- pub fn inputText(label: [:0]const u8, args: struct { - buf: []u8, + buf: [:0]u8, flags: InputTextFlags = .{}, callback: ?InputTextCallback = null, user_data: ?*anyopaque = null, @@ -2243,7 +2402,7 @@ pub fn inputText(label: [:0]const u8, args: struct { return zguiInputText( label, args.buf.ptr, - args.buf.len, + args.buf.len + 1, // + 1 for sentinel args.flags, if (args.callback) |cb| cb else null, args.user_data, @@ -2259,7 +2418,7 @@ extern fn zguiInputText( ) bool; //-------------------------------------------------------------------------------------------------- pub fn inputTextMultiline(label: [:0]const u8, args: struct { - buf: []u8, + buf: [:0]u8, w: f32 = 0.0, h: f32 = 0.0, flags: InputTextFlags = .{}, @@ -2269,7 +2428,7 @@ pub fn inputTextMultiline(label: [:0]const u8, args: struct { return zguiInputTextMultiline( label, args.buf.ptr, - args.buf.len, + args.buf.len + 1, // + 1 for sentinel args.w, args.h, args.flags, @@ -2290,7 +2449,7 @@ extern fn zguiInputTextMultiline( //-------------------------------------------------------------------------------------------------- pub fn inputTextWithHint(label: [:0]const u8, args: struct { hint: [:0]const u8, - buf: []u8, + buf: [:0]u8, flags: InputTextFlags = .{}, callback: ?InputTextCallback = null, user_data: ?*anyopaque = null, @@ -2299,7 +2458,7 @@ pub fn inputTextWithHint(label: [:0]const u8, args: struct { label, args.hint, args.buf.ptr, - args.buf.len, + args.buf.len + 1, // + 1 for sentinel args.flags, if (args.callback) |cb| cb else null, args.user_data, @@ -2620,7 +2779,7 @@ extern fn zguiColorButton( pub const TreeNodeFlags = packed struct(c_int) { selected: bool = false, framed: bool = false, - allow_item_overlap: bool = false, + allow_overlap: bool = false, no_tree_push_on_open: bool = false, no_auto_open_on_log: bool = false, default_open: bool = false, @@ -2631,8 +2790,9 @@ pub const TreeNodeFlags = packed struct(c_int) { frame_padding: bool = false, span_avail_width: bool = false, span_full_width: bool = false, + span_all_columns: bool = false, nav_left_jumps_back_here: bool = false, - _padding: u18 = 0, + _padding: u17 = 0, pub const collapsing_header = TreeNodeFlags{ .framed = true, @@ -2732,7 +2892,7 @@ pub const SelectableFlags = packed struct(c_int) { span_all_columns: bool = false, allow_double_click: bool = false, disabled: bool = false, - allow_item_overlap: bool = false, + allow_overlap: bool = false, _padding: u27 = 0, }; //-------------------------------------------------------------------------------------------------- @@ -3245,11 +3405,13 @@ pub const PopupFlags = packed struct(c_int) { _reserved0: bool = false, _reserved1: bool = false, + no_reopen: bool = false, + _reserved2: bool = false, no_open_over_existing_popup: bool = false, no_open_over_items: bool = false, any_popup_id: bool = false, any_popup_level: bool = false, - _padding: u23 = 0, + _padding: u21 = 0, pub const any_popup = PopupFlags{ .any_popup_id = true, .any_popup_level = true }; }; @@ -3297,7 +3459,8 @@ pub const TabItemFlags = packed struct(c_int) { no_reorder: bool = false, leading: bool = false, trailing: bool = false, - _padding: u24 = 0, + no_assumed_closure: bool = false, + _padding: u23 = 0, }; pub fn beginTabBar(label: [:0]const u8, flags: TabBarFlags) bool { return zguiBeginTabBar(label, flags); @@ -4380,6 +4543,14 @@ pub const DrawList = *opaque { extern fn zguiDrawList_AddResetRenderStateCallback(draw_list: DrawList) void; }; +fn Vector(comptime T: type) type { + return extern struct { + len: c_int, + capacity: c_int, + items: [*]T, + }; +} + test { const testing = std.testing; diff --git a/src/te.zig b/src/te.zig new file mode 100644 index 0000000..efd1eab --- /dev/null +++ b/src/te.zig @@ -0,0 +1,255 @@ +const std = @import("std"); +const zgui = @import("gui.zig"); +const te_enabled = @import("zgui_options").with_te; + +pub const Actions = enum(c_int) { + unknown = 0, + /// Move mouse + hover, + /// Move mouse and click + click, + /// Move mouse and double-click + double_click, + /// Check item if unchecked (Checkbox, MenuItem or any widget reporting ImGuiItemStatusFlags_Checkable) + check, + /// Uncheck item if checked + uncheck, + /// Open item if closed (TreeNode, BeginMenu or any widget reporting ImGuiItemStatusFlags_Openable) + open, + /// Close item if opened + close, + /// Start text inputing into a field (e.g. CTRL+Click on Drags/Slider, click on InputText etc.) + input, + /// Activate item with navigation + nav_activate, +}; + +pub const TestRunFlags = packed struct(c_int) { + /// Used internally to temporarily disable the GUI func (at the end of a test, etc) + gui_func_disable: bool = false, + /// Set when user selects "Run GUI func" + gui_func_only: bool = false, + no_success_mgs: bool = false, + no_stop_on_error: bool = false, + no_break_on_error: bool = false, + /// Disable input submission to let test submission raw input event (in order to test e.g. IO queue) + enable_raw_inputs: bool = false, + manual_run: bool = false, + command_line: bool = false, + _padding: u24 = 0, +}; + +pub const TestOpFlags = packed struct(c_int) { + // Don't check for HoveredId after aiming for a widget. A few situations may want this: while e.g. dragging or another items prevents hovering, or for items that don't use ItemHoverable() + no_check_hovered_id: bool = false, + /// Don't abort/error e.g. if the item cannot be found or the operation doesn't succeed. + no_error: bool = false, + /// Don't focus window when aiming at an item + no_focus_window: bool = false, + /// Disable automatically uncollapsing windows (useful when specifically testing Collapsing behaviors) + no_auto_uncollapse: bool = false, + /// Disable automatically opening intermediaries (e.g. ItemClick("Hello/OK") will automatically first open "Hello" if "OK" isn't found. Only works if ref is a string path. + no_auto_open_full_path: bool = false, + /// Used by recursing functions to indicate a second attempt + is_second_attempt: bool = false, + move_to_edge_l: bool = false, // Simple Dumb aiming helpers to test widget that care about clicking position. May need to replace will better functionalities. + move_to_edge_r: bool = false, + move_to_edge_u: bool = false, + move_to_edge_d: bool = false, + _padding: u22 = 0, +}; + +pub const CheckFlags = packed struct(c_int) { + silent_success: bool = false, + _padding: u31 = 0, +}; + +pub const RunSpeed = enum(c_int) { + /// Run tests as fast as possible (teleport mouse, skip delays, etc.) + fast = 0, + /// Run tests at human watchable speed (for debugging) + normal = 1, + /// Run tests with pauses between actions (for e.g. tutorials) + cinematic = 2, +}; + +pub const TestGroup = enum(c_int) { + unknown = -1, + tests = 0, + perfs = 1, +}; + +pub const Test = anyopaque; + +pub const TestEngine = opaque { + pub fn registerTest( + engine: *TestEngine, + category: [:0]const u8, + name: [:0]const u8, + src: std.builtin.SourceLocation, + comptime Callbacks: type, + ) *Test { + return zguiTe_RegisterTest( + engine, + category.ptr, + name.ptr, + src.file.ptr, + @intCast(src.line), + if (std.meta.hasFn(Callbacks, "gui")) + struct { + fn f(context: *TestContext) callconv(.C) void { + Callbacks.gui(context) catch undefined; + } + }.f + else + null, + if (std.meta.hasFn(Callbacks, "run")) + struct { + fn f(context: *TestContext) callconv(.C) void { + Callbacks.run(context) catch undefined; + } + }.f + else + null, + ); + } + + pub const showTestEngineWindows = zguiTe_ShowTestEngineWindows; + extern fn zguiTe_ShowTestEngineWindows(engine: *TestEngine, p_open: ?*bool) void; + + pub const setRunSpeed = zguiTe_EngineSetRunSpeed; + extern fn zguiTe_EngineSetRunSpeed(engine: *TestEngine, speed: RunSpeed) void; + + pub const stop = zguiTe_Stop; + extern fn zguiTe_Stop(engine: *TestEngine) void; + + pub const tryAbortEngine = zguiTe_TryAbortEngine; + extern fn zguiTe_TryAbortEngine(engine: *TestEngine) void; + + pub const postSwap = zguiTe_PostSwap; + extern fn zguiTe_PostSwap(engine: *TestEngine) void; + + pub const isTestQueueEmpty = zguiTe_IsTestQueueEmpty; + extern fn zguiTe_IsTestQueueEmpty(engine: *TestEngine) bool; + + pub const getResult = zguiTe_GetResult; + extern fn zguiTe_GetResult(engine: *TestEngine, count_tested: *c_int, count_success: *c_int) void; + + pub const printResultSummary = zguiTe_PrintResultSummary; + extern fn zguiTe_PrintResultSummary(engine: *TestEngine) void; + + pub fn queueTests(engine: *TestEngine, group: TestGroup, filter_str: [:0]const u8, run_flags: TestRunFlags) void { + zguiTe_QueueTests(engine, group, filter_str.ptr, run_flags); + } + extern fn zguiTe_QueueTests(engine: *TestEngine, group: TestGroup, filter_str: [*]const u8, run_flags: TestRunFlags) void; + + pub fn exportJunitResult(engine: *TestEngine, filename: [:0]const u8) void { + zguiTe_EngineExportJunitResult(engine, filename.ptr); + } + extern fn zguiTe_EngineExportJunitResult(engine: *TestEngine, filename: [*]const u8) void; +}; + +pub const TestContext = opaque { + pub fn setRef(ctx: *TestContext, ref: [:0]const u8) void { + return zguiTe_ContextSetRef(ctx, ref.ptr); + } + + pub fn windowFocus(ctx: *TestContext, ref: [:0]const u8) void { + return zguiTe_ContextWindowFocus(ctx, ref.ptr); + } + + pub fn yield(ctx: *TestContext, frame_count: i32) void { + return zguiTe_ContextYield(ctx, frame_count); + } + + pub fn itemAction(ctx: *TestContext, action: Actions, ref: [:0]const u8, flags: TestOpFlags, action_arg: ?*anyopaque) void { + return zguiTe_ContextItemAction(ctx, action, ref.ptr, flags, action_arg); + } + + pub fn itemInputStrValue(ctx: *TestContext, ref: [:0]const u8, value: [:0]const u8) void { + return zguiTe_ContextItemInputStrValue(ctx, ref.ptr, value.ptr); + } + + pub fn itemInputIntValue(ctx: *TestContext, ref: [:0]const u8, value: i32) void { + return zguiTe_ContextItemInputIntValue(ctx, ref.ptr, value); + } + + pub fn itemInputFloatValue(ctx: *TestContext, ref: [:0]const u8, value: f32) void { + return zguiTe_ContextItemInputFloatValue(ctx, ref.ptr, value); + } + + pub fn menuAction(ctx: *TestContext, action: Actions, ref: [*]const u8) void { + return zguiTe_ContextMenuAction(ctx, action, ref); + } + + pub fn dragAndDrop(ctx: *TestContext, ref_src: [:0]const u8, ref_dst: [:0]const u8, button: zgui.MouseButton) void { + return zguiTe_ContextDragAndDrop(ctx, ref_src.ptr, ref_dst.ptr, button); + } + + pub fn keyDown(ctx: *TestContext, key_chord: c_int) void { + return zguiTe_ContextKeyDown(ctx, key_chord); + } + + pub fn keyUp(ctx: *TestContext, key_chord: c_int) void { + return zguiTe_ContextKeyUp(ctx, key_chord); + } + + extern fn zguiTe_ContextSetRef(ctx: *TestContext, ref: [*]const u8) void; + extern fn zguiTe_ContextWindowFocus(ctx: *TestContext, ref: [*]const u8) void; + extern fn zguiTe_ContextYield(ctx: *TestContext, frame_count: c_int) void; + extern fn zguiTe_ContextMenuAction(ctx: *TestContext, action: Actions, ref: [*]const u8) void; + extern fn zguiTe_ContextItemAction(ctx: *TestContext, action: Actions, ref: [*]const u8, flags: TestOpFlags, action_arg: ?*anyopaque) void; + extern fn zguiTe_ContextItemInputStrValue(ctx: *TestContext, ref: [*]const u8, value: [*]const u8) void; + extern fn zguiTe_ContextItemInputIntValue(ctx: *TestContext, ref: [*]const u8, value: i32) void; + extern fn zguiTe_ContextItemInputFloatValue(ctx: *TestContext, ref: [*]const u8, value: f32) void; + extern fn zguiTe_ContextDragAndDrop(ctx: *TestContext, ref_src: [*]const u8, ref_dst: [*]const u8, button: zgui.MouseButton) void; + extern fn zguiTe_ContextKeyDown(ctx: *TestContext, key_chord: c_int) void; + extern fn zguiTe_ContextKeyUp(ctx: *TestContext, key_chord: c_int) void; +}; + +const ImGuiTestGuiFunc = fn (context: *TestContext) callconv(.C) void; +const ImGuiTestTestFunc = fn (context: *TestContext) callconv(.C) void; + +pub const createContext = zguiTe_CreateContext; +extern fn zguiTe_CreateContext() *TestEngine; + +pub const destroyContext = zguiTe_DestroyContext; +extern fn zguiTe_DestroyContext(engine: *TestEngine) void; + +extern fn zguiTe_Check(filename: [*]const u8, func: [*]const u8, line: u32, flags: CheckFlags, resul: bool, expr: [*]const u8) bool; + +pub fn check(src: std.builtin.SourceLocation, flags: CheckFlags, resul: bool, expr: [:0]const u8) bool { + return zguiTe_Check(src.file.ptr, src.fn_name.ptr, src.line, flags, resul, expr.ptr); +} + +pub extern fn zguiTe_RegisterTest( + engine: *TestEngine, + category: [*]const u8, + name: [*]const u8, + src: [*]const u8, + src_line: c_int, + gui_fce: ?*const ImGuiTestGuiFunc, + gui_test_fce: ?*const ImGuiTestTestFunc, +) *Test; + +pub fn checkTestError( + src: std.builtin.SourceLocation, + err: anyerror, +) void { + var buff: [128:0]u8 = undefined; + const msg = std.fmt.bufPrintZ(&buff, "Assert error: {}", .{err}) catch undefined; + _ = zguiTe_Check(src.file.ptr, src.fn_name.ptr, src.line, .{}, false, msg.ptr); +} + +var _te_engine: ?*TestEngine = null; +pub fn getTestEngine() ?*TestEngine { + return _te_engine; +} + +pub fn init() void { + _te_engine = createContext(); +} + +pub fn deinit() void { + destroyContext(_te_engine.?); +} diff --git a/src/zgui.cpp b/src/zgui.cpp index 8c5881f..658b009 100644 --- a/src/zgui.cpp +++ b/src/zgui.cpp @@ -4,6 +4,16 @@ #include "implot.h" #endif +#if ZGUI_TE +#include "imgui_te_engine.h" +#include "imgui_te_context.h" +#include "imgui_te_ui.h" +#include "imgui_te_utils.h" +#include "imgui_te_exporters.h" +#endif + +#include "imgui_internal.h" + #ifndef ZGUI_API #define ZGUI_API #endif @@ -51,6 +61,10 @@ ZGUI_API void zguiSetNextWindowBgAlpha(float alpha) { ImGui::SetNextWindowBgAlpha(alpha); } +ZGUI_API void zguiSetWindowFocus(const char* name) { + ImGui::SetWindowFocus(name); +} + ZGUI_API void zguiSetKeyboardFocusHere(int offset) { ImGui::SetKeyboardFocusHere(offset); } @@ -63,12 +77,12 @@ ZGUI_API void zguiEnd(void) { ImGui::End(); } -ZGUI_API bool zguiBeginChild(const char* str_id, float w, float h, bool border, ImGuiWindowFlags flags) { - return ImGui::BeginChild(str_id, { w, h }, border, flags); +ZGUI_API bool zguiBeginChild(const char* str_id, float w, float h, ImGuiChildFlags child_flags, ImGuiWindowFlags window_flags) { + return ImGui::BeginChild(str_id, { w, h }, child_flags, window_flags); } -ZGUI_API bool zguiBeginChildId(ImGuiID id, float w, float h, bool border, ImGuiWindowFlags flags) { - return ImGui::BeginChild(id, { w, h }, border, flags); +ZGUI_API bool zguiBeginChildId(ImGuiID id, float w, float h, ImGuiChildFlags child_flags, ImGuiWindowFlags window_flags) { + return ImGui::BeginChild(id, { w, h }, child_flags, window_flags); } ZGUI_API void zguiEndChild(void) { @@ -213,6 +227,12 @@ ZGUI_API void zguiGetItemRectMin(float rect[2]) { rect[1] = r.y; } +ZGUI_API void zguiGetItemRectSize(float rect[2]) { + const ImVec2 r = ImGui::GetItemRectSize(); + rect[0] = r.x; + rect[1] = r.y; +} + ZGUI_API void zguiGetCursorPos(float pos[2]) { const ImVec2 p = ImGui::GetCursorPos(); pos[0] = p.x; @@ -2208,6 +2228,66 @@ ZGUI_API void zguiViewport_GetWorkSize(ImGuiViewport* viewport, float p[2]) { p[1] = sz.y; } +//-------------------------------------------------------------------------------------------------- +// +// Docking +// +//-------------------------------------------------------------------------------------------------- +ZGUI_API ImGuiID zguiDockSpace(const char* str_id, float size[2], ImGuiDockNodeFlags flags) { + return ImGui::DockSpace(ImGui::GetID(str_id), {size[0], size[1]}, flags); +} + +ZGUI_API ImGuiID zguiDockSpaceOverViewport(const ImGuiViewport* viewport, ImGuiDockNodeFlags dockspace_flags) { + return ImGui::DockSpaceOverViewport(viewport, dockspace_flags); +} + + +//-------------------------------------------------------------------------------------------------- +// +// DockBuilder (Unstable internal imgui API, subject to change, use at own risk) +// +//-------------------------------------------------------------------------------------------------- +ZGUI_API void zguiDockBuilderDockWindow(const char* window_name, ImGuiID node_id) { + ImGui::DockBuilderDockWindow(window_name, node_id); +} + +ZGUI_API ImGuiID zguiDockBuilderAddNode(ImGuiID node_id, ImGuiDockNodeFlags flags) { + return ImGui::DockBuilderAddNode(node_id, flags); +} + +ZGUI_API void zguiDockBuilderRemoveNode(ImGuiID node_id) { + ImGui::DockBuilderRemoveNode(node_id); +} + +ZGUI_API void zguiDockBuilderSetNodePos(ImGuiID node_id, float pos[2]) { + ImGui::DockBuilderSetNodePos(node_id, {pos[0], pos[1]}); +} + +ZGUI_API void zguiDockBuilderSetNodeSize(ImGuiID node_id, float size[2]) { + ImGui::DockBuilderSetNodeSize(node_id, {size[0], size[1]}); +} + +ZGUI_API ImGuiID zguiDockBuilderSplitNode( + ImGuiID node_id, + ImGuiDir split_dir, + float size_ratio_for_node_at_dir, + ImGuiID* out_id_at_dir, + ImGuiID* out_id_at_opposite_dir +) { + return ImGui::DockBuilderSplitNode( + node_id, + split_dir, + size_ratio_for_node_at_dir, + out_id_at_dir, + out_id_at_opposite_dir + ); +} + +ZGUI_API void zguiDockBuilderFinish(ImGuiID node_id) { + ImGui::DockBuilderFinish(node_id); +} + + #if ZGUI_IMPLOT //-------------------------------------------------------------------------------------------------- // @@ -2473,3 +2553,156 @@ ZGUI_API void zguiPlot_PlotText( //-------------------------------------------------------------------------------------------------- } /* extern "C" */ + + +#if ZGUI_TE +//-------------------------------------------------------------------------------------------------- +// +// ImGUI Test Engine +// +//-------------------------------------------------------------------------------------------------- +extern "C" +{ + + ZGUI_API void *zguiTe_CreateContext(void) + { + ImGuiTestEngine *e = ImGuiTestEngine_CreateContext(); + + ImGuiTestEngine_Start(e, ImGui::GetCurrentContext()); + ImGuiTestEngine_InstallDefaultCrashHandler(); + + return e; + } + + ZGUI_API void zguiTe_DestroyContext(ImGuiTestEngine *engine) + { + ImGuiTestEngine_DestroyContext(engine); + } + + ZGUI_API void zguiTe_EngineSetRunSpeed(ImGuiTestEngine *engine, ImGuiTestRunSpeed speed) + { + ImGuiTestEngine_GetIO(engine).ConfigRunSpeed = speed; + } + + ZGUI_API void zguiTe_EngineExportJunitResult(ImGuiTestEngine *engine, const char *filename) + { + ImGuiTestEngine_GetIO(engine).ExportResultsFilename = filename; + ImGuiTestEngine_GetIO(engine).ExportResultsFormat = ImGuiTestEngineExportFormat_JUnitXml; + } + + ZGUI_API void zguiTe_TryAbortEngine(ImGuiTestEngine *engine) + { + ImGuiTestEngine_TryAbortEngine(engine); + } + + ZGUI_API void zguiTe_Stop(ImGuiTestEngine *engine) + { + ImGuiTestEngine_Stop(engine); + } + + ZGUI_API void zguiTe_PostSwap(ImGuiTestEngine *engine) + { + ImGuiTestEngine_PostSwap(engine); + } + + ZGUI_API bool zguiTe_IsTestQueueEmpty(ImGuiTestEngine *engine) + { + return ImGuiTestEngine_IsTestQueueEmpty(engine); + } + + ZGUI_API void zguiTe_GetResult(ImGuiTestEngine *engine, int *count_tested, int *count_success) + { + int ct = 0; + int cs = 0; + ImGuiTestEngine_GetResult(engine, ct, cs); + *count_tested = ct; + *count_success = cs; + } + + ZGUI_API void zguiTe_PrintResultSummary(ImGuiTestEngine *engine) + { + ImGuiTestEngine_PrintResultSummary(engine); + } + + ZGUI_API void zguiTe_QueueTests(ImGuiTestEngine *engine, ImGuiTestGroup group, const char *filter_str, ImGuiTestRunFlags run_flags) + { + ImGuiTestEngine_QueueTests(engine, group, filter_str, run_flags); + } + + ZGUI_API void zguiTe_ShowTestEngineWindows(ImGuiTestEngine *engine, bool *p_open) + { + ImGuiTestEngine_ShowTestEngineWindows(engine, p_open); + } + + ZGUI_API void *zguiTe_RegisterTest(ImGuiTestEngine *engine, const char *category, const char *name, const char *src_file, int src_line, ImGuiTestGuiFunc *gui_fce, ImGuiTestTestFunc *gui_test_fce) + { + auto t = ImGuiTestEngine_RegisterTest(engine, category, name, src_file, src_line); + t->GuiFunc = gui_fce; + t->TestFunc = gui_test_fce; + return t; + } + + ZGUI_API bool zguiTe_Check(const char *file, const char *func, int line, ImGuiTestCheckFlags flags, bool result, const char *expr) + { + return ImGuiTestEngine_Check(file, func, line, flags, result, expr); + } + + // CONTEXT + + ZGUI_API void zguiTe_ContextSetRef(ImGuiTestContext *ctx, const char *ref) + { + ctx->SetRef(ref); + } + + ZGUI_API void zguiTe_ContextWindowFocus(ImGuiTestContext *ctx, const char *ref) + { + ctx->WindowFocus(ref); + } + + ZGUI_API void zguiTe_ContextItemAction(ImGuiTestContext *ctx, ImGuiTestAction action, const char *ref, ImGuiTestOpFlags flags = 0, void *action_arg = NULL) + { + ctx->ItemAction(action, ref, flags, action_arg); + } + + ZGUI_API void zguiTe_ContextItemInputStrValue(ImGuiTestContext *ctx, const char *ref, const char *value) + { + ctx->ItemInputValue(ref, value); + } + + ZGUI_API void zguiTe_ContextItemInputIntValue(ImGuiTestContext *ctx, const char *ref, int value) + { + ctx->ItemInputValue(ref, value); + } + + ZGUI_API void zguiTe_ContextItemInputFloatValue(ImGuiTestContext *ctx, const char *ref, float value) + { + ctx->ItemInputValue(ref, value); + } + + ZGUI_API void zguiTe_ContextYield(ImGuiTestContext *ctx, int frame_count) + { + ctx->Yield(frame_count); + } + + ZGUI_API void zguiTe_ContextMenuAction(ImGuiTestContext *ctx, ImGuiTestAction action, const char *ref) + { + ctx->MenuAction(action, ref); + } + + ZGUI_API void zguiTe_ContextDragAndDrop(ImGuiTestContext *ctx, const char *ref_src, const char *ref_dst, ImGuiMouseButton button) + { + ctx->ItemDragAndDrop(ref_src, ref_dst, button); + } + + ZGUI_API void zguiTe_ContextKeyDown(ImGuiTestContext *ctx, ImGuiKeyChord key_chord) + { + ctx->KeyDown(key_chord); + } + + ZGUI_API void zguiTe_ContextKeyUp(ImGuiTestContext *ctx, ImGuiKeyChord key_chord) + { + ctx->KeyUp(key_chord); + } + +} /* extern "C" */ +#endif