Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,9 @@ manager, and it is only drawn by a per-frame pump. Call one of these once per fr
so `DrawDeferred()` must not also be called. `DockedWindow` only renders under this pump — under
`DrawDeferred()` a `DockedWindow` is registered but never drawn, because Hexa's widget manager is
only driven by that pump. A `DockedWindow` is dockable but **not** auto-docked: it opens floating
and stays there until the user drags it into the dockspace.
and stays there until the user drags it into the dockspace. Collapsing a `DockedWindow` or
tabbing it behind another keeps it registered: Hexa treats any `ImGui.Begin` returning false as a
close, so the adapter vetoes every close except the one from the close button (#600).

They are mutually exclusive: calling both in the same frame draws every dialog twice.
`ImGuiWidgets` detects this (via the ImGui frame counter) and logs a `Trace.TraceWarning`, but does
Expand Down
17 changes: 17 additions & 0 deletions ImGui.Widgets/DockedWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ public static partial class ImGuiWidgets
/// <see cref="Title"/> is both the window caption and its identity, so two windows sharing a
/// title will collide.
/// </para>
/// <para>
/// Collapsing the window, or docking it as a tab behind another, keeps it registered. Only its
/// close button or <see cref="Close"/> unregisters it.
/// </para>
/// </remarks>
public abstract class DockedWindow
{
Expand Down Expand Up @@ -71,6 +75,19 @@ private sealed class Adapter(DockedWindow owner) : HexaImWindow

/// <inheritdoc/>
public override void DrawContent() => owner.DrawContent();

/// <summary>
/// Vetoes every close but the close button's.
/// </summary>
/// <remarks>
/// Hexa calls this whenever <c>ImGui.Begin</c> returns false, which it also does for a
/// collapsed window and for a docked tab that is not selected. Left alone, either one
/// unregisters the window for good, so tabbing two windows together loses the one
/// behind. Only the close button clears <see cref="HexaImWindow.IsShown"/>, so that is
/// the one case allowed through. See ktsu-dev/ImGuiApp#600.
/// </remarks>
/// <param name="handled">Set to true to keep the window registered.</param>
protected override void OnClosed(ref bool handled) => handled |= IsShown;
}
}
}
106 changes: 105 additions & 1 deletion tests/ImGui.Widgets.UITests/DockedWindowTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace ktsu.ImGui.Widgets.UITests;

using System;
using System.Numerics;

using Hexa.NET.ImGui;

Expand Down Expand Up @@ -36,9 +37,30 @@
}

private ProbeWindow window = null!;
private ProbeWindow? other;
private Action? beforePump;

[TestCleanup]
public void CloseWindow() => window?.Close();
public void CloseWindow()
{
window?.Close();
other?.Close();
}

/// <summary>Runs a one-shot action inside the next frame, ahead of the docked pump.</summary>
private void RunInNextFrame(Action action)
{
beforePump = action;
Step();
}

private void DrawPump()
{
Action? action = beforePump;
beforePump = null;
action?.Invoke();
ImGuiWidgets.DrawDeferredDocked();
}

[TestMethod]
public void DockedWindow_IsDrawnByTheDockedPump()
Expand Down Expand Up @@ -118,4 +140,86 @@
drawn.Width < Harness.Options.Width,
$"The window covered the full {Harness.Options.Width}px width, so it was docked rather than floating.");
}

[TestMethod]
public void DockedWindow_TabbedBehindAnotherIsStillThereWhenSelected()
{
// ImGui.Begin answers false for a docked tab that is not selected, and Hexa used to read that
// as a close and unregister the window, so the tab behind was lost for good.
window = new ProbeWindow("Alpha");
other = new ProbeWindow("Beta");
Start(DrawPump, enableDocking: true);

window.Show();
other.Show();
Step(3);

RunInNextFrame(() =>
{
ImGuiP.DockBuilderDockWindow("Alpha", Hexa.NET.ImGui.Widgets.WidgetManager.DockSpaceId);
ImGuiP.DockBuilderDockWindow("Beta", Hexa.NET.ImGui.Widgets.WidgetManager.DockSpaceId);
});
Step(5);

RunInNextFrame(() => ImGui.SetWindowFocus("Alpha"));
Step(2);
int alpha = window.DrawCount;
Step(3);
Assert.IsTrue(window.DrawCount > alpha, "Alpha was not drawn after its tab was selected.");

Check warning on line 168 in tests/ImGui.Widgets.UITests/DockedWindowTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.IsGreaterThan' instead of 'Assert.IsTrue'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_ImGuiApp&issues=AaEWemWq_YuxwL7M5g6Z&open=AaEWemWq_YuxwL7M5g6Z&pullRequest=601

RunInNextFrame(() => ImGui.SetWindowFocus("Beta"));
Step(2);
int beta = other.DrawCount;
Step(3);
Assert.IsTrue(other.DrawCount > beta, "Beta was not drawn after its tab was selected.");

Check warning on line 174 in tests/ImGui.Widgets.UITests/DockedWindowTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.IsGreaterThan' instead of 'Assert.IsTrue'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_ImGuiApp&issues=AaEWemWq_YuxwL7M5g6a&open=AaEWemWq_YuxwL7M5g6a&pullRequest=601
}

[TestMethod]
public void DockedWindow_SurvivesBeingCollapsed()
{
window = new ProbeWindow("Inspector");
Start(DrawPump, enableDocking: true);

window.Show();
Step(3);

RunInNextFrame(() => ImGui.SetWindowCollapsed("Inspector", true));
Step(3);
int collapsed = window.DrawCount;
Step(3);
Assert.AreEqual(collapsed, window.DrawCount, "A collapsed window still drew its content.");

RunInNextFrame(() => ImGui.SetWindowCollapsed("Inspector", false));
Step(3);
Assert.IsTrue(window.DrawCount > collapsed, "The window did not come back after being expanded.");

Check warning on line 194 in tests/ImGui.Widgets.UITests/DockedWindowTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.IsGreaterThan' instead of 'Assert.IsTrue'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_ImGuiApp&issues=AaEWemWq_YuxwL7M5g6b&open=AaEWemWq_YuxwL7M5g6b&pullRequest=601
}

[TestMethod]
public void DockedWindow_CloseButtonStillCloses()
{
// The veto that keeps a collapsed or tabbed window registered must not swallow a real close.
window = new ProbeWindow("Inspector");
Start(DrawPump, enableDocking: true);

window.Show();
Step(3);

Vector2 closeButton = Vector2.Zero;
RunInNextFrame(() =>
{
ImGuiWindowPtr found = ImGuiP.FindWindowByName("Inspector");
ImGuiStylePtr style = ImGui.GetStyle();
float fontSize = ImGui.GetFontSize();
closeButton = new Vector2(
found.Pos.X + found.Size.X - style.FramePadding.X - (fontSize * 0.5f),
found.Pos.Y + style.FramePadding.Y + (fontSize * 0.5f));
});

Harness.Mouse.Click(closeButton.X, closeButton.Y);
Step(3);
int afterClose = window.DrawCount;
Step(3);

Assert.AreEqual(afterClose, window.DrawCount, "The close button did not close the window.");
}
}
Loading