Open sethk opened 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 :(
I think I'll take a crack at it and post something here for people to critique.
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();
}
My most used RAII object for Imgui:
class SetFont
{
public:
SetFont(ImFont* f) { ImGui::PushFont(f); }
~SetFont() { ImGui::PopFont(); }
};
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; }
};
@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");
});
});
Consider
{
ImWindowHelper(str_id, ...);
{
ImChildHelper(str_id, ...);
ImGui::Text("Child 1");
}
}
which is less noisy.
@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.
It works with clang++-6.0 but I didn't lookup the standard spec.
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]),
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.
@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?
@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.
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
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
@sethk Looking good!
Some pieces of feedback:
<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.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. 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.)
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.)
IsExpanded
should beIsContentsVisible
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 ofImGui
here. In the codebase,Im
is reserved for things that don't sit on top ofImGui
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?
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
@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
@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.
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.
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?
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.
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.
Well if it's the same thing then why not stick to the version with no template porn?
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.
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
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.)
Good for me, ImScope or ImScoped.
Yeah ImScoped
may be more semantic.
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
@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.
I think TreePush()
is too low-level indeed and could be removed.
The constructor/copy overrides should probably use a local macro as suggested higher in the thread, if only to bundle the whole thing in a single line describing the intent clearly. Probably also add comments on each lines of the macro.
There is an BeginColumn()/EndColumn()
currently only exposed in imgui_internal.h so down the line columns can be exposed this way. Short term I would advise against it unless we switch to the better columns API (which I will only do once I have a good pass at reworking the columns feature).
(Likewise the generalization of functions such as PushAllowKeyboardFocus
PushButtonRepeat
is PushItemFlag()
in imgui_internal.h but that's not done yet. )
Thiose RAII types may hinder some usage patterns such as:
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?
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).
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^^.
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
?
@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.
Ok, interesting!
@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.
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)
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?
@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.
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.
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)
.
@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.
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.
When conditionally applying a style, if the style object was made in an if block, it would go out of scope at the end of the block and it wouldn't be applied. I though of two possible solutions.
The pop style method pops off the last style, that would not necessarily be the one that the style object was crated for.
@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.
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.
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.