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

direct way to check which inputs ImGui wants to capture #364

Closed
dbregman opened this issue Oct 5, 2015 · 2 comments
Closed

direct way to check which inputs ImGui wants to capture #364

dbregman opened this issue Oct 5, 2015 · 2 comments

Comments

@dbregman
Copy link

dbregman commented Oct 5, 2015

As far as I understand, the current way to do this is:

  1. read & queue inputs, update imgui IO
  2. call NewFrame()
  3. for each queued input, check WantCaptureKeyboard, WantCaptureMouse, WantTextInput, and send/don't send inputs to the app, as appropriate

Depending on what inputs an app receives, the process of queuing and dequeuing these events can be pretty complicated and it makes ImGui difficult to integrate in some engines. For example, I recently used ImGui in an application which used the WM_POINTER family of messages, and these messages have large transient side-state associated with them (see GetPointerInfo etc) which disappears when WNDPROC returns. Queuing/Dequeuing these was a real mess, and necessarily implies the integration will be more intrusive because the magic functions like GetPointerInfo can't be used as-is.

Instead, it would be great if ImGui had some functions to compute WantCaptureKeyboard, WantCaptureMouse, WantTextInput, based on input received so far. This would allow an app to decide right away when an input is received whether to pass it to the app or not.

I'm not sure if this is possible for every case given the nature of immediate mode, but an approximation would be pretty nice. I have a hacky proof of concept of this for you to consider (below). The idea is that you can just put

if (ImGuiWin32::ProcessInput(hWnd, message, wParam, lParam)) {  return 0; }

at the top of your WNDPROC and be done.

#include <windows.h>
#include "imgui_win32.h"
#include "imgui.h"
#include "imgui_internal.h"

enum InputType
{
    DontCare,
    Mouse,
    Keyboard,
    Text,
};

// copypasted from imgui.cpp
static ImGuiWindow* FindHoveredWindow(ImVec2 pos, bool excluding_childs)
{
    ImGuiState& g = *GImGui;
    for (int i = g.Windows.Size - 1; i >= 0; i--)
    {
        ImGuiWindow* window = g.Windows[i];
        if (!window->Active)
            continue;
        if (window->Flags & ImGuiWindowFlags_NoInputs)
            continue;
        if (excluding_childs && (window->Flags & ImGuiWindowFlags_ChildWindow) != 0)
            continue;

        // Using the clipped AABB so a child window will typically be clipped by its parent.
        ImRect bb;
        bb.Min.x = window->ClippedWindowRect.Min.x - g.Style.TouchExtraPadding.x;
        bb.Min.y = window->ClippedWindowRect.Min.y - g.Style.TouchExtraPadding.y;
        bb.Max.x = window->ClippedWindowRect.Max.x + g.Style.TouchExtraPadding.x;
        bb.Max.y = window->ClippedWindowRect.Max.y + g.Style.TouchExtraPadding.y;
        if (bb.Contains(pos))
            return window;
    }
    return NULL;
}

// based on ImGui::NewFrame, but has no side effects
bool ImGuiWantCapture(InputType type)
{
    ImGuiState& g = *GImGui;

    ImGuiWindow* hoveredWindow = FindHoveredWindow(g.IO.MousePos, false);
    int mouse_earliest_button_down = -1;
    bool mouse_any_down = false;
    for (int i = 0; i < IM_ARRAYSIZE(g.IO.MouseDown); i++)
    {
        bool mouseDown = (g.IO.MouseDownDuration[i] > 0.0f);
        mouse_any_down |= mouseDown;
        if (mouseDown)
            if (mouse_earliest_button_down == -1 || g.IO.MouseClickedTime[mouse_earliest_button_down] > g.IO.MouseClickedTime[i])
                mouse_earliest_button_down = i;
    }
    bool mouseDownOwned = false;
    if (mouse_earliest_button_down != -1)
    {
        mouseDownOwned = g.IO.MouseDownOwned[mouse_earliest_button_down];
        bool MouseClicked_earliest = g.IO.MouseDown[mouse_earliest_button_down] && (g.IO.MouseDownDuration[mouse_earliest_button_down] < 0.0f);
        if (MouseClicked_earliest)
        {
            mouseDownOwned = (hoveredWindow != NULL) || (!g.OpenedPopupStack.empty());
        }
    }
    bool mouse_owned_by_application = mouse_earliest_button_down != -1 && !mouseDownOwned;
    bool WantCaptureMouse = (!mouse_owned_by_application && hoveredWindow != NULL) || (!mouse_owned_by_application && mouse_any_down) || (g.ActiveId != 0) || (!g.OpenedPopupStack.empty()) || (g.CaptureMouseNextFrame);
    bool WantCaptureKeyboard = (g.ActiveId != 0) || (g.CaptureKeyboardNextFrame);
    bool WantTextInput = (g.ActiveId != 0 && g.InputTextState.Id == g.ActiveId);

    if (type == InputType::Mouse && WantCaptureMouse ||
        type == InputType::Keyboard && WantCaptureKeyboard ||
        type == InputType::Text && WantTextInput)
    {
        return true;
    }

    return false;
}

bool ImGuiWin32::ProcessInput(
    HWND hWnd,
    UINT message,
    WPARAM wParam,
    LPARAM lParam)
{
    auto io = &ImGui::GetIO();

    InputType type = InputType::DontCare;

    switch (message)
    {
    case WM_CREATE:
        io->KeyMap[ImGuiKey_Tab] = VK_TAB;
        io->KeyMap[ImGuiKey_LeftArrow] = VK_LEFT;
        io->KeyMap[ImGuiKey_RightArrow] = VK_RIGHT;
        io->KeyMap[ImGuiKey_UpArrow] = VK_UP;
        io->KeyMap[ImGuiKey_DownArrow] = VK_DOWN;
        io->KeyMap[ImGuiKey_PageUp] = VK_PRIOR;
        io->KeyMap[ImGuiKey_PageDown] = VK_NEXT;
        io->KeyMap[ImGuiKey_Home] = VK_HOME;
        io->KeyMap[ImGuiKey_End] = VK_END;
        io->KeyMap[ImGuiKey_Delete] = VK_DELETE;
        io->KeyMap[ImGuiKey_Backspace] = VK_BACK;
        io->KeyMap[ImGuiKey_Enter] = VK_RETURN;
        io->KeyMap[ImGuiKey_Escape] = VK_ESCAPE;
        io->KeyMap[ImGuiKey_A] = 'A';
        io->KeyMap[ImGuiKey_C] = 'C';
        io->KeyMap[ImGuiKey_V] = 'V';
        io->KeyMap[ImGuiKey_X] = 'X';
        io->KeyMap[ImGuiKey_Y] = 'Y';
        io->KeyMap[ImGuiKey_Z] = 'Z';
        break;
    case WM_CHAR:
        if (wParam < 256)
        {
            io->AddInputCharacter((ImWchar)wParam);
        }
        type = InputType::Text;
        break;
    case WM_SYSKEYDOWN:
    case WM_KEYDOWN:
        {
            int vkey = (int)wParam;
            io->KeysDown[vkey] = 1;
            if (vkey == VK_CONTROL) io->KeyCtrl = true;
            else if (vkey == VK_MENU) io->KeyAlt = true;
            else if (vkey == VK_SHIFT) io->KeyShift = true;
            type = InputType::Keyboard;
        }
        break;
    case WM_SYSKEYUP:
    case WM_KEYUP:
        {
            int vkey = (int)wParam;
            io->KeysDown[vkey] = 0;
            if (vkey == VK_CONTROL) io->KeyCtrl = false;
            else if (vkey == VK_MENU) io->KeyAlt = false;
            else if (vkey == VK_SHIFT) io->KeyShift = false;
        }
        break;
    case WM_MOUSEMOVE:
        io->MousePos.x = short(LOWORD(lParam));
        io->MousePos.y = short(HIWORD(lParam));
        type = InputType::Mouse;
        break;
    case WM_MOUSEWHEEL:
        io->MouseWheel = GET_WHEEL_DELTA_WPARAM(wParam) / float(WHEEL_DELTA);
        type = InputType::Mouse;
        break;
    case WM_LBUTTONDOWN:
    case WM_RBUTTONDOWN:
    case WM_MBUTTONDOWN:
        if (message == WM_LBUTTONDOWN) io->MouseDown[0] = 1;
        if (message == WM_RBUTTONDOWN) io->MouseDown[1] = 1;
        if (message == WM_MBUTTONDOWN) io->MouseDown[2] = 1;
        type = InputType::Mouse;
        break;
    case WM_LBUTTONUP:
    case WM_RBUTTONUP:
    case WM_MBUTTONUP:
        if (message == WM_LBUTTONUP) io->MouseDown[0] = 0;
        if (message == WM_RBUTTONUP) io->MouseDown[1] = 0;
        if (message == WM_MBUTTONUP) io->MouseDown[2] = 0;
        break;
    case WM_POINTERUPDATE:
    case WM_POINTERDOWN:
        type = InputType::Mouse;
        // Intentional Fallthrough:
            ;
    case WM_POINTERUP:
        {
            WORD pointerId = GET_POINTERID_WPARAM(wParam);
            POINTER_INFO pointerInfo;

            if (::GetPointerInfo(pointerId, &pointerInfo))
            {
                if (pointerInfo.pointerType == PT_MOUSE)
                {
                    // FIXME: handle other mouse buttons
                    if (message == WM_POINTERDOWN)
                    {
                        io->MouseDown[0] = 1;
                    }
                    else if (message == WM_POINTERUP)
                    {
                        io->MouseDown[0] = 0;
                    }

                    POINT p = pointerInfo.ptPixelLocation;
                    ScreenToClient(hWnd, &p);

                    io->MousePos.x = (float)p.x;
                    io->MousePos.y = (float)p.y;
                }
            }
        }
        break;
    }

    bool directInputToImgui = (type != InputType::DontCare) && ImGuiWantCapture(type);

    return directInputToImgui;
}
@ocornut
Copy link
Owner

ocornut commented Oct 5, 2015

You can probably employ a scheme of just always giving mouse position to ImGui and query the WantInput* fields before calling NewFrame(). You'll have a 1 frame delay because transitioning states but it probably won't matter.

You are right that this system should be improved but that workaround may be good enough (and it is way way too complex).

@ocornut ocornut closed this as completed Nov 7, 2015
@ocornut
Copy link
Owner

ocornut commented Nov 7, 2015

I'm keeping this idea in mind but I think there is a viable-enough way of getting around it. You can either choose to have a one-frame delay in the handling of WantInput* (not that you inputs themselves won't have a one frame delay, only reacting to the transition between !Want and Want), either store your inputs, or just pass them to your application and cancel them afterward, there's various ways of handling it.

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

No branches or pull requests

2 participants