ocornut / imgui

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

It would be nice to use RAII for pushing/popping #2096

Open sethk opened 6 years ago

sethk commented 6 years ago

This is especially true because some things need to be popped even when they aren't open (e.g. child regions), but it's difficult to remember that.

ocornut commented 6 years ago

You can create C++ helpers for doing just that, they would be a few lines to implement. I am open the idea of providing an official .h file with those helpers if they are designed carefully.

Begin/BeginChild are inconsistent with other API for historical reasons unfortunately :(

sethk commented 6 years ago

I think I'll take a crack at it and post something here for people to critique.

maxkunes commented 6 years ago

Something I put together a while back, only handles PushStyleVar and PushStyleColor, would probably be nice to improve it to handle other ImGui push/pop methods.

Header (ImStyle_RAII.h):

#pragma once
#include "imgui.h"

class ImStyle_RAII {
public:

    ImStyle_RAII(ImGuiStyleVar idx, const ImVec2& val);

    ImStyle_RAII(ImGuiStyleVar idx, const float& val);

    ImStyle_RAII(ImGuiCol idx, const ImU32& col);

    ~ImStyle_RAII();

private:
    bool bClr;
};

Source (ImStyle_RAII.cpp):

#include "ImStyle_RAII.h"

ImStyle_RAII::ImStyle_RAII(ImGuiStyleVar idx, const ImVec2& val)
{
    bClr = false;
    ImGui::PushStyleVar(idx, val);
}

ImStyle_RAII::ImStyle_RAII(ImGuiStyleVar idx, const float& val)
{
    bClr = false;
    ImGui::PushStyleVar(idx, val);
}

ImStyle_RAII::ImStyle_RAII(ImGuiCol idx, const ImU32 & col)
{
    bClr = true;
    ImGui::PushStyleColor(idx, col);
}

ImStyle_RAII::~ImStyle_RAII()
{
    if (bClr)
        ImGui::PopStyleColor();
    else
        ImGui::PopStyleVar();
}
meshula commented 6 years ago

My most used RAII object for Imgui:

class SetFont
{
public:
    SetFont(ImFont* f) { ImGui::PushFont(f); }
    ~SetFont() { ImGui::PopFont(); }
};
sethk commented 6 years ago

Looks like there's some demand for this! Here's what I'm working with so far:

The only part I can imagine being controversial is that I'm providing operator bool() so that you can say for instance ImWindow window("Blah"); if (window) ....

#include "imgui.h"

#pragma once

class ImWindow
{
public:
    bool IsOpen;

    ImWindow(const char* name, bool* p_open = NULL, ImGuiWindowFlags flags = 0) { IsOpen = ImGui::Begin(name, p_open, flags); }
    ~ImWindow() { if (IsOpen) ImGui::End(); }

    operator bool() { return IsOpen; }
};

class ImPushID
{
public:
    ImPushID(const char* str_id) { ImGui::PushID(str_id); }
    ImPushID(const char* str_id_begin, const char* str_id_end) { ImGui::PushID(str_id_begin, str_id_end); }
    ImPushID(const void* ptr_id) { ImGui::PushID(ptr_id); }
    ImPushID(int int_id) { ImGui::PushID(int_id); }
    ~ImPushID() { ImGui::PopID(); }
};

class ImTreeNode
{
public:
    bool IsOpen;

    ImTreeNode(const char* label) { IsOpen = ImGui::TreeNode(label); }
    ImTreeNode(const char* str_id, const char* fmt, ...) IM_FMTARGS(3) { va_list ap; va_start(ap, fmt); IsOpen = ImGui::TreeNodeV(str_id, fmt, ap); va_end(ap); }
    ImTreeNode(const void* ptr_id, const char* fmt, ...) IM_FMTARGS(3) { va_list ap; va_start(ap, fmt); IsOpen = ImGui::TreeNodeV(ptr_id, fmt, ap); va_end(ap); }
    ~ImTreeNode() { if (IsOpen) ImGui::TreePop(); }

    operator bool() { return IsOpen; }
};

class ImTreeNodeV
{
public:
    bool IsOpen;

    ImTreeNodeV(const char* str_id, const char* fmt, va_list args) IM_FMTLIST(3) { IsOpen = ImGui::TreeNodeV(str_id, fmt, args); }
    ImTreeNodeV(const void* ptr_id, const char* fmt, va_list args) IM_FMTLIST(3) { IsOpen = ImGui::TreeNodeV(ptr_id, fmt, args); }
    ~ImTreeNodeV() { if (IsOpen) ImGui::TreePop(); }

    operator bool() { return IsOpen; }
};

class ImTreeNodeEx
{
public:
    bool IsOpen;

    ImTreeNodeEx(const char* label, ImGuiTreeNodeFlags flags = 0) { IsOpen = ImGui::TreeNodeEx(label, flags); }
    ImTreeNodeEx(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, ...) IM_FMTARGS(4) { va_list ap; va_start(ap, fmt); IsOpen = ImGui::TreeNodeExV(str_id, flags, fmt, ap); va_end(ap); }
    ImTreeNodeEx(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, ...) IM_FMTARGS(4) { va_list ap; va_start(ap, fmt); IsOpen = ImGui::TreeNodeExV(ptr_id, flags, fmt, ap); va_end(ap); }
    ~ImTreeNodeEx() { if (IsOpen) ImGui::TreePop(); }

    operator bool() { return IsOpen; }
};

class ImTreeNodeExV
{
public:
    bool IsOpen;

    ImTreeNodeExV(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args) IM_FMTLIST(4) { IsOpen = ImGui::TreeNodeExV(str_id, flags, fmt, args); }
    ImTreeNodeExV(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args) IM_FMTLIST(4) { IsOpen = ImGui::TreeNodeExV(ptr_id, flags, fmt, args); }
    ~ImTreeNodeExV() { if (IsOpen) ImGui::TreePop(); }

    operator bool() { return IsOpen; }
};
maxkunes commented 6 years ago

@sethk Interesting. I have wrapped ImGui children and windows in a similar fashion but without RAII. I'm currently using lamda's for this use.

ImWindowHelper and ImChildHelper implicitly calls begin/end accordingly.

Ex Pseudo :

ImWindowHelper(str_id ..., [&]() {

    ImChildHelper(str_id, size, ..., [&]() {
        ImGui::Text("Child 1");
    });

    ImChildHelper(str_id, size, ..., [&]() {
        ImGui::Text("Child 2");
    });

});
ice1000 commented 6 years ago

Consider

{
  ImWindowHelper(str_id, ...);
  {
    ImChildHelper(str_id, ...);
    ImGui::Text("Child 1");
  }
}

which is less noisy.

maxkunes commented 6 years ago

@ice1000 Are the destructors garenteed to happen after that call to Text? I'm quite sure you would need to store those raii instances in an actual variable for that code to work correctly. Not sure the standard spec on that. That is the reason I chose the lamda method as I don't need to worry much about scope and how the compiler may treat the code.

ice1000 commented 6 years ago

It works with clang++-6.0 but I didn't lookup the standard spec.

ice1000 commented 6 years ago

Confirmed: if you don't throw exceptions in the constructor, it's destructed at the time as we expected in my code snippet above.

From https://en.cppreference.com/w/cpp/language/destructor :

The destructor is called whenever an object's lifetime ends, which includes
end of scope, for objects with automatic storage duration and for temporaries whose life was extended by binding to a reference

From http://eel.is/c++draft/class.dtor#:destructor,implicit_call :

A destructor is invoked implicitly
for a constructed object with automatic storage duration ([basic.stc.auto]) when the block in which an object is created exits ([stmt.dcl]),

maxkunes commented 6 years ago

Confirmed: if you don't throw exceptions in the constructor, it's destructed at the time as we expected in my code snippet above.

From https://en.cppreference.com/w/cpp/language/destructor :

The destructor is called whenever an object's lifetime ends, which includes end of scope, for objects with automatic storage duration and for temporaries whose life was extended by binding to a reference

From http://eel.is/c++draft/class.dtor#:destructor,implicit_call :

A destructor is invoked implicitly for a constructed object with automatic storage duration ([basic.stc.auto]) when the block in which an object is created exits ([stmt.dcl]),

@ice1000 MSVC doesn't like what your doing which suggests to me the standard isn't actually saying what you think it is, that is unless I'm making a mistake or MSVC isn't following the standard although I believe the former is more likely.

{
        ImWindowHelper("BaseMenu", NULL, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings);

        BeginChildLamda("Menu", menuSize, false, false, [&]() {
            renderTabs();
            renderContent();
            renderBanner();
        });

}

Relevant ImWindowHelper class

ImWindowHelper::ImWindowHelper(const char* name, bool* p_open, ImGuiWindowFlags flags)
{
    ImGui::Begin(name, p_open, flags);
}

ImWindowHelper::~ImWindowHelper()
{
    ImGui::End();
}

And it compiles on MSVC v141 to this as I suspected :

ImWindowHelper::ImWindowHelper((ImWindowHelper *)&_This.tabWidthPercentage + 3, "BaseMenu", 0, 259);
  ImWindowHelper::~ImWindowHelper((ImWindowHelper *)&_This.tabWidthPercentage + 3);

Essentially it gets instantly destructed after it gets constructed, simply doing this :

{
        auto baseMenuWin = ImWindowHelper("BaseMenu", NULL, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings);

        BeginChildLamda("Menu", menuSize, false, false, [&]() {
            renderTabs();
            renderContent();
            renderBanner();
        });
//baseMenuWin will be destructed here.
}

Would work as expected and would be destructed where you want it to be. This is precisely why I chose lamdas, because you can definately ensure what code gets called when.

sethk commented 6 years ago

@ice1000 That syntax reminds me of something I saw in a Facebook C++ talk about some very insidious bugs they had when RAII initializations were missing variable names and thus turned into side-effect free declarations. The compactness is nice if it works, but it loses the ability to bypass code when windows are collapsed or clipped, etc. In any case, that syntax should be possible with my wrappers, if it works for you. What C++ standard are you targetting with Clang?

@maxkunes I like the idea of lambdas as well. Are there any drawbacks? I haven't used C++ lambdas much. Is there any runtime overhead when they're used as coroutines like this?

maxkunes commented 6 years ago

@sethk Regarding the question about RAII you referenced @ice1000 I would direct you to my post above yours where I show that at the very least, MSVC will not work as he expects, my guess is as good as yours if it works other places.

EDIT: Through more research from https://godbolt.org/ seems many compilers will call the destructor instantly after constructor when not allocated locally.

Regarding lamdas, to my knowledge, they don't have many drawbacks, but I heard somewhere that std::function has a little bit of overhead (should fact check that), but for example, if you use function pointers with lamdas, I don't think there is any overhead.

meshula commented 6 years ago
class RAII {
public:
    RAII() { printf("A");}
    ~RAII() { printf("~A");}
};
class RAII2 {
    public:
    RAII2() { printf("B");}
    ~RAII2() { printf("~B");}
};

void bad() { RAII(); RAII2(); } // destruct instantly because scope is itself
void good() { RAII a; RAII2 b; } // destruct according to containing scope
sethk commented 5 years ago

Just to illustrate my point, I totally forgot that ImGui::End() needs to be called regardless of the return value of ImGui::Begin(), because that's how you handle the case where the window is open but collapsed. Anyway, if you want to play along at home I'll be gradually updating this header in this branch as I test it:

https://github.com/sethk/imgui/blob/raii/misc/raii/imgui_raii.h

ocornut commented 5 years ago

@sethk Looking good!

Some pieces of feedback:

jdumas commented 5 years ago

This looks good! Another minor comment I have is: we should probably remove the copy/move constructors of those classes, even though it's unlikely someone will make the mistake of copying those guys. A simple macro can facilitate the job for that:

#define IMGUI_DELETE_MOVE_COPY(Base)     \
    Base(Base&&) = delete;                 \
    Base& operator=(Base&&) = delete;      \
    Base(const Base&) = delete;            \
    Base& operator=(const Base&) = delete; \

(If you want to stay pre-C++11 you can declare them private and not define them instead of deleting them.)

ocornut commented 5 years ago

If you want to stay pre-C++11

I'm happy with enforcing C++11 as a requirement for those C++ extension. (In fact, I don't rule out doing the same for core imgui maybe next year.. will see.)

sethk commented 5 years ago
  • IsExpanded should be IsContentsVisible to be more accurate and explanatory.

How about IsContentVisible? I'm only objecting to the grammar...

  • Even though it is practical, it is a incorrect using the Im prefix instead of ImGui here. In the codebase, Im is reserved for things that don't sit on top of ImGui functions or context.

Sounds good, but there is already an ImGuiWindow struct in the global namespace due to imgui_internal.h. What name should I use for a Begin()/End() wrapper?

oberrich commented 5 years ago

I could imagine using std::unique_ptr to create so-called finally-actions rather than writing a class for each function pair. It makes it a lot more trivial to implement the individual guards.

See this proof of concept for reference

// Original 'finally' function from https://softwareengineering.stackexchange.com/a/308747
template<typename F>
[[nodiscard]] auto im_finally(F f, bool active = true) noexcept(noexcept(F(std::move(f)))) {
  auto x = [f = std::move(f)](void*){ f(); };
  return std::unique_ptr<void, decltype(x)>((void*)(active), std::move(x));
}

[[nodiscard]] inline auto im_window(char const *name, bool *open = nullptr, ImGuiWindowFlags flags = 0) {
  return im_finally([]{ ImGui::End(); }, ImGui::Begin(name, open, flags));
}

// Usage

  if (auto window = im_window("cpp_im_gui", ...)) {

    // add buttons or whatever

  } // ImGui::Begin == true -> unique_ptr.get() != nullptr -> call deleter
    // ImGui::Begin == false -> unique_ptr.get() == nullptr -> dont call deleter

Here is a working demo of the concept https://ideone.com/KDJz25. More elaborate tests here https://godbolt.org/z/KPcgP8

@ocornut I would love to hear your thoughts on this approach

meshula commented 5 years ago

@obermayrrichard Using unique_ptr like that is clever! For reference, I use the finally in Microsoft's GSL implementation - https://github.com/Microsoft/GSL/blob/master/include/gsl/gsl_util

ocornut commented 5 years ago

@sethk Apologies for later answer.

How about IsContentVisible? I'm only objecting to the grammar...

Sure.

Sounds good, but there is already an ImGuiWindow struct in the global namespace due to imgui_internal.h. What name should I use for a Begin()/End() wrapper?

Good question, I don't have the answer to this unfortunately. Let us think about it .. or I wonder if instead we could opt for a specific prefix to denote the type of functions we are discussing here..

@obermayrrichard tbh I find this solution really unnecessary and overkill - harder to understand, harder to debug/step into, probably slower in non-optimized builds, it raises the user C++ minimum proficiency requirements, drag unnecessary dependencies; and none of those things are in the spirit of dear imgui. I see no value in it considering the solution proposed initially is so simple and obvious to write and maintain.

ratchetfreak commented 5 years ago

And the solution breaks down a bit if you do:

auto win1 = im_window("cpp_im_gui", ...);
if(win1){

}

//win1 isn't closed at this point
auto win2 = im_window("cpp_im_gui", ...);
if(win2){

}

because win1 doesn't get destroyed before win2 gets created.

Also if the user doesn't create the variable it gets immediately destroyed after the condition is checked with no warning.

if(im_window("cpp_im_gui", ...)){
    //window is already closed
}

IOW a bit too fragile in normal use for my tastes.

jdumas commented 5 years ago

But that's an issue with both RAII and the unique_ptr no? To be honest I have no idea how this unique_ptr solution works, and I've been coding in C++ for 10 years...

Good question, I don't have the answer to this unfortunately. Let us think about it .. or I wonder if instead we could opt for a specific prefix to denote the type of functions we are discussing here..

How about Impp, or ImGuipp or something like this, to denote the C++ side of it. Or maybe ImStdlib like the filename? Or use its own namespace to be more C++y?

ratchetfreak commented 5 years ago

Actually it's more similar how a scope_guard works for a mutex. You need to create a local variable for the guard and make sure it goes out of scope at the correct time.

With std::unique_ptr it's less of an issue because the unique_ptr itself is your handle to the resource it guards so you are not going to forget making a variable for it.

oberrich commented 5 years ago

Writing a scope-guard class and using a finally action like this is roughly equivalent, imo it's just less code and less painful to implement the individual.

Both writing an RAII class and finally have the same drawbacks in terms of pitfalls users can run into. They are both RAII classes in the end, the one being written manually, the other one taking unique_ptrs existing code, abusing the internal pointer as our bool variable and moving the deleter into it which gets called by its dtor if ptr/our bool is zero.

I don't see any obvious way of how we could prevent users doing if (scope_guard{}). I have tried doing [[nodiscard]] operator bool() {...}. The only way to explicitly disallow this sort of behavior I can think of is removing the bool conversion operator altogether.

jdumas commented 5 years ago

Well if it's the same thing then why not stick to the version with no template porn?

oberrich commented 5 years ago

Well if it's the same thing then why not stick to the version with no template porn?

As I said previously

imo it's just less code and less painful to implement the individual [function pairs].

In the end it comes down to preference. I don't really care which version will be implemented since I'm not actively involved in imgui's development. I don't think it's worth discussing my proposal any further since it will only end in a pointless discussion like tabs vs. spaces.

ocornut commented 5 years ago

Sounds good, but there is already an ImGuiWindow struct in the global namespace due to imgui_internal.h. What name should I use for a Begin()/End() wrapper?

@sethk, as per #1563, the word Scope could be appropriate here?

ImGuiWindowScope
ImGuiTreeNodeScope
ImGuiIDScope

ImGuiScopedWindow
ImGuiScopedTreeNode
ImGuiScopedID

Or use its own namespace to be more C++y?

I like this idea because it more clearly reinforce the the idea that those functions are an add-on over the regular functions. But I don't know how their actual user would feel.

// RAII
ImGuiR::Window
ImGuiR::TreeNode
ImGuiR::ID

// Scope
ImGuiS::Window
ImGuiS:::TreeNode
ImGuiS::ID

// Scope
ImGuiScope::Window
ImGuiScope::TreeNode
ImGuiScope::ID

As you can infer from me aimlessly throwing so many choices out, I'm currently not particularly convinced/thrilled by any of those names.

I am closing #1563 because this topic is worth discussing but also check Ziv proposal: https://github.com/ocornut/imgui/pull/1563/files

jdumas commented 5 years ago

How about ImScope::* then?

// Scope
ImScope::Window
ImScope::TreeNode
ImScope::ID

It's short and readable, while carrying the idea of RAII. (We already have plenty of ImDraw, ImVec, etc.)

ocornut commented 5 years ago

Good for me, ImScope or ImScoped.

jdumas commented 5 years ago

Yeah ImScoped may be more semantic.

sethk commented 5 years ago

Okay, great. Below is the current version (mostly untested). Are any of these helpers obviously useless? Maybe TreePush is too low level?

Also, I personally would find a wrapper for Columns useful, but ImGui doesn't have Push/Pop calls for that.

#pragma once

#include "imgui.h"

namespace ImScoped
{
    struct Window
    {
        bool IsContentVisible;

        Window(const char* name, bool* p_open = NULL, ImGuiWindowFlags flags = 0) { IsContentVisible = ImGui::Begin(name, p_open, flags); }
        ~Window() { ImGui::End(); }

        operator bool() { return IsContentVisible; }

        Window(Window &&) = delete;
        Window &operator=(Window &&) = delete;
        Window(const Window &) = delete;
        Window &operator=(Window &) = delete;
    };

    struct Child
    {
        bool IsContentVisible;

        Child(const char* str_id, const ImVec2& size = ImVec2(0,0), bool border = false, ImGuiWindowFlags flags = 0) { IsContentVisible = ImGui::BeginChild(str_id, size, 0); }
        Child(ImGuiID id, const ImVec2& size = ImVec2(0,0), bool border = false, ImGuiWindowFlags flags = 0) { IsContentVisible = ImGui::BeginChild(id, size, 0); }
        ~Child() { ImGui::EndChild(); }

        operator bool() { return IsContentVisible; }

        Child(Child &&) = delete;
        Child &operator=(Child &&) = delete;
        Child(const Child &) = delete;
        Child &operator=(Child &) = delete;
    };

    struct Font
    {
        Font(ImFont* font) { ImGui::PushFont(font); }
        ~Font() { ImGui::PopFont(); }

        Font(Font &&) = delete;
        Font &operator=(Font &&) = delete;
        Font(const Font &) = delete;
        Font &operator=(Font &) = delete;
    };

    struct StyleColor
    {
        StyleColor(ImGuiCol idx, ImU32 col) { ImGui::PushStyleColor(idx, col); }
        StyleColor(ImGuiCol idx, const ImVec4& col) { ImGui::PushStyleColor(idx, col); }
        ~StyleColor() { ImGui::PopStyleColor(); }

        StyleColor(StyleColor &&) = delete;
        StyleColor &operator=(StyleColor &&) = delete;
        StyleColor(const StyleColor &) = delete;
        StyleColor &operator=(StyleColor &) = delete;
    };

    struct StyleVar
    {
        StyleVar(ImGuiStyleVar idx, float val) { ImGui::PushStyleVar(idx, val); }
        StyleVar(ImGuiStyleVar idx, const ImVec2& val) { ImGui::PushStyleVar(idx, val); }
        ~StyleVar() { ImGui::PopStyleVar(); }

        StyleVar(StyleVar &&) = delete;
        StyleVar &operator=(StyleVar &&) = delete;
        StyleVar(const StyleVar &) = delete;
        StyleVar &operator=(StyleVar &) = delete;
    };

    struct ItemWidth
    {
        ItemWidth(float item_width) { ImGui::PushItemWidth(item_width); }
        ~ItemWidth() { ImGui::PopItemWidth(); }

        ItemWidth(ItemWidth &&) = delete;
        ItemWidth &operator=(ItemWidth &&) = delete;
        ItemWidth(const ItemWidth &) = delete;
        ItemWidth &operator=(ItemWidth &) = delete;
    };

    struct TextWrapPos
    {
        TextWrapPos(float wrap_pos_x = 0.0f) { ImGui::PushTextWrapPos(wrap_pos_x); }
        ~TextWrapPos() { ImGui::PopTextWrapPos(); }

        TextWrapPos(TextWrapPos &&) = delete;
        TextWrapPos &operator=(TextWrapPos &&) = delete;
        TextWrapPos(const TextWrapPos &) = delete;
        TextWrapPos &operator=(TextWrapPos &) = delete;
    };

    struct AllowKeyboardFocus
    {
        AllowKeyboardFocus(bool allow_keyboard_focus) { ImGui::PushAllowKeyboardFocus(allow_keyboard_focus); }
        ~AllowKeyboardFocus() { ImGui::PopAllowKeyboardFocus(); }

        AllowKeyboardFocus(AllowKeyboardFocus &&) = delete;
        AllowKeyboardFocus &operator=(AllowKeyboardFocus &&) = delete;
        AllowKeyboardFocus(const AllowKeyboardFocus &) = delete;
        AllowKeyboardFocus &operator=(AllowKeyboardFocus &) = delete;
    };

    struct ButtonRepeat
    {
        ButtonRepeat(bool repeat) { ImGui::PushButtonRepeat(repeat); }
        ~ButtonRepeat() { ImGui::PopButtonRepeat(); }

        ButtonRepeat(ButtonRepeat &&) = delete;
        ButtonRepeat &operator=(ButtonRepeat &&) = delete;
        ButtonRepeat(const ButtonRepeat &) = delete;
        ButtonRepeat &operator=(ButtonRepeat &) = delete;
    };

    struct ID
    {
        ID(const char* str_id) { ImGui::PushID(str_id); }
        ID(const char* str_id_begin, const char* str_id_end) { ImGui::PushID(str_id_begin, str_id_end); }
        ID(const void* ptr_id) { ImGui::PushID(ptr_id); }
        ID(int int_id) { ImGui::PushID(int_id); }
        ~ID() { ImGui::PopID(); }

        ID(ID &&) = delete;
        ID &operator=(ID &&) = delete;
        ID(const ID &) = delete;
        ID &operator=(ID &) = delete;
    };

    struct Combo
    {
        bool IsOpen;

        Combo(const char* label, const char* preview_value, ImGuiComboFlags flags = 0) { IsOpen = ImGui::BeginCombo(label, preview_value, flags); }
        ~Combo() { if (IsOpen) ImGui::EndCombo(); }

        operator bool() { return IsOpen; }

        Combo(Combo &&) = delete;
        Combo &operator=(Combo &&) = delete;
        Combo(const Combo &) = delete;
        Combo &operator=(Combo &) = delete;
    };

    struct TreeNode
    {
        bool IsOpen;

        TreeNode(const char* label) { IsOpen = ImGui::TreeNode(label); }
        TreeNode(const char* str_id, const char* fmt, ...) IM_FMTARGS(3) { va_list ap; va_start(ap, fmt); IsOpen = ImGui::TreeNodeV(str_id, fmt, ap); va_end(ap); }
        TreeNode(const void* ptr_id, const char* fmt, ...) IM_FMTARGS(3) { va_list ap; va_start(ap, fmt); IsOpen = ImGui::TreeNodeV(ptr_id, fmt, ap); va_end(ap); }
        ~TreeNode() { if (IsOpen) ImGui::TreePop(); }

        operator bool() { return IsOpen; }

        TreeNode(TreeNode &&) = delete;
        TreeNode &operator=(TreeNode &&) = delete;
        TreeNode(const TreeNode &) = delete;
        TreeNode &operator=(TreeNode &) = delete;
    };

    struct TreeNodeV
    {
        bool IsOpen;

        TreeNodeV(const char* str_id, const char* fmt, va_list args) IM_FMTLIST(3) { IsOpen = ImGui::TreeNodeV(str_id, fmt, args); }
        TreeNodeV(const void* ptr_id, const char* fmt, va_list args) IM_FMTLIST(3) { IsOpen = ImGui::TreeNodeV(ptr_id, fmt, args); }
        ~TreeNodeV() { if (IsOpen) ImGui::TreePop(); }

        operator bool() { return IsOpen; }

        TreeNodeV(TreeNodeV &&) = delete;
        TreeNodeV &operator=(TreeNodeV &&) = delete;
        TreeNodeV(const TreeNodeV &) = delete;
        TreeNodeV &operator=(TreeNodeV &) = delete;
    };

    struct TreeNodeEx
    {
        bool IsOpen;

        TreeNodeEx(const char* label, ImGuiTreeNodeFlags flags = 0) { IsOpen = ImGui::TreeNodeEx(label, flags); }
        TreeNodeEx(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, ...) IM_FMTARGS(4) { va_list ap; va_start(ap, fmt); IsOpen = ImGui::TreeNodeExV(str_id, flags, fmt, ap); va_end(ap); }
        TreeNodeEx(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, ...) IM_FMTARGS(4) { va_list ap; va_start(ap, fmt); IsOpen = ImGui::TreeNodeExV(ptr_id, flags, fmt, ap); va_end(ap); }
        ~TreeNodeEx() { if (IsOpen) ImGui::TreePop(); }

        operator bool() { return IsOpen; }

        TreeNodeEx(TreeNodeEx &&) = delete;
        TreeNodeEx &operator=(TreeNodeEx &&) = delete;
        TreeNodeEx(const TreeNodeEx &) = delete;
        TreeNodeEx &operator=(TreeNodeEx &) = delete;
    };

    struct TreeNodeExV
    {
        bool IsOpen;

        TreeNodeExV(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args) IM_FMTLIST(4) { IsOpen = ImGui::TreeNodeExV(str_id, flags, fmt, args); }
        TreeNodeExV(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args) IM_FMTLIST(4) { IsOpen = ImGui::TreeNodeExV(ptr_id, flags, fmt, args); }
        ~TreeNodeExV() { if (IsOpen) ImGui::TreePop(); }

        operator bool() { return IsOpen; }

        TreeNodeExV(TreeNodeExV &&) = delete;
        TreeNodeExV &operator=(TreeNodeExV &&) = delete;
        TreeNodeExV(const TreeNodeExV &) = delete;
        TreeNodeExV &operator=(TreeNodeExV &) = delete;
    };

    struct TreePush
    {
        TreePush(const char* str_id) { ImGui::TreePush(str_id); }
        TreePush(const void* ptr_id = NULL) { ImGui::TreePush(ptr_id); }
        ~TreePush() { ImGui::TreePop(); }

        TreePush(TreePush &&) = delete;
        TreePush &operator=(TreePush &&) = delete;
        TreePush(const TreePush &) = delete;
        TreePush &operator=(TreePush &) = delete;
    };

    struct Menu
    {
        bool IsOpen;

        Menu(const char* label, bool enabled = true) { IsOpen = ImGui::BeginMenu(label, enabled); }
        ~Menu() { if (IsOpen) ImGui::EndMenu(); }

        operator bool() { return IsOpen; }

        Menu(Menu &&) = delete;
        Menu &operator=(Menu &&) = delete;
        Menu(const Menu &) = delete;
        Menu &operator=(Menu &) = delete;
    };

    struct Popup
    {
        bool IsOpen;

        Popup(const char* str_id, ImGuiWindowFlags flags = 0) { IsOpen = ImGui::BeginPopup(str_id, flags); }
        ~Popup() { if (IsOpen) ImGui::EndPopup(); }

        operator bool() { return IsOpen; }

        Popup(Popup &&) = delete;
        Popup &operator=(Popup &&) = delete;
        Popup(const Popup &) = delete;
        Popup &operator=(Popup &) = delete;
    };

    struct PopupContextItem
    {
        bool IsOpen;

        PopupContextItem(const char* str_id = NULL, int mouse_button = 1) { IsOpen = ImGui::BeginPopupContextItem(str_id, mouse_button); }
        ~PopupContextItem() { if (IsOpen) ImGui::EndPopup(); }

        operator bool() { return IsOpen; }

        PopupContextItem(PopupContextItem &&) = delete;
        PopupContextItem &operator=(PopupContextItem &&) = delete;
        PopupContextItem(const PopupContextItem &) = delete;
        PopupContextItem &operator=(PopupContextItem &) = delete;
    };

    struct PopupContextWindow
    {
        bool IsOpen;

        PopupContextWindow(const char* str_id = NULL, int mouse_button = 1, bool also_over_items = true) { IsOpen = ImGui::BeginPopupContextWindow(str_id, mouse_button, also_over_items); }
        ~PopupContextWindow() { if (IsOpen) ImGui::EndPopup(); }

        operator bool() { return IsOpen; }

        PopupContextWindow(PopupContextWindow &&) = delete;
        PopupContextWindow &operator=(PopupContextWindow &&) = delete;
        PopupContextWindow(const PopupContextWindow &) = delete;
        PopupContextWindow &operator=(PopupContextWindow &) = delete;
    };

    struct PopupContextVoid
    {
        bool IsOpen;

        PopupContextVoid(const char* str_id = NULL, int mouse_button = 1) { IsOpen = ImGui::BeginPopupContextVoid(str_id, mouse_button); }
        ~PopupContextVoid() { if (IsOpen) ImGui::EndPopup(); }

        operator bool() { return IsOpen; }

        PopupContextVoid(PopupContextVoid &&) = delete;
        PopupContextVoid &operator=(PopupContextVoid &&) = delete;
        PopupContextVoid(const PopupContextVoid &) = delete;
        PopupContextVoid &operator=(PopupContextVoid &) = delete;
    };

    struct PopupModal
    {
        bool IsOpen;

        PopupModal(const char* name, bool* p_open = NULL, ImGuiWindowFlags flags = 0) { IsOpen = ImGui::BeginPopupModal(name, p_open, flags); }
        ~PopupModal() { if (IsOpen) ImGui::EndPopup(); }

        operator bool() { return IsOpen; }

        PopupModal(PopupModal &&) = delete;
        PopupModal &operator=(PopupModal &&) = delete;
        PopupModal(const PopupModal &) = delete;
        PopupModal &operator=(PopupModal &) = delete;
    };

    struct DragDropSource
    {
        bool IsOpen;

        DragDropSource(ImGuiDragDropFlags flags = 0) { IsOpen = ImGui::BeginDragDropSource(flags); }
        ~DragDropSource() { if (IsOpen) ImGui::EndDragDropSource(); }

        operator bool() { return IsOpen; }

        DragDropSource(DragDropSource &&) = delete;
        DragDropSource &operator=(DragDropSource &&) = delete;
        DragDropSource(const DragDropSource &) = delete;
        DragDropSource &operator=(DragDropSource &) = delete;
    };

    struct ClipRect
    {
        ClipRect(const ImVec2& clip_rect_min, const ImVec2& clip_rect_max, bool intersect_with_current_clip_rect) { ImGui::PushClipRect(clip_rect_min, clip_rect_max, intersect_with_current_clip_rect); }
        ~ClipRect() { ImGui::PopClipRect(); }

        ClipRect(ClipRect &&) = delete;
        ClipRect &operator=(ClipRect &&) = delete;
        ClipRect(const ClipRect &) = delete;
        ClipRect &operator=(ClipRect &) = delete;
    };

    struct ChildFrame
    {
        bool IsOpen;

        ChildFrame(ImGuiID id, const ImVec2& size, ImGuiWindowFlags flags = 0) { IsOpen = ImGui::BeginChildFrame(id, size, flags); }
        ~ChildFrame() { ImGui::EndChildFrame(); }

        operator bool() { return IsOpen; }

        ChildFrame(ChildFrame &&) = delete;
        ChildFrame &operator=(ChildFrame &&) = delete;
        ChildFrame(const ChildFrame &) = delete;
        ChildFrame &operator=(ChildFrame &) = delete;
    };

} // namespace ImScoped
oberrich commented 5 years ago

@sethk The deleted move-constructors and -assignment-operators are not necessary. In C++ if you declare/delete a copy-constructor/-assignment-operator, no move-constructor/-assignment-operator will be implicitly created.

edit Also the operator bool() should be changed to explicit operator bool() const to enable contextual conversion to bool.

edit 2 Your single argument constructors should be marked explicit, I see a lot of likely scenarios for bugs there.

ocornut commented 5 years ago
bool is_open = TreeNode(...);
bool is_hovered = IsItemHovered();
if (is_open)
{
   TreePop();
}

Which are semi-frequent (all the IsItemXXX data are not saved by TreePush/TreePop because it would be too expensive). How would be replicate that with the RAII version?


This is looking good overall! I think it could be considered as a misc/cpp/imgui_scoped.h but I'd prefer to wait until it has been tested a little more. Maybe you can maintain a gist (which has history) or a PR that's easy to grab and we can encourage users to give it a try?

jdumas commented 5 years ago

What's wrong with the operator bool() not being explicit? Similarly, I don't see what can go wrong with the not-explicit constructor if the copy/move methods are deleted.

Also, I find it preferable to explicitly delete both the move and copy operator/constructor (following the Python motto that explicit is better than implicit).

oberrich commented 5 years ago

With the deleted copy ctor and assignment ops the constructors should behave correctly, right. I wrote the response from my phone so I didn't bother to read through the entire source. My mistake. Using explicit constructors does however make the error messages when trying to construct via implicit conversion a lot clearer and more understandable imo.

As for the bool: "explicit operator boolean() const" is just the recommended signature for what we want: contextually convertable to bool. Also const correctness is important, at the very least the operator should have const at the end.

Explicitally deleting the move constructor shouldn't cause any bugs but it bloats the file quote a lot. This one surely is purely a matter of taste. It should be apparent to anyone who cares to look at the ctors and assignment operators that a deleted copy ctor implies a deleted move ctor however. Normal users who lack a more complete understanding of C++ will ignore them and live happily ever after anyways^^.

jdumas commented 5 years ago

Explicitally deleting the move constructor shouldn't cause any bugs but it bloats the file quote a lot.

Yeah, that's why we should use a macro like I suggested earlier in the thread. I personally prefer to avoid relying on arcane knowledge of C++ to infer what is implicitly defined or not in my code, but that's just my opinion.

As for the bool: "explicit operator boolean() const" is just the recommended signature for what we want: contextually convertable to bool.

Agreed for the const correctness. But do you have any example of a bad behavior that could happen without the explicit keyword? E.g. it means you cannot write ImScoped::Window foo; foo() + 1 ?

oberrich commented 5 years ago

@jdumas without explicit:

bool b = window{}; // allowed, bad/might lead to bugs
window wnd{};
bool c = wnd; // allowed, useful

with explicit

bool b = window{}; // disallowed, good
window wnd{};
bool c = wnd; // disallowed, neutral/taste
bool d = (bool)wnd; // allowed, good

for both versions this works as well

imwindow window{};
if (window) {
  //...
}

The explicit version is a bit more safer imo. Depends on what behavior you consider more useful. edit Also the "Safe Bool Problem" might be an issue to consider here (https://i.imgur.com/LQrHzIg.png). Disclaimer: I haven't thought About this longer than a few seconds, I might be completely wrong on this one. I will investigate further when I'm home again on Sunday.

jdumas commented 5 years ago

Ok, interesting!

sethk commented 5 years ago

@ocornut Turns out I wasn't extracting methods without arguments, so I missed a few calls: Group, MainMenuBar, MenuBar, Tooltip, DragDropTarget. Those are useful to wrap, right?

With regard to IsItemXXX(), won't this work?:

ImScoped::TreeNode node(...);
bool is_hovered = ImGui::IsItemHovered();
if (node.IsOpen) // or just if (node)
{
   ...
}

Is it confusing because we're capturing the IsOpen state but other IsXXX state isn't available?

I will make a pull request. It would be great to have more testing and suggestions.

ocornut commented 5 years ago

Those are useful to wrap, right?

Yes.

With regard to IsItemXXX(), won't this work?:

This seems to work but then if you use it this way, how do you handle multiple tree nodes? If they are defined in the same scope they need destruction at the right time, so users needs to declare only 1 tree node per-scope and explicitly add braces, which feels like it is defeating a little the purpose of this idea. It makes a little unclear how exactly those wrapper would be used in real code, perhaps you could try converting a few meaningful chunks of the demo so see how it would work?

Is it confusing because we're capturing the IsOpen state but other IsXXX state isn't available?

This looks right, because this state is the single one returned from the low-level function. We could softly mark IsOpen or equivalent as "[internal]" in the comment to emphasis that the bool test should be used.

I will make a pull request. It would be great to have more testing and suggestions.

Yes. For me the most important aspect of merging such PR would be: will you or someone be willing to maintain it, do you use dear imgui regularly and does your project allows you to keep up to date (both those factors would increase the likehood of maintenance). It is a bit chicken and egg-ey, but the more I have evidence of people actively using it (you included) the more I'll be comfortable with merging because I'll know issues will be reported. So I would say it is best to make the PR to incentive users but don't expect an immediate merge.

(I forgot to link to #1651 which is not talking about the same feature at all, but emerge from a similar need of facilitating the handling of stacked elements. To return back to the top-most post, the fact that Begin/BeginChild are inconsistent is a legacy issue that is confusing for users but also a hurdle to implement some features correctly. I'm expecting a major release e.g. 1.70/1.80/2.00 may change this and/or provide a configuration flag to allow the user to change the global behavior to smooth out the transition. It's mostly up to us to provide nicer error reporting and recovery)

sethk commented 5 years ago

edit 2 Your single argument constructors should be marked explicit, I see a lot of likely scenarios for bugs there.

@obermayrrichard Is there a situation currently where this can happen? This would be if somebody created a method that took an ImScoped::XXX instance, and then passed a value with the single argument type instead? This seems like it would still lead to the desired behavior, right?

oberrich commented 5 years ago

@sethk From what I see there shouldn't be any issues with the ctors even if the copy ctor/assign wasn't deleted. The worst that could happen is the class being able to get constructed from the parameters type. Best to just write some tests and see for yourself.

sethk commented 5 years ago

This seems to work but then if you use it this way, how do you handle multiple tree nodes? If they are defined in the same scope they need destruction at the right time, so users needs to declare only 1 tree node per-scope and explicitly add braces, which feels like it is defeating a little the purpose of this idea.

Yes, you definitely need a scope per tree node, but as @obermayrrichard showed above, you can use a braced initializer to both create and test a scoped wrapper at the beginning of a block (this is definitely new to me). Here are some tree nodes from the demo app:

        if (ImGui::TreeNode("Configuration##2"))
        {
            ImGui::CheckboxFlags("io.ConfigFlags: NavEnableKeyboard", (unsigned int *)&io.ConfigFlags, ImGuiConfigFlags_NavEnableKeyboard);
            ImGui::CheckboxFlags("io.ConfigFlags: NavEnableGamepad", (unsigned int *)&io.ConfigFlags, ImGuiConfigFlags_NavEnableGamepad);
            ...
            ImGui::TreePop();
            ImGui::Separator();
        }

        if (ImGui::TreeNode("Backend Flags"))
        {
            ImGuiBackendFlags backend_flags = io.BackendFlags; // Make a local copy to avoid modifying the back-end flags.
            ImGui::CheckboxFlags("io.BackendFlags: HasGamepad", (unsigned int *)&backend_flags, ImGuiBackendFlags_HasGamepad);
            ...
            ImGui::TreePop();
            ImGui::Separator();
        }

        if (ImGui::TreeNode("Style"))
        {
            ImGui::ShowStyleEditor();
            ImGui::TreePop();
            ImGui::Separator();
        }

And here's what it looks like with my wrappers:

        if (ImScoped::TreeNode node{"Configuration##2"})
        {
            ImGui::CheckboxFlags("io.ConfigFlags: NavEnableKeyboard", (unsigned int *)&io.ConfigFlags, ImGuiConfigFlags_NavEnableKeyboard);
            ImGui::CheckboxFlags("io.ConfigFlags: NavEnableGamepad", (unsigned int *)&io.ConfigFlags, ImGuiConfigFlags_NavEnableGamepad);
            ...
            ImGui::Separator();
        }

        if (ImScoped::TreeNode node{"Backend Flags"})
        {
            ImGuiBackendFlags backend_flags = io.BackendFlags; // Make a local copy to avoid modifying the back-end flags.
            ImGui::CheckboxFlags("io.BackendFlags: HasGamepad", (unsigned int *)&backend_flags, ImGuiBackendFlags_HasGamepad);
            ...
            ImGui::Separator();
        }

        if (ImScoped::TreeNode node{"Style"})
        {
            ImGui::ShowStyleEditor();
            ImGui::Separator();
        }

This works as expected. The one huge drawback is that you have to name your wrapper objects. Doing this:

        if (ImScoped::TreeNode{"Configuration##2"})
        {

compiles, but pops the tree before the block is executed. That's a pretty annoying pitfall, although possibly a little easier to diagnose than an unmatched call to TreeNode() because it doesn't crash, and you can visually see where things aren't being pushed.

Is it confusing because we're capturing the IsOpen state but other IsXXX state isn't available?

This looks right, because this state is the single one returned from the low-level function. We could softly mark IsOpen or equivalent as "[internal]" in the comment to emphasis that the bool test should be used.

Yes, or we could use classes and mark the instance variables private. =) I think I see your more general point though, that it's confusing how some state is captured by the return value of the Begin|Push|Tree functions, but you have to call context-dependent functions like IsItemXXX to access other state.

I will make a pull request. It would be great to have more testing and suggestions.

Yes. For me the most important aspect of merging such PR would be: will you or someone be willing to maintain it, do you use dear imgui regularly and does your project allows you to keep up to date (both those factors would increase the likehood of maintenance). It is a bit chicken and egg-ey, but the more I have evidence of people actively using it (you included) the more I'll be comfortable with merging because I'll know issues will be reported. So I would say it is best to make the PR to incentive users but don't expect an immediate merge.

Yeah, that makes sense. I have used Dear ImGui for several small projects recently, but I am not the type of person to update my open source dependencies unless there are strong incentives to do so. I also am in between jobs (trying to switch to realtime graphics), and it's not clear whether what I land on will allow me to use ImGui professionally, so I can't say what my personal commitment will be.

What I would like to see are these wrappers being used by the people who like me would naturally feel the need to make their own versions as shorthand, and possibly having that influence the design of future ImGui APIs. From that perspective, it's not too important that it's merged quickly or in its current form.

jdumas commented 5 years ago

Regarding unnamed variables, in this SO thread they mention an interesting trick with a macro that expands into a static_assert when using TreeNode(x), but call the correct constructor when using TreeNode foo(x).

sethk commented 5 years ago

@jdumas That is a neat trick. It's a shame it probably won't work with brace initializers, such that the form of initializing a wrapper inside a conditional wouldn't trigger it.

Person-93 commented 5 years ago

I was actually thinking about this all day yesterday. I didn't get around to seeing if someone else had written about it until now.

There were a couple of points of concern that I had, some of them I came up with workarounds some I didn't.

Person-93 commented 5 years ago

@jdumas

This looks good! Another minor comment I have is: we should probably remove the copy/move constructors of those classes, even though it's unlikely someone will make the mistake of copying those guys. A simple macro can facilitate the job for that:

#define IMGUI_DELETE_MOVE_COPY(Base)     \
    Base(Base&&) = delete;                 \
    Base& operator=(Base&&) = delete;      \
    Base(const Base&) = delete;            \
    Base& operator=(const Base&) = delete; \

(If you want to stay pre-C++11 you can declare them private and not define them instead of deleting them.)

I think that deleting the copy constructor is a good idea. It doesn't make sense to apply the style twice. But I think the move constructor should stay. If there is a particular style or set of styles that I make very often, I might have them returned from a method. If there is no copy or move constructor, that would not work. Move-assign can be useful as well. It can pop whatever style is already there, and then the destructor would pop whatver style it's replaced with.

jdumas commented 5 years ago

Well but you can only push a single style var at a time, even with this RAII stuff. If you want to wrap up a custom style, you might as well create your own RAII struct that push and pop whatever combination of styles you like no? Call me old school for not willing to deal with all this move/assign business, but I think keeping it would make implementation more complex and error-prone. E.g. even if you move an object, the destructor of the moved object will be called no? So you'd need to keep a flag to make sure not to pop() if it has been moved, make sure you don't move it twice, etc.