ocornut / imgui

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

Pie menu test #434

Open ocornut opened 8 years ago

ocornut commented 8 years ago

This is more a proof of concept that a finished api.

pie_menu2

#include <imgui_internal.h>

// Return >= 0 on mouse release
// Optional int* p_selected display and update a currently selected item
int PiePopupSelectMenu(const ImVec2& center, const char* popup_id, const char** items, int items_count, int* p_selected)
{
    int ret = -1;

    // FIXME: Missing a call to query if Popup is open so we can move the PushStyleColor inside the BeginPopupBlock (e.g. IsPopupOpen() in imgui.cpp)
    // FIXME: Our PathFill function only handle convex polygons, so we can't have items spanning an arc too large else inner concave edge artifact is too visible, hence the ImMax(7,items_count)
    ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0,0,0,0));
    ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0,0,0,0));
    if (ImGui::BeginPopup(popup_id))
    {
        const ImVec2 drag_delta = ImVec2(ImGui::GetIO().MousePos.x - center.x, ImGui::GetIO().MousePos.y - center.y);
        const float drag_dist2 = drag_delta.x*drag_delta.x + drag_delta.y*drag_delta.y;

        const ImGuiStyle& style = ImGui::GetStyle();
        const float RADIUS_MIN = 30.0f;
        const float RADIUS_MAX = 120.0f;
        const float RADIUS_INTERACT_MIN = 20.0f;
        const int ITEMS_MIN = 6;

        ImDrawList* draw_list = ImGui::GetWindowDrawList();
        //ImGuiWindow* window = ImGui::GetCurrentWindow();
        draw_list->PushClipRectFullScreen();
        draw_list->PathArcTo(center, (RADIUS_MIN + RADIUS_MAX)*0.5f, 0.0f, IM_PI*2.0f*0.99f, 32);   // FIXME: 0.99f look like full arc with closed thick stroke has a bug now
        draw_list->PathStroke(ImColor(0,0,0), true, RADIUS_MAX - RADIUS_MIN);

        const float item_arc_span = 2*IM_PI / ImMax(ITEMS_MIN, items_count);
        float drag_angle = atan2f(drag_delta.y, drag_delta.x);
        if (drag_angle < -0.5f*item_arc_span)
            drag_angle += 2.0f*IM_PI;
        //ImGui::Text("%f", drag_angle);    // [Debug]

        int item_hovered = -1;
        for (int item_n = 0; item_n < items_count; item_n++)
        {
            const char* item_label = items[item_n];
            const float item_ang_min = item_arc_span * (item_n+0.02f) - item_arc_span*0.5f; // FIXME: Could calculate padding angle based on how many pixels they'll take
            const float item_ang_max = item_arc_span * (item_n+0.98f) - item_arc_span*0.5f;

            bool hovered = false;
            if (drag_dist2 >= RADIUS_INTERACT_MIN*RADIUS_INTERACT_MIN)
            {
                if (drag_angle >= item_ang_min && drag_angle < item_ang_max)
                    hovered = true;
            }
            bool selected = p_selected && (*p_selected == item_n);

            int arc_segments = (int)(32 * item_arc_span / (2*IM_PI)) + 1;
            draw_list->PathArcTo(center, RADIUS_MAX - style.ItemInnerSpacing.x, item_ang_min, item_ang_max, arc_segments);
            draw_list->PathArcTo(center, RADIUS_MIN + style.ItemInnerSpacing.x, item_ang_max, item_ang_min, arc_segments);
            //draw_list->PathFill(window->Color(hovered ? ImGuiCol_HeaderHovered : ImGuiCol_FrameBg));
            draw_list->PathFill(hovered ? ImColor(100,100,150) : selected ? ImColor(120,120,140) : ImColor(70,70,70));

            ImVec2 text_size = ImGui::GetWindowFont()->CalcTextSizeA(ImGui::GetWindowFontSize(), FLT_MAX, 0.0f, item_label);
            ImVec2 text_pos = ImVec2(
                center.x + cosf((item_ang_min + item_ang_max) * 0.5f) * (RADIUS_MIN + RADIUS_MAX) * 0.5f - text_size.x * 0.5f,
                center.y + sinf((item_ang_min + item_ang_max) * 0.5f) * (RADIUS_MIN + RADIUS_MAX) * 0.5f - text_size.y * 0.5f);
            draw_list->AddText(text_pos, ImColor(255,255,255), item_label);

            if (hovered)
                item_hovered = item_n;
        }
        draw_list->PopClipRect();

        if (ImGui::IsMouseReleased(0))
        {
            ImGui::CloseCurrentPopup();
            ret = item_hovered;
            if (p_selected)
                *p_selected = item_hovered;
        }
        ImGui::EndPopup();
    }
    ImGui::PopStyleColor(2);
    return ret;
}
{
static const char* test_data = "Menu";
const char* items[] = { "Orange", "Blue", "Purple", "Gray", "Yellow", "Las Vegas" };
int items_count = sizeof(items)/sizeof(*items);

static int selected = -1;

ImGui::Button(selected >= 0 ? items[selected] : "Menu", ImVec2(50,50));
if (ImGui::IsItemActive())          // Don't wait for button release to activate the pie menu
    ImGui::OpenPopup("##piepopup");

ImVec2 pie_menu_center = ImGui::GetIO().MouseClickedPos[0];
int n = PiePopupSelectMenu(pie_menu_center, "##piepopup", items, items_count, &selected);
if (n >= 0)
    printf("returned %d\n", n);
ghost commented 8 years ago

Great job :-)

lundmark commented 8 years ago

Looking really good! Will you be adding it to the lib or will it just be a snippet?

ocornut commented 8 years ago

Eventually it should be added but it's not complete nor good enough yet. In order to support submenus the API would likely need to be changed?

lundmark commented 8 years ago

What would submenues within a pie-popup look like?

ocornut commented 8 years ago

I'm not sure. Open a second pie-popup over the center of the previous button? That would suggest that merely holding mouse would open the sub-menu (like normal menu), which in turn ask the question of how we can get back to the parent menu.

I haven't spend time to think about it in details, just implemented something as fast as I can for someone, which is essentially why this is a proof of concept rather than a feature.

lundmark commented 8 years ago

Another idea would perhaps be to expand the pie outwards, presenting a top-down menu. The amount of choices could be increased by increasing the size of the extended pie-slice upwards/downwards?

ghost commented 8 years ago

It does not work when trying to display in without a window ? I try to display it on a simple OpenGL window ! After a drag & drop... I drop inside a 3D view... and nothing appear !

Horrowind commented 8 years ago

It always bothered me, that the border between the items increased from the inner ring to the outer, so here is a fix for that (only listing the inner rendering for-loop):

    for (int item_n = 0; item_n < items_count; item_n++)
    {
        const char* item_label = items[item_n];
        const float inner_spacing = style.ItemInnerSpacing.x / RADIUS_MIN / 2;
        const float item_inner_ang_min = item_arc_span * (item_n - 0.5f + inner_spacing);
        const float item_inner_ang_max = item_arc_span * (item_n + 0.5f - inner_spacing);
        const float item_outer_ang_min = item_arc_span * (item_n - 0.5f + inner_spacing * (RADIUS_MIN / RADIUS_MAX));
        const float item_outer_ang_max = item_arc_span * (item_n + 0.5f - inner_spacing * (RADIUS_MIN / RADIUS_MAX));

        bool hovered = false;
        if (drag_dist2 >= RADIUS_INTERACT_MIN*RADIUS_INTERACT_MIN)
        {
            if (drag_angle >= item_inner_ang_min && drag_angle < item_inner_ang_max)
                hovered = true;
        }
        bool selected = p_selected && (*p_selected == item_n);

        int arc_segments = (int)(32 * item_arc_span / (2*IM_PI)) + 1;
        draw_list->PathArcTo(center, RADIUS_MAX - style.ItemInnerSpacing.x, item_outer_ang_min, item_outer_ang_max, arc_segments);
        draw_list->PathArcTo(center, RADIUS_MIN + style.ItemInnerSpacing.x, item_inner_ang_max, item_inner_ang_min, arc_segments);
        //draw_list->PathFill(window->Color(hovered ? ImGuiCol_HeaderHovered : ImGuiCol_FrameBg));
        draw_list->PathFill(hovered ? ImColor(100,100,150) : selected ? ImColor(120,120,140) : ImColor(70,70,70));

        ImVec2 text_size = ImGui::GetWindowFont()->CalcTextSizeA(ImGui::GetWindowFontSize(), FLT_MAX, 0.0f, item_label);
        ImVec2 text_pos = ImVec2(
            center.x + cosf((item_inner_ang_min + item_inner_ang_max) * 0.5f) * (RADIUS_MIN + RADIUS_MAX) * 0.5f - text_size.x * 0.5f,
            center.y + sinf((item_inner_ang_min + item_inner_ang_max) * 0.5f) * (RADIUS_MIN + RADIUS_MAX) * 0.5f - text_size.y * 0.5f);
        draw_list->AddText(text_pos, ImColor(255,255,255), item_label);

        if (hovered)
            item_hovered = item_n;
    }

Furthermore here are inspirational videos from other pie menu implementations:

https://www.youtube.com/watch?v=nXh1Tm24kTE&t=50s the submenu implementation from this one is probably not advisable, but I like how it adds a second ring if there are more then six items.

https://www.youtube.com/watch?v=Job4Rg-sbDo this is the one in OneNote; it has some kind of submenu implementation, but from the video, I am unable to understand how it works.

ghost commented 8 years ago

I really like the simple Ubuntu "look", just a simple set of "alpha blended" pies over the screen :-P

ocornut commented 8 years ago

Thanks @Horrowind, really useful!

ocornut commented 8 years ago

You should be able to, the pie-menu is its own window. Krys please provide more detailed information and standalone repro code with your questions when possible! Lots of your questions are really unclear, if you can't clarify the question then please provide the smallest set of code that demonstrate the issue you have. Thanks!

jarikomppa commented 8 years ago

It is impossible to gives a small reproduction case, because the application is huge !

What he means is, start a new, tiny application that just reproduces a single problem.

On Thu, Dec 17, 2015 at 12:25 PM, krys-spectralpixel < notifications@github.com> wrote:

I agree but...

It is impossible to gives a small reproduction case, because the application is huge !

The effect is that nothing appear !

Here is my code, but it is approx. a copy/paste of your code:

        const char* items[] = { "Color", "Specular", "Bump", "Opacity" };
        int items_count = sizeof(items) / sizeof(*items);

        static int selected = -1;
        ImGui::OpenPopup("##piepopup");
        // ImVec2 pie_menu_center = ImGui::GetIO().MouseClickedPos[0];
        ImVec2 pie_menu_center = ImGui::GetMousePos();
        int n = PiePopupSelectMenu(pie_menu_center, "##piepopup", items, items_count, &selected);

— Reply to this email directly or view it on GitHub https://github.com/ocornut/imgui/issues/434#issuecomment-165410961.

ocornut commented 8 years ago

It is your job to provide a repro to ensure that your request is well formed and thought up. It also in many cases helps you understand the issue better. Your code above is not a repro I can paste.

Have you noticed that my pie code above close automatically if mouse is released?

This works without a window for me:

        const char* items[] = { "Color", "Specular", "Bump", "Opacity" };
        int items_count = sizeof(items) / sizeof(*items);

        static int selected = -1;
        if (!ImGui::IsMouseHoveringAnyWindow() && ImGui::IsMouseClicked(0))
            ImGui::OpenPopup("##piepopup");
        int n = PiePopupSelectMenu(ImGui::GetIO().MouseClickedPos[0], "##piepopup", items, items_count, &selected);
ghost commented 8 years ago

I propose to replace:

if (drag_dist2 >= RADIUS_INTERACT_MIN*RADIUS_INTERACT_MIN && drag_dist2 < RADIUS_MAX*RADIUS_MAX)

with

if (&& drag_dist2 >= RADIUS_INTERACT_MIN*RADIUS_INTERACT_MIN && drag_dist2 < RADIUS_MAX*RADIUS_MAX)

In order to avoid selection when the mouse is out of the pie menu !

ocornut commented 8 years ago

I did that intentionally to avoid false negatives and also considering that pie menus have a "gesture" feel to them. e.g. click and drag left, release, by not worrying about distance travelled you can accomplish faster actions. Maybe it isn't as important with mouse controls. Both ways have legit uses, it could be a global setting of the pie menus potentially.

ghost commented 8 years ago

Right. Sure there can be plenty of options for this control :-D

vd3d commented 8 years ago

Hi,

I have noticed that when I use only 4 "items" in the pie menu I have a line going over the items (See the red arrow on the attached picture please).

Have you ever noticed it, can you reproduce it ?

image

ocornut commented 8 years ago

The PathFill function in theory only handle convex fill, so I suppose that's an artefact of this limitation. Proper support for concave fill would be quite some work. The cheap workaround would be to split the arc in smaller section (say, divided by 2) and draw it as two parts. It would however show visible artefacts if your fill alpha is < 1.0f

vd3d commented 8 years ago

Great explanation !

Thanks for it. You helped me a lot.

zhouxs1023 commented 8 years ago

I compiled pie menu with DX9, after run exe ,I found a black area at the centre of the circle, do not know how to cancel this area? default

lapinozz commented 8 years ago

@zhouxs1023 this is caused by the popup background, I don't know why it dosen't follow the style pushed but I "fixed" it by adding this code just before if (ImGui::BeginPopup(popup_id))

ImGui::SetNextWindowPos({-100, -100});

zhouxs1023 commented 8 years ago

Thank you very much!

thennequin commented 6 years ago

A modified version with support of sub menu, it's not finished or optimized and need a new struct for storing values and items label. @ocornut, is there any chance to have a storage of string someday for avoiding this custom struct? ;) Or add a way (with macro?) to add custom variables in ImGuiContext.

circularmenu Can by used like this

if( ImGui::IsWindowHovered() && ImGui::IsMouseClicked( 1 ) )
{
  ImGui::OpenPopup( "PieMenu" );
}

if( BeginPiePopup( "PieMenu", 1 ) )
{
  if( PieMenuItem( "Test1" ) ) { /*TODO*/ }
  if( PieMenuItem( "Test2" ) ) { /*TODO*/ }

  if( PieMenuItem( "Test3", false ) )  { /*TODO*/ }

  if( BeginPieMenu( "Sub" ) )
  {
    if( BeginPieMenu( "Sub sub\nmenu" ) )
    {
      if( PieMenuItem( "SubSub" ) ) { /*TODO*/ }
      if( PieMenuItem( "SubSub2" ) ) { /*TODO*/ }
      EndPieMenu();
    }
    if( PieMenuItem( "TestSub" ) ) { /*TODO*/ }
    if( PieMenuItem( "TestSub2" ) ) { /*TODO*/ }
    EndPieMenu();
  }

  EndPiePopup();
}

Full source here https://gist.github.com/thennequin/64b4b996ec990c6ddc13a48c6a0ba68c

ocornut commented 6 years ago

@thennequin

is there any chance to have a storage of string someday for avoiding this custom struct? ;)

There's a helper ImGuiTextBuffer which is close to what you are doing.

Maybe a stripped down version of https://github.com/ocornut/Str would generally be useful, but if you can use a single buffer like ImGuiTextBuffer it helps.

Or add a way (with macro?) to add custom variables in ImGuiContext.

I've been considering it, perhaps a way for a external subsystem to register a slot instead an array of pointers stored in ImGuiContext, so you can store one PieMenuContext per ImGuiContext. Is that were you were thinking of?

thennequin commented 6 years ago

There's a helper ImGuiTextBuffer which is close to what you are doing.

I use an ImVector< char > for storing strings. Sorry for the misunderstanding of the first question, I just wanted a way to add strings (like bool/int/float/ptr) in the ImGuiStorage for avoiding the use of a new external struct because I store bool/float/int and string.

I've been considering it, perhaps a way for a external subsystem to register a slot instead an array of pointers stored in ImGuiContext, so you can store one PieMenuContext per ImGuiContext. Is that were you were thinking of?

Yes, I want to store one PieMenuContext per ImGuiContext.

Stanlyhalo commented 3 years ago

@thennequin You probably haven't updated this in a while nor remember how you wrote it, but it's worth the shot. I tried your implementation, and I've seen only 1 issue, and a weird quirk I wanted to see if I can change. The issue is that there is no background color, and yes, I've tried to change the color, didn't budge, and the quirk is that if it goes a little offscreen, it pushes itself away from the border, if I wanted to remove this quirk, how would I go about doing that?

thennequin commented 3 years ago

I updated the gist to fix the "quirk" (if you talk about the black rectangle) and I fixed the background (wrong UV).

tmsrise commented 2 years ago

does this work with controller navigation?

ocornut commented 2 years ago

Well no this is old code but nowadays you can use GetKeyData(ImGuiKey_GamepadLStickX)->AnalogValue and ditto for Y to construct a 2D vector and use that in your pie menu code.

tmsrise commented 2 years ago

For those of you that want to implement controller support:

1) There is no ImGuiKey_GamepadLStickX, but there is ImGuiKey_GamepadLStickUp, down, left, right, etc, so you need to subtract.

2) Implement hovered memory so that releasing the joystick keeps the highlight and subsequent return when you close the menu.

3) Mind the inner_spacing gaps. For controllers it creates awkward locations inbetween elements that results in no selection. Either remove it or create separate floats for the actual selectable and visual representation of segments if you want a clean look without usability sacrifice.

4) Implement the controller equivalent to the minimum drag distance (deadzone) so that the kickback from releasing a thumbstick doesn't inadvertently select a different element. I've found |x| or |y| greater than 0.6 works well.

berthubert commented 1 year ago

This could maybe better live within ImPlot? https://github.com/epezent/implot

D7ry commented 1 year ago

Hi! just dropping by to say thanks for the initial idea. I've managed to make sth cool with it

tmsrise commented 1 year ago

Hi! just dropping by to say thanks for the initial idea. I've managed to make sth cool with it

Nice! I used it to make a combat art + prosthetic wheel in Sekiro. Yours seems much more polished though. I was considering doing a rewrite that would fix the bugs, crashes, and terrible spaghetti code. Mind if I yoink parts of that with credit? (if I even get to it lol)

D7ry commented 1 year ago

@tmsrise It's MIT licensed so take anything you'd like :)

ocornut commented 6 months ago

Posting a minor update to the 2015 version (some code simplification) note however that it is functionally the same.

I reckon Thibault's version may be better suited: https://github.com/ocornut/imgui/issues/434#issuecomment-351743369

#include "imgui_internal.h"

// Return >= 0 on mouse release
// Optional int* p_selected display and update a currently selected item
int PiePopupSelectMenu(const ImVec2& center, const char* popup_id, const char** items, int items_count, int* p_selected)
{
    int ret = -1;

    if (ImGui::BeginPopup(popup_id, ImGuiWindowFlags_NoDecoration))
    {
        const ImVec2 drag_delta = ImVec2(ImGui::GetIO().MousePos.x - center.x, ImGui::GetIO().MousePos.y - center.y);
        const float drag_dist2 = drag_delta.x * drag_delta.x + drag_delta.y * drag_delta.y;

        const ImGuiStyle& style = ImGui::GetStyle();
        const float RADIUS_MIN = 30.0f;
        const float RADIUS_MAX = 120.0f;
        const float RADIUS_INTERACT_MIN = 20.0f;    // Handle hit testing slightly below RADIUS_MIN
        const int ITEMS_MIN = 6;                    // If they are less than 6 items, we still make each item fill a 1/6 slice.

        // Draw background
        ImDrawList* draw_list = ImGui::GetWindowDrawList();
        draw_list->PushClipRectFullScreen();
        draw_list->PathArcTo(center, (RADIUS_MIN + RADIUS_MAX) * 0.5f, 0.0f, IM_PI * 2.0f);
        draw_list->PathStroke(IM_COL32(0, 0, 0, 255), ImDrawFlags_Closed, RADIUS_MAX - RADIUS_MIN);

        const float item_arc_span = 2 * IM_PI / ImMax(ITEMS_MIN, items_count);
        float drag_angle = ImAtan2(drag_delta.y, drag_delta.x);
        if (drag_angle < -0.5f * item_arc_span)
            drag_angle += 2.0f * IM_PI;
        //ImGui::Text("%f", drag_angle);    // [Debug]

        // Draw items
        int item_hovered = -1;
        for (int item_n = 0; item_n < items_count; item_n++)
        {
            const char* item_label = items[item_n];
            const float item_ang_min = item_arc_span * (item_n + 0.02f) - item_arc_span * 0.5f; // FIXME: Could calculate padding angle based on how many pixels they'll take
            const float item_ang_max = item_arc_span * (item_n + 0.98f) - item_arc_span * 0.5f;

            bool hovered = false;
            if (drag_dist2 >= RADIUS_INTERACT_MIN * RADIUS_INTERACT_MIN)
                if (drag_angle >= item_ang_min && drag_angle < item_ang_max)
                    hovered = true;

            bool selected = p_selected && (*p_selected == item_n);

            draw_list->PathArcTo(center, RADIUS_MAX - style.ItemInnerSpacing.x, item_ang_min, item_ang_max);
            draw_list->PathArcTo(center, RADIUS_MIN + style.ItemInnerSpacing.x, item_ang_max, item_ang_min);
            draw_list->PathFillConvex(ImGui::GetColorU32(hovered ? ImGuiCol_HeaderHovered : selected ? ImGuiCol_HeaderActive : ImGuiCol_Header));

            ImVec2 text_size = ImGui::CalcTextSize(item_label);
            ImVec2 text_pos = ImVec2(
                center.x + cosf((item_ang_min + item_ang_max) * 0.5f) * (RADIUS_MIN + RADIUS_MAX) * 0.5f - text_size.x * 0.5f,
                center.y + sinf((item_ang_min + item_ang_max) * 0.5f) * (RADIUS_MIN + RADIUS_MAX) * 0.5f - text_size.y * 0.5f);
            draw_list->AddText(text_pos, ImGui::GetColorU32(ImGuiCol_Text), item_label);

            if (hovered)
                item_hovered = item_n;
        }
        draw_list->PopClipRect();

        if (ImGui::IsMouseReleased(0))
        {
            ImGui::CloseCurrentPopup();
            ret = item_hovered;
            if (p_selected)
                *p_selected = item_hovered;
        }
        ImGui::EndPopup();
    }
    return ret;
}

Usage:


    static const char* test_data = "Menu";
    const char* items[] = { "Orange", "Blue", "Purple", "Gray", "Yellow", "Las Vegas" };
    int items_count = sizeof(items) / sizeof(*items);

    static int selected = -1;

    ImGui::Button(selected >= 0 ? items[selected] : "Menu", ImVec2(50, 50));
    if (ImGui::IsItemActive())          // Don't wait for button release to activate the pie menu
        ImGui::OpenPopup("##piepopup");

    ImVec2 pie_menu_center = ImGui::GetIO().MouseClickedPos[0];
    int n = PiePopupSelectMenu(pie_menu_center, "##piepopup", items, items_count, &selected);
    if (n >= 0)
        printf("returned %d\n", n);
}