Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow click-through of transparent windows, but not contents #8360

Open
rihok opened this issue Jan 30, 2025 · 5 comments
Open

Allow click-through of transparent windows, but not contents #8360

rihok opened this issue Jan 30, 2025 · 5 comments
Labels

Comments

@rihok
Copy link

rihok commented Jan 30, 2025

Version/Branch of Dear ImGui:

Not relevant

Back-ends:

Not relevant

Compiler, OS:

Not relevant

Full config/build information:

No response

Details:

Unless I'm missing something obvious somewhere, I don't think it's currently possible to have a transparent window (ImGuiWindowFlags_NoBackground) that allows you to click-through the transparent parts. This is quite common need for overlay type windows, where you'd want the window to not capture any mouse input unless you're actually hovering over an interactive element. There's some hacks I've seen to work around this, but I think a built-in flag for this would be quite helpful.

We've used ImGuiWindowFlags_NoInputs flag on the main window, and then create child windows for individual elements and then try to detect IsWindowHovered on those, but this is quite hacky and makes it very difficult to write nice widget code.

I guess what I'm looking for is a ImGuiWindowFlags_NoTransparentInputs flag, that would ignore inputs on the window itself, but still allow inputs on all child widgets / elements.

Thanks for considering,
Riho

Screenshots/Video:

No response

Minimal, Complete and Verifiable Example code:

// Here's some code anyone can copy and paste to reproduce your issue
ImGui::Begin("My Transparent Overlay", ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoTransparentInputs);

if (ImGui::Button("My Button"))
{
    // do stuff
}

ImGui::End();
@rihok
Copy link
Author

rihok commented Jan 30, 2025

And to be clear, in terms of implementation what I have in mind wouldn't be a pixel mask or something like that, but rather just the existing widget hit zones. So, even if a button inside this window has a transparent background, it should still be clickable.

@ocornut ocornut added the inputs label Jan 30, 2025
@ocornut
Copy link
Owner

ocornut commented Jan 30, 2025

Hello Riho,

Unfortunately I am not sure it is possible to implement in a sane/optimal manner. We latch the HoveredWindow at the beginning of the frame based on its overall geometry. Handling arbitrary pass-through would require a large redesign of many systems, since e.g. Begin() calls are not performed in the same order as they appear.

Internally we allow for one hole for hit-testing (window->HitTestHoleSize/HitTestHoleOffset) but that's unlikely useful to you.

Barring that, I came up with (non-perfect) workarounds last year, but they are only useful if you want the passthrough you reach your background app, rather than another ImGui window:

// V1
// Call just before EndFrame()/Render(), e.g. MakeWindowInputPassthrough("Dear ImGui Demo")
// FIXME: Will incorrectly clear WantCaptureMouse when hovering said window with a popup open.
#include "imgui_internal.h"
void MakeWindowInputPassthrough(const char* name)
{
    if (ImGui::IsAnyItemHovered())
        return;

    // We don't even need to do a FindWindowByName()
    ImGuiContext& g = *GImGui;
    if (g.HoveredWindow == NULL || g.HoveredWindow->RootWindow->ID != ImHashStr(name))
        return;
    if (g.ActiveId == g.HoveredWindow->MoveId) // Undo clicking on window taking ActiveId (even if ImGuiWindowFlags_NoMove is set)
        ImGui::ClearActiveID();
    if (ImGui::IsAnyItemActive())
        return;

    // FIXME: maybe directly write to io.WantCaptureMouse = false so it's available in your input handler earlier?
    ImGui::SetNextFrameWantCaptureMouse(false);
}

// V2
// Call before EndFrame()/Render(), e.g. MakeWindowInputPassthrough("Dear ImGui Demo")
// FIXME: Will incorrectly clear WantCaptureMouse when hovering said window with a popup open.
#include "imgui_internal.h"
void MakeWindowInputPassthrough(const char* name)
{
    if (ImGui::IsAnyItemHovered())
        return;

    ImGuiWindow* window = ImGui::FindWindowByName(name);
    ImGuiContext& g = *GImGui;
    if (ImGui::IsAnyItemActive())
    {
        if (g.ActiveId != window->MoveId)
            return;
    }
    else
    {
        if (g.HoveredWindow && g.HoveredWindow->RootWindow != window)
            return;
    }

    // FIXME: maybe directly write to io.WantCaptureMouse = false so it's available in your input handler earlier?
    ImGui::SetNextFrameWantCaptureMouse(false);
}

That wasn't tested/polished much but I won't mind looking into this further if you think that would be of use.

@ocornut
Copy link
Owner

ocornut commented Jan 30, 2025

I'm not sure I can find satisfying generic answer to it, but if you wish to explain your situation and use case in a little more details (here or privately in e.g. a call) i could try to find if there's a solution that works better.

@rihok
Copy link
Author

rihok commented Jan 30, 2025

Yea maybe we could hop on a call tomorrow to have a look at my case. The clickthrough to the rest of the app might work if I make sure this particular window is the first window drawn maybe?

@ocornut
Copy link
Owner

ocornut commented Jan 31, 2025

Following our call, here's an improved version of the code.
I fixed variety of bugs and made it works with io.WantCaptureKeyboard as well.
I'd like your feedback on whether this does the job, and we can consider integrating that in the library (this would allow removing the necessity to call the function right before EndFrame/Render).

// Usage:
// - call just before EndFrame()/Render(), e.g. MakeWindowVoidInputPassthrough("Dear ImGui Demo").
// Purpose:
// - Allow mouse hover/click in the empty space (void) of window to be passed down underlying game/app.
// - This works by clearing the io.WantCaptureMouse flag, so low-level input handler can keep dispatching mouse to underlying game/app.
// - This doesn't handle multiple overlapped imgui windows, so it is generally expected you use this on a single window that is in the background,
//   or on multiple non-overlapping windows. Calling this on overlapping windows will erroneously clear io.WantCaptureMouse when hovering one,
//   regardless of another one that may sit behind. This is possible to fix with some work.
// - Similarly, handle focusing the window.
// Discussions at https://github.com/ocornut/imgui/issues/8360
// FIXME: What about keyboard?
void MakeWindowVoidInputPassthrough(const char* name);

static bool MakeWindowVoidInputPassthroughTestMouse(ImGuiWindow* window)
{
    ImGuiContext& g = *GImGui;
    if (ImGui::IsAnyItemHovered())
        return false;

    // Any active item is assumed to take inputs unless g.ActiveIdAllowOverlap is set
    // (the variable is historically a bit misnamed, but it allows e.g. InputText() to be active while allowing hovering other items)
    if (ImGui::IsAnyItemActive() && !g.ActiveIdAllowOverlap)
    {
        // Unless we're clicking/dragging in the window's void itself
        // When clicking on window's void we allow it to take the ActiveId.
        if (g.ActiveId != window->MoveId)
            return false;
    }
    else
    {
        if (g.HoveredWindow && g.HoveredWindow->RootWindow != window)
            return false;
    }

    // If a popup is open it eats the click on void
    // FIXME: This could be an optional thing?
    // (low-level input handler may still distinguish io.WantCaptureMouse from ioWantCaptureMouseUnlessPopupClose if they need to)
    if (ImGui::IsPopupOpen(nullptr, ImGuiPopupFlags_AnyPopup))
        return false;

    return true;
}

static bool MakeWindowVoidInputPassthroughTestKeyboard(ImGuiWindow* window)
{
    ImGuiContext& g = *GImGui;

    if (g.NavWindow == nullptr || g.NavWindow->RootWindow != window->RootWindow)
        return false;

    // If any item is active in the window (e.g. an InputText, or a Button) we keep keyboard to imgui
    if (g.ActiveId != 0 && g.ActiveId != window->MoveId)
        if (g.ActiveIdWindow->RootWindow == window->RootWindow)
            return false;

    // Allow navigation to work, tho it is likely you'd want to use ImGuiWindowFlags_NoNav on the window.
    if (g.NavCursorVisible && g.NavId != 0)
        return false;

    // When navigation cursor is cleared, we disable nav on this window for the frame,
    // preventing e.g. arrow keys from resuming navigation while underlying game/app is using them.
    // Next frame's Begin() will clear the flag again.
    // This means navigation on this window may only be activated via:
    // - Ctrl+Tabbing into the window.
    // - Using arrow key while an item is currently active (typically InputText, but it will also
    //   work while holding mouse button over any item even tho that's a low affordance behavior).
    // - And pressing Escape will deactivate navigation and relinquish keyboard underlying game/app.
    // This seems like the best design as we want e.g. clicking a button to end up stealing keyboard.
    window->Flags |= ImGuiWindowFlags_NoNav;

    return true;
}

void MakeWindowVoidInputPassthrough(const char* name)
{
    ImGuiIO& io = ImGui::GetIO();
    ImGuiWindow* window = ImGui::FindWindowByName(name);
    if (window == nullptr)
        return;

    if (MakeWindowVoidInputPassthroughTestMouse(window))
    {
        // Write to io.WantCaptureMouse directly, so it is available in e.g. low-level input handler _before_ the next NewFrame().
        // Technically we don't need to call SetNextFrameWantCaptureMouse(): what matter is that io.WantCaptureMouse is cleared
        // at the time of low-level input handlers applying their filter.
        io.WantCaptureMouse = false;
        ImGui::SetNextFrameWantCaptureMouse(false);
    }

    if (MakeWindowVoidInputPassthroughTestKeyboard(window))
    {
        io.WantCaptureKeyboard = false;
        ImGui::SetNextFrameWantCaptureKeyboard(false);
    }
}

Test bed:
(background is made intentionally visible here)

static bool g_WantCaptureMouseBeforeNewFrame = false;
void TestWindow()
{
    ImGuiIO& io = ImGui::GetIO();

    ImGuiViewport* viewport = ImGui::GetMainViewport();
    ImGui::SetNextWindowPos(viewport->Pos);
    ImGui::SetNextWindowSize(ImVec2(viewport->Size.x / 2, viewport->Size.y / 2));
    ImGui::SetNextWindowBgAlpha(0.1f);
    ImGui::Begin("(Background Overlay)", NULL, // Use a friendly name for Ctrl+Tab
        ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoDecoration
        | (0*ImGuiWindowFlags_NoBackground)
        | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoBringToFrontOnFocus
        //| ImGuiWindowFlags_NoNav 
    );

    ImGui::Button("Some button");
    ImGui::Indent(50.0f);
    static int counter = 0;
    if (ImGui::Button("Another button"))
        counter++;
    ImGui::SameLine();
    ImGui::Text("%d", counter);

    static char text[128]; // To test active item
    ImGui::InputText("Text", text, 128);

    static ImVec4 color(1.0f, 0.0f, 0.0f, 1.0f); // To test popup
    ImGui::ColorEdit4("Color", &color.x);

    ImGui::Text("io.WantCaptureMouse now %d before_new_frame %d", io.WantCaptureMouse, g_WantCaptureMouseBeforeNewFrame);
    ImGui::Text("io.WantCaptureKeyboard %d", io.WantCaptureKeyboard);
    ImGui::Text("g.ActiveID = 0x%08X", ImGui::GetActiveID());
    ImGui::DebugLocateItemOnHover(ImGui::GetActiveID());

    ImGui::End();
}
g_WantCaptureMouseBeforeNewFrame = ImGui::GetIO().WantCaptureMouse;
ImGui::NewFrame();
TestWindow();
[...]
MakeWindowVoidInputPassthrough("(Background Overlay)");
ImGui::EndFrame();

Image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants