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

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

Open
sethk opened this issue Sep 21, 2018 · 95 comments
Open

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

sethk opened this issue Sep 21, 2018 · 95 comments

Comments

@sethk
Copy link

sethk commented Sep 21, 2018

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
Copy link
Owner

ocornut commented Sep 21, 2018

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
Copy link
Author

sethk commented Sep 21, 2018

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

@maxkunes
Copy link

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
Copy link

meshula commented Sep 23, 2018

My most used RAII object for Imgui:

class SetFont
{
public:
	SetFont(ImFont* f) { ImGui::PushFont(f); }
	~SetFont() { ImGui::PopFont(); }
};

@sethk
Copy link
Author

sethk commented Sep 24, 2018

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
Copy link

maxkunes commented Sep 24, 2018

@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
Copy link
Contributor

ice1000 commented Sep 24, 2018

Consider

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

which is less noisy.

@maxkunes
Copy link

maxkunes commented Sep 24, 2018

@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
Copy link
Contributor

ice1000 commented Sep 24, 2018

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

@ice1000
Copy link
Contributor

ice1000 commented Sep 24, 2018

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
Copy link

maxkunes commented Sep 25, 2018

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
Copy link
Author

sethk commented Sep 25, 2018

@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
Copy link

maxkunes commented Sep 25, 2018

@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
Copy link

meshula commented Sep 26, 2018

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
Copy link
Author

sethk commented Oct 5, 2018

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
Copy link
Owner

ocornut commented Oct 5, 2018

@sethk Looking good!

Some pieces of feedback:

  • Because some people would want to avoid <string> but may want to use this, this should probably be kept in a separate file than the existing imgui_stl.h. I will rename the existing folder to misc/cpp to prepare for the possibility to adding more files in there.
  • IsExpanded should be IsContentsVisible to be more accurate and explanatory.
  • 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.
  • Since everything in your class is public, you could use struct and this is what we do everywhere in imgui codebase (I also like it because it challenges C++ dogmas a little :)

ocornut added a commit that referenced this issue Oct 12, 2018
@jdumas
Copy link
Contributor

jdumas commented Oct 12, 2018

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
Copy link
Owner

ocornut commented Oct 12, 2018

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
Copy link
Author

sethk commented Oct 18, 2018

  • 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
Copy link

oberrich commented Oct 30, 2018

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
Copy link

meshula commented Oct 30, 2018

@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
Copy link
Owner

ocornut commented Oct 30, 2018

@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
Copy link

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
Copy link
Contributor

jdumas commented Oct 30, 2018

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
Copy link

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
Copy link

oberrich commented Oct 30, 2018

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
Copy link
Contributor

jdumas commented Oct 30, 2018

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

@oberrich
Copy link

oberrich commented Oct 30, 2018

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.

@TerensTare
Copy link

TerensTare commented Jun 6, 2021

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..

I know I'm some years late, but how about RaiiWindow/ScopedWindow and such?
As for the naming issue, I think we can safely write

if (RaiiWindow _("MyWindow")) // notice the _
// blah blah

which at least to me looks acceptable

@mnesarco
Copy link

mnesarco commented Jun 6, 2021

The problem with

if (RaiiWindow _("MyWindow")) // notice the _
// blah blah

Is that if you forgot to write the "_" it will compile without warning but won't work:

if (RaiiWindow ("MyWindow")) // notice the missing _
// blah blah

That is why I prefer the macro defined scope:

with_Window("My Window") {
  // blah blah
}

It is just a syntactic sugar, generates the same code but ensures that the RAII guard is named.

@TerensTare
Copy link

The problem with

if (RaiiWindow _("MyWindow")) // notice the _
// blah blah

Is that if you forgot to write the "_" it will compile without warning but won't work:

Right, that's a valid issue, but can be fixed by adding ref-qualifiers to the conversion operator. Please see https://godbolt.org/z/PeGGvqn6G.

The solution you propose is really nice (in syntax and hard-to-misuse terms) but I personally prefer avoiding macros, since the rest of the functions avoid macros.

@mnesarco
Copy link

mnesarco commented Jun 7, 2021

Right, that's a valid issue, but can be fixed by adding ref-qualifiers to the conversion operator. Please see https://godbolt.org/z/PeGGvqn6G.

Yes you are right, I just added ref qualifiers to my guards yesterday ;)

The solution you propose is really nice (in syntax and hard-to-misuse terms) but I personally prefer avoiding macros, since the rest of the functions avoid macros.

Yes it is a matter of user preferences, and i know that macros can be evil, but...

  1. The macros used are very innocent:
#define with_Whatever(...) if (Raii _ = Raii(Whatever(__VA_ARGS__)))

Internally I used other macros just to not repeat the same for every function. So i get a very small (<200 lines of code) header (including license and comments).

  1. The generated code is exactly the same
if (Raii _ = Raii(Whatever(args...))) 
  // blah blah
  1. The syntax is very terse
with_Window("Test") 
{
  with_MenuBar 
  {
    with_Menu("File") 
    {
        with_MenuItem("Edit")
            // blah blah
        with_MenuItem("Save")
           // blah blah
    }
  }
}
  1. I feel that the pros overcomes the cons, but that's my personal opinion of course .

So yes, it is a matter of personal preferences. 👍

@kfsone
Copy link
Contributor

kfsone commented Jun 9, 2021

The solution you propose is really nice (in syntax and hard-to-misuse terms) but I personally prefer avoiding macros, since the rest of the functions avoid macros.

Because ImGui is written in pseudo-C, it's very easy to forget how the language you're trying to work in actually works. Unless your constraints preclude the use of more modern C++, you can entirely avoid macros and just rely on good-old C++ object lifetimes by using method-on-a-temporary

https://gcc.godbolt.org/z/1xq737d1Y

and for the scopes, lambdas. This is how I was able to get pure RAII scoping with https://github.com/kfsone/imguiwrap

#include "imguiwrap.dear.h"
#include <array>
#include <string>

ImGuiWrapperReturnType
render_fn()  // opt-in functionality that gets called in a loop.
{
  bool quitting { false };
  dear::Begin("Window 1") && [&quitting](){   // or make quitting a static so capture not required.
    dear::Text("This is window 1");
    dear::Selectable("Click me to quit", &quitting);
  };
  if (quitting)
    return 0;

  dear::Begin("Window 2", nullptr, ImGuiWindowFlags_AlwaysAutoResize) && [](){
    static constexpr size_t boarddim = 3;
    static std::array<std::string, boarddim * boarddim> board { "X", "O", "O", "O", "X", "O", "O", "X", " " };
    dear::Table("0s and Xs", 3, ImGuiTableFlags_Borders) && [](){
      for (const auto& box : board) {
        ImGui::TableNextColumn();
        dear::Text(box);
      }
    };
  };
}

int main(int argc, const char* argv[])
{
    return imgui_main(argv, argv, my_render_fn);
}

@GasimGasimzada
Copy link

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 :(

Is there any information documentation or reference about this? Looking at the imgui_demo, I do not fully understand when the End statements need to be inside the Begin condition scope vs outside of it:

if (ImGui::Begin(...)) {

}

// https://github.com/ocornut/imgui/blob/master/imgui_demo.cpp#L2479
ImGui::End();

if (ImGui::BeginChild(...)) {

}

// https://github.com/ocornut/imgui/blob/master/imgui_demo.cpp#L3107
ImGui::EndChild();

if (ImGui::BeginTable(...)) {

  // https://github.com/ocornut/imgui/blob/master/imgui_demo.cpp#L553
  ImGui::EndTable();
}

if (ImGui::MenuBar(...)) {

  // https://github.com/ocornut/imgui/blob/master/imgui_demo.cpp#L2977
  ImGui::EndMenuBar();
}

if (ImGui::MainMenuBar(...)) {

  // https://github.com/ocornut/imgui/blob/master/imgui_demo.cpp#L6421
  ImGui::EndMainMenuBar();
}

I am thinking of implementing my own RAII implementation and these two implementations matter in terms of when to apply this:

class Window {
public:
   Window(...) { mOpen = ImGui::BeginWindow(...); }

   ~Window() { ImGui::EndWindow(); }
private:
  bool mOpen = false;
};

class MenuBar {
public:
   MenuBar(...) { mOpen = ImGui::BeginMenuBar(...); }

   ~MenuBar() {
     if (mOpen)
       ImGui::EndMenuBar();
    }
private:
  bool mOpen = false;
};

@ocornut
Copy link
Owner

ocornut commented Aug 8, 2022 via email

@mnesarco
Copy link

Please don't implement RAII guards again and again and again.... There are many attempts already.
I implemented one of them myself, but not much people use it:

https://github.com/mnesarco/imgui_sugar

#include <imgui/imgui.h>
#include <imgui_sugar.hpp>

// ...

    static int left = 0, right = 0;
    ImGui::SetNextWindowPos(ImVec2(30, 50), ImGuiCond_FirstUseEver);
    set_StyleColor(ImGuiCol_WindowBg, ImVec4{0.88f, 0.88f, 0.88f, 1.0f});        
    set_StyleColor(ImGuiCol_Text, 0xff000000);

    with_Window("Test Window", nullptr, ImGuiWindowFlags_AlwaysAutoResize) {

        ImGui::Text("Hello");
        
        with_Group {
            ImGui::Text("Left %d", left);
            if (ImGui::Button("Incr Left"))
                ++left;
        }
        
        ImGui::SameLine();
        
        with_Group {
            set_StyleColor(ImGuiCol_Text, 0xffff0000);
        
            ImGui::Text("Right %d", right);
        
            if (ImGui::Button("Incr Right"))
                ++right;
        
            with_Child("##scrolling", ImVec2(200, 80)) {

                ImGui::Text("More text ...");
                ImGui::Text("More text ...");
                ImGui::Text("More text ...");
                
                with_StyleColor(ImGuiCol_Text, ImVec4{ 0, 0.5f, 0, 1.0f })
                    ImGui::Text("More text ...");
                
                ImGui::Text("More text ...");
                
                with_StyleColor(ImGuiCol_Text, ImVec4{ 0.5f, 0.0f, 0, 1.0f }) {
                    ImGui::Text("More text ...");
                    ImGui::Text("More text ...");
                }
            }
        }

        ImGui::Text("Bye...");
    }    


// ...

There are different approaches, Lambdas, Macros, ... Use one of the existent projects...

@GasimGasimzada
Copy link

I have created my own RAII implementation that I am really happy with. It works in the following way:

if (auto table = Table("MyTable", 3)) {
  // covers 95% of the use-cases for table
  // for me
  table.row("I am a text", glm::vec3(2.5f), 25.0f);

  // Custom implementation
  ImGui::TableNextRow();
  ImGui::TableNextColumn();
  ImGui::Button("I want a button inside this column");
  // ..other cols as well
}

if (auto _ = Window("Window title", open)) {
  ImGui::Text("Inside the window");
}

I may create a Macro around the API to make it cleaner but it is not important to me at the moment and it has already fixed one of the error-prone things for me, which was mismatching ends, especially when you have lots of UI code and it is easy to miss.

@sugrob9000
Copy link
Contributor

sugrob9000 commented Mar 10, 2023

In C++17 (I think), you can get C++ to generate most of the above API for you like so:

namespace ImScoped {
namespace detail {
template <auto Begin, auto End, bool UnconditionalEnd = false> class Widget {
    bool shown;
public:
    explicit Widget (auto&&... a)
        : shown{
            [] (auto&&... aa) {
                return Begin(std::forward<decltype(aa)>(aa)...);
            } (std::forward<decltype(a)>(a)...)
        } {}
    ~Widget () { if (UnconditionalEnd || shown) End(); }
    explicit operator bool () const& { return shown; }
    explicit operator bool () && = delete;
};
} // namespace detail

using Window = detail::Widget<ImGui::Begin, ImGui::End, true>;
using TabBar = detail::Widget<ImGui::BeginTabBar, ImGui::EndTabBar>;
using TabItem = detail::Widget<ImGui::BeginTabItem, ImGui::EndTabItem>;
using Table = detail::Widget<ImGui::BeginTable, ImGui::EndTable>;
// etc.
} // namespace ImScoped

Deleting the rvalue-ref overload of operator bool protects you from accidentally writing

if (ImScoped::Window(...)) // Wrong! the temporary would be destroyed immediately

If you wish to add methods, you can use

struct Table: detail::Widget<ImGui::Begin, ImGui::End> {
    using Widget::Widget;
    void row ();
    // etc.
};

Regular parameter pack forwarding fails when omitting defaulted parameters, but wrapping it another time through a variadic lambda works.

One downside is that, ironically, this approach fails for when the function has actual overloads, because you can't bind the non-type template parameter Begin to an overload set. You can make it compile by disambiguating the overload set with static_cast, but that yet again loses information about defaulted arguments. As far as I can see, only BeginChild is out.

@ocornut
Copy link
Owner

ocornut commented Oct 9, 2024

Hello,

Better late than never, I would like to resume work on this and possibly merge it.
@sethk are you still around and this is something you could re-run/regenerate?

I am a little bit concerned by the fact that this design requires a local variable name every-time, don't people find this to be a non-trivial annoyance to have to name something that you didn't have to name before?

At the same time, I don't imagine there's a better solution. I'm afraid I don't really fancy the macro+template+using LINE solution of imgui_sugar (happy that it works for some, but I don't think this is a code style we want to merge or promote). That said, if we end up merging @sethk version of imgui_scoped.h we can always add a link to imgui_sugar on top of the file for people who may prefer this style.


I am concerned that dropping the verb in some instances may be misleading.
In particular, the behavior of the ID Stack is better conveyed by PushID(...) than by ScopedID(...).


Then we have to figure out are the exact names.
I think I am the one who first suggested ImScoped:: but I think it's a bad name, for two reasons:

  • Minor: Using Im prefix instead of ImGui is inconsistent with our general naming principles. Im tends to be systems lower-level than ImGui. I would be willing to ignore that tho.
  • The names being too different makes user code inconsistent with other widgets/code being called locally (consider the possibility of mixing both styles when needed):
  if (ImScoped::Window window("My Window"))
  {
      ImScoped::StyleColor(ImGuiCol_Text, ImVec4(1,0,0,1));
      ImGui::Text("A red hello!");
      for (int n = 0; n < 10; n++)
      {
          ImScoped::ID id(n);
          ImGui::Button("Button");
      }
  }

I think it should be in ImGui namespace with a prefix, e.g.

  if (ImGui::ScopedWindow window("My Window"))
  {
      ImGui::ScopedStyleColor(ImGuiCol_Text, ImVec4(1,0,0,1));
      ImGui::Text("A red hello!");
      for (int n = 0; n < 10; n++)
      {
          ImGui::ScopedID id(n);
          ImGui::Button("Button");
      }
  }

This seems more correct though longer.
What do people think?


My intuition is this desire has been mostly caused by two things:

  • The Begin/End/BeginChild/EndChild inconsistency, which is a huge debacle. I am hoping to fix it within the upcoming year.
  • The lack of error recovery, which was recently improved, see https://github.com/ocornut/imgui/wiki/Error-Handling (for avoidance of doubt, the current default behavior is still an assert but I am hoping to change the default to tooltip when it is better battle-tested).

So I admit I am still puzzled why people prefer having to name things over this:

if (ImGui::Begin("My Window"))
{
    ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1,0,0,1));
    ImGui::Text("A red hello!");
    for (int n = 0; n < 10; n++)
    {
        ImGui::PushID(n);
        ImGui::Button("Button");
        ImGui::PopID();
    }
    ImGui::PopStyleColor();
    ImGui::End()
}

BUT I FEEL i am utterly biased because I know how to not trip on the Begin/End thing.
Here I have intentionally moved the End() inside the brace (which is currently incorrect) to reflect how it would look after the fix, imho it is saner than any of the above (as well as more flexible) but hey....


Manually edited selected bits to reflect how I think it should look:

// dear imgui: RAII/scoped helpers
// see https://github.com/ocornut/imgui/issues/2096

// Usage:
/*
if (ImGui::ScopedWindow window("My Window"))
{
    ImGui::ScopedStyleColor(ImGuiCol_Text, ImVec4(1,0,0,1));
    ImGui::Text("A red hello!");
    for (int n = 0; n < 10; n++)
    {
        ImGui::ScopedID id(n);
        ImGui::Button("Button");
    }
}
*/

#pragma once

#ifndef IMGUI_DISABLE

// Move not allowed, Copy not allowed
#define IMGUI_SCOPED_DISABLE_MOVE_AND_COPY(Base)    \
        Base(Base&&) = delete;                      \
        Base &operator=(Base&&) = delete;           \
        Base(const Base&) = delete;                 \
        Base& operator=(const Base&) = delete

namespace ImGui
{

// Scoped wrapper for Begin()/End()
struct  ScopedWindow
{
    bool    IsVisible;

    ScopedWindow(const char* name, bool* p_open = NULL, ImGuiWindowFlags flags = 0) { IsVisible = Begin(name, p_open, flags); }
    ~ScopedWindow()                                             { End(); }
    explicit operator bool() const                              { return IsVisible; }
    IMGUI_SCOPED_DISABLE_MOVE_AND_COPY(ScopedWindow);
};

// Scoped wrapper by PushStyleColor()/PopStyleColor()
struct  ScopedStyleColor
{
    ScopedStyleColor(ImGuiCol idx, ImU32 col)                   { PushStyleColor(idx, col); }
    ScopedStyleColor(ImGuiCol idx, const ImVec4& col)           { PushStyleColor(idx, col); }
    ~ScopedStyleColor()                                         { PopStyleColor(); }
    IMGUI_SCOPED_DISABLE_MOVE_AND_COPY(ScopedStyleColor);
};

// Scoped wrapper for PushID()/PopID()
struct  ScopedID
{
    ScopedID(const char* str_id)                                { PushID(str_id); }
    ScopedID(const char* str_id_begin, const char* str_id_end)  { PushID(str_id_begin, str_id_end); }
    ScopedID(const void* ptr_id)                                { PushID(ptr_id); }
    ScopedID(int int_id)                                        { PushID(int_id); }
    ~ScopedID()                                                 { PopID(); }
    IMGUI_SCOPED_DISABLE_MOVE_AND_COPY(ScopedID);
};

// Scoped wrapper for TreeNode()/TreePop()
struct  ScopedTreeNode
{
    bool    IsOpen;

    ScopedTreeNode(const char* label)                           { IsOpen = TreeNode(label); }
    ScopedTreeNode(const char* str_id, const char* fmt, ...) IM_FMTARGS(3) { va_list ap; va_start(ap, fmt); IsOpen = TreeNodeV(str_id, fmt, ap); va_end(ap); }
    ScopedTreeNode(const void* ptr_id, const char* fmt, ...) IM_FMTARGS(3) { va_list ap; va_start(ap, fmt); IsOpen = TreeNodeV(ptr_id, fmt, ap); va_end(ap); }
    ~ScopedTreeNode()                                           { if (IsOpen) TreePop(); }
    explicit operator bool() const                              { return IsOpen; }
    IMGUI_SCOPED_DISABLE_MOVE_AND_COPY(ScopedTreeNode);
};

... 

} // namespace ImGuiScoped

#endif // #ifndef IMGUI_DISABLE

If Seth or someone wants to resume that work by tweaking the Ruby script it would be nice to make it look like this?

@GamingMinds-DanielC
Copy link
Contributor

    ImGui::ScopedStyleColor(ImGuiCol_Text, ImVec4(1,0,0,1));
    ImGui::Text("A red hello!");

Maybe I'm not aware of some obscure C++ rules, but I don't think that text would be red. The scoped object here is an anonymous object and should be destroyed again before the call to ImGui::Text happens. Differing behavior, if that occurs, could be down to compiler quirks and I would not rely on them.

@ocornut
Copy link
Owner

ocornut commented Oct 10, 2024

See, i exactly made the mistake i was worried about in my example… :( :)

@mnesarco
Copy link

mnesarco commented Oct 10, 2024

Hello @ocornut , I know there are many projects implementing RAII for ImGui, I also have my own . I use a single struct and some little macros to hide the local variable so naming is not a problem. It would be great to know your thoughts on my approach.

Ref: https://github.com/mnesarco/imgui_sugar

@TerensTare
Copy link

An alternative to writing structures with RAII for everything would be to write functions which call a lambda. The sample provided by @ocornut would look something like this:

ImGui::Window("My Window", [] {
    ImGui::WithStyleColor(ImGuiCol_Text, ImVec4(1,0,0,1), [] {
        ImGui::Text("A red hello!");

        for (int n = 0; n < 10; n++)
        {
            ImGui::WithID(n, [] {
                ImGui::Button("Button");
            });
        }
    });
});

@ocornut
Copy link
Owner

ocornut commented Oct 10, 2024 via email

@maxkunes
Copy link

maxkunes commented Oct 10, 2024

To preface, I think the lambda solution is the best fit.

Anyways... Compilers should optimize out lambda usage like this. Regarding your other point of "debug stepping cognitive overhead". Can you clarify?

See: https://godbolt.org/z/Mr4qGxq1e

@ocornut
Copy link
Owner

ocornut commented Oct 10, 2024 via email

@sergeyn
Copy link
Contributor

sergeyn commented Oct 10, 2024 via email

@nicolasnoble
Copy link
Contributor

Access to local variables may lead user to use captures which is likely to make things even more ugly on some settings.

Not only that, but since C++ has deprecated the usage of the catch-all [&], the user will have to think about which locals they want to use within the lambda, which can vastly disrupt the quick-debugging loop of adding on-the-fly ImGui widgets with Visual Studio's "hot reload" feature of being able to continue execution with modified code. I use this frequently, I don't want it to break.

@jdumas
Copy link
Contributor

jdumas commented Oct 11, 2024

My 2cents:

  • Re:naming, ImGui::Scoped* sounds good to me.
  • Re:anonymous variables, I like this solution from SO. Basically using a static constructor with [[nodiscard]] attribute will at least issue a warning if there is no variable name. We can imagine an interface that looks like this:
if (auto window = ImGui::Scoped::Window("My Window")) {
      auto color = ImGui::Scoped::StyleColor(ImGuiCol_Text, ImVec4(1,0,0,1));
      ImGui::Text("A red hello!");
      for (int n = 0; n < 10; n++)
      {
          auto id = ImGui::Scoped::ID(n);
          ImGui::Button("Button");
      }
  }

Using the following RAII wrappers:

struct Scoped;

struct [[nodiscard]] ScopedWindow
{
private:
    bool    IsVisible;

    ScopedWindow(const char* name, bool* p_open = NULL, ImGuiWindowFlags flags = 0) { IsVisible = Begin(name, p_open, flags); }
    ~ScopedWindow()                                             { End(); }
    explicit operator bool() const                              { return IsVisible; }
    IMGUI_SCOPED_DISABLE_MOVE_AND_COPY(ScopedWindow);
    friend class Scoped;
};

// Scoped wrapper by PushStyleColor()/PopStyleColor()
struct [[nodiscard]] ScopedStyleColor
{
private:
    ScopedStyleColor(ImGuiCol idx, ImU32 col)                   { PushStyleColor(idx, col); }
    ScopedStyleColor(ImGuiCol idx, const ImVec4& col)           { PushStyleColor(idx, col); }
    ~ScopedStyleColor()                                         { PopStyleColor(); }
    IMGUI_SCOPED_DISABLE_MOVE_AND_COPY(ScopedStyleColor);
    friend class Scoped;
};

// Scoped wrapper for PushID()/PopID()
struct [[nodiscard]] ScopedID
{
private:
    ScopedID(const char* str_id)                                { PushID(str_id); }
    ScopedID(const char* str_id_begin, const char* str_id_end)  { PushID(str_id_begin, str_id_end); }
    ScopedID(const void* ptr_id)                                { PushID(ptr_id); }
    ScopedID(int int_id)                                        { PushID(int_id); }
    ~ScopedID()                                                 { PopID(); }
    IMGUI_SCOPED_DISABLE_MOVE_AND_COPY(ScopedID);
    friend class Scoped;
};

struct Scoped {
    static ScopedId ID(int int_id) { return ScopedId(int_id); }
    static ScopedId ID(const void* ptr_id) { return ScopedId(ptr_id); }
    static ScopedId ID(const char* str_id) { return ScopedId(str_id); }
    static ScopedId ID(const char* str_id_begin, const char* str_id_end) { return ScopedId(str_id_begin, str_id_end); }
    static ScopedWindow Window(const char* name, bool* p_open = NULL, ImGuiWindowFlags flags = 0) { return ScopedWindow(name, p_open, flags); }
    static ScopedStyleColor StyleColor(ImGuiCol idx, ImU32 col) { return ScopedStyleColor(idx, col); }
    static ScopedStyleColor StyleColor(ImGuiCol idx, const ImVec4& col) { return ScopedStyleColor(idx, col); }
}

@TerensTare
Copy link

Just wanted to add to this. To me personally the lambda solution is better than the RAII solution and can be made even more readable than what I presented if that's an issue.

First, you don't have to worry about the "oops I forgot to give a name to my guard" problem, you don't have to give a name at all! Second, the code to implement such functions would be small and really simple to write, it can literally be made to just:

void WithId(int n, CallableRef call) {
    PushId(n);
    call();
    PopId();
}

The same formula can be applied to all the other "scoped" calls, for every overload. Now regarding the issues presented:

Access to local variables may lead user to use captures which is likely to make things even more ugly on some settings.

This can be easily solved by capturing everything by reference, ie. [&]. There would be no dangling references as the lambda's lifetime is shorter than the statement itself, thus shorter than that of the variables in scope.

Using a debugger is made noticeably more annoying. Extra debug steps required. Confusing names in callstacks.

I would actually argue that it's not the case. You cannot pick the name of a lambda, but you can pick the name of a function. With this solution you can always pass a function pointer where it makes sense, such as Window (Begin/End) calls. However, my understanding is that this is a matter of preferences so I can't add much more to this point.

C++ has deprecated the usage of the catch-all [&]

@nicolasnoble I don't think the committee deprecated catch-all. Can you please add a link? 🙂

PS. for reference, here's an implementation of CallableRef shown in the sample code, if anyone is wondering. https://godbolt.org/z/caWcMY3jf

@nicolasnoble
Copy link
Contributor

Sorry, you're correct. It's [=] which is deprecated.

@pinwhell
Copy link

Just wanted to add to this. To me personally the lambda solution is better than the RAII solution and can be made even more readable than what I presented if that's an issue.

First, you don't have to worry about the "oops I forgot to give a name to my guard" problem, you don't have to give a name at all! Second, the code to implement such functions would be small and really simple to write, it can literally be made to just:

void WithId(int n, CallableRef call) {
    PushId(n);
    call();
    PopId();
}

The same formula can be applied to all the other "scoped" calls, for every overload. Now regarding the issues presented:

Access to local variables may lead user to use captures which is likely to make things even more ugly on some settings.

This can be easily solved by capturing everything by reference, ie. [&]. There would be no dangling references as the lambda's lifetime is shorter than the statement itself, thus shorter than that of the variables in scope.

Using a debugger is made noticeably more annoying. Extra debug steps required. Confusing names in callstacks.

I would actually argue that it's not the case. You cannot pick the name of a lambda, but you can pick the name of a function. With this solution you can always pass a function pointer where it makes sense, such as Window (Begin/End) calls. However, my understanding is that this is a matter of preferences so I can't add much more to this point.

C++ has deprecated the usage of the catch-all [&]

@nicolasnoble I don't think the committee deprecated catch-all. Can you please add a link? 🙂

PS. for reference, here's an implementation of CallableRef shown in the sample code, if anyone is wondering. https://godbolt.org/z/caWcMY3jf

Maybe lamba would bring lot of overhead... at least more than Basic RAII construction.

@maxkunes
Copy link

maxkunes commented Dec 17, 2024

Maybe lamba would bring lot of overhead... at least more than Basic RAII construction.

Lambdas are almost always zero cost, producing the exact same assembly code as a simple RAII object. (Actually producing better code in the example below). They might always be zero cost (from a RAM / perf perspective) as they are a language, not library, feature.

Lambda (-O2): Compiler Explorer link
RAII (-O2): Compiler Explorer link

With no optimization, lambdas are likely more efficient as well—they generate less assembly while maintaining the same level of indirection.

Lambda (no optimization): Compiler Explorer link
RAII (no optimization): Compiler Explorer link

In my opinion, assuming you use [&] captures, lambdas are objectively more ergonomic in this context. They provide language-enforced scoping, and avoid the need to name a variable purely for the sake of naming.

The only reasonable argument against lambdas is that, without optimization, the call stack might include slightly less intuitive names. However, I don't think this is sufficient to dismiss a solution that is otherwise better.

Additionally, for new programmers, a single example of lambda usage in this context should make its behavior immediately clear. In contrast, RAII with objects can be more confusing, as the point of destruction isn’t always obvious unless every scope is explicitly bracketed—adding extra work for the user.

Edit: Fixed links to include body in both examples.

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

No branches or pull requests