From 5c5b09f460a9080d5fc3337ee23742aa8d60a049 Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Thu, 21 Nov 2024 13:21:57 -0500 Subject: [PATCH] feat: Allow tab pages to match the height of the other tabs. This allows scroll viewers on tab controls to use all remaining height, without tedious height adjustments every time another tab changes. --- Yafc.UI/ImGui/ImGuiLayout.cs | 3 +- Yafc.UI/ImGui/TabControl.cs | 70 +++++++++++++++++++++++++++++++++--- 2 files changed, 68 insertions(+), 5 deletions(-) diff --git a/Yafc.UI/ImGui/ImGuiLayout.cs b/Yafc.UI/ImGui/ImGuiLayout.cs index 2666f6f0..49dc30f5 100644 --- a/Yafc.UI/ImGui/ImGuiLayout.cs +++ b/Yafc.UI/ImGui/ImGuiLayout.cs @@ -267,7 +267,8 @@ public sealed class OverlappingAllocations : IDisposable { private readonly ImGui gui; private readonly bool initialDrawState; private readonly float initialTop; - private float maximumBottom; + internal float currentTop => gui.state.top; + internal float maximumBottom { get; private set; } internal OverlappingAllocations(ImGui gui, bool alsoDraw) { this.gui = gui; diff --git a/Yafc.UI/ImGui/TabControl.cs b/Yafc.UI/ImGui/TabControl.cs index 42385c31..21c1ba11 100644 --- a/Yafc.UI/ImGui/TabControl.cs +++ b/Yafc.UI/ImGui/TabControl.cs @@ -239,6 +239,8 @@ private void PerformLayout() { rows.Reverse(); } + private PageDrawer? drawer; + /// /// Call to draw this and its active page. /// @@ -316,12 +318,36 @@ public void Build(ImGui gui) { using var controller = gui.StartOverlappingAllocations(false); - for (int i = 0; i < tabPages.Length; i++) { - controller.StartNextAllocatePass(i == activePage); - tabPages[i].Drawer?.Invoke(gui); + drawer = new(gui, controller, tabPages, activePage); + while (drawer.DrawNextPage()) { } + drawer = null; + #endregion + } + + /// + /// Requests the tab control report its remaining available content height. As a side effect, the active tab page will pause drawing until + /// all other tabs have been drawn. It is not advisable to draw tab content taller than the height returned by this method. + /// + /// It is possible for multiple tabs to call this method. If that happens, tabs that call this method earlier will get more accurate + /// results. That is, if Tab A calls this method, Tab B draws normally, and Tab C calls this method, Tab C will get a response based on the + /// height of Tab B, and can (but should not) further increase the content height. Tab A will then get a response based on the taller of tabs + /// B and C. Like tab C, A can (but also should not) again increase the content height. If A does, it will defeat tab C's attempt to use all + /// available vertical space. + /// The minimum height that the remaining content needs. The return value will not be smaller than this + /// parameter. + /// The available content height, based on all tabs that did not call this method and any tabs that called this method after the + /// current tab. + /// Thrown if this is not actively drawing tab pages. + public float GetRemainingContentHeight(float minimumHeight = 0) { + if (drawer == null) { + throw new InvalidOperationException($"{nameof(GetRemainingContentHeight)} must only be called from a {nameof(GuiBuilder)} that is currently building a {nameof(TabPage)}."); } - #endregion + using (drawer.RememberState()) { + drawer.gui.AllocateRect(0, minimumHeight); + while (drawer.DrawNextPage()) { } + } + return drawer.GetHeight(); } /// @@ -362,6 +388,42 @@ internal static void BumpTab(TabRow sourceRow, float sourceCompression, TabRow d public static implicit operator TabRow((int Start, int End, float Compression) value) => new TabRow(value.Start, value.End, value.Compression); } + + /// + /// Tracks the necessary details to allow to start drawing a second tab while preserving the drawing + /// state of the current tab. Each call to will interrupt the current tab drawer and the current + /// while (drawer.DrawNextPage()) { } loop and start a new loop. The new loop will drawing the remaining tabs (unless interrupted + /// itself) and will return the height available for use by the calling tab drawer. + /// + private sealed class PageDrawer(ImGui gui, ImGui.OverlappingAllocations controller, TabPage[] tabPages, int activePage) { + public ImGui gui { get; } = gui; + private int i = -1; + private float height; + public bool DrawNextPage() { + if (++i >= tabPages.Length) { + return false; + } + controller.StartNextAllocatePass(i == activePage); + tabPages[i].Drawer?.Invoke(gui); + + return true; + } + + public float GetHeight() => height = controller.maximumBottom - gui.statePosition.Top; + + public IDisposable RememberState() => new State(gui, controller, i == activePage); + + /// + /// Saves and restores the current state when needs to interrupt the current tab drawing. + /// + private sealed class State(ImGui gui, ImGui.OverlappingAllocations controller, bool drawing) : IDisposable { + private readonly float initialTop = controller.currentTop; + public void Dispose() { + controller.StartNextAllocatePass(drawing); + gui.AllocateRect(0, initialTop - controller.currentTop); + } + } + } } ///