mirror of https://github.com/ocornut/imgui
3169 lines
136 KiB
C++
3169 lines
136 KiB
C++
// dear imgui, v1.64 WIP
|
|
// (widgets code)
|
|
|
|
#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS)
|
|
#define _CRT_SECURE_NO_WARNINGS
|
|
#endif
|
|
|
|
#include "imgui.h"
|
|
#ifndef IMGUI_DEFINE_MATH_OPERATORS
|
|
#define IMGUI_DEFINE_MATH_OPERATORS
|
|
#endif
|
|
#include "imgui_internal.h"
|
|
|
|
#include <ctype.h> // toupper, isprint
|
|
|
|
// Visual Studio warnings
|
|
#ifdef _MSC_VER
|
|
#pragma warning (disable: 4127) // condition expression is constant
|
|
#pragma warning (disable: 4996) // 'This function or variable may be unsafe': strcpy, strdup, sprintf, vsnprintf, sscanf, fopen
|
|
#endif
|
|
|
|
// Clang/GCC warnings with -Weverything
|
|
#ifdef __clang__
|
|
#pragma clang diagnostic ignored "-Wformat-nonliteral" // warning : format string is not a string literal // passing non-literal to vsnformat(). yes, user passing incorrect format strings can crash the code.
|
|
#pragma clang diagnostic ignored "-Wsign-conversion" // warning : implicit conversion changes signedness //
|
|
#elif defined(__GNUC__)
|
|
#pragma GCC diagnostic ignored "-Wformat-nonliteral" // warning: format not a string literal, format string not checked
|
|
#if __GNUC__ >= 8
|
|
#pragma GCC diagnostic ignored "-Wclass-memaccess" // warning: 'memset/memcpy' clearing/writing an object of type 'xxxx' with no trivial copy-assignment; use assignment or value-initialization instead
|
|
#endif
|
|
#endif
|
|
|
|
//-------------------------------------------------------------------------
|
|
// Forward Declarations
|
|
//-------------------------------------------------------------------------
|
|
|
|
//-------------------------------------------------------------------------
|
|
// SHARED UTILITIES
|
|
//-------------------------------------------------------------------------
|
|
|
|
//-------------------------------------------------------------------------
|
|
// WIDGETS: Text
|
|
// - TextUnformatted()
|
|
// - Text()
|
|
// - TextV()
|
|
// - TextColored()
|
|
// - TextColoredV()
|
|
// - TextDisabled()
|
|
// - TextDisabledV()
|
|
// - TextWrapped()
|
|
// - TextWrappedV()
|
|
// - LabelText()
|
|
// - LabelTextV()
|
|
// - BulletText()
|
|
// - BulletTextV()
|
|
//-------------------------------------------------------------------------
|
|
|
|
void ImGui::TextUnformatted(const char* text, const char* text_end)
|
|
{
|
|
ImGuiWindow* window = GetCurrentWindow();
|
|
if (window->SkipItems)
|
|
return;
|
|
|
|
ImGuiContext& g = *GImGui;
|
|
IM_ASSERT(text != NULL);
|
|
const char* text_begin = text;
|
|
if (text_end == NULL)
|
|
text_end = text + strlen(text); // FIXME-OPT
|
|
|
|
const ImVec2 text_pos(window->DC.CursorPos.x, window->DC.CursorPos.y + window->DC.CurrentLineTextBaseOffset);
|
|
const float wrap_pos_x = window->DC.TextWrapPos;
|
|
const bool wrap_enabled = wrap_pos_x >= 0.0f;
|
|
if (text_end - text > 2000 && !wrap_enabled)
|
|
{
|
|
// Long text!
|
|
// Perform manual coarse clipping to optimize for long multi-line text
|
|
// From this point we will only compute the width of lines that are visible. Optimization only available when word-wrapping is disabled.
|
|
// We also don't vertically center the text within the line full height, which is unlikely to matter because we are likely the biggest and only item on the line.
|
|
const char* line = text;
|
|
const float line_height = GetTextLineHeight();
|
|
const ImRect clip_rect = window->ClipRect;
|
|
ImVec2 text_size(0,0);
|
|
|
|
if (text_pos.y <= clip_rect.Max.y)
|
|
{
|
|
ImVec2 pos = text_pos;
|
|
|
|
// Lines to skip (can't skip when logging text)
|
|
if (!g.LogEnabled)
|
|
{
|
|
int lines_skippable = (int)((clip_rect.Min.y - text_pos.y) / line_height);
|
|
if (lines_skippable > 0)
|
|
{
|
|
int lines_skipped = 0;
|
|
while (line < text_end && lines_skipped < lines_skippable)
|
|
{
|
|
const char* line_end = strchr(line, '\n');
|
|
if (!line_end)
|
|
line_end = text_end;
|
|
line = line_end + 1;
|
|
lines_skipped++;
|
|
}
|
|
pos.y += lines_skipped * line_height;
|
|
}
|
|
}
|
|
|
|
// Lines to render
|
|
if (line < text_end)
|
|
{
|
|
ImRect line_rect(pos, pos + ImVec2(FLT_MAX, line_height));
|
|
while (line < text_end)
|
|
{
|
|
const char* line_end = strchr(line, '\n');
|
|
if (IsClippedEx(line_rect, 0, false))
|
|
break;
|
|
|
|
const ImVec2 line_size = CalcTextSize(line, line_end, false);
|
|
text_size.x = ImMax(text_size.x, line_size.x);
|
|
RenderText(pos, line, line_end, false);
|
|
if (!line_end)
|
|
line_end = text_end;
|
|
line = line_end + 1;
|
|
line_rect.Min.y += line_height;
|
|
line_rect.Max.y += line_height;
|
|
pos.y += line_height;
|
|
}
|
|
|
|
// Count remaining lines
|
|
int lines_skipped = 0;
|
|
while (line < text_end)
|
|
{
|
|
const char* line_end = strchr(line, '\n');
|
|
if (!line_end)
|
|
line_end = text_end;
|
|
line = line_end + 1;
|
|
lines_skipped++;
|
|
}
|
|
pos.y += lines_skipped * line_height;
|
|
}
|
|
|
|
text_size.y += (pos - text_pos).y;
|
|
}
|
|
|
|
ImRect bb(text_pos, text_pos + text_size);
|
|
ItemSize(bb);
|
|
ItemAdd(bb, 0);
|
|
}
|
|
else
|
|
{
|
|
const float wrap_width = wrap_enabled ? CalcWrapWidthForPos(window->DC.CursorPos, wrap_pos_x) : 0.0f;
|
|
const ImVec2 text_size = CalcTextSize(text_begin, text_end, false, wrap_width);
|
|
|
|
// Account of baseline offset
|
|
ImRect bb(text_pos, text_pos + text_size);
|
|
ItemSize(text_size);
|
|
if (!ItemAdd(bb, 0))
|
|
return;
|
|
|
|
// Render (we don't hide text after ## in this end-user function)
|
|
RenderTextWrapped(bb.Min, text_begin, text_end, wrap_width);
|
|
}
|
|
}
|
|
|
|
void ImGui::Text(const char* fmt, ...)
|
|
{
|
|
va_list args;
|
|
va_start(args, fmt);
|
|
TextV(fmt, args);
|
|
va_end(args);
|
|
}
|
|
|
|
void ImGui::TextV(const char* fmt, va_list args)
|
|
{
|
|
ImGuiWindow* window = GetCurrentWindow();
|
|
if (window->SkipItems)
|
|
return;
|
|
|
|
ImGuiContext& g = *GImGui;
|
|
const char* text_end = g.TempBuffer + ImFormatStringV(g.TempBuffer, IM_ARRAYSIZE(g.TempBuffer), fmt, args);
|
|
TextUnformatted(g.TempBuffer, text_end);
|
|
}
|
|
|
|
void ImGui::TextColored(const ImVec4& col, const char* fmt, ...)
|
|
{
|
|
va_list args;
|
|
va_start(args, fmt);
|
|
TextColoredV(col, fmt, args);
|
|
va_end(args);
|
|
}
|
|
|
|
void ImGui::TextColoredV(const ImVec4& col, const char* fmt, va_list args)
|
|
{
|
|
PushStyleColor(ImGuiCol_Text, col);
|
|
TextV(fmt, args);
|
|
PopStyleColor();
|
|
}
|
|
|
|
void ImGui::TextDisabled(const char* fmt, ...)
|
|
{
|
|
va_list args;
|
|
va_start(args, fmt);
|
|
TextDisabledV(fmt, args);
|
|
va_end(args);
|
|
}
|
|
|
|
void ImGui::TextDisabledV(const char* fmt, va_list args)
|
|
{
|
|
PushStyleColor(ImGuiCol_Text, GImGui->Style.Colors[ImGuiCol_TextDisabled]);
|
|
TextV(fmt, args);
|
|
PopStyleColor();
|
|
}
|
|
|
|
void ImGui::TextWrapped(const char* fmt, ...)
|
|
{
|
|
va_list args;
|
|
va_start(args, fmt);
|
|
TextWrappedV(fmt, args);
|
|
va_end(args);
|
|
}
|
|
|
|
void ImGui::TextWrappedV(const char* fmt, va_list args)
|
|
{
|
|
bool need_wrap = (GImGui->CurrentWindow->DC.TextWrapPos < 0.0f); // Keep existing wrap position is one ia already set
|
|
if (need_wrap) PushTextWrapPos(0.0f);
|
|
TextV(fmt, args);
|
|
if (need_wrap) PopTextWrapPos();
|
|
}
|
|
|
|
void ImGui::LabelText(const char* label, const char* fmt, ...)
|
|
{
|
|
va_list args;
|
|
va_start(args, fmt);
|
|
LabelTextV(label, fmt, args);
|
|
va_end(args);
|
|
}
|
|
|
|
// Add a label+text combo aligned to other label+value widgets
|
|
void ImGui::LabelTextV(const char* label, const char* fmt, va_list args)
|
|
{
|
|
ImGuiWindow* window = GetCurrentWindow();
|
|
if (window->SkipItems)
|
|
return;
|
|
|
|
ImGuiContext& g = *GImGui;
|
|
const ImGuiStyle& style = g.Style;
|
|
const float w = CalcItemWidth();
|
|
|
|
const ImVec2 label_size = CalcTextSize(label, NULL, true);
|
|
const ImRect value_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y*2));
|
|
const ImRect total_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w + (label_size.x > 0.0f ? style.ItemInnerSpacing.x : 0.0f), style.FramePadding.y*2) + label_size);
|
|
ItemSize(total_bb, style.FramePadding.y);
|
|
if (!ItemAdd(total_bb, 0))
|
|
return;
|
|
|
|
// Render
|
|
const char* value_text_begin = &g.TempBuffer[0];
|
|
const char* value_text_end = value_text_begin + ImFormatStringV(g.TempBuffer, IM_ARRAYSIZE(g.TempBuffer), fmt, args);
|
|
RenderTextClipped(value_bb.Min, value_bb.Max, value_text_begin, value_text_end, NULL, ImVec2(0.0f,0.5f));
|
|
if (label_size.x > 0.0f)
|
|
RenderText(ImVec2(value_bb.Max.x + style.ItemInnerSpacing.x, value_bb.Min.y + style.FramePadding.y), label);
|
|
}
|
|
|
|
void ImGui::BulletText(const char* fmt, ...)
|
|
{
|
|
va_list args;
|
|
va_start(args, fmt);
|
|
BulletTextV(fmt, args);
|
|
va_end(args);
|
|
}
|
|
|
|
// Text with a little bullet aligned to the typical tree node.
|
|
void ImGui::BulletTextV(const char* fmt, va_list args)
|
|
{
|
|
ImGuiWindow* window = GetCurrentWindow();
|
|
if (window->SkipItems)
|
|
return;
|
|
|
|
ImGuiContext& g = *GImGui;
|
|
const ImGuiStyle& style = g.Style;
|
|
|
|
const char* text_begin = g.TempBuffer;
|
|
const char* text_end = text_begin + ImFormatStringV(g.TempBuffer, IM_ARRAYSIZE(g.TempBuffer), fmt, args);
|
|
const ImVec2 label_size = CalcTextSize(text_begin, text_end, false);
|
|
const float text_base_offset_y = ImMax(0.0f, window->DC.CurrentLineTextBaseOffset); // Latch before ItemSize changes it
|
|
const float line_height = ImMax(ImMin(window->DC.CurrentLineSize.y, g.FontSize + g.Style.FramePadding.y*2), g.FontSize);
|
|
const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(g.FontSize + (label_size.x > 0.0f ? (label_size.x + style.FramePadding.x*2) : 0.0f), ImMax(line_height, label_size.y))); // Empty text doesn't add padding
|
|
ItemSize(bb);
|
|
if (!ItemAdd(bb, 0))
|
|
return;
|
|
|
|
// Render
|
|
RenderBullet(bb.Min + ImVec2(style.FramePadding.x + g.FontSize*0.5f, line_height*0.5f));
|
|
RenderText(bb.Min+ImVec2(g.FontSize + style.FramePadding.x*2, text_base_offset_y), text_begin, text_end, false);
|
|
}
|
|
|
|
//-------------------------------------------------------------------------
|
|
// WIDGETS: Main
|
|
// - ButtonBehavior() [Internal]
|
|
// - Button()
|
|
// - SmallButton()
|
|
// - InvisibleButton()
|
|
// - ArrowButton()
|
|
// - CloseButton() [Internal]
|
|
// - CollapseButton() [Internal]
|
|
// - Image()
|
|
// - ImageButton()
|
|
// - Checkbox()
|
|
// - CheckboxFlags()
|
|
// - RadioButton()
|
|
// - ProgressBar()
|
|
// - Bullet()
|
|
//-------------------------------------------------------------------------
|
|
|
|
bool ImGui::ButtonBehavior(const ImRect& bb, ImGuiID id, bool* out_hovered, bool* out_held, ImGuiButtonFlags flags)
|
|
{
|
|
ImGuiContext& g = *GImGui;
|
|
ImGuiWindow* window = GetCurrentWindow();
|
|
|
|
if (flags & ImGuiButtonFlags_Disabled)
|
|
{
|
|
if (out_hovered) *out_hovered = false;
|
|
if (out_held) *out_held = false;
|
|
if (g.ActiveId == id) ClearActiveID();
|
|
return false;
|
|
}
|
|
|
|
// Default behavior requires click+release on same spot
|
|
if ((flags & (ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnClick | ImGuiButtonFlags_PressedOnRelease | ImGuiButtonFlags_PressedOnDoubleClick)) == 0)
|
|
flags |= ImGuiButtonFlags_PressedOnClickRelease;
|
|
|
|
ImGuiWindow* backup_hovered_window = g.HoveredWindow;
|
|
if ((flags & ImGuiButtonFlags_FlattenChildren) && g.HoveredRootWindow == window)
|
|
g.HoveredWindow = window;
|
|
|
|
bool pressed = false;
|
|
bool hovered = ItemHoverable(bb, id);
|
|
|
|
// Drag source doesn't report as hovered
|
|
if (hovered && g.DragDropActive && g.DragDropPayload.SourceId == id && !(g.DragDropSourceFlags & ImGuiDragDropFlags_SourceNoDisableHover))
|
|
hovered = false;
|
|
|
|
// Special mode for Drag and Drop where holding button pressed for a long time while dragging another item triggers the button
|
|
if (g.DragDropActive && (flags & ImGuiButtonFlags_PressedOnDragDropHold) && !(g.DragDropSourceFlags & ImGuiDragDropFlags_SourceNoHoldToOpenOthers))
|
|
if (IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem))
|
|
{
|
|
hovered = true;
|
|
SetHoveredID(id);
|
|
if (CalcTypematicPressedRepeatAmount(g.HoveredIdTimer + 0.0001f, g.HoveredIdTimer + 0.0001f - g.IO.DeltaTime, 0.01f, 0.70f)) // FIXME: Our formula for CalcTypematicPressedRepeatAmount() is fishy
|
|
{
|
|
pressed = true;
|
|
FocusWindow(window);
|
|
}
|
|
}
|
|
|
|
if ((flags & ImGuiButtonFlags_FlattenChildren) && g.HoveredRootWindow == window)
|
|
g.HoveredWindow = backup_hovered_window;
|
|
|
|
// AllowOverlap mode (rarely used) requires previous frame HoveredId to be null or to match. This allows using patterns where a later submitted widget overlaps a previous one.
|
|
if (hovered && (flags & ImGuiButtonFlags_AllowItemOverlap) && (g.HoveredIdPreviousFrame != id && g.HoveredIdPreviousFrame != 0))
|
|
hovered = false;
|
|
|
|
// Mouse
|
|
if (hovered)
|
|
{
|
|
if (!(flags & ImGuiButtonFlags_NoKeyModifiers) || (!g.IO.KeyCtrl && !g.IO.KeyShift && !g.IO.KeyAlt))
|
|
{
|
|
// | CLICKING | HOLDING with ImGuiButtonFlags_Repeat
|
|
// PressedOnClickRelease | <on release>* | <on repeat> <on repeat> .. (NOT on release) <-- MOST COMMON! (*) only if both click/release were over bounds
|
|
// PressedOnClick | <on click> | <on click> <on repeat> <on repeat> ..
|
|
// PressedOnRelease | <on release> | <on repeat> <on repeat> .. (NOT on release)
|
|
// PressedOnDoubleClick | <on dclick> | <on dclick> <on repeat> <on repeat> ..
|
|
// FIXME-NAV: We don't honor those different behaviors.
|
|
if ((flags & ImGuiButtonFlags_PressedOnClickRelease) && g.IO.MouseClicked[0])
|
|
{
|
|
SetActiveID(id, window);
|
|
if (!(flags & ImGuiButtonFlags_NoNavFocus))
|
|
SetFocusID(id, window);
|
|
FocusWindow(window);
|
|
}
|
|
if (((flags & ImGuiButtonFlags_PressedOnClick) && g.IO.MouseClicked[0]) || ((flags & ImGuiButtonFlags_PressedOnDoubleClick) && g.IO.MouseDoubleClicked[0]))
|
|
{
|
|
pressed = true;
|
|
if (flags & ImGuiButtonFlags_NoHoldingActiveID)
|
|
ClearActiveID();
|
|
else
|
|
SetActiveID(id, window); // Hold on ID
|
|
FocusWindow(window);
|
|
}
|
|
if ((flags & ImGuiButtonFlags_PressedOnRelease) && g.IO.MouseReleased[0])
|
|
{
|
|
if (!((flags & ImGuiButtonFlags_Repeat) && g.IO.MouseDownDurationPrev[0] >= g.IO.KeyRepeatDelay)) // Repeat mode trumps <on release>
|
|
pressed = true;
|
|
ClearActiveID();
|
|
}
|
|
|
|
// 'Repeat' mode acts when held regardless of _PressedOn flags (see table above).
|
|
// Relies on repeat logic of IsMouseClicked() but we may as well do it ourselves if we end up exposing finer RepeatDelay/RepeatRate settings.
|
|
if ((flags & ImGuiButtonFlags_Repeat) && g.ActiveId == id && g.IO.MouseDownDuration[0] > 0.0f && IsMouseClicked(0, true))
|
|
pressed = true;
|
|
}
|
|
|
|
if (pressed)
|
|
g.NavDisableHighlight = true;
|
|
}
|
|
|
|
// Gamepad/Keyboard navigation
|
|
// We report navigated item as hovered but we don't set g.HoveredId to not interfere with mouse.
|
|
if (g.NavId == id && !g.NavDisableHighlight && g.NavDisableMouseHover && (g.ActiveId == 0 || g.ActiveId == id || g.ActiveId == window->MoveId))
|
|
hovered = true;
|
|
|
|
if (g.NavActivateDownId == id)
|
|
{
|
|
bool nav_activated_by_code = (g.NavActivateId == id);
|
|
bool nav_activated_by_inputs = IsNavInputPressed(ImGuiNavInput_Activate, (flags & ImGuiButtonFlags_Repeat) ? ImGuiInputReadMode_Repeat : ImGuiInputReadMode_Pressed);
|
|
if (nav_activated_by_code || nav_activated_by_inputs)
|
|
pressed = true;
|
|
if (nav_activated_by_code || nav_activated_by_inputs || g.ActiveId == id)
|
|
{
|
|
// Set active id so it can be queried by user via IsItemActive(), equivalent of holding the mouse button.
|
|
g.NavActivateId = id; // This is so SetActiveId assign a Nav source
|
|
SetActiveID(id, window);
|
|
if (!(flags & ImGuiButtonFlags_NoNavFocus))
|
|
SetFocusID(id, window);
|
|
g.ActiveIdAllowNavDirFlags = (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right) | (1 << ImGuiDir_Up) | (1 << ImGuiDir_Down);
|
|
}
|
|
}
|
|
|
|
bool held = false;
|
|
if (g.ActiveId == id)
|
|
{
|
|
if (g.ActiveIdSource == ImGuiInputSource_Mouse)
|
|
{
|
|
if (g.ActiveIdIsJustActivated)
|
|
g.ActiveIdClickOffset = g.IO.MousePos - bb.Min;
|
|
if (g.IO.MouseDown[0])
|
|
{
|
|
held = true;
|
|
}
|
|
else
|
|
{
|
|
if (hovered && (flags & ImGuiButtonFlags_PressedOnClickRelease))
|
|
if (!((flags & ImGuiButtonFlags_Repeat) && g.IO.MouseDownDurationPrev[0] >= g.IO.KeyRepeatDelay)) // Repeat mode trumps <on release>
|
|
if (!g.DragDropActive)
|
|
pressed = true;
|
|
ClearActiveID();
|
|
}
|
|
if (!(flags & ImGuiButtonFlags_NoNavFocus))
|
|
g.NavDisableHighlight = true;
|
|
}
|
|
else if (g.ActiveIdSource == ImGuiInputSource_Nav)
|
|
{
|
|
if (g.NavActivateDownId != id)
|
|
ClearActiveID();
|
|
}
|
|
}
|
|
|
|
if (out_hovered) *out_hovered = hovered;
|
|
if (out_held) *out_held = held;
|
|
|
|
return pressed;
|
|
}
|
|
|
|
bool ImGui::ButtonEx(const char* label, const ImVec2& size_arg, ImGuiButtonFlags flags)
|
|
{
|
|
ImGuiWindow* window = GetCurrentWindow();
|
|
if (window->SkipItems)
|
|
return false;
|
|
|
|
ImGuiContext& g = *GImGui;
|
|
const ImGuiStyle& style = g.Style;
|
|
const ImGuiID id = window->GetID(label);
|
|
const ImVec2 label_size = CalcTextSize(label, NULL, true);
|
|
|
|
ImVec2 pos = window->DC.CursorPos;
|
|
if ((flags & ImGuiButtonFlags_AlignTextBaseLine) && style.FramePadding.y < window->DC.CurrentLineTextBaseOffset) // Try to vertically align buttons that are smaller/have no padding so that text baseline matches (bit hacky, since it shouldn't be a flag)
|
|
pos.y += window->DC.CurrentLineTextBaseOffset - style.FramePadding.y;
|
|
ImVec2 size = CalcItemSize(size_arg, label_size.x + style.FramePadding.x * 2.0f, label_size.y + style.FramePadding.y * 2.0f);
|
|
|
|
const ImRect bb(pos, pos + size);
|
|
ItemSize(bb, style.FramePadding.y);
|
|
if (!ItemAdd(bb, id))
|
|
return false;
|
|
|
|
if (window->DC.ItemFlags & ImGuiItemFlags_ButtonRepeat)
|
|
flags |= ImGuiButtonFlags_Repeat;
|
|
bool hovered, held;
|
|
bool pressed = ButtonBehavior(bb, id, &hovered, &held, flags);
|
|
if (pressed)
|
|
MarkItemEdited(id);
|
|
|
|
// Render
|
|
const ImU32 col = GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
|
|
RenderNavHighlight(bb, id);
|
|
RenderFrame(bb.Min, bb.Max, col, true, style.FrameRounding);
|
|
RenderTextClipped(bb.Min + style.FramePadding, bb.Max - style.FramePadding, label, NULL, &label_size, style.ButtonTextAlign, &bb);
|
|
|
|
// Automatically close popups
|
|
//if (pressed && !(flags & ImGuiButtonFlags_DontClosePopups) && (window->Flags & ImGuiWindowFlags_Popup))
|
|
// CloseCurrentPopup();
|
|
|
|
return pressed;
|
|
}
|
|
|
|
bool ImGui::Button(const char* label, const ImVec2& size_arg)
|
|
{
|
|
return ButtonEx(label, size_arg, 0);
|
|
}
|
|
|
|
// Small buttons fits within text without additional vertical spacing.
|
|
bool ImGui::SmallButton(const char* label)
|
|
{
|
|
ImGuiContext& g = *GImGui;
|
|
float backup_padding_y = g.Style.FramePadding.y;
|
|
g.Style.FramePadding.y = 0.0f;
|
|
bool pressed = ButtonEx(label, ImVec2(0, 0), ImGuiButtonFlags_AlignTextBaseLine);
|
|
g.Style.FramePadding.y = backup_padding_y;
|
|
return pressed;
|
|
}
|
|
|
|
// Tip: use ImGui::PushID()/PopID() to push indices or pointers in the ID stack.
|
|
// Then you can keep 'str_id' empty or the same for all your buttons (instead of creating a string based on a non-string id)
|
|
bool ImGui::InvisibleButton(const char* str_id, const ImVec2& size_arg)
|
|
{
|
|
ImGuiWindow* window = GetCurrentWindow();
|
|
if (window->SkipItems)
|
|
return false;
|
|
|
|
// Cannot use zero-size for InvisibleButton(). Unlike Button() there is not way to fallback using the label size.
|
|
IM_ASSERT(size_arg.x != 0.0f && size_arg.y != 0.0f);
|
|
|
|
const ImGuiID id = window->GetID(str_id);
|
|
ImVec2 size = CalcItemSize(size_arg, 0.0f, 0.0f);
|
|
const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size);
|
|
ItemSize(bb);
|
|
if (!ItemAdd(bb, id))
|
|
return false;
|
|
|
|
bool hovered, held;
|
|
bool pressed = ButtonBehavior(bb, id, &hovered, &held);
|
|
|
|
return pressed;
|
|
}
|
|
|
|
bool ImGui::ArrowButtonEx(const char* str_id, ImGuiDir dir, ImVec2 size, ImGuiButtonFlags flags)
|
|
{
|
|
ImGuiWindow* window = GetCurrentWindow();
|
|
if (window->SkipItems)
|
|
return false;
|
|
|
|
ImGuiContext& g = *GImGui;
|
|
const ImGuiID id = window->GetID(str_id);
|
|
const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size);
|
|
const float default_size = GetFrameHeight();
|
|
ItemSize(bb, (size.y >= default_size) ? g.Style.FramePadding.y : 0.0f);
|
|
if (!ItemAdd(bb, id))
|
|
return false;
|
|
|
|
if (window->DC.ItemFlags & ImGuiItemFlags_ButtonRepeat)
|
|
flags |= ImGuiButtonFlags_Repeat;
|
|
|
|
bool hovered, held;
|
|
bool pressed = ButtonBehavior(bb, id, &hovered, &held, flags);
|
|
|
|
// Render
|
|
const ImU32 col = GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
|
|
RenderNavHighlight(bb, id);
|
|
RenderFrame(bb.Min, bb.Max, col, true, g.Style.FrameRounding);
|
|
RenderArrow(bb.Min + ImVec2(ImMax(0.0f, size.x - g.FontSize - g.Style.FramePadding.x), ImMax(0.0f, size.y - g.FontSize - g.Style.FramePadding.y)), dir);
|
|
|
|
return pressed;
|
|
}
|
|
|
|
bool ImGui::ArrowButton(const char* str_id, ImGuiDir dir)
|
|
{
|
|
float sz = GetFrameHeight();
|
|
return ArrowButtonEx(str_id, dir, ImVec2(sz, sz), 0);
|
|
}
|
|
|
|
// Button to close a window
|
|
bool ImGui::CloseButton(ImGuiID id, const ImVec2& pos, float radius)
|
|
{
|
|
ImGuiContext& g = *GImGui;
|
|
ImGuiWindow* window = g.CurrentWindow;
|
|
|
|
// We intentionally allow interaction when clipped so that a mechanical Alt,Right,Validate sequence close a window.
|
|
// (this isn't the regular behavior of buttons, but it doesn't affect the user much because navigation tends to keep items visible).
|
|
const ImRect bb(pos - ImVec2(radius,radius), pos + ImVec2(radius,radius));
|
|
bool is_clipped = !ItemAdd(bb, id);
|
|
|
|
bool hovered, held;
|
|
bool pressed = ButtonBehavior(bb, id, &hovered, &held);
|
|
if (is_clipped)
|
|
return pressed;
|
|
|
|
// Render
|
|
ImVec2 center = bb.GetCenter();
|
|
if (hovered)
|
|
window->DrawList->AddCircleFilled(center, ImMax(2.0f, radius), GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered), 9);
|
|
|
|
float cross_extent = (radius * 0.7071f) - 1.0f;
|
|
ImU32 cross_col = GetColorU32(ImGuiCol_Text);
|
|
center -= ImVec2(0.5f, 0.5f);
|
|
window->DrawList->AddLine(center + ImVec2(+cross_extent,+cross_extent), center + ImVec2(-cross_extent,-cross_extent), cross_col, 1.0f);
|
|
window->DrawList->AddLine(center + ImVec2(+cross_extent,-cross_extent), center + ImVec2(-cross_extent,+cross_extent), cross_col, 1.0f);
|
|
|
|
return pressed;
|
|
}
|
|
|
|
bool ImGui::CollapseButton(ImGuiID id, const ImVec2& pos)
|
|
{
|
|
ImGuiContext& g = *GImGui;
|
|
ImGuiWindow* window = g.CurrentWindow;
|
|
|
|
ImRect bb(pos, pos + ImVec2(g.FontSize, g.FontSize) + g.Style.FramePadding * 2.0f);
|
|
ItemAdd(bb, id);
|
|
bool hovered, held;
|
|
bool pressed = ButtonBehavior(bb, id, &hovered, &held, ImGuiButtonFlags_None);
|
|
|
|
ImU32 col = GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
|
|
if (hovered || held)
|
|
window->DrawList->AddCircleFilled(bb.GetCenter() + ImVec2(0.0f, -0.5f), g.FontSize * 0.5f + 1.0f, col, 9);
|
|
RenderArrow(bb.Min + g.Style.FramePadding, window->Collapsed ? ImGuiDir_Right : ImGuiDir_Down, 1.0f);
|
|
|
|
// Switch to moving the window after mouse is moved beyond the initial drag threshold
|
|
if (IsItemActive() && IsMouseDragging())
|
|
StartMouseMovingWindow(window);
|
|
|
|
return pressed;
|
|
}
|
|
|
|
void ImGui::Image(ImTextureID user_texture_id, const ImVec2& size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& tint_col, const ImVec4& border_col)
|
|
{
|
|
ImGuiWindow* window = GetCurrentWindow();
|
|
if (window->SkipItems)
|
|
return;
|
|
|
|
ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size);
|
|
if (border_col.w > 0.0f)
|
|
bb.Max += ImVec2(2, 2);
|
|
ItemSize(bb);
|
|
if (!ItemAdd(bb, 0))
|
|
return;
|
|
|
|
if (border_col.w > 0.0f)
|
|
{
|
|
window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(border_col), 0.0f);
|
|
window->DrawList->AddImage(user_texture_id, bb.Min + ImVec2(1, 1), bb.Max - ImVec2(1, 1), uv0, uv1, GetColorU32(tint_col));
|
|
}
|
|
else
|
|
{
|
|
window->DrawList->AddImage(user_texture_id, bb.Min, bb.Max, uv0, uv1, GetColorU32(tint_col));
|
|
}
|
|
}
|
|
|
|
// frame_padding < 0: uses FramePadding from style (default)
|
|
// frame_padding = 0: no framing
|
|
// frame_padding > 0: set framing size
|
|
// The color used are the button colors.
|
|
bool ImGui::ImageButton(ImTextureID user_texture_id, const ImVec2& size, const ImVec2& uv0, const ImVec2& uv1, int frame_padding, const ImVec4& bg_col, const ImVec4& tint_col)
|
|
{
|
|
ImGuiWindow* window = GetCurrentWindow();
|
|
if (window->SkipItems)
|
|
return false;
|
|
|
|
ImGuiContext& g = *GImGui;
|
|
const ImGuiStyle& style = g.Style;
|
|
|
|
// Default to using texture ID as ID. User can still push string/integer prefixes.
|
|
// We could hash the size/uv to create a unique ID but that would prevent the user from animating UV.
|
|
PushID((void*)user_texture_id);
|
|
const ImGuiID id = window->GetID("#image");
|
|
PopID();
|
|
|
|
const ImVec2 padding = (frame_padding >= 0) ? ImVec2((float)frame_padding, (float)frame_padding) : style.FramePadding;
|
|
const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size + padding * 2);
|
|
const ImRect image_bb(window->DC.CursorPos + padding, window->DC.CursorPos + padding + size);
|
|
ItemSize(bb);
|
|
if (!ItemAdd(bb, id))
|
|
return false;
|
|
|
|
bool hovered, held;
|
|
bool pressed = ButtonBehavior(bb, id, &hovered, &held);
|
|
|
|
// Render
|
|
const ImU32 col = GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
|
|
RenderNavHighlight(bb, id);
|
|
RenderFrame(bb.Min, bb.Max, col, true, ImClamp((float)ImMin(padding.x, padding.y), 0.0f, style.FrameRounding));
|
|
if (bg_col.w > 0.0f)
|
|
window->DrawList->AddRectFilled(image_bb.Min, image_bb.Max, GetColorU32(bg_col));
|
|
window->DrawList->AddImage(user_texture_id, image_bb.Min, image_bb.Max, uv0, uv1, GetColorU32(tint_col));
|
|
|
|
return pressed;
|
|
}
|
|
|
|
bool ImGui::Checkbox(const char* label, bool* v)
|
|
{
|
|
ImGuiWindow* window = GetCurrentWindow();
|
|
if (window->SkipItems)
|
|
return false;
|
|
|
|
ImGuiContext& g = *GImGui;
|
|
const ImGuiStyle& style = g.Style;
|
|
const ImGuiID id = window->GetID(label);
|
|
const ImVec2 label_size = CalcTextSize(label, NULL, true);
|
|
|
|
const ImRect check_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(label_size.y + style.FramePadding.y*2, label_size.y + style.FramePadding.y*2)); // We want a square shape to we use Y twice
|
|
ItemSize(check_bb, style.FramePadding.y);
|
|
|
|
ImRect total_bb = check_bb;
|
|
if (label_size.x > 0)
|
|
SameLine(0, style.ItemInnerSpacing.x);
|
|
const ImRect text_bb(window->DC.CursorPos + ImVec2(0,style.FramePadding.y), window->DC.CursorPos + ImVec2(0,style.FramePadding.y) + label_size);
|
|
if (label_size.x > 0)
|
|
{
|
|
ItemSize(ImVec2(text_bb.GetWidth(), check_bb.GetHeight()), style.FramePadding.y);
|
|
total_bb = ImRect(ImMin(check_bb.Min, text_bb.Min), ImMax(check_bb.Max, text_bb.Max));
|
|
}
|
|
|
|
if (!ItemAdd(total_bb, id))
|
|
return false;
|
|
|
|
bool hovered, held;
|
|
bool pressed = ButtonBehavior(total_bb, id, &hovered, &held);
|
|
if (pressed)
|
|
{
|
|
*v = !(*v);
|
|
MarkItemEdited(id);
|
|
}
|
|
|
|
RenderNavHighlight(total_bb, id);
|
|
RenderFrame(check_bb.Min, check_bb.Max, GetColorU32((held && hovered) ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg), true, style.FrameRounding);
|
|
if (*v)
|
|
{
|
|
const float check_sz = ImMin(check_bb.GetWidth(), check_bb.GetHeight());
|
|
const float pad = ImMax(1.0f, (float)(int)(check_sz / 6.0f));
|
|
RenderCheckMark(check_bb.Min + ImVec2(pad,pad), GetColorU32(ImGuiCol_CheckMark), check_bb.GetWidth() - pad*2.0f);
|
|
}
|
|
|
|
if (g.LogEnabled)
|
|
LogRenderedText(&text_bb.Min, *v ? "[x]" : "[ ]");
|
|
if (label_size.x > 0.0f)
|
|
RenderText(text_bb.Min, label);
|
|
|
|
return pressed;
|
|
}
|
|
|
|
bool ImGui::CheckboxFlags(const char* label, unsigned int* flags, unsigned int flags_value)
|
|
{
|
|
bool v = ((*flags & flags_value) == flags_value);
|
|
bool pressed = Checkbox(label, &v);
|
|
if (pressed)
|
|
{
|
|
if (v)
|
|
*flags |= flags_value;
|
|
else
|
|
*flags &= ~flags_value;
|
|
}
|
|
|
|
return pressed;
|
|
}
|
|
|
|
bool ImGui::RadioButton(const char* label, bool active)
|
|
{
|
|
ImGuiWindow* window = GetCurrentWindow();
|
|
if (window->SkipItems)
|
|
return false;
|
|
|
|
ImGuiContext& g = *GImGui;
|
|
const ImGuiStyle& style = g.Style;
|
|
const ImGuiID id = window->GetID(label);
|
|
const ImVec2 label_size = CalcTextSize(label, NULL, true);
|
|
|
|
const ImRect check_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(label_size.y + style.FramePadding.y*2-1, label_size.y + style.FramePadding.y*2-1));
|
|
ItemSize(check_bb, style.FramePadding.y);
|
|
|
|
ImRect total_bb = check_bb;
|
|
if (label_size.x > 0)
|
|
SameLine(0, style.ItemInnerSpacing.x);
|
|
const ImRect text_bb(window->DC.CursorPos + ImVec2(0, style.FramePadding.y), window->DC.CursorPos + ImVec2(0, style.FramePadding.y) + label_size);
|
|
if (label_size.x > 0)
|
|
{
|
|
ItemSize(ImVec2(text_bb.GetWidth(), check_bb.GetHeight()), style.FramePadding.y);
|
|
total_bb.Add(text_bb);
|
|
}
|
|
|
|
if (!ItemAdd(total_bb, id))
|
|
return false;
|
|
|
|
ImVec2 center = check_bb.GetCenter();
|
|
center.x = (float)(int)center.x + 0.5f;
|
|
center.y = (float)(int)center.y + 0.5f;
|
|
const float radius = check_bb.GetHeight() * 0.5f;
|
|
|
|
bool hovered, held;
|
|
bool pressed = ButtonBehavior(total_bb, id, &hovered, &held);
|
|
if (pressed)
|
|
MarkItemEdited(id);
|
|
|
|
RenderNavHighlight(total_bb, id);
|
|
window->DrawList->AddCircleFilled(center, radius, GetColorU32((held && hovered) ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg), 16);
|
|
if (active)
|
|
{
|
|
const float check_sz = ImMin(check_bb.GetWidth(), check_bb.GetHeight());
|
|
const float pad = ImMax(1.0f, (float)(int)(check_sz / 6.0f));
|
|
window->DrawList->AddCircleFilled(center, radius-pad, GetColorU32(ImGuiCol_CheckMark), 16);
|
|
}
|
|
|
|
if (style.FrameBorderSize > 0.0f)
|
|
{
|
|
window->DrawList->AddCircle(center+ImVec2(1,1), radius, GetColorU32(ImGuiCol_BorderShadow), 16, style.FrameBorderSize);
|
|
window->DrawList->AddCircle(center, radius, GetColorU32(ImGuiCol_Border), 16, style.FrameBorderSize);
|
|
}
|
|
|
|
if (g.LogEnabled)
|
|
LogRenderedText(&text_bb.Min, active ? "(x)" : "( )");
|
|
if (label_size.x > 0.0f)
|
|
RenderText(text_bb.Min, label);
|
|
|
|
return pressed;
|
|
}
|
|
|
|
bool ImGui::RadioButton(const char* label, int* v, int v_button)
|
|
{
|
|
const bool pressed = RadioButton(label, *v == v_button);
|
|
if (pressed)
|
|
*v = v_button;
|
|
return pressed;
|
|
}
|
|
|
|
// size_arg (for each axis) < 0.0f: align to end, 0.0f: auto, > 0.0f: specified size
|
|
void ImGui::ProgressBar(float fraction, const ImVec2& size_arg, const char* overlay)
|
|
{
|
|
ImGuiWindow* window = GetCurrentWindow();
|
|
if (window->SkipItems)
|
|
return;
|
|
|
|
ImGuiContext& g = *GImGui;
|
|
const ImGuiStyle& style = g.Style;
|
|
|
|
ImVec2 pos = window->DC.CursorPos;
|
|
ImRect bb(pos, pos + CalcItemSize(size_arg, CalcItemWidth(), g.FontSize + style.FramePadding.y*2.0f));
|
|
ItemSize(bb, style.FramePadding.y);
|
|
if (!ItemAdd(bb, 0))
|
|
return;
|
|
|
|
// Render
|
|
fraction = ImSaturate(fraction);
|
|
RenderFrame(bb.Min, bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding);
|
|
bb.Expand(ImVec2(-style.FrameBorderSize, -style.FrameBorderSize));
|
|
const ImVec2 fill_br = ImVec2(ImLerp(bb.Min.x, bb.Max.x, fraction), bb.Max.y);
|
|
RenderRectFilledRangeH(window->DrawList, bb, GetColorU32(ImGuiCol_PlotHistogram), 0.0f, fraction, style.FrameRounding);
|
|
|
|
// Default displaying the fraction as percentage string, but user can override it
|
|
char overlay_buf[32];
|
|
if (!overlay)
|
|
{
|
|
ImFormatString(overlay_buf, IM_ARRAYSIZE(overlay_buf), "%.0f%%", fraction*100+0.01f);
|
|
overlay = overlay_buf;
|
|
}
|
|
|
|
ImVec2 overlay_size = CalcTextSize(overlay, NULL);
|
|
if (overlay_size.x > 0.0f)
|
|
RenderTextClipped(ImVec2(ImClamp(fill_br.x + style.ItemSpacing.x, bb.Min.x, bb.Max.x - overlay_size.x - style.ItemInnerSpacing.x), bb.Min.y), bb.Max, overlay, NULL, &overlay_size, ImVec2(0.0f,0.5f), &bb);
|
|
}
|
|
|
|
void ImGui::Bullet()
|
|
{
|
|
ImGuiWindow* window = GetCurrentWindow();
|
|
if (window->SkipItems)
|
|
return;
|
|
|
|
ImGuiContext& g = *GImGui;
|
|
const ImGuiStyle& style = g.Style;
|
|
const float line_height = ImMax(ImMin(window->DC.CurrentLineSize.y, g.FontSize + g.Style.FramePadding.y*2), g.FontSize);
|
|
const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(g.FontSize, line_height));
|
|
ItemSize(bb);
|
|
if (!ItemAdd(bb, 0))
|
|
{
|
|
SameLine(0, style.FramePadding.x*2);
|
|
return;
|
|
}
|
|
|
|
// Render and stay on same line
|
|
RenderBullet(bb.Min + ImVec2(style.FramePadding.x + g.FontSize*0.5f, line_height*0.5f));
|
|
SameLine(0, style.FramePadding.x*2);
|
|
}
|
|
|
|
//-------------------------------------------------------------------------
|
|
// WIDGETS: Combo Box
|
|
// - BeginCombo()
|
|
// - EndCombo()
|
|
// - Combo()
|
|
//-------------------------------------------------------------------------
|
|
|
|
static float CalcMaxPopupHeightFromItemCount(int items_count)
|
|
{
|
|
ImGuiContext& g = *GImGui;
|
|
if (items_count <= 0)
|
|
return FLT_MAX;
|
|
return (g.FontSize + g.Style.ItemSpacing.y) * items_count - g.Style.ItemSpacing.y + (g.Style.WindowPadding.y * 2);
|
|
}
|
|
|
|
bool ImGui::BeginCombo(const char* label, const char* preview_value, ImGuiComboFlags flags)
|
|
{
|
|
// Always consume the SetNextWindowSizeConstraint() call in our early return paths
|
|
ImGuiContext& g = *GImGui;
|
|
ImGuiCond backup_next_window_size_constraint = g.NextWindowData.SizeConstraintCond;
|
|
g.NextWindowData.SizeConstraintCond = 0;
|
|
|
|
ImGuiWindow* window = GetCurrentWindow();
|
|
if (window->SkipItems)
|
|
return false;
|
|
|
|
IM_ASSERT((flags & (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)) != (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)); // Can't use both flags together
|
|
|
|
const ImGuiStyle& style = g.Style;
|
|
const ImGuiID id = window->GetID(label);
|
|
|
|
const float arrow_size = (flags & ImGuiComboFlags_NoArrowButton) ? 0.0f : GetFrameHeight();
|
|
const ImVec2 label_size = CalcTextSize(label, NULL, true);
|
|
const float w = (flags & ImGuiComboFlags_NoPreview) ? arrow_size : CalcItemWidth();
|
|
const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y*2.0f));
|
|
const ImRect total_bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f));
|
|
ItemSize(total_bb, style.FramePadding.y);
|
|
if (!ItemAdd(total_bb, id, &frame_bb))
|
|
return false;
|
|
|
|
bool hovered, held;
|
|
bool pressed = ButtonBehavior(frame_bb, id, &hovered, &held);
|
|
bool popup_open = IsPopupOpen(id);
|
|
|
|
const ImRect value_bb(frame_bb.Min, frame_bb.Max - ImVec2(arrow_size, 0.0f));
|
|
const ImU32 frame_col = GetColorU32(hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
|
|
RenderNavHighlight(frame_bb, id);
|
|
if (!(flags & ImGuiComboFlags_NoPreview))
|
|
window->DrawList->AddRectFilled(frame_bb.Min, ImVec2(frame_bb.Max.x - arrow_size, frame_bb.Max.y), frame_col, style.FrameRounding, ImDrawCornerFlags_Left);
|
|
if (!(flags & ImGuiComboFlags_NoArrowButton))
|
|
{
|
|
window->DrawList->AddRectFilled(ImVec2(frame_bb.Max.x - arrow_size, frame_bb.Min.y), frame_bb.Max, GetColorU32((popup_open || hovered) ? ImGuiCol_ButtonHovered : ImGuiCol_Button), style.FrameRounding, (w <= arrow_size) ? ImDrawCornerFlags_All : ImDrawCornerFlags_Right);
|
|
RenderArrow(ImVec2(frame_bb.Max.x - arrow_size + style.FramePadding.y, frame_bb.Min.y + style.FramePadding.y), ImGuiDir_Down);
|
|
}
|
|
RenderFrameBorder(frame_bb.Min, frame_bb.Max, style.FrameRounding);
|
|
if (preview_value != NULL && !(flags & ImGuiComboFlags_NoPreview))
|
|
RenderTextClipped(frame_bb.Min + style.FramePadding, value_bb.Max, preview_value, NULL, NULL, ImVec2(0.0f,0.0f));
|
|
if (label_size.x > 0)
|
|
RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label);
|
|
|
|
if ((pressed || g.NavActivateId == id) && !popup_open)
|
|
{
|
|
if (window->DC.NavLayerCurrent == 0)
|
|
window->NavLastIds[0] = id;
|
|
OpenPopupEx(id);
|
|
popup_open = true;
|
|
}
|
|
|
|
if (!popup_open)
|
|
return false;
|
|
|
|
if (backup_next_window_size_constraint)
|
|
{
|
|
g.NextWindowData.SizeConstraintCond = backup_next_window_size_constraint;
|
|
g.NextWindowData.SizeConstraintRect.Min.x = ImMax(g.NextWindowData.SizeConstraintRect.Min.x, w);
|
|
}
|
|
else
|
|
{
|
|
if ((flags & ImGuiComboFlags_HeightMask_) == 0)
|
|
flags |= ImGuiComboFlags_HeightRegular;
|
|
IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiComboFlags_HeightMask_)); // Only one
|
|
int popup_max_height_in_items = -1;
|
|
if (flags & ImGuiComboFlags_HeightRegular) popup_max_height_in_items = 8;
|
|
else if (flags & ImGuiComboFlags_HeightSmall) popup_max_height_in_items = 4;
|
|
else if (flags & ImGuiComboFlags_HeightLarge) popup_max_height_in_items = 20;
|
|
SetNextWindowSizeConstraints(ImVec2(w, 0.0f), ImVec2(FLT_MAX, CalcMaxPopupHeightFromItemCount(popup_max_height_in_items)));
|
|
}
|
|
|
|
char name[16];
|
|
ImFormatString(name, IM_ARRAYSIZE(name), "##Combo_%02d", g.CurrentPopupStack.Size); // Recycle windows based on depth
|
|
|
|
// Peak into expected window size so we can position it
|
|
if (ImGuiWindow* popup_window = FindWindowByName(name))
|
|
if (popup_window->WasActive)
|
|
{
|
|
ImVec2 size_expected = CalcWindowExpectedSize(popup_window);
|
|
if (flags & ImGuiComboFlags_PopupAlignLeft)
|
|
popup_window->AutoPosLastDirection = ImGuiDir_Left;
|
|
ImRect r_outer = GetWindowAllowedExtentRect(popup_window);
|
|
ImVec2 pos = FindBestWindowPosForPopupEx(frame_bb.GetBL(), size_expected, &popup_window->AutoPosLastDirection, r_outer, frame_bb, ImGuiPopupPositionPolicy_ComboBox);
|
|
SetNextWindowPos(pos);
|
|
}
|
|
|
|
// Horizontally align ourselves with the framed text
|
|
ImGuiWindowFlags window_flags = ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_Popup | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings;
|
|
PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(style.FramePadding.x, style.WindowPadding.y));
|
|
bool ret = Begin(name, NULL, window_flags);
|
|
PopStyleVar();
|
|
if (!ret)
|
|
{
|
|
EndPopup();
|
|
IM_ASSERT(0); // This should never happen as we tested for IsPopupOpen() above
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void ImGui::EndCombo()
|
|
{
|
|
EndPopup();
|
|
}
|
|
|
|
// Getter for the old Combo() API: const char*[]
|
|
static bool Items_ArrayGetter(void* data, int idx, const char** out_text)
|
|
{
|
|
const char* const* items = (const char* const*)data;
|
|
if (out_text)
|
|
*out_text = items[idx];
|
|
return true;
|
|
}
|
|
|
|
// Getter for the old Combo() API: "item1\0item2\0item3\0"
|
|
static bool Items_SingleStringGetter(void* data, int idx, const char** out_text)
|
|
{
|
|
// FIXME-OPT: we could pre-compute the indices to fasten this. But only 1 active combo means the waste is limited.
|
|
const char* items_separated_by_zeros = (const char*)data;
|
|
int items_count = 0;
|
|
const char* p = items_separated_by_zeros;
|
|
while (*p)
|
|
{
|
|
if (idx == items_count)
|
|
break;
|
|
p += strlen(p) + 1;
|
|
items_count++;
|
|
}
|
|
if (!*p)
|
|
return false;
|
|
if (out_text)
|
|
*out_text = p;
|
|
return true;
|
|
}
|
|
|
|
// Old API, prefer using BeginCombo() nowadays if you can.
|
|
bool ImGui::Combo(const char* label, int* current_item, bool (*items_getter)(void*, int, const char**), void* data, int items_count, int popup_max_height_in_items)
|
|
{
|
|
ImGuiContext& g = *GImGui;
|
|
|
|
// Call the getter to obtain the preview string which is a parameter to BeginCombo()
|
|
const char* preview_value = NULL;
|
|
if (*current_item >= 0 && *current_item < items_count)
|
|
items_getter(data, *current_item, &preview_value);
|
|
|
|
// The old Combo() API exposed "popup_max_height_in_items". The new more general BeginCombo() API doesn't have/need it, but we emulate it here.
|
|
if (popup_max_height_in_items != -1 && !g.NextWindowData.SizeConstraintCond)
|
|
SetNextWindowSizeConstraints(ImVec2(0,0), ImVec2(FLT_MAX, CalcMaxPopupHeightFromItemCount(popup_max_height_in_items)));
|
|
|
|
if (!BeginCombo(label, preview_value, ImGuiComboFlags_None))
|
|
return false;
|
|
|
|
// Display items
|
|
// FIXME-OPT: Use clipper (but we need to disable it on the appearing frame to make sure our call to SetItemDefaultFocus() is processed)
|
|
bool value_changed = false;
|
|
for (int i = 0; i < items_count; i++)
|
|
{
|
|
PushID((void*)(intptr_t)i);
|
|
const bool item_selected = (i == *current_item);
|
|
const char* item_text;
|
|
if (!items_getter(data, i, &item_text))
|
|
item_text = "*Unknown item*";
|
|
if (Selectable(item_text, item_selected))
|
|
{
|
|
value_changed = true;
|
|
*current_item = i;
|
|
}
|
|
if (item_selected)
|
|
SetItemDefaultFocus();
|
|
PopID();
|
|
}
|
|
|
|
EndCombo();
|
|
return value_changed;
|
|
}
|
|
|
|
// Combo box helper allowing to pass an array of strings.
|
|
bool ImGui::Combo(const char* label, int* current_item, const char* const items[], int items_count, int height_in_items)
|
|
{
|
|
const bool value_changed = Combo(label, current_item, Items_ArrayGetter, (void*)items, items_count, height_in_items);
|
|
return value_changed;
|
|
}
|
|
|
|
// Combo box helper allowing to pass all items in a single string literal holding multiple zero-terminated items "item1\0item2\0"
|
|
bool ImGui::Combo(const char* label, int* current_item, const char* items_separated_by_zeros, int height_in_items)
|
|
{
|
|
int items_count = 0;
|
|
const char* p = items_separated_by_zeros; // FIXME-OPT: Avoid computing this, or at least only when combo is open
|
|
while (*p)
|
|
{
|
|
p += strlen(p) + 1;
|
|
items_count++;
|
|
}
|
|
bool value_changed = Combo(label, current_item, Items_SingleStringGetter, (void*)items_separated_by_zeros, items_count, height_in_items);
|
|
return value_changed;
|
|
}
|
|
|
|
//-------------------------------------------------------------------------
|
|
// WIDGETS: Data Type and Data Formatting Helpers [Internal]
|
|
// - PatchFormatStringFloatToInt()
|
|
// - DataTypeFormatString()
|
|
// - DataTypeApplyOp()
|
|
// - DataTypeApplyOpFromText()
|
|
// - GetMinimumStepAtDecimalPrecision
|
|
// - RoundScalarWithFormat<>()
|
|
//-------------------------------------------------------------------------
|
|
|
|
|
|
//-------------------------------------------------------------------------
|
|
// WIDGETS: Drags
|
|
// - DragBehaviorT<>() [Internal]
|
|
// - DragBehavior() [Internal]
|
|
// - DragScalar()
|
|
// - DragScalarN()
|
|
// - DragFloat()
|
|
// - DragFloat2()
|
|
// - DragFloat3()
|
|
// - DragFloat4()
|
|
// - DragFloatRange2()
|
|
// - DragInt()
|
|
// - DragInt2()
|
|
// - DragInt3()
|
|
// - DragInt4()
|
|
// - DragIntRange2()
|
|
//-------------------------------------------------------------------------
|
|
|
|
|
|
//-------------------------------------------------------------------------
|
|
// WIDGETS: Sliders
|
|
// - SliderBehaviorT<>() [Internal]
|
|
// - SliderBehavior() [Internal]
|
|
// - SliderScalar()
|
|
// - SliderScalarN()
|
|
// - SliderFloat()
|
|
// - SliderFloat2()
|
|
// - SliderFloat3()
|
|
// - SliderFloat4()
|
|
// - SliderAngle()
|
|
// - SliderInt()
|
|
// - SliderInt2()
|
|
// - SliderInt3()
|
|
// - SliderInt4()
|
|
// - VSliderScalar()
|
|
// - VSliderFloat()
|
|
// - VSliderInt()
|
|
//-------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//-------------------------------------------------------------------------
|
|
// WIDGETS: Inputs (_excepted InputText_)
|
|
// - ImParseFormatFindStart()
|
|
// - ImParseFormatFindEnd()
|
|
// - ImParseFormatTrimDecorations()
|
|
// - ImParseFormatPrecision()
|
|
// - InputScalarAsWidgetReplacement() [Internal]
|
|
// - InputScalar()
|
|
// - InputScalarN()
|
|
// - InputFloat()
|
|
// - InputFloat2()
|
|
// - InputFloat3()
|
|
// - InputFloat4()
|
|
// - InputInt()
|
|
// - InputInt2()
|
|
// - InputInt3()
|
|
// - InputInt4()
|
|
// - InputDouble()
|
|
//-------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//-------------------------------------------------------------------------
|
|
// WIDGETS: InputText
|
|
// - InputText()
|
|
// - InputTextMultiline()
|
|
// - InputTextEx() [Internal]
|
|
//-------------------------------------------------------------------------
|
|
|
|
|
|
//-------------------------------------------------------------------------
|
|
// WIDGETS: Color Editor / Picker
|
|
// - ColorEdit3()
|
|
// - ColorEdit4()
|
|
// - ColorPicker3()
|
|
// - RenderColorRectWithAlphaCheckerboard() [Internal]
|
|
// - ColorPicker4()
|
|
// - ColorButton()
|
|
// - SetColorEditOptions()
|
|
// - ColorTooltip() [Internal]
|
|
// - ColorEditOptionsPopup() [Internal]
|
|
// - ColorPickerOptionsPopup() [Internal]
|
|
//-------------------------------------------------------------------------
|
|
|
|
bool ImGui::ColorEdit3(const char* label, float col[3], ImGuiColorEditFlags flags)
|
|
{
|
|
return ColorEdit4(label, col, flags | ImGuiColorEditFlags_NoAlpha);
|
|
}
|
|
|
|
// Edit colors components (each component in 0.0f..1.0f range).
|
|
// See enum ImGuiColorEditFlags_ for available options. e.g. Only access 3 floats if ImGuiColorEditFlags_NoAlpha flag is set.
|
|
// With typical options: Left-click on colored square to open color picker. Right-click to open option menu. CTRL-Click over input fields to edit them and TAB to go to next item.
|
|
bool ImGui::ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flags)
|
|
{
|
|
ImGuiWindow* window = GetCurrentWindow();
|
|
if (window->SkipItems)
|
|
return false;
|
|
|
|
ImGuiContext& g = *GImGui;
|
|
const ImGuiStyle& style = g.Style;
|
|
const float square_sz = GetFrameHeight();
|
|
const float w_extra = (flags & ImGuiColorEditFlags_NoSmallPreview) ? 0.0f : (square_sz + style.ItemInnerSpacing.x);
|
|
const float w_items_all = CalcItemWidth() - w_extra;
|
|
const char* label_display_end = FindRenderedTextEnd(label);
|
|
|
|
BeginGroup();
|
|
PushID(label);
|
|
|
|
// If we're not showing any slider there's no point in doing any HSV conversions
|
|
const ImGuiColorEditFlags flags_untouched = flags;
|
|
if (flags & ImGuiColorEditFlags_NoInputs)
|
|
flags = (flags & (~ImGuiColorEditFlags__InputsMask)) | ImGuiColorEditFlags_RGB | ImGuiColorEditFlags_NoOptions;
|
|
|
|
// Context menu: display and modify options (before defaults are applied)
|
|
if (!(flags & ImGuiColorEditFlags_NoOptions))
|
|
ColorEditOptionsPopup(col, flags);
|
|
|
|
// Read stored options
|
|
if (!(flags & ImGuiColorEditFlags__InputsMask))
|
|
flags |= (g.ColorEditOptions & ImGuiColorEditFlags__InputsMask);
|
|
if (!(flags & ImGuiColorEditFlags__DataTypeMask))
|
|
flags |= (g.ColorEditOptions & ImGuiColorEditFlags__DataTypeMask);
|
|
if (!(flags & ImGuiColorEditFlags__PickerMask))
|
|
flags |= (g.ColorEditOptions & ImGuiColorEditFlags__PickerMask);
|
|
flags |= (g.ColorEditOptions & ~(ImGuiColorEditFlags__InputsMask | ImGuiColorEditFlags__DataTypeMask | ImGuiColorEditFlags__PickerMask));
|
|
|
|
const bool alpha = (flags & ImGuiColorEditFlags_NoAlpha) == 0;
|
|
const bool hdr = (flags & ImGuiColorEditFlags_HDR) != 0;
|
|
const int components = alpha ? 4 : 3;
|
|
|
|
// Convert to the formats we need
|
|
float f[4] = { col[0], col[1], col[2], alpha ? col[3] : 1.0f };
|
|
if (flags & ImGuiColorEditFlags_HSV)
|
|
ColorConvertRGBtoHSV(f[0], f[1], f[2], f[0], f[1], f[2]);
|
|
int i[4] = { IM_F32_TO_INT8_UNBOUND(f[0]), IM_F32_TO_INT8_UNBOUND(f[1]), IM_F32_TO_INT8_UNBOUND(f[2]), IM_F32_TO_INT8_UNBOUND(f[3]) };
|
|
|
|
bool value_changed = false;
|
|
bool value_changed_as_float = false;
|
|
|
|
if ((flags & (ImGuiColorEditFlags_RGB | ImGuiColorEditFlags_HSV)) != 0 && (flags & ImGuiColorEditFlags_NoInputs) == 0)
|
|
{
|
|
// RGB/HSV 0..255 Sliders
|
|
const float w_item_one = ImMax(1.0f, (float)(int)((w_items_all - (style.ItemInnerSpacing.x) * (components-1)) / (float)components));
|
|
const float w_item_last = ImMax(1.0f, (float)(int)(w_items_all - (w_item_one + style.ItemInnerSpacing.x) * (components-1)));
|
|
|
|
const bool hide_prefix = (w_item_one <= CalcTextSize((flags & ImGuiColorEditFlags_Float) ? "M:0.000" : "M:000").x);
|
|
const char* ids[4] = { "##X", "##Y", "##Z", "##W" };
|
|
const char* fmt_table_int[3][4] =
|
|
{
|
|
{ "%3d", "%3d", "%3d", "%3d" }, // Short display
|
|
{ "R:%3d", "G:%3d", "B:%3d", "A:%3d" }, // Long display for RGBA
|
|
{ "H:%3d", "S:%3d", "V:%3d", "A:%3d" } // Long display for HSVA
|
|
};
|
|
const char* fmt_table_float[3][4] =
|
|
{
|
|
{ "%0.3f", "%0.3f", "%0.3f", "%0.3f" }, // Short display
|
|
{ "R:%0.3f", "G:%0.3f", "B:%0.3f", "A:%0.3f" }, // Long display for RGBA
|
|
{ "H:%0.3f", "S:%0.3f", "V:%0.3f", "A:%0.3f" } // Long display for HSVA
|
|
};
|
|
const int fmt_idx = hide_prefix ? 0 : (flags & ImGuiColorEditFlags_HSV) ? 2 : 1;
|
|
|
|
PushItemWidth(w_item_one);
|
|
for (int n = 0; n < components; n++)
|
|
{
|
|
if (n > 0)
|
|
SameLine(0, style.ItemInnerSpacing.x);
|
|
if (n + 1 == components)
|
|
PushItemWidth(w_item_last);
|
|
if (flags & ImGuiColorEditFlags_Float)
|
|
value_changed = value_changed_as_float = value_changed | DragFloat(ids[n], &f[n], 1.0f/255.0f, 0.0f, hdr ? 0.0f : 1.0f, fmt_table_float[fmt_idx][n]);
|
|
else
|
|
value_changed |= DragInt(ids[n], &i[n], 1.0f, 0, hdr ? 0 : 255, fmt_table_int[fmt_idx][n]);
|
|
if (!(flags & ImGuiColorEditFlags_NoOptions))
|
|
OpenPopupOnItemClick("context");
|
|
}
|
|
PopItemWidth();
|
|
PopItemWidth();
|
|
}
|
|
else if ((flags & ImGuiColorEditFlags_HEX) != 0 && (flags & ImGuiColorEditFlags_NoInputs) == 0)
|
|
{
|
|
// RGB Hexadecimal Input
|
|
char buf[64];
|
|
if (alpha)
|
|
ImFormatString(buf, IM_ARRAYSIZE(buf), "#%02X%02X%02X%02X", ImClamp(i[0],0,255), ImClamp(i[1],0,255), ImClamp(i[2],0,255), ImClamp(i[3],0,255));
|
|
else
|
|
ImFormatString(buf, IM_ARRAYSIZE(buf), "#%02X%02X%02X", ImClamp(i[0],0,255), ImClamp(i[1],0,255), ImClamp(i[2],0,255));
|
|
PushItemWidth(w_items_all);
|
|
if (InputText("##Text", buf, IM_ARRAYSIZE(buf), ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_CharsUppercase))
|
|
{
|
|
value_changed = true;
|
|
char* p = buf;
|
|
while (*p == '#' || ImCharIsBlankA(*p))
|
|
p++;
|
|
i[0] = i[1] = i[2] = i[3] = 0;
|
|
if (alpha)
|
|
sscanf(p, "%02X%02X%02X%02X", (unsigned int*)&i[0], (unsigned int*)&i[1], (unsigned int*)&i[2], (unsigned int*)&i[3]); // Treat at unsigned (%X is unsigned)
|
|
else
|
|
sscanf(p, "%02X%02X%02X", (unsigned int*)&i[0], (unsigned int*)&i[1], (unsigned int*)&i[2]);
|
|
}
|
|
if (!(flags & ImGuiColorEditFlags_NoOptions))
|
|
OpenPopupOnItemClick("context");
|
|
PopItemWidth();
|
|
}
|
|
|
|
ImGuiWindow* picker_active_window = NULL;
|
|
if (!(flags & ImGuiColorEditFlags_NoSmallPreview))
|
|
{
|
|
if (!(flags & ImGuiColorEditFlags_NoInputs))
|
|
SameLine(0, style.ItemInnerSpacing.x);
|
|
|
|
const ImVec4 col_v4(col[0], col[1], col[2], alpha ? col[3] : 1.0f);
|
|
if (ColorButton("##ColorButton", col_v4, flags))
|
|
{
|
|
if (!(flags & ImGuiColorEditFlags_NoPicker))
|
|
{
|
|
// Store current color and open a picker
|
|
g.ColorPickerRef = col_v4;
|
|
OpenPopup("picker");
|
|
SetNextWindowPos(window->DC.LastItemRect.GetBL() + ImVec2(-1,style.ItemSpacing.y));
|
|
}
|
|
}
|
|
if (!(flags & ImGuiColorEditFlags_NoOptions))
|
|
OpenPopupOnItemClick("context");
|
|
|
|
if (BeginPopup("picker"))
|
|
{
|
|
picker_active_window = g.CurrentWindow;
|
|
if (label != label_display_end)
|
|
{
|
|
TextUnformatted(label, label_display_end);
|
|
Separator();
|
|
}
|
|
ImGuiColorEditFlags picker_flags_to_forward = ImGuiColorEditFlags__DataTypeMask | ImGuiColorEditFlags__PickerMask | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaBar;
|
|
ImGuiColorEditFlags picker_flags = (flags_untouched & picker_flags_to_forward) | ImGuiColorEditFlags__InputsMask | ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_AlphaPreviewHalf;
|
|
PushItemWidth(square_sz * 12.0f); // Use 256 + bar sizes?
|
|
value_changed |= ColorPicker4("##picker", col, picker_flags, &g.ColorPickerRef.x);
|
|
PopItemWidth();
|
|
EndPopup();
|
|
}
|
|
}
|
|
|
|
if (label != label_display_end && !(flags & ImGuiColorEditFlags_NoLabel))
|
|
{
|
|
SameLine(0, style.ItemInnerSpacing.x);
|
|
TextUnformatted(label, label_display_end);
|
|
}
|
|
|
|
// Convert back
|
|
if (picker_active_window == NULL)
|
|
{
|
|
if (!value_changed_as_float)
|
|
for (int n = 0; n < 4; n++)
|
|
f[n] = i[n] / 255.0f;
|
|
if (flags & ImGuiColorEditFlags_HSV)
|
|
ColorConvertHSVtoRGB(f[0], f[1], f[2], f[0], f[1], f[2]);
|
|
if (value_changed)
|
|
{
|
|
col[0] = f[0];
|
|
col[1] = f[1];
|
|
col[2] = f[2];
|
|
if (alpha)
|
|
col[3] = f[3];
|
|
}
|
|
}
|
|
|
|
PopID();
|
|
EndGroup();
|
|
|
|
// Drag and Drop Target
|
|
// NB: The flag test is merely an optional micro-optimization, BeginDragDropTarget() does the same test.
|
|
if ((window->DC.LastItemStatusFlags & ImGuiItemStatusFlags_HoveredRect) && !(flags & ImGuiColorEditFlags_NoDragDrop) && BeginDragDropTarget())
|
|
{
|
|
if (const ImGuiPayload* payload = AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_3F))
|
|
{
|
|
memcpy((float*)col, payload->Data, sizeof(float) * 3);
|
|
value_changed = true;
|
|
}
|
|
if (const ImGuiPayload* payload = AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_4F))
|
|
{
|
|
memcpy((float*)col, payload->Data, sizeof(float) * components);
|
|
value_changed = true;
|
|
}
|
|
EndDragDropTarget();
|
|
}
|
|
|
|
// When picker is being actively used, use its active id so IsItemActive() will function on ColorEdit4().
|
|
if (picker_active_window && g.ActiveId != 0 && g.ActiveIdWindow == picker_active_window)
|
|
window->DC.LastItemId = g.ActiveId;
|
|
|
|
if (value_changed)
|
|
MarkItemEdited(window->DC.LastItemId);
|
|
|
|
return value_changed;
|
|
}
|
|
|
|
bool ImGui::ColorPicker3(const char* label, float col[3], ImGuiColorEditFlags flags)
|
|
{
|
|
float col4[4] = { col[0], col[1], col[2], 1.0f };
|
|
if (!ColorPicker4(label, col4, flags | ImGuiColorEditFlags_NoAlpha))
|
|
return false;
|
|
col[0] = col4[0]; col[1] = col4[1]; col[2] = col4[2];
|
|
return true;
|
|
}
|
|
|
|
static inline ImU32 ImAlphaBlendColor(ImU32 col_a, ImU32 col_b)
|
|
{
|
|
float t = ((col_b >> IM_COL32_A_SHIFT) & 0xFF) / 255.f;
|
|
int r = ImLerp((int)(col_a >> IM_COL32_R_SHIFT) & 0xFF, (int)(col_b >> IM_COL32_R_SHIFT) & 0xFF, t);
|
|
int g = ImLerp((int)(col_a >> IM_COL32_G_SHIFT) & 0xFF, (int)(col_b >> IM_COL32_G_SHIFT) & 0xFF, t);
|
|
int b = ImLerp((int)(col_a >> IM_COL32_B_SHIFT) & 0xFF, (int)(col_b >> IM_COL32_B_SHIFT) & 0xFF, t);
|
|
return IM_COL32(r, g, b, 0xFF);
|
|
}
|
|
|
|
// Helper for ColorPicker4()
|
|
// NB: This is rather brittle and will show artifact when rounding this enabled if rounded corners overlap multiple cells. Caller currently responsible for avoiding that.
|
|
// I spent a non reasonable amount of time trying to getting this right for ColorButton with rounding+anti-aliasing+ImGuiColorEditFlags_HalfAlphaPreview flag + various grid sizes and offsets, and eventually gave up... probably more reasonable to disable rounding alltogether.
|
|
void ImGui::RenderColorRectWithAlphaCheckerboard(ImVec2 p_min, ImVec2 p_max, ImU32 col, float grid_step, ImVec2 grid_off, float rounding, int rounding_corners_flags)
|
|
{
|
|
ImGuiWindow* window = GetCurrentWindow();
|
|
if (((col & IM_COL32_A_MASK) >> IM_COL32_A_SHIFT) < 0xFF)
|
|
{
|
|
ImU32 col_bg1 = GetColorU32(ImAlphaBlendColor(IM_COL32(204,204,204,255), col));
|
|
ImU32 col_bg2 = GetColorU32(ImAlphaBlendColor(IM_COL32(128,128,128,255), col));
|
|
window->DrawList->AddRectFilled(p_min, p_max, col_bg1, rounding, rounding_corners_flags);
|
|
|
|
int yi = 0;
|
|
for (float y = p_min.y + grid_off.y; y < p_max.y; y += grid_step, yi++)
|
|
{
|
|
float y1 = ImClamp(y, p_min.y, p_max.y), y2 = ImMin(y + grid_step, p_max.y);
|
|
if (y2 <= y1)
|
|
continue;
|
|
for (float x = p_min.x + grid_off.x + (yi & 1) * grid_step; x < p_max.x; x += grid_step * 2.0f)
|
|
{
|
|
float x1 = ImClamp(x, p_min.x, p_max.x), x2 = ImMin(x + grid_step, p_max.x);
|
|
if (x2 <= x1)
|
|
continue;
|
|
int rounding_corners_flags_cell = 0;
|
|
if (y1 <= p_min.y) { if (x1 <= p_min.x) rounding_corners_flags_cell |= ImDrawCornerFlags_TopLeft; if (x2 >= p_max.x) rounding_corners_flags_cell |= ImDrawCornerFlags_TopRight; }
|
|
if (y2 >= p_max.y) { if (x1 <= p_min.x) rounding_corners_flags_cell |= ImDrawCornerFlags_BotLeft; if (x2 >= p_max.x) rounding_corners_flags_cell |= ImDrawCornerFlags_BotRight; }
|
|
rounding_corners_flags_cell &= rounding_corners_flags;
|
|
window->DrawList->AddRectFilled(ImVec2(x1,y1), ImVec2(x2,y2), col_bg2, rounding_corners_flags_cell ? rounding : 0.0f, rounding_corners_flags_cell);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
window->DrawList->AddRectFilled(p_min, p_max, col, rounding, rounding_corners_flags);
|
|
}
|
|
}
|
|
|
|
// Helper for ColorPicker4()
|
|
static void RenderArrowsForVerticalBar(ImDrawList* draw_list, ImVec2 pos, ImVec2 half_sz, float bar_w)
|
|
{
|
|
ImGui::RenderArrowPointingAt(draw_list, ImVec2(pos.x + half_sz.x + 1, pos.y), ImVec2(half_sz.x + 2, half_sz.y + 1), ImGuiDir_Right, IM_COL32_BLACK);
|
|
ImGui::RenderArrowPointingAt(draw_list, ImVec2(pos.x + half_sz.x, pos.y), half_sz, ImGuiDir_Right, IM_COL32_WHITE);
|
|
ImGui::RenderArrowPointingAt(draw_list, ImVec2(pos.x + bar_w - half_sz.x - 1, pos.y), ImVec2(half_sz.x + 2, half_sz.y + 1), ImGuiDir_Left, IM_COL32_BLACK);
|
|
ImGui::RenderArrowPointingAt(draw_list, ImVec2(pos.x + bar_w - half_sz.x, pos.y), half_sz, ImGuiDir_Left, IM_COL32_WHITE);
|
|
}
|
|
|
|
// Note: ColorPicker4() only accesses 3 floats if ImGuiColorEditFlags_NoAlpha flag is set.
|
|
// FIXME: we adjust the big color square height based on item width, which may cause a flickering feedback loop (if automatic height makes a vertical scrollbar appears, affecting automatic width..)
|
|
bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags flags, const float* ref_col)
|
|
{
|
|
ImGuiContext& g = *GImGui;
|
|
ImGuiWindow* window = GetCurrentWindow();
|
|
ImDrawList* draw_list = window->DrawList;
|
|
|
|
ImGuiStyle& style = g.Style;
|
|
ImGuiIO& io = g.IO;
|
|
|
|
PushID(label);
|
|
BeginGroup();
|
|
|
|
if (!(flags & ImGuiColorEditFlags_NoSidePreview))
|
|
flags |= ImGuiColorEditFlags_NoSmallPreview;
|
|
|
|
// Context menu: display and store options.
|
|
if (!(flags & ImGuiColorEditFlags_NoOptions))
|
|
ColorPickerOptionsPopup(col, flags);
|
|
|
|
// Read stored options
|
|
if (!(flags & ImGuiColorEditFlags__PickerMask))
|
|
flags |= ((g.ColorEditOptions & ImGuiColorEditFlags__PickerMask) ? g.ColorEditOptions : ImGuiColorEditFlags__OptionsDefault) & ImGuiColorEditFlags__PickerMask;
|
|
IM_ASSERT(ImIsPowerOfTwo((int)(flags & ImGuiColorEditFlags__PickerMask))); // Check that only 1 is selected
|
|
if (!(flags & ImGuiColorEditFlags_NoOptions))
|
|
flags |= (g.ColorEditOptions & ImGuiColorEditFlags_AlphaBar);
|
|
|
|
// Setup
|
|
int components = (flags & ImGuiColorEditFlags_NoAlpha) ? 3 : 4;
|
|
bool alpha_bar = (flags & ImGuiColorEditFlags_AlphaBar) && !(flags & ImGuiColorEditFlags_NoAlpha);
|
|
ImVec2 picker_pos = window->DC.CursorPos;
|
|
float square_sz = GetFrameHeight();
|
|
float bars_width = square_sz; // Arbitrary smallish width of Hue/Alpha picking bars
|
|
float sv_picker_size = ImMax(bars_width * 1, CalcItemWidth() - (alpha_bar ? 2 : 1) * (bars_width + style.ItemInnerSpacing.x)); // Saturation/Value picking box
|
|
float bar0_pos_x = picker_pos.x + sv_picker_size + style.ItemInnerSpacing.x;
|
|
float bar1_pos_x = bar0_pos_x + bars_width + style.ItemInnerSpacing.x;
|
|
float bars_triangles_half_sz = (float)(int)(bars_width * 0.20f);
|
|
|
|
float backup_initial_col[4];
|
|
memcpy(backup_initial_col, col, components * sizeof(float));
|
|
|
|
float wheel_thickness = sv_picker_size * 0.08f;
|
|
float wheel_r_outer = sv_picker_size * 0.50f;
|
|
float wheel_r_inner = wheel_r_outer - wheel_thickness;
|
|
ImVec2 wheel_center(picker_pos.x + (sv_picker_size + bars_width)*0.5f, picker_pos.y + sv_picker_size*0.5f);
|
|
|
|
// Note: the triangle is displayed rotated with triangle_pa pointing to Hue, but most coordinates stays unrotated for logic.
|
|
float triangle_r = wheel_r_inner - (int)(sv_picker_size * 0.027f);
|
|
ImVec2 triangle_pa = ImVec2(triangle_r, 0.0f); // Hue point.
|
|
ImVec2 triangle_pb = ImVec2(triangle_r * -0.5f, triangle_r * -0.866025f); // Black point.
|
|
ImVec2 triangle_pc = ImVec2(triangle_r * -0.5f, triangle_r * +0.866025f); // White point.
|
|
|
|
float H,S,V;
|
|
ColorConvertRGBtoHSV(col[0], col[1], col[2], H, S, V);
|
|
|
|
bool value_changed = false, value_changed_h = false, value_changed_sv = false;
|
|
|
|
PushItemFlag(ImGuiItemFlags_NoNav, true);
|
|
if (flags & ImGuiColorEditFlags_PickerHueWheel)
|
|
{
|
|
// Hue wheel + SV triangle logic
|
|
InvisibleButton("hsv", ImVec2(sv_picker_size + style.ItemInnerSpacing.x + bars_width, sv_picker_size));
|
|
if (IsItemActive())
|
|
{
|
|
ImVec2 initial_off = g.IO.MouseClickedPos[0] - wheel_center;
|
|
ImVec2 current_off = g.IO.MousePos - wheel_center;
|
|
float initial_dist2 = ImLengthSqr(initial_off);
|
|
if (initial_dist2 >= (wheel_r_inner-1)*(wheel_r_inner-1) && initial_dist2 <= (wheel_r_outer+1)*(wheel_r_outer+1))
|
|
{
|
|
// Interactive with Hue wheel
|
|
H = ImAtan2(current_off.y, current_off.x) / IM_PI*0.5f;
|
|
if (H < 0.0f)
|
|
H += 1.0f;
|
|
value_changed = value_changed_h = true;
|
|
}
|
|
float cos_hue_angle = ImCos(-H * 2.0f * IM_PI);
|
|
float sin_hue_angle = ImSin(-H * 2.0f * IM_PI);
|
|
if (ImTriangleContainsPoint(triangle_pa, triangle_pb, triangle_pc, ImRotate(initial_off, cos_hue_angle, sin_hue_angle)))
|
|
{
|
|
// Interacting with SV triangle
|
|
ImVec2 current_off_unrotated = ImRotate(current_off, cos_hue_angle, sin_hue_angle);
|
|
if (!ImTriangleContainsPoint(triangle_pa, triangle_pb, triangle_pc, current_off_unrotated))
|
|
current_off_unrotated = ImTriangleClosestPoint(triangle_pa, triangle_pb, triangle_pc, current_off_unrotated);
|
|
float uu, vv, ww;
|
|
ImTriangleBarycentricCoords(triangle_pa, triangle_pb, triangle_pc, current_off_unrotated, uu, vv, ww);
|
|
V = ImClamp(1.0f - vv, 0.0001f, 1.0f);
|
|
S = ImClamp(uu / V, 0.0001f, 1.0f);
|
|
value_changed = value_changed_sv = true;
|
|
}
|
|
}
|
|
if (!(flags & ImGuiColorEditFlags_NoOptions))
|
|
OpenPopupOnItemClick("context");
|
|
}
|
|
else if (flags & ImGuiColorEditFlags_PickerHueBar)
|
|
{
|
|
// SV rectangle logic
|
|
InvisibleButton("sv", ImVec2(sv_picker_size, sv_picker_size));
|
|
if (IsItemActive())
|
|
{
|
|
S = ImSaturate((io.MousePos.x - picker_pos.x) / (sv_picker_size-1));
|
|
V = 1.0f - ImSaturate((io.MousePos.y - picker_pos.y) / (sv_picker_size-1));
|
|
value_changed = value_changed_sv = true;
|
|
}
|
|
if (!(flags & ImGuiColorEditFlags_NoOptions))
|
|
OpenPopupOnItemClick("context");
|
|
|
|
// Hue bar logic
|
|
SetCursorScreenPos(ImVec2(bar0_pos_x, picker_pos.y));
|
|
InvisibleButton("hue", ImVec2(bars_width, sv_picker_size));
|
|
if (IsItemActive())
|
|
{
|
|
H = ImSaturate((io.MousePos.y - picker_pos.y) / (sv_picker_size-1));
|
|
value_changed = value_changed_h = true;
|
|
}
|
|
}
|
|
|
|
// Alpha bar logic
|
|
if (alpha_bar)
|
|
{
|
|
SetCursorScreenPos(ImVec2(bar1_pos_x, picker_pos.y));
|
|
InvisibleButton("alpha", ImVec2(bars_width, sv_picker_size));
|
|
if (IsItemActive())
|
|
{
|
|
col[3] = 1.0f - ImSaturate((io.MousePos.y - picker_pos.y) / (sv_picker_size-1));
|
|
value_changed = true;
|
|
}
|
|
}
|
|
PopItemFlag(); // ImGuiItemFlags_NoNav
|
|
|
|
if (!(flags & ImGuiColorEditFlags_NoSidePreview))
|
|
{
|
|
SameLine(0, style.ItemInnerSpacing.x);
|
|
BeginGroup();
|
|
}
|
|
|
|
if (!(flags & ImGuiColorEditFlags_NoLabel))
|
|
{
|
|
const char* label_display_end = FindRenderedTextEnd(label);
|
|
if (label != label_display_end)
|
|
{
|
|
if ((flags & ImGuiColorEditFlags_NoSidePreview))
|
|
SameLine(0, style.ItemInnerSpacing.x);
|
|
TextUnformatted(label, label_display_end);
|
|
}
|
|
}
|
|
|
|
if (!(flags & ImGuiColorEditFlags_NoSidePreview))
|
|
{
|
|
PushItemFlag(ImGuiItemFlags_NoNavDefaultFocus, true);
|
|
ImVec4 col_v4(col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]);
|
|
if ((flags & ImGuiColorEditFlags_NoLabel))
|
|
Text("Current");
|
|
ColorButton("##current", col_v4, (flags & (ImGuiColorEditFlags_HDR|ImGuiColorEditFlags_AlphaPreview|ImGuiColorEditFlags_AlphaPreviewHalf|ImGuiColorEditFlags_NoTooltip)), ImVec2(square_sz * 3, square_sz * 2));
|
|
if (ref_col != NULL)
|
|
{
|
|
Text("Original");
|
|
ImVec4 ref_col_v4(ref_col[0], ref_col[1], ref_col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : ref_col[3]);
|
|
if (ColorButton("##original", ref_col_v4, (flags & (ImGuiColorEditFlags_HDR|ImGuiColorEditFlags_AlphaPreview|ImGuiColorEditFlags_AlphaPreviewHalf|ImGuiColorEditFlags_NoTooltip)), ImVec2(square_sz * 3, square_sz * 2)))
|
|
{
|
|
memcpy(col, ref_col, components * sizeof(float));
|
|
value_changed = true;
|
|
}
|
|
}
|
|
PopItemFlag();
|
|
EndGroup();
|
|
}
|
|
|
|
// Convert back color to RGB
|
|
if (value_changed_h || value_changed_sv)
|
|
ColorConvertHSVtoRGB(H >= 1.0f ? H - 10 * 1e-6f : H, S > 0.0f ? S : 10*1e-6f, V > 0.0f ? V : 1e-6f, col[0], col[1], col[2]);
|
|
|
|
// R,G,B and H,S,V slider color editor
|
|
bool value_changed_fix_hue_wrap = false;
|
|
if ((flags & ImGuiColorEditFlags_NoInputs) == 0)
|
|
{
|
|
PushItemWidth((alpha_bar ? bar1_pos_x : bar0_pos_x) + bars_width - picker_pos.x);
|
|
ImGuiColorEditFlags sub_flags_to_forward = ImGuiColorEditFlags__DataTypeMask | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_NoOptions | ImGuiColorEditFlags_NoSmallPreview | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf;
|
|
ImGuiColorEditFlags sub_flags = (flags & sub_flags_to_forward) | ImGuiColorEditFlags_NoPicker;
|
|
if (flags & ImGuiColorEditFlags_RGB || (flags & ImGuiColorEditFlags__InputsMask) == 0)
|
|
if (ColorEdit4("##rgb", col, sub_flags | ImGuiColorEditFlags_RGB))
|
|
{
|
|
// FIXME: Hackily differenciating using the DragInt (ActiveId != 0 && !ActiveIdAllowOverlap) vs. using the InputText or DropTarget.
|
|
// For the later we don't want to run the hue-wrap canceling code. If you are well versed in HSV picker please provide your input! (See #2050)
|
|
value_changed_fix_hue_wrap = (g.ActiveId != 0 && !g.ActiveIdAllowOverlap);
|
|
value_changed = true;
|
|
}
|
|
if (flags & ImGuiColorEditFlags_HSV || (flags & ImGuiColorEditFlags__InputsMask) == 0)
|
|
value_changed |= ColorEdit4("##hsv", col, sub_flags | ImGuiColorEditFlags_HSV);
|
|
if (flags & ImGuiColorEditFlags_HEX || (flags & ImGuiColorEditFlags__InputsMask) == 0)
|
|
value_changed |= ColorEdit4("##hex", col, sub_flags | ImGuiColorEditFlags_HEX);
|
|
PopItemWidth();
|
|
}
|
|
|
|
// Try to cancel hue wrap (after ColorEdit4 call), if any
|
|
if (value_changed_fix_hue_wrap)
|
|
{
|
|
float new_H, new_S, new_V;
|
|
ColorConvertRGBtoHSV(col[0], col[1], col[2], new_H, new_S, new_V);
|
|
if (new_H <= 0 && H > 0)
|
|
{
|
|
if (new_V <= 0 && V != new_V)
|
|
ColorConvertHSVtoRGB(H, S, new_V <= 0 ? V * 0.5f : new_V, col[0], col[1], col[2]);
|
|
else if (new_S <= 0)
|
|
ColorConvertHSVtoRGB(H, new_S <= 0 ? S * 0.5f : new_S, new_V, col[0], col[1], col[2]);
|
|
}
|
|
}
|
|
|
|
ImVec4 hue_color_f(1, 1, 1, 1); ColorConvertHSVtoRGB(H, 1, 1, hue_color_f.x, hue_color_f.y, hue_color_f.z);
|
|
ImU32 hue_color32 = ColorConvertFloat4ToU32(hue_color_f);
|
|
ImU32 col32_no_alpha = ColorConvertFloat4ToU32(ImVec4(col[0], col[1], col[2], 1.0f));
|
|
|
|
const ImU32 hue_colors[6+1] = { IM_COL32(255,0,0,255), IM_COL32(255,255,0,255), IM_COL32(0,255,0,255), IM_COL32(0,255,255,255), IM_COL32(0,0,255,255), IM_COL32(255,0,255,255), IM_COL32(255,0,0,255) };
|
|
ImVec2 sv_cursor_pos;
|
|
|
|
if (flags & ImGuiColorEditFlags_PickerHueWheel)
|
|
{
|
|
// Render Hue Wheel
|
|
const float aeps = 1.5f / wheel_r_outer; // Half a pixel arc length in radians (2pi cancels out).
|
|
const int segment_per_arc = ImMax(4, (int)wheel_r_outer / 12);
|
|
for (int n = 0; n < 6; n++)
|
|
{
|
|
const float a0 = (n) /6.0f * 2.0f * IM_PI - aeps;
|
|
const float a1 = (n+1.0f)/6.0f * 2.0f * IM_PI + aeps;
|
|
const int vert_start_idx = draw_list->VtxBuffer.Size;
|
|
draw_list->PathArcTo(wheel_center, (wheel_r_inner + wheel_r_outer)*0.5f, a0, a1, segment_per_arc);
|
|
draw_list->PathStroke(IM_COL32_WHITE, false, wheel_thickness);
|
|
const int vert_end_idx = draw_list->VtxBuffer.Size;
|
|
|
|
// Paint colors over existing vertices
|
|
ImVec2 gradient_p0(wheel_center.x + ImCos(a0) * wheel_r_inner, wheel_center.y + ImSin(a0) * wheel_r_inner);
|
|
ImVec2 gradient_p1(wheel_center.x + ImCos(a1) * wheel_r_inner, wheel_center.y + ImSin(a1) * wheel_r_inner);
|
|
ShadeVertsLinearColorGradientKeepAlpha(draw_list, vert_start_idx, vert_end_idx, gradient_p0, gradient_p1, hue_colors[n], hue_colors[n+1]);
|
|
}
|
|
|
|
// Render Cursor + preview on Hue Wheel
|
|
float cos_hue_angle = ImCos(H * 2.0f * IM_PI);
|
|
float sin_hue_angle = ImSin(H * 2.0f * IM_PI);
|
|
ImVec2 hue_cursor_pos(wheel_center.x + cos_hue_angle * (wheel_r_inner+wheel_r_outer)*0.5f, wheel_center.y + sin_hue_angle * (wheel_r_inner+wheel_r_outer)*0.5f);
|
|
float hue_cursor_rad = value_changed_h ? wheel_thickness * 0.65f : wheel_thickness * 0.55f;
|
|
int hue_cursor_segments = ImClamp((int)(hue_cursor_rad / 1.4f), 9, 32);
|
|
draw_list->AddCircleFilled(hue_cursor_pos, hue_cursor_rad, hue_color32, hue_cursor_segments);
|
|
draw_list->AddCircle(hue_cursor_pos, hue_cursor_rad+1, IM_COL32(128,128,128,255), hue_cursor_segments);
|
|
draw_list->AddCircle(hue_cursor_pos, hue_cursor_rad, IM_COL32_WHITE, hue_cursor_segments);
|
|
|
|
// Render SV triangle (rotated according to hue)
|
|
ImVec2 tra = wheel_center + ImRotate(triangle_pa, cos_hue_angle, sin_hue_angle);
|
|
ImVec2 trb = wheel_center + ImRotate(triangle_pb, cos_hue_angle, sin_hue_angle);
|
|
ImVec2 trc = wheel_center + ImRotate(triangle_pc, cos_hue_angle, sin_hue_angle);
|
|
ImVec2 uv_white = GetFontTexUvWhitePixel();
|
|
draw_list->PrimReserve(6, 6);
|
|
draw_list->PrimVtx(tra, uv_white, hue_color32);
|
|
draw_list->PrimVtx(trb, uv_white, hue_color32);
|
|
draw_list->PrimVtx(trc, uv_white, IM_COL32_WHITE);
|
|
draw_list->PrimVtx(tra, uv_white, IM_COL32_BLACK_TRANS);
|
|
draw_list->PrimVtx(trb, uv_white, IM_COL32_BLACK);
|
|
draw_list->PrimVtx(trc, uv_white, IM_COL32_BLACK_TRANS);
|
|
draw_list->AddTriangle(tra, trb, trc, IM_COL32(128,128,128,255), 1.5f);
|
|
sv_cursor_pos = ImLerp(ImLerp(trc, tra, ImSaturate(S)), trb, ImSaturate(1 - V));
|
|
}
|
|
else if (flags & ImGuiColorEditFlags_PickerHueBar)
|
|
{
|
|
// Render SV Square
|
|
draw_list->AddRectFilledMultiColor(picker_pos, picker_pos + ImVec2(sv_picker_size,sv_picker_size), IM_COL32_WHITE, hue_color32, hue_color32, IM_COL32_WHITE);
|
|
draw_list->AddRectFilledMultiColor(picker_pos, picker_pos + ImVec2(sv_picker_size,sv_picker_size), IM_COL32_BLACK_TRANS, IM_COL32_BLACK_TRANS, IM_COL32_BLACK, IM_COL32_BLACK);
|
|
RenderFrameBorder(picker_pos, picker_pos + ImVec2(sv_picker_size,sv_picker_size), 0.0f);
|
|
sv_cursor_pos.x = ImClamp((float)(int)(picker_pos.x + ImSaturate(S) * sv_picker_size + 0.5f), picker_pos.x + 2, picker_pos.x + sv_picker_size - 2); // Sneakily prevent the circle to stick out too much
|
|
sv_cursor_pos.y = ImClamp((float)(int)(picker_pos.y + ImSaturate(1 - V) * sv_picker_size + 0.5f), picker_pos.y + 2, picker_pos.y + sv_picker_size - 2);
|
|
|
|
// Render Hue Bar
|
|
for (int i = 0; i < 6; ++i)
|
|
draw_list->AddRectFilledMultiColor(ImVec2(bar0_pos_x, picker_pos.y + i * (sv_picker_size / 6)), ImVec2(bar0_pos_x + bars_width, picker_pos.y + (i + 1) * (sv_picker_size / 6)), hue_colors[i], hue_colors[i], hue_colors[i + 1], hue_colors[i + 1]);
|
|
float bar0_line_y = (float)(int)(picker_pos.y + H * sv_picker_size + 0.5f);
|
|
RenderFrameBorder(ImVec2(bar0_pos_x, picker_pos.y), ImVec2(bar0_pos_x + bars_width, picker_pos.y + sv_picker_size), 0.0f);
|
|
RenderArrowsForVerticalBar(draw_list, ImVec2(bar0_pos_x - 1, bar0_line_y), ImVec2(bars_triangles_half_sz + 1, bars_triangles_half_sz), bars_width + 2.0f);
|
|
}
|
|
|
|
// Render cursor/preview circle (clamp S/V within 0..1 range because floating points colors may lead HSV values to be out of range)
|
|
float sv_cursor_rad = value_changed_sv ? 10.0f : 6.0f;
|
|
draw_list->AddCircleFilled(sv_cursor_pos, sv_cursor_rad, col32_no_alpha, 12);
|
|
draw_list->AddCircle(sv_cursor_pos, sv_cursor_rad+1, IM_COL32(128,128,128,255), 12);
|
|
draw_list->AddCircle(sv_cursor_pos, sv_cursor_rad, IM_COL32_WHITE, 12);
|
|
|
|
// Render alpha bar
|
|
if (alpha_bar)
|
|
{
|
|
float alpha = ImSaturate(col[3]);
|
|
ImRect bar1_bb(bar1_pos_x, picker_pos.y, bar1_pos_x + bars_width, picker_pos.y + sv_picker_size);
|
|
RenderColorRectWithAlphaCheckerboard(bar1_bb.Min, bar1_bb.Max, IM_COL32(0,0,0,0), bar1_bb.GetWidth() / 2.0f, ImVec2(0.0f, 0.0f));
|
|
draw_list->AddRectFilledMultiColor(bar1_bb.Min, bar1_bb.Max, col32_no_alpha, col32_no_alpha, col32_no_alpha & ~IM_COL32_A_MASK, col32_no_alpha & ~IM_COL32_A_MASK);
|
|
float bar1_line_y = (float)(int)(picker_pos.y + (1.0f - alpha) * sv_picker_size + 0.5f);
|
|
RenderFrameBorder(bar1_bb.Min, bar1_bb.Max, 0.0f);
|
|
RenderArrowsForVerticalBar(draw_list, ImVec2(bar1_pos_x - 1, bar1_line_y), ImVec2(bars_triangles_half_sz + 1, bars_triangles_half_sz), bars_width + 2.0f);
|
|
}
|
|
|
|
EndGroup();
|
|
|
|
if (value_changed && memcmp(backup_initial_col, col, components * sizeof(float)) == 0)
|
|
value_changed = false;
|
|
if (value_changed)
|
|
MarkItemEdited(window->DC.LastItemId);
|
|
|
|
PopID();
|
|
|
|
return value_changed;
|
|
}
|
|
|
|
// A little colored square. Return true when clicked.
|
|
// FIXME: May want to display/ignore the alpha component in the color display? Yet show it in the tooltip.
|
|
// 'desc_id' is not called 'label' because we don't display it next to the button, but only in the tooltip.
|
|
bool ImGui::ColorButton(const char* desc_id, const ImVec4& col, ImGuiColorEditFlags flags, ImVec2 size)
|
|
{
|
|
ImGuiWindow* window = GetCurrentWindow();
|
|
if (window->SkipItems)
|
|
return false;
|
|
|
|
ImGuiContext& g = *GImGui;
|
|
const ImGuiID id = window->GetID(desc_id);
|
|
float default_size = GetFrameHeight();
|
|
if (size.x == 0.0f)
|
|
size.x = default_size;
|
|
if (size.y == 0.0f)
|
|
size.y = default_size;
|
|
const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size);
|
|
ItemSize(bb, (size.y >= default_size) ? g.Style.FramePadding.y : 0.0f);
|
|
if (!ItemAdd(bb, id))
|
|
return false;
|
|
|
|
bool hovered, held;
|
|
bool pressed = ButtonBehavior(bb, id, &hovered, &held);
|
|
|
|
if (flags & ImGuiColorEditFlags_NoAlpha)
|
|
flags &= ~(ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf);
|
|
|
|
ImVec4 col_without_alpha(col.x, col.y, col.z, 1.0f);
|
|
float grid_step = ImMin(size.x, size.y) / 2.99f;
|
|
float rounding = ImMin(g.Style.FrameRounding, grid_step * 0.5f);
|
|
ImRect bb_inner = bb;
|
|
float off = -0.75f; // The border (using Col_FrameBg) tends to look off when color is near-opaque and rounding is enabled. This offset seemed like a good middle ground to reduce those artifacts.
|
|
bb_inner.Expand(off);
|
|
if ((flags & ImGuiColorEditFlags_AlphaPreviewHalf) && col.w < 1.0f)
|
|
{
|
|
float mid_x = (float)(int)((bb_inner.Min.x + bb_inner.Max.x) * 0.5f + 0.5f);
|
|
RenderColorRectWithAlphaCheckerboard(ImVec2(bb_inner.Min.x + grid_step, bb_inner.Min.y), bb_inner.Max, GetColorU32(col), grid_step, ImVec2(-grid_step + off, off), rounding, ImDrawCornerFlags_TopRight| ImDrawCornerFlags_BotRight);
|
|
window->DrawList->AddRectFilled(bb_inner.Min, ImVec2(mid_x, bb_inner.Max.y), GetColorU32(col_without_alpha), rounding, ImDrawCornerFlags_TopLeft|ImDrawCornerFlags_BotLeft);
|
|
}
|
|
else
|
|
{
|
|
// Because GetColorU32() multiplies by the global style Alpha and we don't want to display a checkerboard if the source code had no alpha
|
|
ImVec4 col_source = (flags & ImGuiColorEditFlags_AlphaPreview) ? col : col_without_alpha;
|
|
if (col_source.w < 1.0f)
|
|
RenderColorRectWithAlphaCheckerboard(bb_inner.Min, bb_inner.Max, GetColorU32(col_source), grid_step, ImVec2(off, off), rounding);
|
|
else
|
|
window->DrawList->AddRectFilled(bb_inner.Min, bb_inner.Max, GetColorU32(col_source), rounding, ImDrawCornerFlags_All);
|
|
}
|
|
RenderNavHighlight(bb, id);
|
|
if (g.Style.FrameBorderSize > 0.0f)
|
|
RenderFrameBorder(bb.Min, bb.Max, rounding);
|
|
else
|
|
window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(ImGuiCol_FrameBg), rounding); // Color button are often in need of some sort of border
|
|
|
|
// Drag and Drop Source
|
|
// NB: The ActiveId test is merely an optional micro-optimization, BeginDragDropSource() does the same test.
|
|
if (g.ActiveId == id && !(flags & ImGuiColorEditFlags_NoDragDrop) && BeginDragDropSource())
|
|
{
|
|
if (flags & ImGuiColorEditFlags_NoAlpha)
|
|
SetDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_3F, &col, sizeof(float) * 3, ImGuiCond_Once);
|
|
else
|
|
SetDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_4F, &col, sizeof(float) * 4, ImGuiCond_Once);
|
|
ColorButton(desc_id, col, flags);
|
|
SameLine();
|
|
TextUnformatted("Color");
|
|
EndDragDropSource();
|
|
}
|
|
|
|
// Tooltip
|
|
if (!(flags & ImGuiColorEditFlags_NoTooltip) && hovered)
|
|
ColorTooltip(desc_id, &col.x, flags & (ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf));
|
|
|
|
if (pressed)
|
|
MarkItemEdited(id);
|
|
|
|
return pressed;
|
|
}
|
|
|
|
void ImGui::SetColorEditOptions(ImGuiColorEditFlags flags)
|
|
{
|
|
ImGuiContext& g = *GImGui;
|
|
if ((flags & ImGuiColorEditFlags__InputsMask) == 0)
|
|
flags |= ImGuiColorEditFlags__OptionsDefault & ImGuiColorEditFlags__InputsMask;
|
|
if ((flags & ImGuiColorEditFlags__DataTypeMask) == 0)
|
|
flags |= ImGuiColorEditFlags__OptionsDefault & ImGuiColorEditFlags__DataTypeMask;
|
|
if ((flags & ImGuiColorEditFlags__PickerMask) == 0)
|
|
flags |= ImGuiColorEditFlags__OptionsDefault & ImGuiColorEditFlags__PickerMask;
|
|
IM_ASSERT(ImIsPowerOfTwo((int)(flags & ImGuiColorEditFlags__InputsMask))); // Check only 1 option is selected
|
|
IM_ASSERT(ImIsPowerOfTwo((int)(flags & ImGuiColorEditFlags__DataTypeMask))); // Check only 1 option is selected
|
|
IM_ASSERT(ImIsPowerOfTwo((int)(flags & ImGuiColorEditFlags__PickerMask))); // Check only 1 option is selected
|
|
g.ColorEditOptions = flags;
|
|
}
|
|
|
|
// Note: only access 3 floats if ImGuiColorEditFlags_NoAlpha flag is set.
|
|
void ImGui::ColorTooltip(const char* text, const float* col, ImGuiColorEditFlags flags)
|
|
{
|
|
ImGuiContext& g = *GImGui;
|
|
|
|
int cr = IM_F32_TO_INT8_SAT(col[0]), cg = IM_F32_TO_INT8_SAT(col[1]), cb = IM_F32_TO_INT8_SAT(col[2]), ca = (flags & ImGuiColorEditFlags_NoAlpha) ? 255 : IM_F32_TO_INT8_SAT(col[3]);
|
|
BeginTooltipEx(0, true);
|
|
|
|
const char* text_end = text ? FindRenderedTextEnd(text, NULL) : text;
|
|
if (text_end > text)
|
|
{
|
|
TextUnformatted(text, text_end);
|
|
Separator();
|
|
}
|
|
|
|
ImVec2 sz(g.FontSize * 3 + g.Style.FramePadding.y * 2, g.FontSize * 3 + g.Style.FramePadding.y * 2);
|
|
ColorButton("##preview", ImVec4(col[0], col[1], col[2], col[3]), (flags & (ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf)) | ImGuiColorEditFlags_NoTooltip, sz);
|
|
SameLine();
|
|
if (flags & ImGuiColorEditFlags_NoAlpha)
|
|
Text("#%02X%02X%02X\nR: %d, G: %d, B: %d\n(%.3f, %.3f, %.3f)", cr, cg, cb, cr, cg, cb, col[0], col[1], col[2]);
|
|
else
|
|
Text("#%02X%02X%02X%02X\nR:%d, G:%d, B:%d, A:%d\n(%.3f, %.3f, %.3f, %.3f)", cr, cg, cb, ca, cr, cg, cb, ca, col[0], col[1], col[2], col[3]);
|
|
EndTooltip();
|
|
}
|
|
|
|
void ImGui::ColorEditOptionsPopup(const float* col, ImGuiColorEditFlags flags)
|
|
{
|
|
bool allow_opt_inputs = !(flags & ImGuiColorEditFlags__InputsMask);
|
|
bool allow_opt_datatype = !(flags & ImGuiColorEditFlags__DataTypeMask);
|
|
if ((!allow_opt_inputs && !allow_opt_datatype) || !BeginPopup("context"))
|
|
return;
|
|
ImGuiContext& g = *GImGui;
|
|
ImGuiColorEditFlags opts = g.ColorEditOptions;
|
|
if (allow_opt_inputs)
|
|
{
|
|
if (RadioButton("RGB", (opts & ImGuiColorEditFlags_RGB) != 0)) opts = (opts & ~ImGuiColorEditFlags__InputsMask) | ImGuiColorEditFlags_RGB;
|
|
if (RadioButton("HSV", (opts & ImGuiColorEditFlags_HSV) != 0)) opts = (opts & ~ImGuiColorEditFlags__InputsMask) | ImGuiColorEditFlags_HSV;
|
|
if (RadioButton("HEX", (opts & ImGuiColorEditFlags_HEX) != 0)) opts = (opts & ~ImGuiColorEditFlags__InputsMask) | ImGuiColorEditFlags_HEX;
|
|
}
|
|
if (allow_opt_datatype)
|
|
{
|
|
if (allow_opt_inputs) Separator();
|
|
if (RadioButton("0..255", (opts & ImGuiColorEditFlags_Uint8) != 0)) opts = (opts & ~ImGuiColorEditFlags__DataTypeMask) | ImGuiColorEditFlags_Uint8;
|
|
if (RadioButton("0.00..1.00", (opts & ImGuiColorEditFlags_Float) != 0)) opts = (opts & ~ImGuiColorEditFlags__DataTypeMask) | ImGuiColorEditFlags_Float;
|
|
}
|
|
|
|
if (allow_opt_inputs || allow_opt_datatype)
|
|
Separator();
|
|
if (Button("Copy as..", ImVec2(-1,0)))
|
|
OpenPopup("Copy");
|
|
if (BeginPopup("Copy"))
|
|
{
|
|
int cr = IM_F32_TO_INT8_SAT(col[0]), cg = IM_F32_TO_INT8_SAT(col[1]), cb = IM_F32_TO_INT8_SAT(col[2]), ca = (flags & ImGuiColorEditFlags_NoAlpha) ? 255 : IM_F32_TO_INT8_SAT(col[3]);
|
|
char buf[64];
|
|
ImFormatString(buf, IM_ARRAYSIZE(buf), "(%.3ff, %.3ff, %.3ff, %.3ff)", col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]);
|
|
if (Selectable(buf))
|
|
SetClipboardText(buf);
|
|
ImFormatString(buf, IM_ARRAYSIZE(buf), "(%d,%d,%d,%d)", cr, cg, cb, ca);
|
|
if (Selectable(buf))
|
|
SetClipboardText(buf);
|
|
if (flags & ImGuiColorEditFlags_NoAlpha)
|
|
ImFormatString(buf, IM_ARRAYSIZE(buf), "0x%02X%02X%02X", cr, cg, cb);
|
|
else
|
|
ImFormatString(buf, IM_ARRAYSIZE(buf), "0x%02X%02X%02X%02X", cr, cg, cb, ca);
|
|
if (Selectable(buf))
|
|
SetClipboardText(buf);
|
|
EndPopup();
|
|
}
|
|
|
|
g.ColorEditOptions = opts;
|
|
EndPopup();
|
|
}
|
|
|
|
void ImGui::ColorPickerOptionsPopup(const float* ref_col, ImGuiColorEditFlags flags)
|
|
{
|
|
bool allow_opt_picker = !(flags & ImGuiColorEditFlags__PickerMask);
|
|
bool allow_opt_alpha_bar = !(flags & ImGuiColorEditFlags_NoAlpha) && !(flags & ImGuiColorEditFlags_AlphaBar);
|
|
if ((!allow_opt_picker && !allow_opt_alpha_bar) || !ImGui::BeginPopup("context"))
|
|
return;
|
|
ImGuiContext& g = *GImGui;
|
|
if (allow_opt_picker)
|
|
{
|
|
ImVec2 picker_size(g.FontSize * 8, ImMax(g.FontSize * 8 - (ImGui::GetFrameHeight() + g.Style.ItemInnerSpacing.x), 1.0f)); // FIXME: Picker size copied from main picker function
|
|
ImGui::PushItemWidth(picker_size.x);
|
|
for (int picker_type = 0; picker_type < 2; picker_type++)
|
|
{
|
|
// Draw small/thumbnail version of each picker type (over an invisible button for selection)
|
|
if (picker_type > 0) ImGui::Separator();
|
|
ImGui::PushID(picker_type);
|
|
ImGuiColorEditFlags picker_flags = ImGuiColorEditFlags_NoInputs|ImGuiColorEditFlags_NoOptions|ImGuiColorEditFlags_NoLabel|ImGuiColorEditFlags_NoSidePreview|(flags & ImGuiColorEditFlags_NoAlpha);
|
|
if (picker_type == 0) picker_flags |= ImGuiColorEditFlags_PickerHueBar;
|
|
if (picker_type == 1) picker_flags |= ImGuiColorEditFlags_PickerHueWheel;
|
|
ImVec2 backup_pos = ImGui::GetCursorScreenPos();
|
|
if (ImGui::Selectable("##selectable", false, 0, picker_size)) // By default, Selectable() is closing popup
|
|
g.ColorEditOptions = (g.ColorEditOptions & ~ImGuiColorEditFlags__PickerMask) | (picker_flags & ImGuiColorEditFlags__PickerMask);
|
|
ImGui::SetCursorScreenPos(backup_pos);
|
|
ImVec4 dummy_ref_col;
|
|
memcpy(&dummy_ref_col.x, ref_col, sizeof(float) * (picker_flags & ImGuiColorEditFlags_NoAlpha ? 3 : 4));
|
|
ImGui::ColorPicker4("##dummypicker", &dummy_ref_col.x, picker_flags);
|
|
ImGui::PopID();
|
|
}
|
|
ImGui::PopItemWidth();
|
|
}
|
|
if (allow_opt_alpha_bar)
|
|
{
|
|
if (allow_opt_picker) ImGui::Separator();
|
|
ImGui::CheckboxFlags("Alpha Bar", (unsigned int*)&g.ColorEditOptions, ImGuiColorEditFlags_AlphaBar);
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
|
|
//-------------------------------------------------------------------------
|
|
// WIDGETS: Trees
|
|
// - TreeNode()
|
|
// - TreeNodeV()
|
|
// - TreeNodeEx()
|
|
// - TreeNodeExV()
|
|
// - TreeNodeBehavior() [Internal]
|
|
// - TreePush()
|
|
// - TreePop()
|
|
// - TreeAdvanceToLabelPos()
|
|
// - GetTreeNodeToLabelSpacing()
|
|
// - SetNextTreeNodeOpen()
|
|
// - CollapsingHeader()
|
|
//-------------------------------------------------------------------------
|
|
|
|
bool ImGui::TreeNode(const char* str_id, const char* fmt, ...)
|
|
{
|
|
va_list args;
|
|
va_start(args, fmt);
|
|
bool is_open = TreeNodeExV(str_id, 0, fmt, args);
|
|
va_end(args);
|
|
return is_open;
|
|
}
|
|
|
|
bool ImGui::TreeNode(const void* ptr_id, const char* fmt, ...)
|
|
{
|
|
va_list args;
|
|
va_start(args, fmt);
|
|
bool is_open = TreeNodeExV(ptr_id, 0, fmt, args);
|
|
va_end(args);
|
|
return is_open;
|
|
}
|
|
|
|
bool ImGui::TreeNode(const char* label)
|
|
{
|
|
ImGuiWindow* window = GetCurrentWindow();
|
|
if (window->SkipItems)
|
|
return false;
|
|
return TreeNodeBehavior(window->GetID(label), 0, label, NULL);
|
|
}
|
|
|
|
bool ImGui::TreeNodeV(const char* str_id, const char* fmt, va_list args)
|
|
{
|
|
return TreeNodeExV(str_id, 0, fmt, args);
|
|
}
|
|
|
|
bool ImGui::TreeNodeV(const void* ptr_id, const char* fmt, va_list args)
|
|
{
|
|
return TreeNodeExV(ptr_id, 0, fmt, args);
|
|
}
|
|
|
|
bool ImGui::TreeNodeEx(const char* label, ImGuiTreeNodeFlags flags)
|
|
{
|
|
ImGuiWindow* window = GetCurrentWindow();
|
|
if (window->SkipItems)
|
|
return false;
|
|
|
|
return TreeNodeBehavior(window->GetID(label), flags, label, NULL);
|
|
}
|
|
|
|
bool ImGui::TreeNodeEx(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, ...)
|
|
{
|
|
va_list args;
|
|
va_start(args, fmt);
|
|
bool is_open = TreeNodeExV(str_id, flags, fmt, args);
|
|
va_end(args);
|
|
return is_open;
|
|
}
|
|
|
|
bool ImGui::TreeNodeEx(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, ...)
|
|
{
|
|
va_list args;
|
|
va_start(args, fmt);
|
|
bool is_open = TreeNodeExV(ptr_id, flags, fmt, args);
|
|
va_end(args);
|
|
return is_open;
|
|
}
|
|
|
|
bool ImGui::TreeNodeExV(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args)
|
|
{
|
|
ImGuiWindow* window = GetCurrentWindow();
|
|
if (window->SkipItems)
|
|
return false;
|
|
|
|
ImGuiContext& g = *GImGui;
|
|
const char* label_end = g.TempBuffer + ImFormatStringV(g.TempBuffer, IM_ARRAYSIZE(g.TempBuffer), fmt, args);
|
|
return TreeNodeBehavior(window->GetID(str_id), flags, g.TempBuffer, label_end);
|
|
}
|
|
|
|
bool ImGui::TreeNodeExV(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args)
|
|
{
|
|
ImGuiWindow* window = GetCurrentWindow();
|
|
if (window->SkipItems)
|
|
return false;
|
|
|
|
ImGuiContext& g = *GImGui;
|
|
const char* label_end = g.TempBuffer + ImFormatStringV(g.TempBuffer, IM_ARRAYSIZE(g.TempBuffer), fmt, args);
|
|
return TreeNodeBehavior(window->GetID(ptr_id), flags, g.TempBuffer, label_end);
|
|
}
|
|
|
|
bool ImGui::TreeNodeBehaviorIsOpen(ImGuiID id, ImGuiTreeNodeFlags flags)
|
|
{
|
|
if (flags & ImGuiTreeNodeFlags_Leaf)
|
|
return true;
|
|
|
|
// We only write to the tree storage if the user clicks (or explicitly use SetNextTreeNode*** functions)
|
|
ImGuiContext& g = *GImGui;
|
|
ImGuiWindow* window = g.CurrentWindow;
|
|
ImGuiStorage* storage = window->DC.StateStorage;
|
|
|
|
bool is_open;
|
|
if (g.NextTreeNodeOpenCond != 0)
|
|
{
|
|
if (g.NextTreeNodeOpenCond & ImGuiCond_Always)
|
|
{
|
|
is_open = g.NextTreeNodeOpenVal;
|
|
storage->SetInt(id, is_open);
|
|
}
|
|
else
|
|
{
|
|
// We treat ImGuiCond_Once and ImGuiCond_FirstUseEver the same because tree node state are not saved persistently.
|
|
const int stored_value = storage->GetInt(id, -1);
|
|
if (stored_value == -1)
|
|
{
|
|
is_open = g.NextTreeNodeOpenVal;
|
|
storage->SetInt(id, is_open);
|
|
}
|
|
else
|
|
{
|
|
is_open = stored_value != 0;
|
|
}
|
|
}
|
|
g.NextTreeNodeOpenCond = 0;
|
|
}
|
|
else
|
|
{
|
|
is_open = storage->GetInt(id, (flags & ImGuiTreeNodeFlags_DefaultOpen) ? 1 : 0) != 0;
|
|
}
|
|
|
|
// When logging is enabled, we automatically expand tree nodes (but *NOT* collapsing headers.. seems like sensible behavior).
|
|
// NB- If we are above max depth we still allow manually opened nodes to be logged.
|
|
if (g.LogEnabled && !(flags & ImGuiTreeNodeFlags_NoAutoOpenOnLog) && window->DC.TreeDepth < g.LogAutoExpandMaxDepth)
|
|
is_open = true;
|
|
|
|
return is_open;
|
|
}
|
|
|
|
bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* label, const char* label_end)
|
|
{
|
|
ImGuiWindow* window = GetCurrentWindow();
|
|
if (window->SkipItems)
|
|
return false;
|
|
|
|
ImGuiContext& g = *GImGui;
|
|
const ImGuiStyle& style = g.Style;
|
|
const bool display_frame = (flags & ImGuiTreeNodeFlags_Framed) != 0;
|
|
const ImVec2 padding = (display_frame || (flags & ImGuiTreeNodeFlags_FramePadding)) ? style.FramePadding : ImVec2(style.FramePadding.x, 0.0f);
|
|
|
|
if (!label_end)
|
|
label_end = FindRenderedTextEnd(label);
|
|
const ImVec2 label_size = CalcTextSize(label, label_end, false);
|
|
|
|
// We vertically grow up to current line height up the typical widget height.
|
|
const float text_base_offset_y = ImMax(padding.y, window->DC.CurrentLineTextBaseOffset); // Latch before ItemSize changes it
|
|
const float frame_height = ImMax(ImMin(window->DC.CurrentLineSize.y, g.FontSize + style.FramePadding.y*2), label_size.y + padding.y*2);
|
|
ImRect frame_bb = ImRect(window->DC.CursorPos, ImVec2(window->Pos.x + GetContentRegionMax().x, window->DC.CursorPos.y + frame_height));
|
|
if (display_frame)
|
|
{
|
|
// Framed header expand a little outside the default padding
|
|
frame_bb.Min.x -= (float)(int)(window->WindowPadding.x*0.5f) - 1;
|
|
frame_bb.Max.x += (float)(int)(window->WindowPadding.x*0.5f) - 1;
|
|
}
|
|
|
|
const float text_offset_x = (g.FontSize + (display_frame ? padding.x*3 : padding.x*2)); // Collapser arrow width + Spacing
|
|
const float text_width = g.FontSize + (label_size.x > 0.0f ? label_size.x + padding.x*2 : 0.0f); // Include collapser
|
|
ItemSize(ImVec2(text_width, frame_height), text_base_offset_y);
|
|
|
|
// For regular tree nodes, we arbitrary allow to click past 2 worth of ItemSpacing
|
|
// (Ideally we'd want to add a flag for the user to specify if we want the hit test to be done up to the right side of the content or not)
|
|
const ImRect interact_bb = display_frame ? frame_bb : ImRect(frame_bb.Min.x, frame_bb.Min.y, frame_bb.Min.x + text_width + style.ItemSpacing.x*2, frame_bb.Max.y);
|
|
bool is_open = TreeNodeBehaviorIsOpen(id, flags);
|
|
|
|
// Store a flag for the current depth to tell if we will allow closing this node when navigating one of its child.
|
|
// For this purpose we essentially compare if g.NavIdIsAlive went from 0 to 1 between TreeNode() and TreePop().
|
|
// This is currently only support 32 level deep and we are fine with (1 << Depth) overflowing into a zero.
|
|
if (is_open && !g.NavIdIsAlive && (flags & ImGuiTreeNodeFlags_NavLeftJumpsBackHere) && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen))
|
|
window->DC.TreeDepthMayJumpToParentOnPop |= (1 << window->DC.TreeDepth);
|
|
|
|
bool item_add = ItemAdd(interact_bb, id);
|
|
window->DC.LastItemStatusFlags |= ImGuiItemStatusFlags_HasDisplayRect;
|
|
window->DC.LastItemDisplayRect = frame_bb;
|
|
|
|
if (!item_add)
|
|
{
|
|
if (is_open && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen))
|
|
TreePushRawID(id);
|
|
return is_open;
|
|
}
|
|
|
|
// Flags that affects opening behavior:
|
|
// - 0(default) ..................... single-click anywhere to open
|
|
// - OpenOnDoubleClick .............. double-click anywhere to open
|
|
// - OpenOnArrow .................... single-click on arrow to open
|
|
// - OpenOnDoubleClick|OpenOnArrow .. single-click on arrow or double-click anywhere to open
|
|
ImGuiButtonFlags button_flags = ImGuiButtonFlags_NoKeyModifiers | ((flags & ImGuiTreeNodeFlags_AllowItemOverlap) ? ImGuiButtonFlags_AllowItemOverlap : 0);
|
|
if (!(flags & ImGuiTreeNodeFlags_Leaf))
|
|
button_flags |= ImGuiButtonFlags_PressedOnDragDropHold;
|
|
if (flags & ImGuiTreeNodeFlags_OpenOnDoubleClick)
|
|
button_flags |= ImGuiButtonFlags_PressedOnDoubleClick | ((flags & ImGuiTreeNodeFlags_OpenOnArrow) ? ImGuiButtonFlags_PressedOnClickRelease : 0);
|
|
|
|
bool hovered, held, pressed = ButtonBehavior(interact_bb, id, &hovered, &held, button_flags);
|
|
if (!(flags & ImGuiTreeNodeFlags_Leaf))
|
|
{
|
|
bool toggled = false;
|
|
if (pressed)
|
|
{
|
|
toggled = !(flags & (ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) || (g.NavActivateId == id);
|
|
if (flags & ImGuiTreeNodeFlags_OpenOnArrow)
|
|
toggled |= IsMouseHoveringRect(interact_bb.Min, ImVec2(interact_bb.Min.x + text_offset_x, interact_bb.Max.y)) && (!g.NavDisableMouseHover);
|
|
if (flags & ImGuiTreeNodeFlags_OpenOnDoubleClick)
|
|
toggled |= g.IO.MouseDoubleClicked[0];
|
|
if (g.DragDropActive && is_open) // When using Drag and Drop "hold to open" we keep the node highlighted after opening, but never close it again.
|
|
toggled = false;
|
|
}
|
|
|
|
if (g.NavId == id && g.NavMoveRequest && g.NavMoveDir == ImGuiDir_Left && is_open)
|
|
{
|
|
toggled = true;
|
|
NavMoveRequestCancel();
|
|
}
|
|
if (g.NavId == id && g.NavMoveRequest && g.NavMoveDir == ImGuiDir_Right && !is_open) // If there's something upcoming on the line we may want to give it the priority?
|
|
{
|
|
toggled = true;
|
|
NavMoveRequestCancel();
|
|
}
|
|
|
|
if (toggled)
|
|
{
|
|
is_open = !is_open;
|
|
window->DC.StateStorage->SetInt(id, is_open);
|
|
}
|
|
}
|
|
if (flags & ImGuiTreeNodeFlags_AllowItemOverlap)
|
|
SetItemAllowOverlap();
|
|
|
|
// Render
|
|
const ImU32 col = GetColorU32((held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header);
|
|
const ImVec2 text_pos = frame_bb.Min + ImVec2(text_offset_x, text_base_offset_y);
|
|
if (display_frame)
|
|
{
|
|
// Framed type
|
|
RenderFrame(frame_bb.Min, frame_bb.Max, col, true, style.FrameRounding);
|
|
RenderNavHighlight(frame_bb, id, ImGuiNavHighlightFlags_TypeThin);
|
|
RenderArrow(frame_bb.Min + ImVec2(padding.x, text_base_offset_y), is_open ? ImGuiDir_Down : ImGuiDir_Right, 1.0f);
|
|
if (g.LogEnabled)
|
|
{
|
|
// NB: '##' is normally used to hide text (as a library-wide feature), so we need to specify the text range to make sure the ## aren't stripped out here.
|
|
const char log_prefix[] = "\n##";
|
|
const char log_suffix[] = "##";
|
|
LogRenderedText(&text_pos, log_prefix, log_prefix+3);
|
|
RenderTextClipped(text_pos, frame_bb.Max, label, label_end, &label_size);
|
|
LogRenderedText(&text_pos, log_suffix+1, log_suffix+3);
|
|
}
|
|
else
|
|
{
|
|
RenderTextClipped(text_pos, frame_bb.Max, label, label_end, &label_size);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Unframed typed for tree nodes
|
|
if (hovered || (flags & ImGuiTreeNodeFlags_Selected))
|
|
{
|
|
RenderFrame(frame_bb.Min, frame_bb.Max, col, false);
|
|
RenderNavHighlight(frame_bb, id, ImGuiNavHighlightFlags_TypeThin);
|
|
}
|
|
|
|
if (flags & ImGuiTreeNodeFlags_Bullet)
|
|
RenderBullet(frame_bb.Min + ImVec2(text_offset_x * 0.5f, g.FontSize*0.50f + text_base_offset_y));
|
|
else if (!(flags & ImGuiTreeNodeFlags_Leaf))
|
|
RenderArrow(frame_bb.Min + ImVec2(padding.x, g.FontSize*0.15f + text_base_offset_y), is_open ? ImGuiDir_Down : ImGuiDir_Right, 0.70f);
|
|
if (g.LogEnabled)
|
|
LogRenderedText(&text_pos, ">");
|
|
RenderText(text_pos, label, label_end, false);
|
|
}
|
|
|
|
if (is_open && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen))
|
|
TreePushRawID(id);
|
|
return is_open;
|
|
}
|
|
|
|
void ImGui::TreePush(const char* str_id)
|
|
{
|
|
ImGuiWindow* window = GetCurrentWindow();
|
|
Indent();
|
|
window->DC.TreeDepth++;
|
|
PushID(str_id ? str_id : "#TreePush");
|
|
}
|
|
|
|
void ImGui::TreePush(const void* ptr_id)
|
|
{
|
|
ImGuiWindow* window = GetCurrentWindow();
|
|
Indent();
|
|
window->DC.TreeDepth++;
|
|
PushID(ptr_id ? ptr_id : (const void*)"#TreePush");
|
|
}
|
|
|
|
void ImGui::TreePushRawID(ImGuiID id)
|
|
{
|
|
ImGuiWindow* window = GetCurrentWindow();
|
|
Indent();
|
|
window->DC.TreeDepth++;
|
|
window->IDStack.push_back(id);
|
|
}
|
|
|
|
void ImGui::TreePop()
|
|
{
|
|
ImGuiContext& g = *GImGui;
|
|
ImGuiWindow* window = g.CurrentWindow;
|
|
Unindent();
|
|
|
|
window->DC.TreeDepth--;
|
|
if (g.NavMoveDir == ImGuiDir_Left && g.NavWindow == window && NavMoveRequestButNoResultYet())
|
|
if (g.NavIdIsAlive && (window->DC.TreeDepthMayJumpToParentOnPop & (1 << window->DC.TreeDepth)))
|
|
{
|
|
SetNavID(window->IDStack.back(), g.NavLayer);
|
|
NavMoveRequestCancel();
|
|
}
|
|
window->DC.TreeDepthMayJumpToParentOnPop &= (1 << window->DC.TreeDepth) - 1;
|
|
|
|
IM_ASSERT(window->IDStack.Size > 1); // There should always be 1 element in the IDStack (pushed during window creation). If this triggers you called TreePop/PopID too much.
|
|
PopID();
|
|
}
|
|
|
|
void ImGui::TreeAdvanceToLabelPos()
|
|
{
|
|
ImGuiContext& g = *GImGui;
|
|
g.CurrentWindow->DC.CursorPos.x += GetTreeNodeToLabelSpacing();
|
|
}
|
|
|
|
// Horizontal distance preceding label when using TreeNode() or Bullet()
|
|
float ImGui::GetTreeNodeToLabelSpacing()
|
|
{
|
|
ImGuiContext& g = *GImGui;
|
|
return g.FontSize + (g.Style.FramePadding.x * 2.0f);
|
|
}
|
|
|
|
void ImGui::SetNextTreeNodeOpen(bool is_open, ImGuiCond cond)
|
|
{
|
|
ImGuiContext& g = *GImGui;
|
|
if (g.CurrentWindow->SkipItems)
|
|
return;
|
|
g.NextTreeNodeOpenVal = is_open;
|
|
g.NextTreeNodeOpenCond = cond ? cond : ImGuiCond_Always;
|
|
}
|
|
|
|
// CollapsingHeader returns true when opened but do not indent nor push into the ID stack (because of the ImGuiTreeNodeFlags_NoTreePushOnOpen flag).
|
|
// This is basically the same as calling TreeNodeEx(label, ImGuiTreeNodeFlags_CollapsingHeader). You can remove the _NoTreePushOnOpen flag if you want behavior closer to normal TreeNode().
|
|
bool ImGui::CollapsingHeader(const char* label, ImGuiTreeNodeFlags flags)
|
|
{
|
|
ImGuiWindow* window = GetCurrentWindow();
|
|
if (window->SkipItems)
|
|
return false;
|
|
|
|
return TreeNodeBehavior(window->GetID(label), flags | ImGuiTreeNodeFlags_CollapsingHeader, label);
|
|
}
|
|
|
|
bool ImGui::CollapsingHeader(const char* label, bool* p_open, ImGuiTreeNodeFlags flags)
|
|
{
|
|
ImGuiWindow* window = GetCurrentWindow();
|
|
if (window->SkipItems)
|
|
return false;
|
|
|
|
if (p_open && !*p_open)
|
|
return false;
|
|
|
|
ImGuiID id = window->GetID(label);
|
|
bool is_open = TreeNodeBehavior(id, flags | ImGuiTreeNodeFlags_CollapsingHeader | (p_open ? ImGuiTreeNodeFlags_AllowItemOverlap : 0), label);
|
|
if (p_open)
|
|
{
|
|
// Create a small overlapping close button // FIXME: We can evolve this into user accessible helpers to add extra buttons on title bars, headers, etc.
|
|
ImGuiContext& g = *GImGui;
|
|
ImGuiItemHoveredDataBackup last_item_backup;
|
|
float button_radius = g.FontSize * 0.5f;
|
|
ImVec2 button_center = ImVec2(ImMin(window->DC.LastItemRect.Max.x, window->ClipRect.Max.x) - g.Style.FramePadding.x - button_radius, window->DC.LastItemRect.GetCenter().y);
|
|
if (CloseButton(window->GetID((void*)(intptr_t)(id+1)), button_center, button_radius))
|
|
*p_open = false;
|
|
last_item_backup.Restore();
|
|
}
|
|
|
|
return is_open;
|
|
}
|
|
|
|
//-------------------------------------------------------------------------
|
|
// WIDGETS: Selectables
|
|
// - Selectable()
|
|
//-------------------------------------------------------------------------
|
|
|
|
// Tip: pass an empty label (e.g. "##dummy") then you can use the space to draw other text or image.
|
|
// But you need to make sure the ID is unique, e.g. enclose calls in PushID/PopID or use ##unique_id.
|
|
bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags flags, const ImVec2& size_arg)
|
|
{
|
|
ImGuiWindow* window = GetCurrentWindow();
|
|
if (window->SkipItems)
|
|
return false;
|
|
|
|
ImGuiContext& g = *GImGui;
|
|
const ImGuiStyle& style = g.Style;
|
|
|
|
if ((flags & ImGuiSelectableFlags_SpanAllColumns) && window->DC.ColumnsSet) // FIXME-OPT: Avoid if vertically clipped.
|
|
PopClipRect();
|
|
|
|
ImGuiID id = window->GetID(label);
|
|
ImVec2 label_size = CalcTextSize(label, NULL, true);
|
|
ImVec2 size(size_arg.x != 0.0f ? size_arg.x : label_size.x, size_arg.y != 0.0f ? size_arg.y : label_size.y);
|
|
ImVec2 pos = window->DC.CursorPos;
|
|
pos.y += window->DC.CurrentLineTextBaseOffset;
|
|
ImRect bb_inner(pos, pos + size);
|
|
ItemSize(bb_inner);
|
|
|
|
// Fill horizontal space.
|
|
ImVec2 window_padding = window->WindowPadding;
|
|
float max_x = (flags & ImGuiSelectableFlags_SpanAllColumns) ? GetWindowContentRegionMax().x : GetContentRegionMax().x;
|
|
float w_draw = ImMax(label_size.x, window->Pos.x + max_x - window_padding.x - window->DC.CursorPos.x);
|
|
ImVec2 size_draw((size_arg.x != 0 && !(flags & ImGuiSelectableFlags_DrawFillAvailWidth)) ? size_arg.x : w_draw, size_arg.y != 0.0f ? size_arg.y : size.y);
|
|
ImRect bb(pos, pos + size_draw);
|
|
if (size_arg.x == 0.0f || (flags & ImGuiSelectableFlags_DrawFillAvailWidth))
|
|
bb.Max.x += window_padding.x;
|
|
|
|
// Selectables are tightly packed together, we extend the box to cover spacing between selectable.
|
|
float spacing_L = (float)(int)(style.ItemSpacing.x * 0.5f);
|
|
float spacing_U = (float)(int)(style.ItemSpacing.y * 0.5f);
|
|
float spacing_R = style.ItemSpacing.x - spacing_L;
|
|
float spacing_D = style.ItemSpacing.y - spacing_U;
|
|
bb.Min.x -= spacing_L;
|
|
bb.Min.y -= spacing_U;
|
|
bb.Max.x += spacing_R;
|
|
bb.Max.y += spacing_D;
|
|
if (!ItemAdd(bb, (flags & ImGuiSelectableFlags_Disabled) ? 0 : id))
|
|
{
|
|
if ((flags & ImGuiSelectableFlags_SpanAllColumns) && window->DC.ColumnsSet)
|
|
PushColumnClipRect();
|
|
return false;
|
|
}
|
|
|
|
// We use NoHoldingActiveID on menus so user can click and _hold_ on a menu then drag to browse child entries
|
|
ImGuiButtonFlags button_flags = 0;
|
|
if (flags & ImGuiSelectableFlags_NoHoldingActiveID) button_flags |= ImGuiButtonFlags_NoHoldingActiveID;
|
|
if (flags & ImGuiSelectableFlags_PressedOnClick) button_flags |= ImGuiButtonFlags_PressedOnClick;
|
|
if (flags & ImGuiSelectableFlags_PressedOnRelease) button_flags |= ImGuiButtonFlags_PressedOnRelease;
|
|
if (flags & ImGuiSelectableFlags_Disabled) button_flags |= ImGuiButtonFlags_Disabled;
|
|
if (flags & ImGuiSelectableFlags_AllowDoubleClick) button_flags |= ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnDoubleClick;
|
|
bool hovered, held;
|
|
bool pressed = ButtonBehavior(bb, id, &hovered, &held, button_flags);
|
|
if (flags & ImGuiSelectableFlags_Disabled)
|
|
selected = false;
|
|
|
|
// Hovering selectable with mouse updates NavId accordingly so navigation can be resumed with gamepad/keyboard (this doesn't happen on most widgets)
|
|
if (pressed || hovered)
|
|
if (!g.NavDisableMouseHover && g.NavWindow == window && g.NavLayer == window->DC.NavLayerCurrent)
|
|
{
|
|
g.NavDisableHighlight = true;
|
|
SetNavID(id, window->DC.NavLayerCurrent);
|
|
}
|
|
if (pressed)
|
|
MarkItemEdited(id);
|
|
|
|
// Render
|
|
if (hovered || selected)
|
|
{
|
|
const ImU32 col = GetColorU32((held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header);
|
|
RenderFrame(bb.Min, bb.Max, col, false, 0.0f);
|
|
RenderNavHighlight(bb, id, ImGuiNavHighlightFlags_TypeThin | ImGuiNavHighlightFlags_NoRounding);
|
|
}
|
|
|
|
if ((flags & ImGuiSelectableFlags_SpanAllColumns) && window->DC.ColumnsSet)
|
|
{
|
|
PushColumnClipRect();
|
|
bb.Max.x -= (GetContentRegionMax().x - max_x);
|
|
}
|
|
|
|
if (flags & ImGuiSelectableFlags_Disabled) PushStyleColor(ImGuiCol_Text, g.Style.Colors[ImGuiCol_TextDisabled]);
|
|
RenderTextClipped(bb_inner.Min, bb.Max, label, NULL, &label_size, ImVec2(0.0f,0.0f));
|
|
if (flags & ImGuiSelectableFlags_Disabled) PopStyleColor();
|
|
|
|
// Automatically close popups
|
|
if (pressed && (window->Flags & ImGuiWindowFlags_Popup) && !(flags & ImGuiSelectableFlags_DontClosePopups) && !(window->DC.ItemFlags & ImGuiItemFlags_SelectableDontClosePopup))
|
|
CloseCurrentPopup();
|
|
return pressed;
|
|
}
|
|
|
|
bool ImGui::Selectable(const char* label, bool* p_selected, ImGuiSelectableFlags flags, const ImVec2& size_arg)
|
|
{
|
|
if (Selectable(label, *p_selected, flags, size_arg))
|
|
{
|
|
*p_selected = !*p_selected;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
//-------------------------------------------------------------------------
|
|
// WIDGETS: List Box
|
|
// - ListBox()
|
|
// - ListBoxHeader()
|
|
// - ListBoxFooter()
|
|
//-------------------------------------------------------------------------
|
|
|
|
// FIXME: Rename to BeginListBox()
|
|
// Helper to calculate the size of a listbox and display a label on the right.
|
|
// Tip: To have a list filling the entire window width, PushItemWidth(-1) and pass an empty label "##empty"
|
|
bool ImGui::ListBoxHeader(const char* label, const ImVec2& size_arg)
|
|
{
|
|
ImGuiWindow* window = GetCurrentWindow();
|
|
if (window->SkipItems)
|
|
return false;
|
|
|
|
const ImGuiStyle& style = GetStyle();
|
|
const ImGuiID id = GetID(label);
|
|
const ImVec2 label_size = CalcTextSize(label, NULL, true);
|
|
|
|
// Size default to hold ~7 items. Fractional number of items helps seeing that we can scroll down/up without looking at scrollbar.
|
|
ImVec2 size = CalcItemSize(size_arg, CalcItemWidth(), GetTextLineHeightWithSpacing() * 7.4f + style.ItemSpacing.y);
|
|
ImVec2 frame_size = ImVec2(size.x, ImMax(size.y, label_size.y));
|
|
ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size);
|
|
ImRect bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f));
|
|
window->DC.LastItemRect = bb; // Forward storage for ListBoxFooter.. dodgy.
|
|
|
|
BeginGroup();
|
|
if (label_size.x > 0)
|
|
RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label);
|
|
|
|
BeginChildFrame(id, frame_bb.GetSize());
|
|
return true;
|
|
}
|
|
|
|
// FIXME: Rename to BeginListBox()
|
|
bool ImGui::ListBoxHeader(const char* label, int items_count, int height_in_items)
|
|
{
|
|
// Size default to hold ~7 items. Fractional number of items helps seeing that we can scroll down/up without looking at scrollbar.
|
|
// We don't add +0.40f if items_count <= height_in_items. It is slightly dodgy, because it means a dynamic list of items will make the widget resize occasionally when it crosses that size.
|
|
// I am expecting that someone will come and complain about this behavior in a remote future, then we can advise on a better solution.
|
|
if (height_in_items < 0)
|
|
height_in_items = ImMin(items_count, 7);
|
|
float height_in_items_f = height_in_items < items_count ? (height_in_items + 0.40f) : (height_in_items + 0.00f);
|
|
|
|
// We include ItemSpacing.y so that a list sized for the exact number of items doesn't make a scrollbar appears. We could also enforce that by passing a flag to BeginChild().
|
|
ImVec2 size;
|
|
size.x = 0.0f;
|
|
size.y = GetTextLineHeightWithSpacing() * height_in_items_f + GetStyle().ItemSpacing.y;
|
|
return ListBoxHeader(label, size);
|
|
}
|
|
|
|
// FIXME: Rename to EndListBox()
|
|
void ImGui::ListBoxFooter()
|
|
{
|
|
ImGuiWindow* parent_window = GetCurrentWindow()->ParentWindow;
|
|
const ImRect bb = parent_window->DC.LastItemRect;
|
|
const ImGuiStyle& style = GetStyle();
|
|
|
|
EndChildFrame();
|
|
|
|
// Redeclare item size so that it includes the label (we have stored the full size in LastItemRect)
|
|
// We call SameLine() to restore DC.CurrentLine* data
|
|
SameLine();
|
|
parent_window->DC.CursorPos = bb.Min;
|
|
ItemSize(bb, style.FramePadding.y);
|
|
EndGroup();
|
|
}
|
|
|
|
bool ImGui::ListBox(const char* label, int* current_item, const char* const items[], int items_count, int height_items)
|
|
{
|
|
const bool value_changed = ListBox(label, current_item, Items_ArrayGetter, (void*)items, items_count, height_items);
|
|
return value_changed;
|
|
}
|
|
|
|
bool ImGui::ListBox(const char* label, int* current_item, bool (*items_getter)(void*, int, const char**), void* data, int items_count, int height_in_items)
|
|
{
|
|
if (!ListBoxHeader(label, items_count, height_in_items))
|
|
return false;
|
|
|
|
// Assume all items have even height (= 1 line of text). If you need items of different or variable sizes you can create a custom version of ListBox() in your code without using the clipper.
|
|
ImGuiContext& g = *GImGui;
|
|
bool value_changed = false;
|
|
ImGuiListClipper clipper(items_count, GetTextLineHeightWithSpacing()); // We know exactly our line height here so we pass it as a minor optimization, but generally you don't need to.
|
|
while (clipper.Step())
|
|
for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
|
|
{
|
|
const bool item_selected = (i == *current_item);
|
|
const char* item_text;
|
|
if (!items_getter(data, i, &item_text))
|
|
item_text = "*Unknown item*";
|
|
|
|
PushID(i);
|
|
if (Selectable(item_text, item_selected))
|
|
{
|
|
*current_item = i;
|
|
value_changed = true;
|
|
}
|
|
if (item_selected)
|
|
SetItemDefaultFocus();
|
|
PopID();
|
|
}
|
|
ListBoxFooter();
|
|
if (value_changed)
|
|
MarkItemEdited(g.CurrentWindow->DC.LastItemId);
|
|
|
|
return value_changed;
|
|
}
|
|
|
|
//-------------------------------------------------------------------------
|
|
// WIDGETS: Data Plotting
|
|
// - PlotEx() [Internal]
|
|
// - PlotLines()
|
|
// - PlotHistogram()
|
|
//-------------------------------------------------------------------------
|
|
|
|
void ImGui::PlotEx(ImGuiPlotType plot_type, const char* label, float (*values_getter)(void* data, int idx), void* data, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, ImVec2 graph_size)
|
|
{
|
|
ImGuiWindow* window = GetCurrentWindow();
|
|
if (window->SkipItems)
|
|
return;
|
|
|
|
ImGuiContext& g = *GImGui;
|
|
const ImGuiStyle& style = g.Style;
|
|
|
|
const ImVec2 label_size = CalcTextSize(label, NULL, true);
|
|
if (graph_size.x == 0.0f)
|
|
graph_size.x = CalcItemWidth();
|
|
if (graph_size.y == 0.0f)
|
|
graph_size.y = label_size.y + (style.FramePadding.y * 2);
|
|
|
|
const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(graph_size.x, graph_size.y));
|
|
const ImRect inner_bb(frame_bb.Min + style.FramePadding, frame_bb.Max - style.FramePadding);
|
|
const ImRect total_bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0));
|
|
ItemSize(total_bb, style.FramePadding.y);
|
|
if (!ItemAdd(total_bb, 0, &frame_bb))
|
|
return;
|
|
const bool hovered = ItemHoverable(inner_bb, 0);
|
|
|
|
// Determine scale from values if not specified
|
|
if (scale_min == FLT_MAX || scale_max == FLT_MAX)
|
|
{
|
|
float v_min = FLT_MAX;
|
|
float v_max = -FLT_MAX;
|
|
for (int i = 0; i < values_count; i++)
|
|
{
|
|
const float v = values_getter(data, i);
|
|
v_min = ImMin(v_min, v);
|
|
v_max = ImMax(v_max, v);
|
|
}
|
|
if (scale_min == FLT_MAX)
|
|
scale_min = v_min;
|
|
if (scale_max == FLT_MAX)
|
|
scale_max = v_max;
|
|
}
|
|
|
|
RenderFrame(frame_bb.Min, frame_bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding);
|
|
|
|
if (values_count > 0)
|
|
{
|
|
int res_w = ImMin((int)graph_size.x, values_count) + ((plot_type == ImGuiPlotType_Lines) ? -1 : 0);
|
|
int item_count = values_count + ((plot_type == ImGuiPlotType_Lines) ? -1 : 0);
|
|
|
|
// Tooltip on hover
|
|
int v_hovered = -1;
|
|
if (hovered)
|
|
{
|
|
const float t = ImClamp((g.IO.MousePos.x - inner_bb.Min.x) / (inner_bb.Max.x - inner_bb.Min.x), 0.0f, 0.9999f);
|
|
const int v_idx = (int)(t * item_count);
|
|
IM_ASSERT(v_idx >= 0 && v_idx < values_count);
|
|
|
|
const float v0 = values_getter(data, (v_idx + values_offset) % values_count);
|
|
const float v1 = values_getter(data, (v_idx + 1 + values_offset) % values_count);
|
|
if (plot_type == ImGuiPlotType_Lines)
|
|
SetTooltip("%d: %8.4g\n%d: %8.4g", v_idx, v0, v_idx+1, v1);
|
|
else if (plot_type == ImGuiPlotType_Histogram)
|
|
SetTooltip("%d: %8.4g", v_idx, v0);
|
|
v_hovered = v_idx;
|
|
}
|
|
|
|
const float t_step = 1.0f / (float)res_w;
|
|
const float inv_scale = (scale_min == scale_max) ? 0.0f : (1.0f / (scale_max - scale_min));
|
|
|
|
float v0 = values_getter(data, (0 + values_offset) % values_count);
|
|
float t0 = 0.0f;
|
|
ImVec2 tp0 = ImVec2( t0, 1.0f - ImSaturate((v0 - scale_min) * inv_scale) ); // Point in the normalized space of our target rectangle
|
|
float histogram_zero_line_t = (scale_min * scale_max < 0.0f) ? (-scale_min * inv_scale) : (scale_min < 0.0f ? 0.0f : 1.0f); // Where does the zero line stands
|
|
|
|
const ImU32 col_base = GetColorU32((plot_type == ImGuiPlotType_Lines) ? ImGuiCol_PlotLines : ImGuiCol_PlotHistogram);
|
|
const ImU32 col_hovered = GetColorU32((plot_type == ImGuiPlotType_Lines) ? ImGuiCol_PlotLinesHovered : ImGuiCol_PlotHistogramHovered);
|
|
|
|
for (int n = 0; n < res_w; n++)
|
|
{
|
|
const float t1 = t0 + t_step;
|
|
const int v1_idx = (int)(t0 * item_count + 0.5f);
|
|
IM_ASSERT(v1_idx >= 0 && v1_idx < values_count);
|
|
const float v1 = values_getter(data, (v1_idx + values_offset + 1) % values_count);
|
|
const ImVec2 tp1 = ImVec2( t1, 1.0f - ImSaturate((v1 - scale_min) * inv_scale) );
|
|
|
|
// NB: Draw calls are merged together by the DrawList system. Still, we should render our batch are lower level to save a bit of CPU.
|
|
ImVec2 pos0 = ImLerp(inner_bb.Min, inner_bb.Max, tp0);
|
|
ImVec2 pos1 = ImLerp(inner_bb.Min, inner_bb.Max, (plot_type == ImGuiPlotType_Lines) ? tp1 : ImVec2(tp1.x, histogram_zero_line_t));
|
|
if (plot_type == ImGuiPlotType_Lines)
|
|
{
|
|
window->DrawList->AddLine(pos0, pos1, v_hovered == v1_idx ? col_hovered : col_base);
|
|
}
|
|
else if (plot_type == ImGuiPlotType_Histogram)
|
|
{
|
|
if (pos1.x >= pos0.x + 2.0f)
|
|
pos1.x -= 1.0f;
|
|
window->DrawList->AddRectFilled(pos0, pos1, v_hovered == v1_idx ? col_hovered : col_base);
|
|
}
|
|
|
|
t0 = t1;
|
|
tp0 = tp1;
|
|
}
|
|
}
|
|
|
|
// Text overlay
|
|
if (overlay_text)
|
|
RenderTextClipped(ImVec2(frame_bb.Min.x, frame_bb.Min.y + style.FramePadding.y), frame_bb.Max, overlay_text, NULL, NULL, ImVec2(0.5f,0.0f));
|
|
|
|
if (label_size.x > 0.0f)
|
|
RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, inner_bb.Min.y), label);
|
|
}
|
|
|
|
struct ImGuiPlotArrayGetterData
|
|
{
|
|
const float* Values;
|
|
int Stride;
|
|
|
|
ImGuiPlotArrayGetterData(const float* values, int stride) { Values = values; Stride = stride; }
|
|
};
|
|
|
|
static float Plot_ArrayGetter(void* data, int idx)
|
|
{
|
|
ImGuiPlotArrayGetterData* plot_data = (ImGuiPlotArrayGetterData*)data;
|
|
const float v = *(const float*)(const void*)((const unsigned char*)plot_data->Values + (size_t)idx * plot_data->Stride);
|
|
return v;
|
|
}
|
|
|
|
void ImGui::PlotLines(const char* label, const float* values, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, ImVec2 graph_size, int stride)
|
|
{
|
|
ImGuiPlotArrayGetterData data(values, stride);
|
|
PlotEx(ImGuiPlotType_Lines, label, &Plot_ArrayGetter, (void*)&data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size);
|
|
}
|
|
|
|
void ImGui::PlotLines(const char* label, float (*values_getter)(void* data, int idx), void* data, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, ImVec2 graph_size)
|
|
{
|
|
PlotEx(ImGuiPlotType_Lines, label, values_getter, data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size);
|
|
}
|
|
|
|
void ImGui::PlotHistogram(const char* label, const float* values, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, ImVec2 graph_size, int stride)
|
|
{
|
|
ImGuiPlotArrayGetterData data(values, stride);
|
|
PlotEx(ImGuiPlotType_Histogram, label, &Plot_ArrayGetter, (void*)&data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size);
|
|
}
|
|
|
|
void ImGui::PlotHistogram(const char* label, float (*values_getter)(void* data, int idx), void* data, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, ImVec2 graph_size)
|
|
{
|
|
PlotEx(ImGuiPlotType_Histogram, label, values_getter, data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size);
|
|
}
|
|
|
|
//-------------------------------------------------------------------------
|
|
// WIDGETS: Value() helpers
|
|
// - Value()
|
|
//-------------------------------------------------------------------------
|
|
|
|
void ImGui::Value(const char* prefix, bool b)
|
|
{
|
|
Text("%s: %s", prefix, (b ? "true" : "false"));
|
|
}
|
|
|
|
void ImGui::Value(const char* prefix, int v)
|
|
{
|
|
Text("%s: %d", prefix, v);
|
|
}
|
|
|
|
void ImGui::Value(const char* prefix, unsigned int v)
|
|
{
|
|
Text("%s: %d", prefix, v);
|
|
}
|
|
|
|
void ImGui::Value(const char* prefix, float v, const char* float_format)
|
|
{
|
|
if (float_format)
|
|
{
|
|
char fmt[64];
|
|
ImFormatString(fmt, IM_ARRAYSIZE(fmt), "%%s: %s", float_format);
|
|
Text(fmt, prefix, v);
|
|
}
|
|
else
|
|
{
|
|
Text("%s: %.3f", prefix, v);
|
|
}
|
|
}
|
|
|
|
//-------------------------------------------------------------------------
|
|
// WIDGETS: Menus
|
|
// - ImGuiMenuColumns
|
|
// - BeginMainMenuBar()
|
|
// - EndMainMenuBar()
|
|
// - BeginMenuBar()
|
|
// - EndMenuBar()
|
|
// - BeginMenu()
|
|
// - EndMenu()
|
|
// - MenuItem()
|
|
//-------------------------------------------------------------------------
|
|
|
|
// Helpers for internal use
|
|
ImGuiMenuColumns::ImGuiMenuColumns()
|
|
{
|
|
Count = 0;
|
|
Spacing = Width = NextWidth = 0.0f;
|
|
memset(Pos, 0, sizeof(Pos));
|
|
memset(NextWidths, 0, sizeof(NextWidths));
|
|
}
|
|
|
|
void ImGuiMenuColumns::Update(int count, float spacing, bool clear)
|
|
{
|
|
IM_ASSERT(Count <= IM_ARRAYSIZE(Pos));
|
|
Count = count;
|
|
Width = NextWidth = 0.0f;
|
|
Spacing = spacing;
|
|
if (clear) memset(NextWidths, 0, sizeof(NextWidths));
|
|
for (int i = 0; i < Count; i++)
|
|
{
|
|
if (i > 0 && NextWidths[i] > 0.0f)
|
|
Width += Spacing;
|
|
Pos[i] = (float)(int)Width;
|
|
Width += NextWidths[i];
|
|
NextWidths[i] = 0.0f;
|
|
}
|
|
}
|
|
|
|
float ImGuiMenuColumns::DeclColumns(float w0, float w1, float w2) // not using va_arg because they promote float to double
|
|
{
|
|
NextWidth = 0.0f;
|
|
NextWidths[0] = ImMax(NextWidths[0], w0);
|
|
NextWidths[1] = ImMax(NextWidths[1], w1);
|
|
NextWidths[2] = ImMax(NextWidths[2], w2);
|
|
for (int i = 0; i < 3; i++)
|
|
NextWidth += NextWidths[i] + ((i > 0 && NextWidths[i] > 0.0f) ? Spacing : 0.0f);
|
|
return ImMax(Width, NextWidth);
|
|
}
|
|
|
|
float ImGuiMenuColumns::CalcExtraSpace(float avail_w)
|
|
{
|
|
return ImMax(0.0f, avail_w - Width);
|
|
}
|
|
|
|
// For the main menu bar, which cannot be moved, we honor g.Style.DisplaySafeAreaPadding to ensure text can be visible on a TV set.
|
|
bool ImGui::BeginMainMenuBar()
|
|
{
|
|
ImGuiContext& g = *GImGui;
|
|
g.NextWindowData.MenuBarOffsetMinVal = ImVec2(g.Style.DisplaySafeAreaPadding.x, ImMax(g.Style.DisplaySafeAreaPadding.y - g.Style.FramePadding.y, 0.0f));
|
|
SetNextWindowPos(ImVec2(0.0f, 0.0f));
|
|
SetNextWindowSize(ImVec2(g.IO.DisplaySize.x, g.NextWindowData.MenuBarOffsetMinVal.y + g.FontBaseSize + g.Style.FramePadding.y));
|
|
PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
|
|
PushStyleVar(ImGuiStyleVar_WindowMinSize, ImVec2(0,0));
|
|
ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_MenuBar;
|
|
bool is_open = Begin("##MainMenuBar", NULL, window_flags) && BeginMenuBar();
|
|
PopStyleVar(2);
|
|
g.NextWindowData.MenuBarOffsetMinVal = ImVec2(0.0f, 0.0f);
|
|
if (!is_open)
|
|
{
|
|
End();
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void ImGui::EndMainMenuBar()
|
|
{
|
|
EndMenuBar();
|
|
|
|
// When the user has left the menu layer (typically: closed menus through activation of an item), we restore focus to the previous window
|
|
ImGuiContext& g = *GImGui;
|
|
if (g.CurrentWindow == g.NavWindow && g.NavLayer == 0)
|
|
FocusFrontMostActiveWindowIgnoringOne(g.NavWindow);
|
|
|
|
End();
|
|
}
|
|
|
|
bool ImGui::BeginMenuBar()
|
|
{
|
|
ImGuiWindow* window = GetCurrentWindow();
|
|
if (window->SkipItems)
|
|
return false;
|
|
if (!(window->Flags & ImGuiWindowFlags_MenuBar))
|
|
return false;
|
|
|
|
IM_ASSERT(!window->DC.MenuBarAppending);
|
|
BeginGroup(); // Backup position on layer 0
|
|
PushID("##menubar");
|
|
|
|
// We don't clip with current window clipping rectangle as it is already set to the area below. However we clip with window full rect.
|
|
// We remove 1 worth of rounding to Max.x to that text in long menus and small windows don't tend to display over the lower-right rounded area, which looks particularly glitchy.
|
|
ImRect bar_rect = window->MenuBarRect();
|
|
ImRect clip_rect(ImFloor(bar_rect.Min.x + 0.5f), ImFloor(bar_rect.Min.y + window->WindowBorderSize + 0.5f), ImFloor(ImMax(bar_rect.Min.x, bar_rect.Max.x - window->WindowRounding) + 0.5f), ImFloor(bar_rect.Max.y + 0.5f));
|
|
clip_rect.ClipWith(window->OuterRectClipped);
|
|
PushClipRect(clip_rect.Min, clip_rect.Max, false);
|
|
|
|
window->DC.CursorPos = ImVec2(bar_rect.Min.x + window->DC.MenuBarOffset.x, bar_rect.Min.y + window->DC.MenuBarOffset.y);
|
|
window->DC.LayoutType = ImGuiLayoutType_Horizontal;
|
|
window->DC.NavLayerCurrent++;
|
|
window->DC.NavLayerCurrentMask <<= 1;
|
|
window->DC.MenuBarAppending = true;
|
|
AlignTextToFramePadding();
|
|
return true;
|
|
}
|
|
|
|
void ImGui::EndMenuBar()
|
|
{
|
|
ImGuiWindow* window = GetCurrentWindow();
|
|
if (window->SkipItems)
|
|
return;
|
|
ImGuiContext& g = *GImGui;
|
|
|
|
// Nav: When a move request within one of our child menu failed, capture the request to navigate among our siblings.
|
|
if (NavMoveRequestButNoResultYet() && (g.NavMoveDir == ImGuiDir_Left || g.NavMoveDir == ImGuiDir_Right) && (g.NavWindow->Flags & ImGuiWindowFlags_ChildMenu))
|
|
{
|
|
ImGuiWindow* nav_earliest_child = g.NavWindow;
|
|
while (nav_earliest_child->ParentWindow && (nav_earliest_child->ParentWindow->Flags & ImGuiWindowFlags_ChildMenu))
|
|
nav_earliest_child = nav_earliest_child->ParentWindow;
|
|
if (nav_earliest_child->ParentWindow == window && nav_earliest_child->DC.ParentLayoutType == ImGuiLayoutType_Horizontal && g.NavMoveRequestForward == ImGuiNavForward_None)
|
|
{
|
|
// To do so we claim focus back, restore NavId and then process the movement request for yet another frame.
|
|
// This involve a one-frame delay which isn't very problematic in this situation. We could remove it by scoring in advance for multiple window (probably not worth the hassle/cost)
|
|
IM_ASSERT(window->DC.NavLayerActiveMaskNext & 0x02); // Sanity check
|
|
FocusWindow(window);
|
|
SetNavIDWithRectRel(window->NavLastIds[1], 1, window->NavRectRel[1]);
|
|
g.NavLayer = 1;
|
|
g.NavDisableHighlight = true; // Hide highlight for the current frame so we don't see the intermediary selection.
|
|
g.NavMoveRequestForward = ImGuiNavForward_ForwardQueued;
|
|
NavMoveRequestCancel();
|
|
}
|
|
}
|
|
|
|
IM_ASSERT(window->Flags & ImGuiWindowFlags_MenuBar);
|
|
IM_ASSERT(window->DC.MenuBarAppending);
|
|
PopClipRect();
|
|
PopID();
|
|
window->DC.MenuBarOffset.x = window->DC.CursorPos.x - window->MenuBarRect().Min.x; // Save horizontal position so next append can reuse it. This is kinda equivalent to a per-layer CursorPos.
|
|
window->DC.GroupStack.back().AdvanceCursor = false;
|
|
EndGroup(); // Restore position on layer 0
|
|
window->DC.LayoutType = ImGuiLayoutType_Vertical;
|
|
window->DC.NavLayerCurrent--;
|
|
window->DC.NavLayerCurrentMask >>= 1;
|
|
window->DC.MenuBarAppending = false;
|
|
}
|
|
|
|
bool ImGui::BeginMenu(const char* label, bool enabled)
|
|
{
|
|
ImGuiWindow* window = GetCurrentWindow();
|
|
if (window->SkipItems)
|
|
return false;
|
|
|
|
ImGuiContext& g = *GImGui;
|
|
const ImGuiStyle& style = g.Style;
|
|
const ImGuiID id = window->GetID(label);
|
|
|
|
ImVec2 label_size = CalcTextSize(label, NULL, true);
|
|
|
|
bool pressed;
|
|
bool menu_is_open = IsPopupOpen(id);
|
|
bool menuset_is_open = !(window->Flags & ImGuiWindowFlags_Popup) && (g.OpenPopupStack.Size > g.CurrentPopupStack.Size && g.OpenPopupStack[g.CurrentPopupStack.Size].OpenParentId == window->IDStack.back());
|
|
ImGuiWindow* backed_nav_window = g.NavWindow;
|
|
if (menuset_is_open)
|
|
g.NavWindow = window; // Odd hack to allow hovering across menus of a same menu-set (otherwise we wouldn't be able to hover parent)
|
|
|
|
// The reference position stored in popup_pos will be used by Begin() to find a suitable position for the child menu (using FindBestWindowPosForPopup).
|
|
ImVec2 popup_pos, pos = window->DC.CursorPos;
|
|
if (window->DC.LayoutType == ImGuiLayoutType_Horizontal)
|
|
{
|
|
// Menu inside an horizontal menu bar
|
|
// Selectable extend their highlight by half ItemSpacing in each direction.
|
|
// For ChildMenu, the popup position will be overwritten by the call to FindBestWindowPosForPopup() in Begin()
|
|
popup_pos = ImVec2(pos.x - window->WindowPadding.x, pos.y - style.FramePadding.y + window->MenuBarHeight());
|
|
window->DC.CursorPos.x += (float)(int)(style.ItemSpacing.x * 0.5f);
|
|
PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing * 2.0f);
|
|
float w = label_size.x;
|
|
pressed = Selectable(label, menu_is_open, ImGuiSelectableFlags_NoHoldingActiveID | ImGuiSelectableFlags_PressedOnClick | ImGuiSelectableFlags_DontClosePopups | (!enabled ? ImGuiSelectableFlags_Disabled : 0), ImVec2(w, 0.0f));
|
|
PopStyleVar();
|
|
window->DC.CursorPos.x += (float)(int)(style.ItemSpacing.x * (-1.0f + 0.5f)); // -1 spacing to compensate the spacing added when Selectable() did a SameLine(). It would also work to call SameLine() ourselves after the PopStyleVar().
|
|
}
|
|
else
|
|
{
|
|
// Menu inside a menu
|
|
popup_pos = ImVec2(pos.x, pos.y - style.WindowPadding.y);
|
|
float w = window->MenuColumns.DeclColumns(label_size.x, 0.0f, (float)(int)(g.FontSize * 1.20f)); // Feedback to next frame
|
|
float extra_w = ImMax(0.0f, GetContentRegionAvail().x - w);
|
|
pressed = Selectable(label, menu_is_open, ImGuiSelectableFlags_NoHoldingActiveID | ImGuiSelectableFlags_PressedOnClick | ImGuiSelectableFlags_DontClosePopups | ImGuiSelectableFlags_DrawFillAvailWidth | (!enabled ? ImGuiSelectableFlags_Disabled : 0), ImVec2(w, 0.0f));
|
|
if (!enabled) PushStyleColor(ImGuiCol_Text, g.Style.Colors[ImGuiCol_TextDisabled]);
|
|
RenderArrow(pos + ImVec2(window->MenuColumns.Pos[2] + extra_w + g.FontSize * 0.30f, 0.0f), ImGuiDir_Right);
|
|
if (!enabled) PopStyleColor();
|
|
}
|
|
|
|
const bool hovered = enabled && ItemHoverable(window->DC.LastItemRect, id);
|
|
if (menuset_is_open)
|
|
g.NavWindow = backed_nav_window;
|
|
|
|
bool want_open = false, want_close = false;
|
|
if (window->DC.LayoutType == ImGuiLayoutType_Vertical) // (window->Flags & (ImGuiWindowFlags_Popup|ImGuiWindowFlags_ChildMenu))
|
|
{
|
|
// Implement http://bjk5.com/post/44698559168/breaking-down-amazons-mega-dropdown to avoid using timers, so menus feels more reactive.
|
|
bool moving_within_opened_triangle = false;
|
|
if (g.HoveredWindow == window && g.OpenPopupStack.Size > g.CurrentPopupStack.Size && g.OpenPopupStack[g.CurrentPopupStack.Size].ParentWindow == window && !(window->Flags & ImGuiWindowFlags_MenuBar))
|
|
{
|
|
if (ImGuiWindow* next_window = g.OpenPopupStack[g.CurrentPopupStack.Size].Window)
|
|
{
|
|
ImRect next_window_rect = next_window->Rect();
|
|
ImVec2 ta = g.IO.MousePos - g.IO.MouseDelta;
|
|
ImVec2 tb = (window->Pos.x < next_window->Pos.x) ? next_window_rect.GetTL() : next_window_rect.GetTR();
|
|
ImVec2 tc = (window->Pos.x < next_window->Pos.x) ? next_window_rect.GetBL() : next_window_rect.GetBR();
|
|
float extra = ImClamp(ImFabs(ta.x - tb.x) * 0.30f, 5.0f, 30.0f); // add a bit of extra slack.
|
|
ta.x += (window->Pos.x < next_window->Pos.x) ? -0.5f : +0.5f; // to avoid numerical issues
|
|
tb.y = ta.y + ImMax((tb.y - extra) - ta.y, -100.0f); // triangle is maximum 200 high to limit the slope and the bias toward large sub-menus // FIXME: Multiply by fb_scale?
|
|
tc.y = ta.y + ImMin((tc.y + extra) - ta.y, +100.0f);
|
|
moving_within_opened_triangle = ImTriangleContainsPoint(ta, tb, tc, g.IO.MousePos);
|
|
//window->DrawList->PushClipRectFullScreen(); window->DrawList->AddTriangleFilled(ta, tb, tc, moving_within_opened_triangle ? IM_COL32(0,128,0,128) : IM_COL32(128,0,0,128)); window->DrawList->PopClipRect(); // Debug
|
|
}
|
|
}
|
|
|
|
want_close = (menu_is_open && !hovered && g.HoveredWindow == window && g.HoveredIdPreviousFrame != 0 && g.HoveredIdPreviousFrame != id && !moving_within_opened_triangle);
|
|
want_open = (!menu_is_open && hovered && !moving_within_opened_triangle) || (!menu_is_open && hovered && pressed);
|
|
|
|
if (g.NavActivateId == id)
|
|
{
|
|
want_close = menu_is_open;
|
|
want_open = !menu_is_open;
|
|
}
|
|
if (g.NavId == id && g.NavMoveRequest && g.NavMoveDir == ImGuiDir_Right) // Nav-Right to open
|
|
{
|
|
want_open = true;
|
|
NavMoveRequestCancel();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Menu bar
|
|
if (menu_is_open && pressed && menuset_is_open) // Click an open menu again to close it
|
|
{
|
|
want_close = true;
|
|
want_open = menu_is_open = false;
|
|
}
|
|
else if (pressed || (hovered && menuset_is_open && !menu_is_open)) // First click to open, then hover to open others
|
|
{
|
|
want_open = true;
|
|
}
|
|
else if (g.NavId == id && g.NavMoveRequest && g.NavMoveDir == ImGuiDir_Down) // Nav-Down to open
|
|
{
|
|
want_open = true;
|
|
NavMoveRequestCancel();
|
|
}
|
|
}
|
|
|
|
if (!enabled) // explicitly close if an open menu becomes disabled, facilitate users code a lot in pattern such as 'if (BeginMenu("options", has_object)) { ..use object.. }'
|
|
want_close = true;
|
|
if (want_close && IsPopupOpen(id))
|
|
ClosePopupToLevel(g.CurrentPopupStack.Size);
|
|
|
|
if (!menu_is_open && want_open && g.OpenPopupStack.Size > g.CurrentPopupStack.Size)
|
|
{
|
|
// Don't recycle same menu level in the same frame, first close the other menu and yield for a frame.
|
|
OpenPopup(label);
|
|
return false;
|
|
}
|
|
|
|
menu_is_open |= want_open;
|
|
if (want_open)
|
|
OpenPopup(label);
|
|
|
|
if (menu_is_open)
|
|
{
|
|
// Sub-menus are ChildWindow so that mouse can be hovering across them (otherwise top-most popup menu would steal focus and not allow hovering on parent menu)
|
|
SetNextWindowPos(popup_pos, ImGuiCond_Always);
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_ChildMenu | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoNavFocus;
|
|
if (window->Flags & (ImGuiWindowFlags_Popup|ImGuiWindowFlags_ChildMenu))
|
|
flags |= ImGuiWindowFlags_ChildWindow;
|
|
menu_is_open = BeginPopupEx(id, flags); // menu_is_open can be 'false' when the popup is completely clipped (e.g. zero size display)
|
|
}
|
|
|
|
return menu_is_open;
|
|
}
|
|
|
|
void ImGui::EndMenu()
|
|
{
|
|
// Nav: When a left move request _within our child menu_ failed, close the menu.
|
|
// A menu doesn't close itself because EndMenuBar() wants the catch the last Left<>Right inputs.
|
|
// However, it means that with the current code, a BeginMenu() from outside another menu or a menu-bar won't be closable with the Left direction.
|
|
ImGuiContext& g = *GImGui;
|
|
ImGuiWindow* window = g.CurrentWindow;
|
|
if (g.NavWindow && g.NavWindow->ParentWindow == window && g.NavMoveDir == ImGuiDir_Left && NavMoveRequestButNoResultYet() && window->DC.LayoutType == ImGuiLayoutType_Vertical)
|
|
{
|
|
ClosePopupToLevel(g.OpenPopupStack.Size - 1);
|
|
NavMoveRequestCancel();
|
|
}
|
|
|
|
EndPopup();
|
|
}
|
|
|
|
bool ImGui::MenuItem(const char* label, const char* shortcut, bool selected, bool enabled)
|
|
{
|
|
ImGuiWindow* window = GetCurrentWindow();
|
|
if (window->SkipItems)
|
|
return false;
|
|
|
|
ImGuiContext& g = *GImGui;
|
|
ImGuiStyle& style = g.Style;
|
|
ImVec2 pos = window->DC.CursorPos;
|
|
ImVec2 label_size = CalcTextSize(label, NULL, true);
|
|
|
|
ImGuiSelectableFlags flags = ImGuiSelectableFlags_PressedOnRelease | (enabled ? 0 : ImGuiSelectableFlags_Disabled);
|
|
bool pressed;
|
|
if (window->DC.LayoutType == ImGuiLayoutType_Horizontal)
|
|
{
|
|
// Mimic the exact layout spacing of BeginMenu() to allow MenuItem() inside a menu bar, which is a little misleading but may be useful
|
|
// Note that in this situation we render neither the shortcut neither the selected tick mark
|
|
float w = label_size.x;
|
|
window->DC.CursorPos.x += (float)(int)(style.ItemSpacing.x * 0.5f);
|
|
PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing * 2.0f);
|
|
pressed = Selectable(label, false, flags, ImVec2(w, 0.0f));
|
|
PopStyleVar();
|
|
window->DC.CursorPos.x += (float)(int)(style.ItemSpacing.x * (-1.0f + 0.5f)); // -1 spacing to compensate the spacing added when Selectable() did a SameLine(). It would also work to call SameLine() ourselves after the PopStyleVar().
|
|
}
|
|
else
|
|
{
|
|
ImVec2 shortcut_size = shortcut ? CalcTextSize(shortcut, NULL) : ImVec2(0.0f, 0.0f);
|
|
float w = window->MenuColumns.DeclColumns(label_size.x, shortcut_size.x, (float)(int)(g.FontSize * 1.20f)); // Feedback for next frame
|
|
float extra_w = ImMax(0.0f, GetContentRegionAvail().x - w);
|
|
pressed = Selectable(label, false, flags | ImGuiSelectableFlags_DrawFillAvailWidth, ImVec2(w, 0.0f));
|
|
if (shortcut_size.x > 0.0f)
|
|
{
|
|
PushStyleColor(ImGuiCol_Text, g.Style.Colors[ImGuiCol_TextDisabled]);
|
|
RenderText(pos + ImVec2(window->MenuColumns.Pos[1] + extra_w, 0.0f), shortcut, NULL, false);
|
|
PopStyleColor();
|
|
}
|
|
if (selected)
|
|
RenderCheckMark(pos + ImVec2(window->MenuColumns.Pos[2] + extra_w + g.FontSize * 0.40f, g.FontSize * 0.134f * 0.5f), GetColorU32(enabled ? ImGuiCol_Text : ImGuiCol_TextDisabled), g.FontSize * 0.866f);
|
|
}
|
|
return pressed;
|
|
}
|
|
|
|
bool ImGui::MenuItem(const char* label, const char* shortcut, bool* p_selected, bool enabled)
|
|
{
|
|
if (MenuItem(label, shortcut, p_selected ? *p_selected : false, enabled))
|
|
{
|
|
if (p_selected)
|
|
*p_selected = !*p_selected;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|