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

Dear ImGui tips & trick café! #7081

Open
ocornut opened this issue Dec 1, 2023 · 22 comments
Open

Dear ImGui tips & trick café! #7081

ocornut opened this issue Dec 1, 2023 · 22 comments

Comments

@ocornut
Copy link
Owner

ocornut commented Dec 1, 2023

In the spirit of 2020's ImDrawList coding party (#3606) - which led to beautiful stuff like this disco ball and more, I thought about opening a new community thread.

This is a thread where you are encouraged to post your own tip & tricks about using dear imgui.

Are you frequently making use of an clever idiom or trick which saves you time?
Did you just discover some feature that you wish you knew before?
Share them!

Dear ImGui has countless features, flags, which may be hard to discover, or sometimes can be assembled together in creative or unusual ways. While I am trying to make them discoverable via the demo, wiki and variety of threads, I feel that allowing everyone to share their own discoveries in a light-hearted manner may provide for a more attractive read.

Don't hesitate to include pictures/gifs in your post, as they are going to make those tips nicer to adsorb.

@ocornut
Copy link
Owner Author

ocornut commented Dec 1, 2023

I'll post something to get the ball rolling..

Appending to a window

I believe this is widely known? But it is possible to use call Begin(), BeginChild(), BeginMenu(), BeginTabBar(), BeginTooltp() with the same identifier to append to the same window.

// called first
void Function1()
{
    ImGui::Begin("Some window");
    ImGui::Text("Hello world");
    ImGui::End();
}

// called later
void Function2()
{
    ImGui::Begin("Some window");
    ImGui::Checkbox("Checkbox", &some_value);
    ImGui::End();
}

image

One nice property of that is that you can easily trace an algorithm by calling code from leaf functions:

#define IM_TRACE_LOCATION()  if (ImGui::Begin("Function Trace")) { ImGui::Text("%s(), %s::%d", __FUNCTION__, __FILE__, __LINE__);} ImGui::End();

void Function1()
{
    IM_TRACE_LOCATION();
    //...
}

void Function2()
{
    IM_TRACE_LOCATION();
    //...
}

void MainLoop()
{
   Function1();
   Function2();
   Function1();
}

image

@ocornut
Copy link
Owner Author

ocornut commented Dec 5, 2023

Your turn ! :)

@GamingMinds-DanielC
Copy link
Contributor

GamingMinds-DanielC commented Dec 6, 2023

A rather simple trick, but useful nonetheless:
ImGui::CheckboxFlags is not only useful for flags, but for multi editing as well. If you have multiple objects selected in an editor and want to edit all of them at once, you can do this for checkboxes: set bit value 1 if any of the objects has the option set. Set bit value 2 if all objects have the option set. Now use ImGui::CheckboxFlags with the combined value 3 and you have a perfectly working tri-state checkbox. Makes it easy to see if all, none or a subset of the selected objects have that option set.

	if ( ImGui::Begin( "MultiCheckbox" ) )
	{
		static bool first  = false;
		static bool second = false;
		static bool third  = false;

		ImGui::Checkbox( "First", &first );
		ImGui::Checkbox( "Second", &second );
		ImGui::Checkbox( "Third", &third );

		ImGui::Separator();

		bool*  all[] = { &first, &second, &third };
		size_t count = sizeof( all ) / sizeof( all[ 0 ] );

		size_t numSet = 0;
		for ( size_t i = 0; i < count; ++i )
			if ( *all[ i ] )
				numSet++;

		int flags = (int)( numSet == count ) * 2 + (int)( numSet > 0 );
		if ( ImGui::CheckboxFlags( "All", &flags, 3 ) )
		{
			for ( size_t i = 0; i < count; ++i )
				*all[ i ] = flags != 0;
		}
	}
	ImGui::End();

multi_checkbox

@pthom
Copy link
Contributor

pthom commented Dec 12, 2023

I don't know if that counts, but here is a simple button with shadow effect:

bool ButtonWithShadow(const char*label, ImVec2 buttonSize, ImVec4 color)
{
    ImGui::PushStyleColor(ImGuiCol_Button, color);
    bool pressed = ImGui::Button(label, buttonSize);

    // Draw a gradient on top of the button
    {
        ImVec2 tl = ImGui::GetItemRectMin();
        ImVec2 br = ImGui::GetItemRectMax();
        ImVec2 size = ImGui::GetItemRectSize();

        float k = 0.3f;

        ImVec2 tl_middle(tl.x, tl.y + size.y * (1.f - k));
        ImVec2 br_middle(br.x, tl.y + size.y * k);

        ImVec4 col_darker(0.f, 0.f, 0.f, 0.2f);
        ImVec4 col_interm(0.f, 0.f, 0.f, 0.1f);
        ImVec4 col_transp(0.f, 0.f, 0.f, 0.f);

        ImGui::GetForegroundDrawList()->AddRectFilledMultiColor(
            tl,
            br_middle,
            ImGui::GetColorU32(col_interm), // upper left
            ImGui::GetColorU32(col_interm), // upper right
            ImGui::GetColorU32(col_transp), // bottom right
            ImGui::GetColorU32(col_transp)  // bottom left
        );
        ImGui::GetForegroundDrawList()->AddRectFilledMultiColor(
            tl_middle,
            br,
            ImGui::GetColorU32(col_transp), // upper left
            ImGui::GetColorU32(col_transp), // upper right
            ImGui::GetColorU32(col_darker), // bottom right
            ImGui::GetColorU32(col_darker)  // bottom left
        );

    }
    ImGui::PopStyleColor();
    return pressed;
}
image

In use here

@Dregu
Copy link

Dregu commented Dec 15, 2023

Opening specific main menus with keyboard shortcuts using OpenPopup. I believe this is undocumented, unless you know menus are just popups. Also some other menu shortcut ideas.

There might be some better api baking for this, but it's pretty simple already...

Edit: Make MenuItem get the shortcut name automatically

namespace ImGui {
// Wrapper for menu that can be opened with a global shortcut
// or submenu with a local shortcut
inline bool BeginMenu(const char* label, const ImGuiKeyChord key)
{
    if (ImGui::IsKeyChordPressed(key))
        ImGui::OpenPopup(label);
    return ImGui::BeginMenu(label);
};
// Wrapper for menuitem that can be opened with a local shortcut
inline bool MenuItem(const char* label, const ImGuiKeyChord key)
{
    char shortcut[32];
    ImGui::GetKeyChordName(key, shortcut, IM_ARRAYSIZE(shortcut));
    return ImGui::MenuItem(label, shortcut) || ImGui::IsKeyChordPressed(key);
}
}

if (ImGui::BeginMainMenuBar()) {
    // Alt+F opens the File menu by popup id and allows
    // further keyboard navigation with arrows too
    if (ImGui::BeginMenu("File", ImGuiMod_Alt | ImGuiKey_F)) {
        // This global shortcut is handled later
        if (ImGui::MenuItem("Global Exit", "Alt+X"))
            done = true;

        // Local shortcuts can be handled inline
        if (ImGui::MenuItem("Local Exit", ImGuiMod_Alt | ImGuiKey_Z))
            done = true;
        // The S shortcut works locally, but there is no code to draw shortcuts for submenus
        // The shortcut code from MenuItemEx could be used in the wrapper to draw it though
        if (ImGui::BeginMenu("Submenu", ImGuiKey_S)) {
            if (ImGui::MenuItem("Also Exit", ImGuiKey_A))
                done = true;
            ImGui::EndMenu();
        }
        ImGui::EndMenu();
    }

    // Alt+E opens the Edit menu
    if (ImGui::BeginMenu("Edit", ImGuiMod_Alt | ImGuiKey_E)) {
        if (ImGui::MenuItem("Nope")) {}
        ImGui::EndMenu();
    }

    // Handle that global shortcut too
    if (ImGui::IsKeyChordPressed(ImGuiMod_Alt | ImGuiKey_X))
        done = true;

    ImGui::EndMainMenuBar();
}

@ocornut
Copy link
Owner Author

ocornut commented Dec 15, 2023

I don't know if that counts, but here is a simple button with shadow effect:

@pthom Great! Also linking to #4722 for similar ideas.

Opening specific main menus with keyboard shortcuts using OpenPopup. I believe this is undocumented, unless you know menus are just popups. Also some other menu shortcut ideas.

@Dregu Great! I didn't even know it was possible, as I'd have been mislead by the fact that menus re-use custom window names, but instead of the popup id system is standard. FYI i will be working on ways to support that (#456) but there has been lots of yak-shaving on the way forward. You may remove the "const char* shortcut, " parameter and use GetKeyChordName() to convert ImGuiKeyChord to a string to make that part automatic.

@pthom
Copy link
Contributor

pthom commented Dec 16, 2023

A simple one: implement EmVec2 (see below) and use it when you want place or size widgets in a DPI independent way (otherwise, size and positions given via ImVec2 may vary depending on the platform).

Instead of:

ImGui::SetCursorPos(ImVec2(145.f, 290.f));
ImGui::Button("whatever", ImVec2(14.5f, 29.f));

use:

ImGui::SetCursorPos(EmVec2(10.f, 20.f));
ImGui::Button("whatever", ImVec2(1.f, 2.f));

Note: EmToVec2() returns a size in multiples of the font height. It is somewhat comparable to the em CSS Unit. By default, the font height on ImGui is 14.5, hence the new values.

Alternatively, you can use kDpi and keep the original pixel values:

ImGui::SetCursorPos(ImVec2(145.f * kDpi(), 290.f * kDpi()));
ImGui::Button("whatever", ImVec2(14.5f * kDpi(), 29.f * kDpi()));

And here are the definitions of EmVec2 and kDPi:

ImVec2 EmVec2(float x, float y)  { float k = ImGui::GetFontSize(); return ImVec2(k * x, k * y); }
float kDpi()   { return ImGui::GetFontSize() / 14.5f; }
float EmSize(float nbLines = 1.f) { return ImGui::GetFontSize() * nbLines; }

@Chaojimengnan
Copy link

Chaojimengnan commented Dec 17, 2023

A lightweight RAII helper class ( Requires C++17 or higher )

template <auto fn, auto... args>
class scope_exit
{
public:
    constexpr scope_exit() = default;
    constexpr scope_exit(bool turn_on) : turn_on(turn_on) { }
    constexpr scope_exit(const scope_exit&) = delete;
    constexpr scope_exit& operator=(const scope_exit&) = delete;
    constexpr scope_exit(scope_exit&& rhs) noexcept : turn_on(rhs.turn_on) { rhs.turn_on = false; }
    constexpr scope_exit& operator=(scope_exit&& rhs) noexcept
    {
        bool temp = rhs.turn_on;
        rhs.turn_on = false;
        turn_on = temp;
        return *this;
    }
    constexpr ~scope_exit()
    {
        if (turn_on)
            fn(args...);
    }
    constexpr operator bool() const noexcept
    {
        return turn_on;
    }

    bool turn_on = true;
};

RAII wrapper function for imgui

template <auto fn>
struct window_guard
{
    scope_exit<fn> guard;
    bool not_skip;

    operator bool() const noexcept
    {
        return not_skip;
    }
};

[[nodiscard]] inline window_guard<ImGui::End>
make_scoped_window(const char* name, bool* p_open = nullptr, ImGuiWindowFlags flags = 0)
{
    return { .not_skip = ImGui::Begin(name, p_open, flags) };
}

[[nodiscard]] inline scope_exit<ImGui::EndMenuBar> make_scoped_menu_bar()
{
    return { ImGui::BeginMenuBar() };
}

[[nodiscard]] inline scope_exit<ImGui::PopStyleColor, 1>
make_scoped_style_color(ImGuiCol idx, ImU32 col)
{
    ImGui::PushStyleColor(idx, col);
    return {};
}

example (a simple window):

before

if (ImGui::Begin("window", nullptr, ImGuiWindowFlags_MenuBar))
{
    if (ImGui::BeginMenuBar())
    {
        ImGui::Text("this is a menu bar!");
        ImGui::EndMenuBar();
    }

    ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(ImGuiCol_Button));
    ImGui::Text("this is colored text!");
    ImGui::PopStyleColor();
}
ImGui::End();

after

if (auto window = make_scoped_window("window", nullptr, ImGuiWindowFlags_MenuBar))
{
    if (auto menu = make_scoped_menu_bar())
        ImGui::Text("this is a menu bar!");

    auto color = make_scoped_style_color(ImGuiCol_Text, ImGui::GetColorU32(ImGuiCol_Button));
    ImGui::Text("this is colored text!");
}

@ocornut
Copy link
Owner Author

ocornut commented Dec 18, 2023

Not sure where to draw the line but I think it'd be good if the thread was focused on standalone, narrow-scope, readily usable tips, rather than ones which already have whole issues/topics/wiki sections open and may be wider topics.

For DPI also see "dpi" labels. There are many alternative ways to handle DPI, I suppose our problem is not settling on a common idiom satisfying all cases (it's a bit tentacular due to the connection with multi-viewports coordinates).

For RAII also see #2197 #2096 and https://github.com/ocornut/imgui/wiki/Useful-Extensions#cness

@pthom
Copy link
Contributor

pthom commented Dec 18, 2023

For DPI also see "dpi" labels. There are many alternative ways to handle DPI, I suppose our problem is not settling on a common idiom satisfying all cases (it's a bit tentacular due to the connection with multi-viewports coordinates).

Agreed. I simplified the Dpi related tip to the simple EmVec2.

@kyle-sylvestre
Copy link

I have a trick for windows imgui applications. The default prerender screen is white so you get flash banged when opening dark mode type applications. You can set a variable in WNDCLASSEX to prevent this from happening:
win_class.hbrBackground = CreateSolidBrush(RGB(30, 30, 30));

default_background_color.mp4
dark_background_color.mp4

@PapaNaxos
Copy link

Quick and dirty function for right alignment.

bool ImRightAlign(const char* str_id)
{
    if(ImGui::BeginTable(str_id, 2, ImGuiTableFlags_SizingFixedFit, ImVec2(-1,0)))
    {
        ImGui::TableSetupColumn("a", ImGuiTableColumnFlags_WidthStretch);

        ImGui::TableNextColumn();
        ImGui::TableNextColumn();
        return true;
    }
    return false;
}
#define ImEndRightAlign ImGui::EndTable

Usage

ImGui::Text("Some left aligned text");

ImGui::SameLine();

if (ImRightAlign("btns"))
{
    if (ImGui::Button("ButtonA")) btnA();
    ImGui::SameLine();
    if (ImGui::Button("ButtonB")) btnB();
    ImGui::SameLine();
    if (ImGui::Button("ButtonC")) btnC();

    ImEndRightAlign();
}

@sugrob9000
Copy link
Contributor

sugrob9000 commented Jan 20, 2024

Wrapping dear imgui's runtime type dispatch in several widgets with compile-time dispatch for simpler usage, type safety between arguments, and an API cleanup (taking min/max by value):

namespace ImGui {
template<typename> constexpr ImGuiDataType DataTypeEnum = ImGuiDataType_COUNT;
template<> constexpr inline ImGuiDataType DataTypeEnum<int8_t>   = ImGuiDataType_S8;
template<> constexpr inline ImGuiDataType DataTypeEnum<uint8_t>  = ImGuiDataType_U8;
template<> constexpr inline ImGuiDataType DataTypeEnum<int16_t>  = ImGuiDataType_S16;
template<> constexpr inline ImGuiDataType DataTypeEnum<uint16_t> = ImGuiDataType_U16;
template<> constexpr inline ImGuiDataType DataTypeEnum<int32_t>  = ImGuiDataType_S32;
template<> constexpr inline ImGuiDataType DataTypeEnum<uint32_t> = ImGuiDataType_U32;
template<> constexpr inline ImGuiDataType DataTypeEnum<int64_t>  = ImGuiDataType_S64;
template<> constexpr inline ImGuiDataType DataTypeEnum<uint64_t> = ImGuiDataType_U64;
template<> constexpr inline ImGuiDataType DataTypeEnum<float>    = ImGuiDataType_Float;
template<> constexpr inline ImGuiDataType DataTypeEnum<double>   = ImGuiDataType_Double;

template<typename T> concept ScalarType = (DataTypeEnum<T> != ImGuiDataType_COUNT);

template<ScalarType T>
bool Drag(const char* l, T* p, float speed, T min = std::numeric_limits<T>::lowest(), T max = std::numeric_limits<T>::max(), const char* fmt = nullptr, ImGuiSliderFlags flags = 0) {
  return DragScalar(l, DataTypeEnum<T>, p, speed, &min, &max, fmt, flags);
}

template<ScalarType T>
bool DragN(const char* l, T* p, int n, float speed, T min = std::numeric_limits<T>::lowest(), T max = std::numeric_limits<T>::max(), const char* fmt = nullptr, ImGuiSliderFlags flags = 0) {
  return DragScalarN(l, DataTypeEnum<T>, p, n, speed, &min, &max, fmt, flags);
}

template<ScalarType T>
bool Slider(const char* l, T* p, T min = std::numeric_limits<T>::lowest(), T max = std::numeric_limits<T>::max(), const char* fmt = nullptr, ImGuiSliderFlags flags = 0) {
  return SliderScalar(l, DataTypeEnum<T>, p, &min, &max, fmt, flags);
}

template<ScalarType T>
bool SliderN(const char* l, T* p, int n, T min = std::numeric_limits<T>::lowest(), T max = std::numeric_limits<T>::max(), const char* fmt = nullptr, ImGuiSliderFlags flags = 0) {
  return SliderScalar(l, DataTypeEnum<T>, p, n, &min, &max, fmt, flags);
}

template<ScalarType T>
bool InputNumber(const char* l, T* p, T step = 0, T step_fast = 0, const char* fmt = nullptr, ImGuiInputTextFlags flags = 0) {
  return InputScalar(l, DataTypeEnum<T>, p, (step == 0 ? nullptr : &step), (step_fast == 0 ? nullptr : &step_fast), fmt, flags);
}

template<ScalarType T>
bool InputNumberN(const char* l, T* p, int n, T step = 0, T step_fast = 0, const char* fmt = nullptr, ImGuiInputTextFlags flags = 0) {
  return InputScalarN(l, DataTypeEnum<T>, p, n, (step == 0 ? nullptr : &step), (step_fast == 0 ? nullptr : &step_fast), fmt, flags);
}
} // namespace ImGui

Trivial further improvements:

  • dropping T* in favor of T& in single-value varaints
  • dropping the T* + int interface in favor of std::span<T> in multi-value variants
  • preventing the deduction of T from preferring min or step would simplify some callsites

example usage:

static int i;
static double d;
ImGui::Slider("i", &i);
ImGui::Slider("d", &d);

@agorangetek
Copy link

I have a trick for windows imgui applications. The default prerender screen is white so you get flash banged when opening dark mode type applications. You can set a variable in WNDCLASSEX to prevent this from happening: win_class.hbrBackground = CreateSolidBrush(RGB(30, 30, 30));

default_background_color.mp4
dark_background_color.mp4

Care to share how i can do this?

@uusdnfdsfhnttyh
Copy link
Contributor

I have a trick for windows imgui applications. The default prerender screen is white so you get flash banged when opening dark mode type applications. You can set a variable in WNDCLASSEX to prevent this from happening: win_class.hbrBackground = CreateSolidBrush(RGB(30, 30, 30));
default_background_color.mp4
dark_background_color.mp4

Care to share how i can do this?

My simple solution:

WNDCLASSEXW wc =
        {sizeof(wc), CS_CLASSDC, WndProc, 0L, 0L, GetModuleHandle(nullptr), nullptr, nullptr,
         ::CreateSolidBrush(RGB(239.6, 239.6, 239.6)), nullptr,
         window_name_, nullptr};

@agorangetek
Copy link

I have a trick for windows imgui applications. The default prerender screen is white so you get flash banged when opening dark mode type applications. You can set a variable in WNDCLASSEX to prevent this from happening: win_class.hbrBackground = CreateSolidBrush(RGB(30, 30, 30));
default_background_color.mp4
dark_background_color.mp4

Care to share how i can do this?

My simple solution:

WNDCLASSEXW wc =
        {sizeof(wc), CS_CLASSDC, WndProc, 0L, 0L, GetModuleHandle(nullptr), nullptr, nullptr,
         ::CreateSolidBrush(RGB(239.6, 239.6, 239.6)), nullptr,
         window_name_, nullptr};

Hi, which source file would i apply this to and where in the file? Im a begginer but i have managed to build imgui successfuly. Thanks.

@GamingMinds-DanielC
Copy link
Contributor

Hi, which source file would i apply this to and where in the file? Im a begginer but i have managed to build imgui successfuly. Thanks.

That is part of your own code, so in whichever source file you are registering window classes. You might have copied that part from the ImGui example applications, in those examples it is the main.cpp file.

@agorangetek
Copy link

agorangetek commented Feb 2, 2024

Hi, which source file would i apply this to and where in the file? Im a begginer but i have managed to build imgui successfuly. Thanks.

That is part of your own code, so in whichever source file you are registering window classes. You might have copied that part from the ImGui example applications, in those examples it is the main.cpp file.

Thanks, it's actually pyGUI that im using so i'll try to figure it out.
EDIT:
I figured it out. Thanks for your help :)

@EricPlayZ
Copy link

EricPlayZ commented Feb 17, 2024

A quick hacky function I made that spans tab buttons across a specified width:

static size_t tabIndex = 1;
void SpanTabAcrossWidth(const float width, const size_t tabs = 1) {
	if (width <= 0.0f)
		return;

	const float oneTabWidthWithSpacing = width / tabs;
	const float oneTabWidth = oneTabWidthWithSpacing - (tabIndex == tabs ? 0.0f : GImGui->Style.ItemSpacing.x / 2.0f);
	ImGui::SetNextItemWidth(oneTabWidth);

	tabIndex++;
}
void EndTabBar() {
	ImGui::EndTabBar();
	tabIndex = 1;
}

Example:

static constexpr ImGuiChildFlags childFlags = ImGuiChildFlags_AlwaysAutoResize | ImGuiChildFlags_AutoResizeX;

ImGui::Begin("Test Window", nullptr, ImGuiWindowFlags_AlwaysAutoResize); {
	if (ImGui::BeginTabBar("##MainTabBar")) {
		static float childWidth = 0.0f;

		SpanTabAcrossWidth(childWidth, 2);
		if (ImGui::BeginTabItem("FirstTab")) {
			ImGui::BeginChild("##TabChild", ImVec2(0.0f, 25.0f), childFlags);
			{
				childWidth = ImGui::GetItemRectSize().x;

				ImGui::Text("This child is kinda short!");
				ImGui::EndChild();
			}
			ImGui::EndTabItem();
		}

		SpanTabAcrossWidth(childWidth, 2);
		if (ImGui::BeginTabItem("SecondTab")) {
			ImGui::BeginChild("##TabChild", ImVec2(0.0f, 25.0f), childFlags);
			{
				childWidth = ImGui::GetItemRectSize().x;

				ImGui::Text("This child is way longerrrrrrrrrrrrrrrr!");
				ImGui::EndChild();
			}
			ImGui::EndTabItem();
		}
		EndTabBar();
	}
	ImGui::End();
}

sOuulj9N32

@vhollander
Copy link

image
image

the ability to have mac os alike global menu bar

   static bool menuOpened = true;
    ImGui::Begin("database", 0, ImGuiWindowFlags_NoScrollWithMouse);    
    if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) || menuOpened ) {
        menuOpened = false;
        ImGui::BeginMainMenuBar();
              if ( ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) )
                  menuOpened = true;
      
              if (ImGui::BeginMenu("File")) {
                  ImGui::EndMenu();
                  menuOpened = true;
              }
        ImGui::EndMainMenuBar();
    }

Repository owner deleted a comment from mohitsainiknl Mar 29, 2024
@pthom
Copy link
Contributor

pthom commented Mar 29, 2024

A slider that can edit positive floats in any range, with a given number of significant digits. For any value, the slider will interactively adapt its range to the value, and briefly flash red when the range changed.

The code is a bit too long to fit in here, so it is available in a gist

Below in action with 5 significant digits, and a current range of 0-100: it is about to change range if the user drags to the right...

image

And after the user continued to drag right, there is a brief red flash, and a new interval can be used
image

@pthom
Copy link
Contributor

pthom commented Apr 27, 2024

Make any widget resizable (input_text_multiline, implot, etc.).

Example of a resizable plot
image

https://gist.github.com/pthom/0a532a1d487ded4744f113a7b100511c

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