ocornut / imgui

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

Allow ImGui::SetNextWindowPos to optionally auto-fit to the nearest Platform Monitor #7150

Open quantumrain opened 11 months ago

quantumrain commented 11 months ago

Version/Branch of Dear ImGui:

Version: c5c1c4134b7727a3a8f38daaa86c2ad8ea9448ec Branch: docking

Back-end/Renderer/Compiler/OS

Back-ends: custom Operating System: windows

My Issue/Question:

Dear ImGui's default positioning of tooltips obscures the hovered item.

This is undesirable when the tooltip is displaying extra information to be considered alongside the hovered item.

Forcing a position for the tooltip using SetNextWindowPos solves the issue of the tooltip obscuring the item, however if the forced position is near the edge of a Platform Monitor then the tooltip can span multiple monitors.

It's possible to compensate for this, however to do so you need to know the size of the tooltip, which requires knowing the generated name of the tooltip window which is an internal ImGui detail.

A flag to SetNextWindowPos, or a new API (SetNextWindowFitToMonitor?) which instructs ImGui to adjust the given position such that the window fits into the nearest Platform Monitor would be useful.

Simply fitting to the nearest monitor would be fine, being able to specify the slide direction, and a rect to avoid would be a bonus.

Standalone, minimal, complete and verifiable example:

    // Move this window near the edge of the screen and hover over the button to see that the tooltip spans two monitors
    if (ImGui::Begin("Test"))
    {
        ImGui::Button("A button that has a tooltip");

        if (ImGui::IsItemHovered(ImGuiHoveredFlags_ForTooltip))
        {
            ImGui::SetNextWindowPos(ImGui::GetItemRectMax(), ImGuiCond_Always, ImVec2(0.0f, 1.0f));

            if (ImGui::BeginTooltipEx(ImGuiTooltipFlags_OverridePrevious, ImGuiWindowFlags_None))
            {
                ImGui::TextUnformatted("A long tooltip message");
                ImGui::EndTooltip();
            }
        }
    }

    ImGui::End();
ocornut commented 11 months ago

I guess one solution would be to find a way to pass the "avoid" rectangle used for auto-positioning: In FindBestWindowPosForPopup():

if (window->Flags & ImGuiWindowFlags_Tooltip)
{
    // 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 * 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);
}

Via e.g. a SetNextWindowAutoPosAvoidRect() function.

ocornut commented 11 months ago

I toyed with this a little bit, the problem is that the current tooltip positioning logic is designed for small moving rect where one cardinal side tends to be locked when a placement is found. So this would need rework as in this case we'd want the tooltip to follow cursor on both axises while being projected outside the avoid rect.

imgui-6bbe595-WIP 7150.patch imgui-caa9231-WIP 7150.patch

(Added the ImGuiTooltipFlags_AvoidLastItemRect flag to BeginTooltip/SetTooltip calls for debugging convenience, but ultimately the important thing would be the low-level SetNextWindowAvoidRect() api).

quantumrain commented 10 months ago

I'm expanding on the original bug here to give a bit more context, as in our particular case it's desirable for the tooltip to not follow the mouse, and instead have a fixed location relative to the item the tooltip relates to.

We're displaying tooltips when you hover items in the status bar of the main window of the application, as the status bar is docked at the bottom of the main window when that window is maximised the status bar is very close to the limits of the Platform Monitor.

The default position of tooltips doesn't work well in this situation, so we position our tooltips above and aligned to the left of the related item (similar to the logic for combo boxes, except expanding upwards rather than downwards).

This generally works fine, and can be achieved with a single extra line, e.g.:

ImGui::SetNextWindowPos(ImGui::GetItemRectMin(), ImGuiCond_Always, ImGui2(0.0f, 1.0f));

if (ImGui::BeginToolTip(...)) ...

But when the tooltip is near the right edge of the Platform Monitor this causes the tooltip to extend off the edge.

I have a workaround for this- by replicating the internal logic to generate tooltip window names to find the size of the tooltip window and adjust the position appropriately.

However, as this is quite a common issue generally for popups (e.g. DearImGui itself has logic for several widgets to compensate for this), it feels like this would benefit from being exposed as a feature.

The simplest implementation would be to have an API that simply requests the next window be constrained to the nearest Platform Monitor. e.g.:

ImGui::SetNextWindowConstrainToNearestPlatformMonitor()

If the window simply moved onto whatever the nearest monitor was it would be an improvement over it straddling two monitors.

Taking this further, we'd ideally select the Platform Monitor the original item was positioned on for our constraint, so we always appear on the same monitor as our hovered item.

However, this might cause the window to move back and obscure our hovered item, so an enhanced version would also take the Avoid Rect, and a Preferred Direction to shift the window to avoid that rectangle while still being constrained within the Platform Window.

So the final code to position a tooltip as suggested above would look something like:

ImGui::SetNextWindowPos(ImGui::GetItemRectMin(), ImGuiCond_Always, ImGui2(0.0f, 1.0f));
ImGui::SetNextWindowAvoidLastItemRect(ImGuiDir_Up);
ImGui::SetNextWindowConstrainToNearestPlatformMonitor();

if (ImGui::BeginToolTip(...)) ...

The location given to SetNextWindowPos would be used as the anchor to select the appropriate Platform Monitor, and if the code was unable to satisfy the Avoid Rect constraint and also keep the window on the Platform Monitor, it should ignore the avoid constraint and just fit to the Platform Monitor.