ocornut / imgui

Dear ImGui: Bloat-free Graphical User interface for C++ with minimal dependencies
MIT License
59.83k stars 10.18k forks source link

`SplitterBehavior(...)` resize feedback loop when scrollbar is at bottom #1720

Open nikki93 opened 6 years ago

nikki93 commented 6 years ago

I'm using SplitterBehavior(...) to have resizable child windows inside a scrolling region in a containing window. This works pretty well, except for one snag: when you resize down and the scrollbar is at the bottom of the container window, the content size of the container reduces and so everything moves down, causing more resizing, eventually causing the resizable child to feedback to the minimum size. The main way around this I can think of is to add dummy space at the bottom of the content view equal to the lost height while resizing, and once done the dummy is removed to reset the layout. Wondering if that is the best approach / if you have any tips.

Here's a video of this behavior: https://www.youtube.com/watch?v=rK4km7dERWc

For reference, here's the wrapper around SplitterBehavior(...) as suggested in https://github.com/ocornut/imgui/issues/319#issuecomment-345795629:

EXPORT bool uiSplitter(const char *sub_id, bool splitVertically, float thickness,
    float *size1, float *size2, float minSize1, float minSize2,
    float splitterLongAxisSize)
{
    ImGuiContext& g = *GImGui;
    ImGuiWindow* window = g.CurrentWindow;
    ImGuiID id = window->GetID(sub_id);
    ImRect bb;
    bb.Min = window->DC.CursorPos + (splitVertically ? ImVec2(*size1, 0.0f) : ImVec2(0.0f, *size1));
    bb.Max = bb.Min + CalcItemSize(splitVertically ?
        ImVec2(thickness, splitterLongAxisSize) :
        ImVec2(splitterLongAxisSize, thickness), 0.0f, 0.0f);
    return SplitterBehavior(id, bb, splitVertically ? ImGuiAxis_X : ImGuiAxis_Y,
        size1, size2, minSize1, minSize2,
        4.0f);
}

I create a utility ui.beginChildResizable(...) in Lua as follows (.beginChild(...), .endChild(), .spacing() are properly wrapped already):

ffi.cdef[[
bool uiSplitter(const char *sub_id, bool splitVertically, float thickness,
    float *size1, float *size2, float minSize1, float minSize2,
    float splitterLongAxisSize);
]]
function ui.splitter(id, opts)
    opts = opts or {}

    local size1 = ffi.new('float[1]', opts.size1 or 300)
    local size2 = ffi.new('float[1]', opts.size2 or 300)

    local splitVertically
    if opts.splitVertically ~= nil then
        splitVertically = opts.splitVertically
    else
        splitVertically = false
    end

    local r = C.uiSplitter(id, splitVertically, opts.thickness or 1.2,
        size1, size2,
        opts.minSize1 or 2 * ui.getTextLineHeight(),
        opts.minSize2 or 2 * ui.getTextLineHeight(),
        opts.splitterLongAxisSize or -0.2)
    return size1[0], size2[0], r
end

local resizableChildHeights = {}

function ui.beginChildResizable(label, opts)
    opts = opts or {}
    local id = ui.getIDStr(label .. '##splitter')
    local height = resizableChildHeights[id] or opts.defaultHeight or 8 * ui.getTextLineHeight()
    height = ui.splitter(label .. '##splitter', {
        size1 = height,
        minSize1 = opts.minHeight or 2 * ui.getTextLineHeight()
    })
    ui.beginChild(label, 0, height - ui.getStyle().ItemSpacing.y - 1, opts)
    resizableChildHeights[id] = height
end

function ui.endChildResizable()
    ui.endChild()
    ui.spacing()
    ui.spacing()
end

Which is used, for example, as:

ui.beginChildResizable(name, {
    extraFlags = { HorizontalScrollbar = true },
})
ui.textUnformatted(source)
ui.endChildResizable()
ocornut commented 6 years ago

Hello @nikki93,

Thanks for your report. A fully contained C++ repro would be appreciated, because it takes non trivial amount of time figuring one out.

What I see from your code it that doesn't use Splitter as initially designed: you are ignoring the second part of the Size component size2 (and likewise I am ignoring it in my repro), to the total size become variable. I realize this sort of usage is convenient and has many potential uses so I'd like to support it. Maybe we should be using a cursor/mouse delta in that sort of situation, instead of locating the target position from the absolute cursor/mouse position. There are merit to both approaches in different situations. I'll have to think about it.

Here's a repro for now:

#define IMGUI_DEFINE_MATH_OPERATORS
#include "imgui_internal.h"
bool Splitter(bool split_vertically, float thickness, float* size1, float* size2, float min_size1, float min_size2, float splitter_long_axis_size = -1.0f)
{
    using namespace ImGui;
    ImGuiContext& g = *GImGui;
    ImGuiWindow* window = g.CurrentWindow;
    ImGuiID id = window->GetID("##Splitter");
    ImRect bb;
    bb.Min = window->DC.CursorPos + (split_vertically ? ImVec2(*size1, 0.0f) : ImVec2(0.0f, *size1));
    bb.Max = bb.Min + CalcItemSize(split_vertically ? ImVec2(thickness, splitter_long_axis_size) : ImVec2(splitter_long_axis_size, thickness), 0.0f, 0.0f);
    return SplitterBehavior(id, bb, split_vertically ? ImGuiAxis_X : ImGuiAxis_Y, size1, size2, min_size1, min_size2, 0.0f);
}
ImGui::Begin("Issue #1720");
static float size1 = 300;
static float size2 = 300;
Splitter(false, 4.0f, &size1, &size2, 10.0f, 10.0f);
ImGui::BeginChild("top", ImVec2(-1, size1), true, ImGuiWindowFlags_NoScrollWithMouse);
ImGui::Text("Hello from top");
ImGui::EndChild();
ImGui::BeginChild("bottom", ImVec2(-1, 300), true, ImGuiWindowFlags_NoScrollWithMouse);
ImGui::Text("Hello from bottom");
ImGui::EndChild();
ImGui::End();
nikki93 commented 6 years ago

That’s fair, will do a cpp repro! And yeah that’s a good point I could work around by actually using size2 as zero and adding a dummy container at the end of the containing scroll that is size2 size, until you release, at which point we delete it. That way the dummy expands to prevent scrolling backwards.