summaryrefslogtreecommitdiffstatshomepage
path: root/3rdparty/bgfx/3rdparty/dear-imgui/imgui_widgets.cpp
diff options
context:
space:
mode:
Diffstat (limited to '3rdparty/bgfx/3rdparty/dear-imgui/imgui_widgets.cpp')
-rw-r--r--3rdparty/bgfx/3rdparty/dear-imgui/imgui_widgets.cpp4922
1 files changed, 2862 insertions, 2060 deletions
diff --git a/3rdparty/bgfx/3rdparty/dear-imgui/imgui_widgets.cpp b/3rdparty/bgfx/3rdparty/dear-imgui/imgui_widgets.cpp
index e6f61616d5b..3b8be01e7d4 100644
--- a/3rdparty/bgfx/3rdparty/dear-imgui/imgui_widgets.cpp
+++ b/3rdparty/bgfx/3rdparty/dear-imgui/imgui_widgets.cpp
@@ -1,4 +1,4 @@
-// dear imgui, v1.74 WIP
+// dear imgui, v1.89.1 WIP
// (widgets code)
/*
@@ -33,46 +33,65 @@ Index of this file:
#endif
#include "imgui.h"
+#ifndef IMGUI_DISABLE
+
#ifndef IMGUI_DEFINE_MATH_OPERATORS
#define IMGUI_DEFINE_MATH_OPERATORS
#endif
#include "imgui_internal.h"
-#include <ctype.h> // toupper
+// System includes
#if defined(_MSC_VER) && _MSC_VER <= 1500 // MSVC 2008 or earlier
#include <stddef.h> // intptr_t
#else
#include <stdint.h> // intptr_t
#endif
+//-------------------------------------------------------------------------
+// Warnings
+//-------------------------------------------------------------------------
+
// 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
+#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
+#if defined(_MSC_VER) && _MSC_VER >= 1922 // MSVC 2019 16.2 or later
+#pragma warning (disable: 5054) // operator '|': deprecated between enumerations of different types
+#endif
+#pragma warning (disable: 26451) // [Static Analyzer] Arithmetic overflow : Using operator 'xxx' on a 4 byte value and then casting the result to a 8 byte value. Cast the value to the wider type before calling operator 'xxx' to avoid overflow(io.2).
+#pragma warning (disable: 26812) // [Static Analyzer] The enum type 'xxx' is unscoped. Prefer 'enum class' over 'enum' (Enum.3).
#endif
// Clang/GCC warnings with -Weverything
#if defined(__clang__)
-#pragma clang diagnostic ignored "-Wold-style-cast" // warning : use of old-style cast // yes, they are more terse.
-#pragma clang diagnostic ignored "-Wfloat-equal" // warning : comparing floating point with == or != is unsafe // storing and comparing against same constants (typically 0.0f) is ok.
-#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 //
-#if __has_warning("-Wzero-as-null-pointer-constant")
-#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" // warning : zero as null pointer constant // some standard header variations use #define NULL 0
-#endif
-#if __has_warning("-Wdouble-promotion")
-#pragma clang diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function // using printf() is a misery with this as C++ va_arg ellipsis changes float to double.
+#if __has_warning("-Wunknown-warning-option")
+#pragma clang diagnostic ignored "-Wunknown-warning-option" // warning: unknown warning group 'xxx' // not all warnings are known by all Clang versions and they tend to be rename-happy.. so ignoring warnings triggers new warnings on some configuration. Great!
#endif
+#pragma clang diagnostic ignored "-Wunknown-pragmas" // warning: unknown warning group 'xxx'
+#pragma clang diagnostic ignored "-Wold-style-cast" // warning: use of old-style cast // yes, they are more terse.
+#pragma clang diagnostic ignored "-Wfloat-equal" // warning: comparing floating point with == or != is unsafe // storing and comparing against same constants (typically 0.0f) is ok.
+#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
+#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" // warning: zero as null pointer constant // some standard header variations use #define NULL 0
+#pragma clang diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function // using printf() is a misery with this as C++ va_arg ellipsis changes float to double.
+#pragma clang diagnostic ignored "-Wenum-enum-conversion" // warning: bitwise operation between different enumeration types ('XXXFlags_' and 'XXXFlagsPrivate_')
+#pragma clang diagnostic ignored "-Wdeprecated-enum-enum-conversion"// warning: bitwise operation between different enumeration types ('XXXFlags_' and 'XXXFlagsPrivate_') is deprecated
+#pragma clang diagnostic ignored "-Wimplicit-int-float-conversion" // warning: implicit conversion from 'xxx' to 'float' may lose precision
#elif defined(__GNUC__)
-#pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind
-#pragma GCC diagnostic ignored "-Wformat-nonliteral" // warning: format not a string literal, format string not checked
-#pragma GCC diagnostic ignored "-Wclass-memaccess" // [__GNUC__ >= 8] warning: 'memset/memcpy' clearing/writing an object of type 'xxxx' with no trivial copy-assignment; use assignment or value-initialization instead
+#pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind
+#pragma GCC diagnostic ignored "-Wformat-nonliteral" // warning: format not a string literal, format string not checked
+#pragma GCC diagnostic ignored "-Wclass-memaccess" // [__GNUC__ >= 8] warning: 'memset/memcpy' clearing/writing an object of type 'xxxx' with no trivial copy-assignment; use assignment or value-initialization instead
+#pragma GCC diagnostic ignored "-Wdeprecated-enum-enum-conversion" // warning: bitwise operation between different enumeration types ('XXXFlags_' and 'XXXFlagsPrivate_') is deprecated
#endif
//-------------------------------------------------------------------------
// Data
//-------------------------------------------------------------------------
+// Widgets
+static const float DRAGDROP_HOLD_TO_OPEN_TIMER = 0.70f; // Time for drag-hold to activate items accepting the ImGuiButtonFlags_PressedOnDragDropHold button behavior.
+static const float DRAG_MOUSE_THRESHOLD_FACTOR = 0.50f; // Multiplier for the default value of io.MouseDragThreshold to make DragFloat/DragInt react faster to mouse drags.
+
// Those MIN/MAX values are not define because we need to point to them
static const signed char IM_S8_MIN = -128;
static const signed char IM_S8_MAX = 127;
@@ -105,7 +124,7 @@ static const ImU64 IM_U64_MAX = (2ULL * 9223372036854775807LL + 1);
//-------------------------------------------------------------------------
// For InputTextEx()
-static bool InputTextFilterCharacter(unsigned int* p_char, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data);
+static bool InputTextFilterCharacter(unsigned int* p_char, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data, ImGuiInputSource input_source);
static int InputTextCalcTextLenAndLineCount(const char* text_begin, const char** out_text_end);
static ImVec2 InputTextCalcTextSizeW(const ImWchar* text_begin, const ImWchar* text_end, const ImWchar** remaining = NULL, ImVec2* out_offset = NULL, bool stop_on_new_line = false);
@@ -133,9 +152,13 @@ void ImGui::TextEx(const char* text, const char* text_end, ImGuiTextFlags flags)
ImGuiWindow* window = GetCurrentWindow();
if (window->SkipItems)
return;
-
ImGuiContext& g = *GImGui;
- IM_ASSERT(text != NULL);
+
+ // Accept null ranges
+ if (text == text_end)
+ text = text_end = "";
+
+ // Calculate length
const char* text_begin = text;
if (text_end == NULL)
text_end = text + strlen(text); // FIXME-OPT
@@ -143,7 +166,21 @@ void ImGui::TextEx(const char* text, const char* text_end, ImGuiTextFlags flags)
const ImVec2 text_pos(window->DC.CursorPos.x, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
const float wrap_pos_x = window->DC.TextWrapPos;
const bool wrap_enabled = (wrap_pos_x >= 0.0f);
- if (text_end - text > 2000 && !wrap_enabled)
+ if (text_end - text <= 2000 || wrap_enabled)
+ {
+ // Common case
+ 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);
+
+ ImRect bb(text_pos, text_pos + text_size);
+ ItemSize(text_size, 0.0f);
+ 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);
+ }
+ else
{
// Long text!
// Perform manual coarse clipping to optimize for long multi-line text
@@ -152,7 +189,7 @@ void ImGui::TextEx(const char* text, const char* text_end, ImGuiTextFlags flags)
// - We use memchr(), pay attention that well optimized versions of those str/mem functions are much faster than a casually written loop.
const char* line = text;
const float line_height = GetTextLineHeight();
- ImVec2 text_size(0,0);
+ ImVec2 text_size(0, 0);
// Lines to skip (can't skip when logging text)
ImVec2 pos = text_pos;
@@ -182,7 +219,7 @@ void ImGui::TextEx(const char* text, const char* text_end, ImGuiTextFlags flags)
ImRect line_rect(pos, pos + ImVec2(FLT_MAX, line_height));
while (line < text_end)
{
- if (IsClippedEx(line_rect, 0, false))
+ if (IsClippedEx(line_rect, 0))
break;
const char* line_end = (const char*)memchr(line, '\n', text_end - line);
@@ -216,19 +253,6 @@ void ImGui::TextEx(const char* text, const char* text_end, ImGuiTextFlags flags)
ItemSize(text_size, 0.0f);
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);
-
- ImRect bb(text_pos, text_pos + text_size);
- ItemSize(text_size, 0.0f);
- 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::TextUnformatted(const char* text, const char* text_end)
@@ -250,9 +274,10 @@ void ImGui::TextV(const char* fmt, va_list args)
if (window->SkipItems)
return;
- ImGuiContext& g = *GImGui;
- const char* text_end = g.TempBuffer + ImFormatStringV(g.TempBuffer, IM_ARRAYSIZE(g.TempBuffer), fmt, args);
- TextEx(g.TempBuffer, text_end, ImGuiTextFlags_NoWidthForLargeClippedText);
+ // FIXME-OPT: Handle the %s shortcut?
+ const char* text, *text_end;
+ ImFormatStringToTempBufferV(&text, &text_end, fmt, args);
+ TextEx(text, text_end, ImGuiTextFlags_NoWidthForLargeClippedText);
}
void ImGui::TextColored(const ImVec4& col, const char* fmt, ...)
@@ -266,7 +291,10 @@ void ImGui::TextColored(const ImVec4& col, const char* fmt, ...)
void ImGui::TextColoredV(const ImVec4& col, const char* fmt, va_list args)
{
PushStyleColor(ImGuiCol_Text, col);
- TextV(fmt, args);
+ if (fmt[0] == '%' && fmt[1] == 's' && fmt[2] == 0)
+ TextEx(va_arg(args, const char*), NULL, ImGuiTextFlags_NoWidthForLargeClippedText); // Skip formatting
+ else
+ TextV(fmt, args);
PopStyleColor();
}
@@ -280,8 +308,12 @@ void ImGui::TextDisabled(const char* fmt, ...)
void ImGui::TextDisabledV(const char* fmt, va_list args)
{
- PushStyleColor(ImGuiCol_Text, GImGui->Style.Colors[ImGuiCol_TextDisabled]);
- TextV(fmt, args);
+ ImGuiContext& g = *GImGui;
+ PushStyleColor(ImGuiCol_Text, g.Style.Colors[ImGuiCol_TextDisabled]);
+ if (fmt[0] == '%' && fmt[1] == 's' && fmt[2] == 0)
+ TextEx(va_arg(args, const char*), NULL, ImGuiTextFlags_NoWidthForLargeClippedText); // Skip formatting
+ else
+ TextV(fmt, args);
PopStyleColor();
}
@@ -295,11 +327,14 @@ void ImGui::TextWrapped(const char* fmt, ...)
void ImGui::TextWrappedV(const char* fmt, va_list args)
{
- ImGuiWindow* window = GetCurrentWindow();
- bool need_backup = (window->DC.TextWrapPos < 0.0f); // Keep existing wrap position if one is already set
+ ImGuiContext& g = *GImGui;
+ bool need_backup = (g.CurrentWindow->DC.TextWrapPos < 0.0f); // Keep existing wrap position if one is already set
if (need_backup)
PushTextWrapPos(0.0f);
- TextV(fmt, args);
+ if (fmt[0] == '%' && fmt[1] == 's' && fmt[2] == 0)
+ TextEx(va_arg(args, const char*), NULL, ImGuiTextFlags_NoWidthForLargeClippedText); // Skip formatting
+ else
+ TextV(fmt, args);
if (need_backup)
PopTextWrapPos();
}
@@ -323,17 +358,20 @@ void ImGui::LabelTextV(const char* label, const char* fmt, va_list args)
const ImGuiStyle& style = g.Style;
const float w = CalcItemWidth();
+ const char* value_text_begin, *value_text_end;
+ ImFormatStringToTempBufferV(&value_text_begin, &value_text_end, fmt, args);
+ const ImVec2 value_size = CalcTextSize(value_text_begin, value_text_end, false);
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);
+
+ const ImVec2 pos = window->DC.CursorPos;
+ const ImRect value_bb(pos, pos + ImVec2(w, value_size.y + style.FramePadding.y * 2));
+ const ImRect total_bb(pos, pos + ImVec2(w + (label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f), ImMax(value_size.y, label_size.y) + style.FramePadding.y * 2));
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));
+ RenderTextClipped(value_bb.Min + style.FramePadding, value_bb.Max, value_text_begin, value_text_end, &value_size, ImVec2(0.0f, 0.0f));
if (label_size.x > 0.0f)
RenderText(ImVec2(value_bb.Max.x + style.ItemInnerSpacing.x, value_bb.Min.y + style.FramePadding.y), label);
}
@@ -356,8 +394,8 @@ void ImGui::BulletTextV(const char* fmt, va_list args)
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 char* text_begin, *text_end;
+ ImFormatStringToTempBufferV(&text_begin, &text_end, fmt, args);
const ImVec2 label_size = CalcTextSize(text_begin, text_end, false);
const ImVec2 total_size = ImVec2(g.FontSize + (label_size.x > 0.0f ? (label_size.x + style.FramePadding.x * 2) : 0.0f), label_size.y); // Empty text doesn't add padding
ImVec2 pos = window->DC.CursorPos;
@@ -369,7 +407,7 @@ void ImGui::BulletTextV(const char* fmt, va_list args)
// Render
ImU32 text_col = GetColorU32(ImGuiCol_Text);
- RenderBullet(window->DrawList, bb.Min + ImVec2(style.FramePadding.x + g.FontSize*0.5f, g.FontSize*0.5f), text_col);
+ RenderBullet(window->DrawList, bb.Min + ImVec2(style.FramePadding.x + g.FontSize * 0.5f, g.FontSize * 0.5f), text_col);
RenderText(bb.Min + ImVec2(g.FontSize + style.FramePadding.x * 2, 0.0f), text_begin, text_end, false);
}
@@ -383,11 +421,14 @@ void ImGui::BulletTextV(const char* fmt, va_list args)
// - ArrowButton()
// - CloseButton() [Internal]
// - CollapseButton() [Internal]
-// - ScrollbarEx() [Internal]
+// - GetWindowScrollbarID() [Internal]
+// - GetWindowScrollbarRect() [Internal]
// - Scrollbar() [Internal]
+// - ScrollbarEx() [Internal]
// - Image()
// - ImageButton()
// - Checkbox()
+// - CheckboxFlagsT() [Internal]
// - CheckboxFlags()
// - RadioButton()
// - ProgressBar()
@@ -453,26 +494,22 @@ bool ImGui::ButtonBehavior(const ImRect& bb, ImGuiID id, bool* out_hovered, bool
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 only reacts to left mouse button
+ if ((flags & ImGuiButtonFlags_MouseButtonMask_) == 0)
+ flags |= ImGuiButtonFlags_MouseButtonDefault_;
- // Default behavior requires click+release on same spot
- if ((flags & (ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnClick | ImGuiButtonFlags_PressedOnRelease | ImGuiButtonFlags_PressedOnDoubleClick)) == 0)
- flags |= ImGuiButtonFlags_PressedOnClickRelease;
+ // Default behavior requires click + release inside bounding box
+ if ((flags & ImGuiButtonFlags_PressedOnMask_) == 0)
+ flags |= ImGuiButtonFlags_PressedOnDefault_;
ImGuiWindow* backup_hovered_window = g.HoveredWindow;
- const bool flatten_hovered_children = (flags & ImGuiButtonFlags_FlattenChildren) && g.HoveredRootWindow == window;
+ const bool flatten_hovered_children = (flags & ImGuiButtonFlags_FlattenChildren) && g.HoveredWindow && g.HoveredWindow->RootWindow == window;
if (flatten_hovered_children)
g.HoveredWindow = window;
#ifdef IMGUI_ENABLE_TEST_ENGINE
- if (id != 0 && window->DC.LastItemId != id)
- ImGuiTestEngineHook_ItemAdd(&g, bb, id);
+ if (id != 0 && g.LastItemData.ID != id)
+ IMGUI_TEST_ENGINE_ITEM_ADD(bb, id);
#endif
bool pressed = false;
@@ -488,9 +525,10 @@ bool ImGui::ButtonBehavior(const ImRect& bb, ImGuiID id, bool* out_hovered, bool
{
hovered = true;
SetHoveredID(id);
- if (CalcTypematicRepeatAmount(g.HoveredIdTimer + 0.0001f - g.IO.DeltaTime, g.HoveredIdTimer + 0.0001f, 0.70f, 0.00f))
+ if (g.HoveredIdTimer - g.IO.DeltaTime <= DRAGDROP_HOLD_TO_OPEN_TIMER && g.HoveredIdTimer >= DRAGDROP_HOLD_TO_OPEN_TIMER)
{
pressed = true;
+ g.DragDropHoldJustPressedId = id;
FocusWindow(window);
}
}
@@ -502,38 +540,64 @@ bool ImGui::ButtonBehavior(const ImRect& bb, ImGuiID id, bool* out_hovered, bool
if (hovered && (flags & ImGuiButtonFlags_AllowItemOverlap) && (g.HoveredIdPreviousFrame != id && g.HoveredIdPreviousFrame != 0))
hovered = false;
- // Mouse
+ // Mouse handling
+ const ImGuiID test_owner_id = (flags & ImGuiButtonFlags_NoTestKeyOwner) ? ImGuiKeyOwner_Any : id;
if (hovered)
{
if (!(flags & ImGuiButtonFlags_NoKeyModifiers) || (!g.IO.KeyCtrl && !g.IO.KeyShift && !g.IO.KeyAlt))
{
- if ((flags & ImGuiButtonFlags_PressedOnClickRelease) && g.IO.MouseClicked[0])
+ // Poll buttons
+ int mouse_button_clicked = -1;
+ if ((flags & ImGuiButtonFlags_MouseButtonLeft) && IsMouseClicked(0, test_owner_id)) { mouse_button_clicked = 0; }
+ else if ((flags & ImGuiButtonFlags_MouseButtonRight) && IsMouseClicked(1, test_owner_id)) { mouse_button_clicked = 1; }
+ else if ((flags & ImGuiButtonFlags_MouseButtonMiddle) && IsMouseClicked(2, test_owner_id)) { mouse_button_clicked = 2; }
+ if (mouse_button_clicked != -1 && g.ActiveId != id)
{
- SetActiveID(id, window);
- if (!(flags & ImGuiButtonFlags_NoNavFocus))
- SetFocusID(id, window);
- FocusWindow(window);
+ if (!(flags & ImGuiButtonFlags_NoSetKeyOwner))
+ SetKeyOwner(MouseButtonToKey(mouse_button_clicked), id);
+ if (flags & (ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnClickReleaseAnywhere))
+ {
+ SetActiveID(id, window);
+ g.ActiveIdMouseButton = mouse_button_clicked;
+ if (!(flags & ImGuiButtonFlags_NoNavFocus))
+ SetFocusID(id, window);
+ FocusWindow(window);
+ }
+ if ((flags & ImGuiButtonFlags_PressedOnClick) || ((flags & ImGuiButtonFlags_PressedOnDoubleClick) && g.IO.MouseClickedCount[mouse_button_clicked] == 2))
+ {
+ pressed = true;
+ if (flags & ImGuiButtonFlags_NoHoldingActiveId)
+ ClearActiveID();
+ else
+ SetActiveID(id, window); // Hold on ID
+ if (!(flags & ImGuiButtonFlags_NoNavFocus))
+ SetFocusID(id, window);
+ g.ActiveIdMouseButton = mouse_button_clicked;
+ FocusWindow(window);
+ }
}
- if (((flags & ImGuiButtonFlags_PressedOnClick) && g.IO.MouseClicked[0]) || ((flags & ImGuiButtonFlags_PressedOnDoubleClick) && g.IO.MouseDoubleClicked[0]))
+ if (flags & ImGuiButtonFlags_PressedOnRelease)
{
- pressed = true;
- if (flags & ImGuiButtonFlags_NoHoldingActiveID)
+ int mouse_button_released = -1;
+ if ((flags & ImGuiButtonFlags_MouseButtonLeft) && IsMouseReleased(0, test_owner_id)) { mouse_button_released = 0; }
+ else if ((flags & ImGuiButtonFlags_MouseButtonRight) && IsMouseReleased(1, test_owner_id)) { mouse_button_released = 1; }
+ else if ((flags & ImGuiButtonFlags_MouseButtonMiddle) && IsMouseReleased(2, test_owner_id)) { mouse_button_released = 2; }
+ if (mouse_button_released != -1)
+ {
+ const bool has_repeated_at_least_once = (flags & ImGuiButtonFlags_Repeat) && g.IO.MouseDownDurationPrev[mouse_button_released] >= g.IO.KeyRepeatDelay; // Repeat mode trumps on release behavior
+ if (!has_repeated_at_least_once)
+ pressed = true;
+ if (!(flags & ImGuiButtonFlags_NoNavFocus))
+ SetFocusID(id, window);
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 (g.ActiveId == id && (flags & ImGuiButtonFlags_Repeat))
+ if (g.IO.MouseDownDuration[g.ActiveIdMouseButton] > 0.0f && IsMouseClicked(g.ActiveIdMouseButton, test_owner_id, ImGuiInputFlags_Repeat))
+ pressed = true;
}
if (pressed)
@@ -543,45 +607,56 @@ bool ImGui::ButtonBehavior(const ImRect& bb, ImGuiID id, bool* out_hovered, bool
// 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))
- if (!(flags & ImGuiButtonFlags_NoHoveredOnNav))
+ if (!(flags & ImGuiButtonFlags_NoHoveredOnFocus))
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);
+ bool nav_activated_by_inputs = (g.NavActivatePressedId == id);
+ if (!nav_activated_by_inputs && (flags & ImGuiButtonFlags_Repeat))
+ {
+ // Avoid pressing both keys from triggering double amount of repeat events
+ const ImGuiKeyData* key1 = GetKeyData(ImGuiKey_Space);
+ const ImGuiKeyData* key2 = GetKeyData(ImGuiKey_NavGamepadActivate);
+ const float t1 = ImMax(key1->DownDuration, key2->DownDuration);
+ nav_activated_by_inputs = CalcTypematicRepeatAmount(t1 - g.IO.DeltaTime, t1, g.IO.KeyRepeatDelay, g.IO.KeyRepeatRate) > 0;
+ }
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
+ pressed = true;
SetActiveID(id, window);
- if ((nav_activated_by_code || nav_activated_by_inputs) && !(flags & ImGuiButtonFlags_NoNavFocus))
+ g.ActiveIdSource = ImGuiInputSource_Nav;
+ if (!(flags & ImGuiButtonFlags_NoNavFocus))
SetFocusID(id, window);
}
}
+ // Process while held
bool held = false;
if (g.ActiveId == id)
{
- if (pressed)
- g.ActiveIdHasBeenPressedBefore = true;
if (g.ActiveIdSource == ImGuiInputSource_Mouse)
{
if (g.ActiveIdIsJustActivated)
g.ActiveIdClickOffset = g.IO.MousePos - bb.Min;
- if (g.IO.MouseDown[0])
+
+ const int mouse_button = g.ActiveIdMouseButton;
+ if (IsMouseDown(mouse_button, test_owner_id))
{
held = true;
}
else
{
- if (hovered && (flags & ImGuiButtonFlags_PressedOnClickRelease) && !g.DragDropActive)
+ bool release_in = hovered && (flags & ImGuiButtonFlags_PressedOnClickRelease) != 0;
+ bool release_anywhere = (flags & ImGuiButtonFlags_PressedOnClickReleaseAnywhere) != 0;
+ if ((release_in || release_anywhere) && !g.DragDropActive)
{
- bool is_double_click_release = (flags & ImGuiButtonFlags_PressedOnDoubleClick) && g.IO.MouseDownWasDoubleClick[0];
- bool is_repeating_already = (flags & ImGuiButtonFlags_Repeat) && g.IO.MouseDownDurationPrev[0] >= g.IO.KeyRepeatDelay; // Repeat mode trumps <on release>
- if (!is_double_click_release && !is_repeating_already)
+ // Report as pressed when releasing the mouse (this is the most common path)
+ bool is_double_click_release = (flags & ImGuiButtonFlags_PressedOnDoubleClick) && g.IO.MouseReleased[mouse_button] && g.IO.MouseClickedLastCount[mouse_button] == 2;
+ bool is_repeating_already = (flags & ImGuiButtonFlags_Repeat) && g.IO.MouseDownDurationPrev[mouse_button] >= g.IO.KeyRepeatDelay; // Repeat mode trumps <on release>
+ bool is_button_avail_or_owned = TestKeyOwner(MouseButtonToKey(mouse_button), test_owner_id);
+ if (!is_double_click_release && !is_repeating_already && is_button_avail_or_owned)
pressed = true;
}
ClearActiveID();
@@ -591,9 +666,12 @@ bool ImGui::ButtonBehavior(const ImRect& bb, ImGuiID id, bool* out_hovered, bool
}
else if (g.ActiveIdSource == ImGuiInputSource_Nav)
{
+ // When activated using Nav, we hold on the ActiveID until activation button is released
if (g.NavActivateDownId != id)
ClearActiveID();
}
+ if (pressed)
+ g.ActiveIdHasBeenPressedBefore = true;
}
if (out_hovered) *out_hovered = hovered;
@@ -623,8 +701,9 @@ bool ImGui::ButtonEx(const char* label, const ImVec2& size_arg, ImGuiButtonFlags
if (!ItemAdd(bb, id))
return false;
- if (window->DC.ItemFlags & ImGuiItemFlags_ButtonRepeat)
+ if (g.LastItemData.InFlags & ImGuiItemFlags_ButtonRepeat)
flags |= ImGuiButtonFlags_Repeat;
+
bool hovered, held;
bool pressed = ButtonBehavior(bb, id, &hovered, &held, flags);
@@ -632,19 +711,22 @@ bool ImGui::ButtonEx(const char* label, const ImVec2& size_arg, ImGuiButtonFlags
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);
+
+ if (g.LogEnabled)
+ LogSetNextTextDecoration("[", "]");
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();
- IMGUI_TEST_ENGINE_ITEM_INFO(id, label, window->DC.LastItemStatusFlags);
+ IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
return pressed;
}
bool ImGui::Button(const char* label, const ImVec2& size_arg)
{
- return ButtonEx(label, size_arg, 0);
+ return ButtonEx(label, size_arg, ImGuiButtonFlags_None);
}
// Small buttons fits within text without additional vertical spacing.
@@ -660,8 +742,9 @@ bool ImGui::SmallButton(const char* label)
// 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)
+bool ImGui::InvisibleButton(const char* str_id, const ImVec2& size_arg, ImGuiButtonFlags flags)
{
+ ImGuiContext& g = *GImGui;
ImGuiWindow* window = GetCurrentWindow();
if (window->SkipItems)
return false;
@@ -677,18 +760,19 @@ bool ImGui::InvisibleButton(const char* str_id, const ImVec2& size_arg)
return false;
bool hovered, held;
- bool pressed = ButtonBehavior(bb, id, &hovered, &held);
+ bool pressed = ButtonBehavior(bb, id, &hovered, &held, flags);
+ IMGUI_TEST_ENGINE_ITEM_INFO(id, str_id, g.LastItemData.StatusFlags);
return pressed;
}
bool ImGui::ArrowButtonEx(const char* str_id, ImGuiDir dir, ImVec2 size, ImGuiButtonFlags flags)
{
+ ImGuiContext& g = *GImGui;
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();
@@ -696,7 +780,7 @@ bool ImGui::ArrowButtonEx(const char* str_id, ImGuiDir dir, ImVec2 size, ImGuiBu
if (!ItemAdd(bb, id))
return false;
- if (window->DC.ItemFlags & ImGuiItemFlags_ButtonRepeat)
+ if (g.LastItemData.InFlags & ImGuiItemFlags_ButtonRepeat)
flags |= ImGuiButtonFlags_Repeat;
bool hovered, held;
@@ -709,32 +793,41 @@ bool ImGui::ArrowButtonEx(const char* str_id, ImGuiDir dir, ImVec2 size, ImGuiBu
RenderFrame(bb.Min, bb.Max, bg_col, true, g.Style.FrameRounding);
RenderArrow(window->DrawList, bb.Min + ImVec2(ImMax(0.0f, (size.x - g.FontSize) * 0.5f), ImMax(0.0f, (size.y - g.FontSize) * 0.5f)), text_col, dir);
+ IMGUI_TEST_ENGINE_ITEM_INFO(id, str_id, g.LastItemData.StatusFlags);
return pressed;
}
bool ImGui::ArrowButton(const char* str_id, ImGuiDir dir)
{
float sz = GetFrameHeight();
- return ArrowButtonEx(str_id, dir, ImVec2(sz, sz), 0);
+ return ArrowButtonEx(str_id, dir, ImVec2(sz, sz), ImGuiButtonFlags_None);
}
// Button to close a window
-bool ImGui::CloseButton(ImGuiID id, const ImVec2& pos)//, float size)
+bool ImGui::CloseButton(ImGuiID id, const ImVec2& pos)
{
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).
+ // Tweak 1: Shrink hit-testing area if button covers an abnormally large proportion of the visible region. That's in order to facilitate moving the window away. (#3825)
+ // This may better be applied as a general hit-rect reduction mechanism for all widgets to ensure the area to move window is always accessible?
const ImRect bb(pos, pos + ImVec2(g.FontSize, g.FontSize) + g.Style.FramePadding * 2.0f);
- bool is_clipped = !ItemAdd(bb, id);
+ ImRect bb_interact = bb;
+ const float area_to_visible_ratio = window->OuterRectClipped.GetArea() / bb.GetArea();
+ if (area_to_visible_ratio < 1.5f)
+ bb_interact.Expand(ImFloor(bb_interact.GetSize() * -0.25f));
+
+ // Tweak 2: We intentionally allow interaction when clipped so that a mechanical Alt,Right,Activate sequence can always 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).
+ bool is_clipped = !ItemAdd(bb_interact, id);
bool hovered, held;
- bool pressed = ButtonBehavior(bb, id, &hovered, &held);
+ bool pressed = ButtonBehavior(bb_interact, id, &hovered, &held);
if (is_clipped)
return pressed;
// Render
+ // FIXME: Clarify this mess
ImU32 col = GetColorU32(held ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered);
ImVec2 center = bb.GetCenter();
if (hovered)
@@ -743,8 +836,8 @@ bool ImGui::CloseButton(ImGuiID id, const ImVec2& pos)//, float size)
float cross_extent = g.FontSize * 0.5f * 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);
+ 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;
}
@@ -762,21 +855,63 @@ bool ImGui::CollapseButton(ImGuiID id, const ImVec2& pos)
// Render
ImU32 bg_col = GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
ImU32 text_col = GetColorU32(ImGuiCol_Text);
- ImVec2 center = bb.GetCenter();
if (hovered || held)
- window->DrawList->AddCircleFilled(center/*+ ImVec2(0.0f, -0.5f)*/, g.FontSize * 0.5f + 1.0f, bg_col, 12);
+ window->DrawList->AddCircleFilled(bb.GetCenter()/*+ ImVec2(0.0f, -0.5f)*/, g.FontSize * 0.5f + 1.0f, bg_col, 12);
RenderArrow(window->DrawList, bb.Min + g.Style.FramePadding, text_col, 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())
+ if (IsItemActive() && IsMouseDragging(0))
StartMouseMovingWindow(window);
return pressed;
}
-ImGuiID ImGui::GetScrollbarID(ImGuiWindow* window, ImGuiAxis axis)
+ImGuiID ImGui::GetWindowScrollbarID(ImGuiWindow* window, ImGuiAxis axis)
{
- return window->GetIDNoKeepAlive(axis == ImGuiAxis_X ? "#SCROLLX" : "#SCROLLY");
+ return window->GetID(axis == ImGuiAxis_X ? "#SCROLLX" : "#SCROLLY");
+}
+
+// Return scrollbar rectangle, must only be called for corresponding axis if window->ScrollbarX/Y is set.
+ImRect ImGui::GetWindowScrollbarRect(ImGuiWindow* window, ImGuiAxis axis)
+{
+ const ImRect outer_rect = window->Rect();
+ const ImRect inner_rect = window->InnerRect;
+ const float border_size = window->WindowBorderSize;
+ const float scrollbar_size = window->ScrollbarSizes[axis ^ 1]; // (ScrollbarSizes.x = width of Y scrollbar; ScrollbarSizes.y = height of X scrollbar)
+ IM_ASSERT(scrollbar_size > 0.0f);
+ if (axis == ImGuiAxis_X)
+ return ImRect(inner_rect.Min.x, ImMax(outer_rect.Min.y, outer_rect.Max.y - border_size - scrollbar_size), inner_rect.Max.x, outer_rect.Max.y);
+ else
+ return ImRect(ImMax(outer_rect.Min.x, outer_rect.Max.x - border_size - scrollbar_size), inner_rect.Min.y, outer_rect.Max.x, inner_rect.Max.y);
+}
+
+void ImGui::Scrollbar(ImGuiAxis axis)
+{
+ ImGuiContext& g = *GImGui;
+ ImGuiWindow* window = g.CurrentWindow;
+ const ImGuiID id = GetWindowScrollbarID(window, axis);
+
+ // Calculate scrollbar bounding box
+ ImRect bb = GetWindowScrollbarRect(window, axis);
+ ImDrawFlags rounding_corners = ImDrawFlags_RoundCornersNone;
+ if (axis == ImGuiAxis_X)
+ {
+ rounding_corners |= ImDrawFlags_RoundCornersBottomLeft;
+ if (!window->ScrollbarY)
+ rounding_corners |= ImDrawFlags_RoundCornersBottomRight;
+ }
+ else
+ {
+ if ((window->Flags & ImGuiWindowFlags_NoTitleBar) && !(window->Flags & ImGuiWindowFlags_MenuBar))
+ rounding_corners |= ImDrawFlags_RoundCornersTopRight;
+ if (!window->ScrollbarX)
+ rounding_corners |= ImDrawFlags_RoundCornersBottomRight;
+ }
+ float size_avail = window->InnerRect.Max[axis] - window->InnerRect.Min[axis];
+ float size_contents = window->ContentSize[axis] + window->WindowPadding[axis] * 2.0f;
+ ImS64 scroll = (ImS64)window->Scroll[axis];
+ ScrollbarEx(bb, id, axis, &scroll, (ImS64)size_avail, (ImS64)size_contents, rounding_corners);
+ window->Scroll[axis] = (float)scroll;
}
// Vertical/Horizontal scrollbar
@@ -785,7 +920,7 @@ ImGuiID ImGui::GetScrollbarID(ImGuiWindow* window, ImGuiAxis axis)
// - We store values as normalized ratio and in a form that allows the window content to change while we are holding on a scrollbar
// - We handle both horizontal and vertical scrollbars, which makes the terminology not ideal.
// Still, the code should probably be made simpler..
-bool ImGui::ScrollbarEx(const ImRect& bb_frame, ImGuiID id, ImGuiAxis axis, float* p_scroll_v, float size_avail_v, float size_contents_v, ImDrawCornerFlags rounding_corners)
+bool ImGui::ScrollbarEx(const ImRect& bb_frame, ImGuiID id, ImGuiAxis axis, ImS64* p_scroll_v, ImS64 size_avail_v, ImS64 size_contents_v, ImDrawFlags flags)
{
ImGuiContext& g = *GImGui;
ImGuiWindow* window = g.CurrentWindow;
@@ -797,7 +932,7 @@ bool ImGui::ScrollbarEx(const ImRect& bb_frame, ImGuiID id, ImGuiAxis axis, floa
if (bb_frame_width <= 0.0f || bb_frame_height <= 0.0f)
return false;
- // When we are too small, start hiding and disabling the grab (this reduce visual noise on very small window and facilitate using the resize grab)
+ // When we are too small, start hiding and disabling the grab (this reduce visual noise on very small window and facilitate using the window resize grab)
float alpha = 1.0f;
if ((axis == ImGuiAxis_Y) && bb_frame_height < g.FontSize + g.Style.FramePadding.y * 2.0f)
alpha = ImSaturate((bb_frame_height - g.FontSize) / (g.Style.FramePadding.y * 2.0f));
@@ -806,33 +941,33 @@ bool ImGui::ScrollbarEx(const ImRect& bb_frame, ImGuiID id, ImGuiAxis axis, floa
const ImGuiStyle& style = g.Style;
const bool allow_interaction = (alpha >= 1.0f);
- const bool horizontal = (axis == ImGuiAxis_X);
ImRect bb = bb_frame;
bb.Expand(ImVec2(-ImClamp(IM_FLOOR((bb_frame_width - 2.0f) * 0.5f), 0.0f, 3.0f), -ImClamp(IM_FLOOR((bb_frame_height - 2.0f) * 0.5f), 0.0f, 3.0f)));
// V denote the main, longer axis of the scrollbar (= height for a vertical scrollbar)
- const float scrollbar_size_v = horizontal ? bb.GetWidth() : bb.GetHeight();
+ const float scrollbar_size_v = (axis == ImGuiAxis_X) ? bb.GetWidth() : bb.GetHeight();
// Calculate the height of our grabbable box. It generally represent the amount visible (vs the total scrollable amount)
// But we maintain a minimum size in pixel to allow for the user to still aim inside.
IM_ASSERT(ImMax(size_contents_v, size_avail_v) > 0.0f); // Adding this assert to check if the ImMax(XXX,1.0f) is still needed. PLEASE CONTACT ME if this triggers.
- const float win_size_v = ImMax(ImMax(size_contents_v, size_avail_v), 1.0f);
- const float grab_h_pixels = ImClamp(scrollbar_size_v * (size_avail_v / win_size_v), style.GrabMinSize, scrollbar_size_v);
+ const ImS64 win_size_v = ImMax(ImMax(size_contents_v, size_avail_v), (ImS64)1);
+ const float grab_h_pixels = ImClamp(scrollbar_size_v * ((float)size_avail_v / (float)win_size_v), style.GrabMinSize, scrollbar_size_v);
const float grab_h_norm = grab_h_pixels / scrollbar_size_v;
// Handle input right away. None of the code of Begin() is relying on scrolling position before calling Scrollbar().
bool held = false;
bool hovered = false;
+ ItemAdd(bb_frame, id, NULL, ImGuiItemFlags_NoNav);
ButtonBehavior(bb, id, &hovered, &held, ImGuiButtonFlags_NoNavFocus);
- float scroll_max = ImMax(1.0f, size_contents_v - size_avail_v);
- float scroll_ratio = ImSaturate(*p_scroll_v / scroll_max);
- float grab_v_norm = scroll_ratio * (scrollbar_size_v - grab_h_pixels) / scrollbar_size_v;
+ const ImS64 scroll_max = ImMax((ImS64)1, size_contents_v - size_avail_v);
+ float scroll_ratio = ImSaturate((float)*p_scroll_v / (float)scroll_max);
+ float grab_v_norm = scroll_ratio * (scrollbar_size_v - grab_h_pixels) / scrollbar_size_v; // Grab position in normalized space
if (held && allow_interaction && grab_h_norm < 1.0f)
{
- float scrollbar_pos_v = horizontal ? bb.Min.x : bb.Min.y;
- float mouse_pos_v = horizontal ? g.IO.MousePos.x : g.IO.MousePos.y;
+ const float scrollbar_pos_v = bb.Min[axis];
+ const float mouse_pos_v = g.IO.MousePos[axis];
// Click position in scrollbar normalized space (0.0f->1.0f)
const float clicked_v_norm = ImSaturate((mouse_pos_v - scrollbar_pos_v) / scrollbar_size_v);
@@ -849,13 +984,13 @@ bool ImGui::ScrollbarEx(const ImRect& bb_frame, ImGuiID id, ImGuiAxis axis, floa
g.ScrollbarClickDeltaToGrabCenter = clicked_v_norm - grab_v_norm - grab_h_norm * 0.5f;
}
- // Apply scroll
+ // Apply scroll (p_scroll_v will generally point on one member of window->Scroll)
// It is ok to modify Scroll here because we are being called in Begin() after the calculation of ContentSize and before setting up our starting position
const float scroll_v_norm = ImSaturate((clicked_v_norm - g.ScrollbarClickDeltaToGrabCenter - grab_h_norm * 0.5f) / (1.0f - grab_h_norm));
- *p_scroll_v = IM_ROUND(scroll_v_norm * scroll_max);//(win_size_contents_v - win_size_v));
+ *p_scroll_v = (ImS64)(scroll_v_norm * scroll_max);
// Update values for rendering
- scroll_ratio = ImSaturate(*p_scroll_v / scroll_max);
+ scroll_ratio = ImSaturate((float)*p_scroll_v / (float)scroll_max);
grab_v_norm = scroll_ratio * (scrollbar_size_v - grab_h_pixels) / scrollbar_size_v;
// Update distance to grab now that we have seeked and saturated
@@ -864,10 +999,11 @@ bool ImGui::ScrollbarEx(const ImRect& bb_frame, ImGuiID id, ImGuiAxis axis, floa
}
// Render
- window->DrawList->AddRectFilled(bb_frame.Min, bb_frame.Max, GetColorU32(ImGuiCol_ScrollbarBg), window->WindowRounding, rounding_corners);
+ const ImU32 bg_col = GetColorU32(ImGuiCol_ScrollbarBg);
const ImU32 grab_col = GetColorU32(held ? ImGuiCol_ScrollbarGrabActive : hovered ? ImGuiCol_ScrollbarGrabHovered : ImGuiCol_ScrollbarGrab, alpha);
+ window->DrawList->AddRectFilled(bb_frame.Min, bb_frame.Max, bg_col, window->WindowRounding, flags);
ImRect grab_rect;
- if (horizontal)
+ if (axis == ImGuiAxis_X)
grab_rect = ImRect(ImLerp(bb.Min.x, bb.Max.x, grab_v_norm), bb.Min.y, ImLerp(bb.Min.x, bb.Max.x, grab_v_norm) + grab_h_pixels, bb.Max.y);
else
grab_rect = ImRect(bb.Min.x, ImLerp(bb.Min.y, bb.Max.y, grab_v_norm), bb.Max.x, ImLerp(bb.Min.y, bb.Max.y, grab_v_norm) + grab_h_pixels);
@@ -876,38 +1012,6 @@ bool ImGui::ScrollbarEx(const ImRect& bb_frame, ImGuiID id, ImGuiAxis axis, floa
return held;
}
-void ImGui::Scrollbar(ImGuiAxis axis)
-{
- ImGuiContext& g = *GImGui;
- ImGuiWindow* window = g.CurrentWindow;
-
- const ImGuiID id = GetScrollbarID(window, axis);
- KeepAliveID(id);
-
- // Calculate scrollbar bounding box
- const ImRect outer_rect = window->Rect();
- const ImRect inner_rect = window->InnerRect;
- const float border_size = window->WindowBorderSize;
- const float scrollbar_size = window->ScrollbarSizes[axis ^ 1];
- IM_ASSERT(scrollbar_size > 0.0f);
- const float other_scrollbar_size = window->ScrollbarSizes[axis];
- ImDrawCornerFlags rounding_corners = (other_scrollbar_size <= 0.0f) ? ImDrawCornerFlags_BotRight : 0;
- ImRect bb;
- if (axis == ImGuiAxis_X)
- {
- bb.Min = ImVec2(inner_rect.Min.x, ImMax(outer_rect.Min.y, outer_rect.Max.y - border_size - scrollbar_size));
- bb.Max = ImVec2(inner_rect.Max.x, outer_rect.Max.y);
- rounding_corners |= ImDrawCornerFlags_BotLeft;
- }
- else
- {
- bb.Min = ImVec2(ImMax(outer_rect.Min.x, outer_rect.Max.x - border_size - scrollbar_size), inner_rect.Min.y);
- bb.Max = ImVec2(outer_rect.Max.x, window->InnerRect.Max.y);
- rounding_corners |= ((window->Flags & ImGuiWindowFlags_NoTitleBar) && !(window->Flags & ImGuiWindowFlags_MenuBar)) ? ImDrawCornerFlags_TopRight : 0;
- }
- ScrollbarEx(bb, id, axis, &window->Scroll[axis], inner_rect.Max[axis] - inner_rect.Min[axis], window->ContentSize[axis] + window->WindowPadding[axis] * 2.0f, rounding_corners);
-}
-
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();
@@ -932,28 +1036,17 @@ void ImGui::Image(ImTextureID user_texture_id, const ImVec2& size, const ImVec2&
}
}
-// 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)
+// ImageButton() is flawed as 'id' is always derived from 'texture_id' (see #2464 #1390)
+// We provide this internal helper to write your own variant while we figure out how to redesign the public ImageButton() API.
+bool ImGui::ImageButtonEx(ImGuiID id, ImTextureID texture_id, const ImVec2& size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& bg_col, const ImVec4& tint_col)
{
+ ImGuiContext& g = *GImGui;
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*)(intptr_t)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);
+ const ImVec2 padding = g.Style.FramePadding;
+ const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size + padding * 2.0f);
ItemSize(bb);
if (!ItemAdd(bb, id))
return false;
@@ -964,14 +1057,50 @@ bool ImGui::ImageButton(ImTextureID user_texture_id, const ImVec2& size, const I
// 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));
+ RenderFrame(bb.Min, bb.Max, col, true, ImClamp((float)ImMin(padding.x, padding.y), 0.0f, g.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));
+ window->DrawList->AddRectFilled(bb.Min + padding, bb.Max - padding, GetColorU32(bg_col));
+ window->DrawList->AddImage(texture_id, bb.Min + padding, bb.Max - padding, uv0, uv1, GetColorU32(tint_col));
return pressed;
}
+bool ImGui::ImageButton(const char* str_id, ImTextureID user_texture_id, const ImVec2& size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& bg_col, const ImVec4& tint_col)
+{
+ ImGuiContext& g = *GImGui;
+ ImGuiWindow* window = g.CurrentWindow;
+ if (window->SkipItems)
+ return false;
+
+ return ImageButtonEx(window->GetID(str_id), user_texture_id, size, uv0, uv1, bg_col, tint_col);
+}
+
+#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
+// Legacy API obsoleted in 1.89. Two differences with new ImageButton()
+// - new ImageButton() requires an explicit 'const char* str_id' Old ImageButton() used opaque imTextureId (created issue with: multiple buttons with same image, transient texture id values, opaque computation of ID)
+// - new ImageButton() always use style.FramePadding Old ImageButton() had an override argument.
+// If you need to change padding with new ImageButton() you can use PushStyleVar(ImGuiStyleVar_FramePadding, value), consistent with other Button functions.
+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)
+{
+ ImGuiContext& g = *GImGui;
+ ImGuiWindow* window = g.CurrentWindow;
+ if (window->SkipItems)
+ return false;
+
+ // Default to using texture ID as ID. User can still push string/integer prefixes.
+ PushID((void*)(intptr_t)user_texture_id);
+ const ImGuiID id = window->GetID("#image");
+ PopID();
+
+ if (frame_padding >= 0)
+ PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2((float)frame_padding, (float)frame_padding));
+ bool ret = ImageButtonEx(id, user_texture_id, size, uv0, uv1, bg_col, tint_col);
+ if (frame_padding >= 0)
+ PopStyleVar();
+ return ret;
+}
+#endif // #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
+
bool ImGui::Checkbox(const char* label, bool* v)
{
ImGuiWindow* window = GetCurrentWindow();
@@ -988,7 +1117,10 @@ bool ImGui::Checkbox(const char* label, bool* v)
const ImRect total_bb(pos, pos + ImVec2(square_sz + (label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f), label_size.y + style.FramePadding.y * 2.0f));
ItemSize(total_bb, style.FramePadding.y);
if (!ItemAdd(total_bb, id))
+ {
+ IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (*v ? ImGuiItemStatusFlags_Checked : 0));
return false;
+ }
bool hovered, held;
bool pressed = ButtonBehavior(total_bb, id, &hovered, &held);
@@ -1002,42 +1134,79 @@ bool ImGui::Checkbox(const char* label, bool* v)
RenderNavHighlight(total_bb, id);
RenderFrame(check_bb.Min, check_bb.Max, GetColorU32((held && hovered) ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg), true, style.FrameRounding);
ImU32 check_col = GetColorU32(ImGuiCol_CheckMark);
- if (window->DC.ItemFlags & ImGuiItemFlags_MixedValue)
+ bool mixed_value = (g.LastItemData.InFlags & ImGuiItemFlags_MixedValue) != 0;
+ if (mixed_value)
{
// Undocumented tristate/mixed/indeterminate checkbox (#2644)
+ // This may seem awkwardly designed because the aim is to make ImGuiItemFlags_MixedValue supported by all widgets (not just checkbox)
ImVec2 pad(ImMax(1.0f, IM_FLOOR(square_sz / 3.6f)), ImMax(1.0f, IM_FLOOR(square_sz / 3.6f)));
window->DrawList->AddRectFilled(check_bb.Min + pad, check_bb.Max - pad, check_col, style.FrameRounding);
}
else if (*v)
{
const float pad = ImMax(1.0f, IM_FLOOR(square_sz / 6.0f));
- RenderCheckMark(check_bb.Min + ImVec2(pad, pad), check_col, square_sz - pad*2.0f);
+ RenderCheckMark(window->DrawList, check_bb.Min + ImVec2(pad, pad), check_col, square_sz - pad * 2.0f);
}
+ ImVec2 label_pos = ImVec2(check_bb.Max.x + style.ItemInnerSpacing.x, check_bb.Min.y + style.FramePadding.y);
if (g.LogEnabled)
- LogRenderedText(&total_bb.Min, *v ? "[x]" : "[ ]");
+ LogRenderedText(&label_pos, mixed_value ? "[~]" : *v ? "[x]" : "[ ]");
if (label_size.x > 0.0f)
- RenderText(ImVec2(check_bb.Max.x + style.ItemInnerSpacing.x, check_bb.Min.y + style.FramePadding.y), label);
+ RenderText(label_pos, label);
- IMGUI_TEST_ENGINE_ITEM_INFO(id, label, window->DC.ItemFlags | ImGuiItemStatusFlags_Checkable | (*v ? ImGuiItemStatusFlags_Checked : 0));
+ IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (*v ? ImGuiItemStatusFlags_Checked : 0));
return pressed;
}
-bool ImGui::CheckboxFlags(const char* label, unsigned int* flags, unsigned int flags_value)
+template<typename T>
+bool ImGui::CheckboxFlagsT(const char* label, T* flags, T flags_value)
{
- bool v = ((*flags & flags_value) == flags_value);
- bool pressed = Checkbox(label, &v);
+ bool all_on = (*flags & flags_value) == flags_value;
+ bool any_on = (*flags & flags_value) != 0;
+ bool pressed;
+ if (!all_on && any_on)
+ {
+ ImGuiContext& g = *GImGui;
+ ImGuiItemFlags backup_item_flags = g.CurrentItemFlags;
+ g.CurrentItemFlags |= ImGuiItemFlags_MixedValue;
+ pressed = Checkbox(label, &all_on);
+ g.CurrentItemFlags = backup_item_flags;
+ }
+ else
+ {
+ pressed = Checkbox(label, &all_on);
+
+ }
if (pressed)
{
- if (v)
+ if (all_on)
*flags |= flags_value;
else
*flags &= ~flags_value;
}
-
return pressed;
}
+bool ImGui::CheckboxFlags(const char* label, int* flags, int flags_value)
+{
+ return CheckboxFlagsT(label, flags, flags_value);
+}
+
+bool ImGui::CheckboxFlags(const char* label, unsigned int* flags, unsigned int flags_value)
+{
+ return CheckboxFlagsT(label, flags, flags_value);
+}
+
+bool ImGui::CheckboxFlags(const char* label, ImS64* flags, ImS64 flags_value)
+{
+ return CheckboxFlagsT(label, flags, flags_value);
+}
+
+bool ImGui::CheckboxFlags(const char* label, ImU64* flags, ImU64 flags_value)
+{
+ return CheckboxFlagsT(label, flags, flags_value);
+}
+
bool ImGui::RadioButton(const char* label, bool active)
{
ImGuiWindow* window = GetCurrentWindow();
@@ -1077,15 +1246,17 @@ bool ImGui::RadioButton(const char* label, bool active)
if (style.FrameBorderSize > 0.0f)
{
- window->DrawList->AddCircle(center + ImVec2(1,1), radius, GetColorU32(ImGuiCol_BorderShadow), 16, style.FrameBorderSize);
+ 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);
}
+ ImVec2 label_pos = ImVec2(check_bb.Max.x + style.ItemInnerSpacing.x, check_bb.Min.y + style.FramePadding.y);
if (g.LogEnabled)
- LogRenderedText(&total_bb.Min, active ? "(x)" : "( )");
+ LogRenderedText(&label_pos, active ? "(x)" : "( )");
if (label_size.x > 0.0f)
- RenderText(ImVec2(check_bb.Max.x + style.ItemInnerSpacing.x, check_bb.Min.y + style.FramePadding.y), label);
+ RenderText(label_pos, label);
+ IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
return pressed;
}
@@ -1109,7 +1280,7 @@ void ImGui::ProgressBar(float fraction, const ImVec2& size_arg, const char* over
const ImGuiStyle& style = g.Style;
ImVec2 pos = window->DC.CursorPos;
- ImVec2 size = CalcItemSize(size_arg, CalcItemWidth(), g.FontSize + style.FramePadding.y*2.0f);
+ ImVec2 size = CalcItemSize(size_arg, CalcItemWidth(), g.FontSize + style.FramePadding.y * 2.0f);
ImRect bb(pos, pos + size);
ItemSize(size, style.FramePadding.y);
if (!ItemAdd(bb, 0))
@@ -1126,13 +1297,13 @@ void ImGui::ProgressBar(float fraction, const ImVec2& size_arg, const char* over
char overlay_buf[32];
if (!overlay)
{
- ImFormatString(overlay_buf, IM_ARRAYSIZE(overlay_buf), "%.0f%%", fraction*100+0.01f);
+ 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);
+ 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()
@@ -1143,18 +1314,18 @@ void ImGui::Bullet()
ImGuiContext& g = *GImGui;
const ImGuiStyle& style = g.Style;
- const float line_height = ImMax(ImMin(window->DC.CurrLineSize.y, g.FontSize + g.Style.FramePadding.y*2), g.FontSize);
+ const float line_height = ImMax(ImMin(window->DC.CurrLineSize.y, g.FontSize + 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);
+ SameLine(0, style.FramePadding.x * 2);
return;
}
// Render and stay on same line
ImU32 text_col = GetColorU32(ImGuiCol_Text);
- RenderBullet(window->DrawList, bb.Min + ImVec2(style.FramePadding.x + g.FontSize*0.5f, line_height*0.5f), text_col);
+ RenderBullet(window->DrawList, bb.Min + ImVec2(style.FramePadding.x + g.FontSize * 0.5f, line_height * 0.5f), text_col);
SameLine(0, style.FramePadding.x * 2.0f);
}
@@ -1176,7 +1347,7 @@ void ImGui::Spacing()
ImGuiWindow* window = GetCurrentWindow();
if (window->SkipItems)
return;
- ItemSize(ImVec2(0,0));
+ ItemSize(ImVec2(0, 0));
}
void ImGui::Dummy(const ImVec2& size)
@@ -1199,8 +1370,9 @@ void ImGui::NewLine()
ImGuiContext& g = *GImGui;
const ImGuiLayoutType backup_layout_type = window->DC.LayoutType;
window->DC.LayoutType = ImGuiLayoutType_Vertical;
+ window->DC.IsSameLine = false;
if (window->DC.CurrLineSize.y > 0.0f) // In the event that we are on a line with items that is smaller that FontSize high, we will preserve its height.
- ItemSize(ImVec2(0,0));
+ ItemSize(ImVec2(0, 0));
else
ItemSize(ImVec2(0.0f, g.FontSize));
window->DC.LayoutType = backup_layout_type;
@@ -1249,31 +1421,36 @@ void ImGui::SeparatorEx(ImGuiSeparatorFlags flags)
// Horizontal Separator
float x1 = window->Pos.x;
float x2 = window->Pos.x + window->Size.x;
- if (!window->DC.GroupStack.empty())
+
+ // FIXME-WORKRECT: old hack (#205) until we decide of consistent behavior with WorkRect/Indent and Separator
+ if (g.GroupStack.Size > 0 && g.GroupStack.back().WindowID == window->ID)
x1 += window->DC.Indent.x;
- ImGuiColumns* columns = (flags & ImGuiSeparatorFlags_SpanAllColumns) ? window->DC.CurrentColumns : NULL;
+ // FIXME-WORKRECT: In theory we should simply be using WorkRect.Min.x/Max.x everywhere but it isn't aesthetically what we want,
+ // need to introduce a variant of WorkRect for that purpose. (#4787)
+ if (ImGuiTable* table = g.CurrentTable)
+ {
+ x1 = table->Columns[table->CurrentColumn].MinX;
+ x2 = table->Columns[table->CurrentColumn].MaxX;
+ }
+
+ ImGuiOldColumns* columns = (flags & ImGuiSeparatorFlags_SpanAllColumns) ? window->DC.CurrentColumns : NULL;
if (columns)
PushColumnsBackground();
// We don't provide our width to the layout so that it doesn't get feed back into AutoFit
+ // FIXME: This prevents ->CursorMaxPos based bounding box evaluation from working (e.g. TableEndCell)
const ImRect bb(ImVec2(x1, window->DC.CursorPos.y), ImVec2(x2, window->DC.CursorPos.y + thickness_draw));
ItemSize(ImVec2(0.0f, thickness_layout));
- if (!ItemAdd(bb, 0))
+ const bool item_visible = ItemAdd(bb, 0);
+ if (item_visible)
{
- if (columns)
- {
- PopColumnsBackground();
- columns->LineMinY = window->DC.CursorPos.y;
- }
- return;
- }
-
- // Draw
- window->DrawList->AddLine(bb.Min, ImVec2(bb.Max.x, bb.Min.y), GetColorU32(ImGuiCol_Separator));
- if (g.LogEnabled)
- LogRenderedText(&bb.Min, "--------------------------------");
+ // Draw
+ window->DrawList->AddLine(bb.Min, ImVec2(bb.Max.x, bb.Min.y), GetColorU32(ImGuiCol_Separator));
+ if (g.LogEnabled)
+ LogRenderedText(&bb.Min, "--------------------------------\n");
+ }
if (columns)
{
PopColumnsBackground();
@@ -1291,7 +1468,7 @@ void ImGui::Separator()
// Those flags should eventually be overridable by the user
ImGuiSeparatorFlags flags = (window->DC.LayoutType == ImGuiLayoutType_Horizontal) ? ImGuiSeparatorFlags_Vertical : ImGuiSeparatorFlags_Horizontal;
- flags |= ImGuiSeparatorFlags_SpanAllColumns;
+ flags |= ImGuiSeparatorFlags_SpanAllColumns; // NB: this only applies to legacy Columns() api as they relied on Separator() a lot.
SeparatorEx(flags);
}
@@ -1301,21 +1478,19 @@ bool ImGui::SplitterBehavior(const ImRect& bb, ImGuiID id, ImGuiAxis axis, float
ImGuiContext& g = *GImGui;
ImGuiWindow* window = g.CurrentWindow;
- const ImGuiItemFlags item_flags_backup = window->DC.ItemFlags;
- window->DC.ItemFlags |= ImGuiItemFlags_NoNav | ImGuiItemFlags_NoNavDefaultFocus;
- bool item_add = ItemAdd(bb, id);
- window->DC.ItemFlags = item_flags_backup;
- if (!item_add)
+ if (!ItemAdd(bb, id, NULL, ImGuiItemFlags_NoNav))
return false;
bool hovered, held;
ImRect bb_interact = bb;
bb_interact.Expand(axis == ImGuiAxis_Y ? ImVec2(0.0f, hover_extend) : ImVec2(hover_extend, 0.0f));
ButtonBehavior(bb_interact, id, &hovered, &held, ImGuiButtonFlags_FlattenChildren | ImGuiButtonFlags_AllowItemOverlap);
+ if (hovered)
+ g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredRect; // for IsItemHovered(), because bb_interact is larger than bb
if (g.ActiveId != id)
SetItemAllowOverlap();
- if (held || (g.HoveredId == id && g.HoveredIdPreviousFrame == id && g.HoveredIdTimer >= hover_visibility_delay))
+ if (held || (hovered && g.HoveredIdPreviousFrame == id && g.HoveredIdTimer >= hover_visibility_delay))
SetMouseCursor(axis == ImGuiAxis_Y ? ImGuiMouseCursor_ResizeNS : ImGuiMouseCursor_ResizeEW);
ImRect bb_render = bb;
@@ -1363,11 +1538,13 @@ static int IMGUI_CDECL ShrinkWidthItemComparer(const void* lhs, const void* rhs)
}
// Shrink excess width from a set of item, by removing width from the larger items first.
+// Set items Width to -1.0f to disable shrinking this item.
void ImGui::ShrinkWidths(ImGuiShrinkWidthItem* items, int count, float width_excess)
{
if (count == 1)
{
- items[0].Width = ImMax(items[0].Width - width_excess, 1.0f);
+ if (items[0].Width >= 0.0f)
+ items[0].Width = ImMax(items[0].Width - width_excess, 1.0f);
return;
}
ImQsort(items, (size_t)count, sizeof(ImGuiShrinkWidthItem), ShrinkWidthItemComparer);
@@ -1376,14 +1553,16 @@ void ImGui::ShrinkWidths(ImGuiShrinkWidthItem* items, int count, float width_exc
{
while (count_same_width < count && items[0].Width <= items[count_same_width].Width)
count_same_width++;
- float max_width_to_remove_per_item = (count_same_width < count) ? (items[0].Width - items[count_same_width].Width) : (items[0].Width - 1.0f);
+ float max_width_to_remove_per_item = (count_same_width < count && items[count_same_width].Width >= 0.0f) ? (items[0].Width - items[count_same_width].Width) : (items[0].Width - 1.0f);
+ if (max_width_to_remove_per_item <= 0.0f)
+ break;
float width_to_remove_per_item = ImMin(width_excess / count_same_width, max_width_to_remove_per_item);
for (int item_n = 0; item_n < count_same_width; item_n++)
items[item_n].Width -= width_to_remove_per_item;
width_excess -= width_to_remove_per_item * count_same_width;
}
- // Round width and redistribute remainder left-to-right (could make it an option of the function?)
+ // Round width and redistribute remainder
// Ensure that e.g. the right-most tab of a shrunk tab-bar always reaches exactly at the same distance from the right-most edge of the tab bar separator.
width_excess = 0.0f;
for (int n = 0; n < count; n++)
@@ -1392,17 +1571,24 @@ void ImGui::ShrinkWidths(ImGuiShrinkWidthItem* items, int count, float width_exc
width_excess += items[n].Width - width_rounded;
items[n].Width = width_rounded;
}
- if (width_excess > 0.0f)
- for (int n = 0; n < count; n++)
- if (items[n].Index < (int)(width_excess + 0.01f))
- items[n].Width += 1.0f;
+ while (width_excess > 0.0f)
+ for (int n = 0; n < count && width_excess > 0.0f; n++)
+ {
+ float width_to_add = ImMin(items[n].InitialWidth - items[n].Width, 1.0f);
+ items[n].Width += width_to_add;
+ width_excess -= width_to_add;
+ }
}
//-------------------------------------------------------------------------
// [SECTION] Widgets: ComboBox
//-------------------------------------------------------------------------
+// - CalcMaxPopupHeightFromItemCount() [Internal]
// - BeginCombo()
+// - BeginComboPopup() [Internal]
// - EndCombo()
+// - BeginComboPreview() [Internal]
+// - EndComboPreview() [Internal]
// - Combo()
//-------------------------------------------------------------------------
@@ -1416,74 +1602,99 @@ static float CalcMaxPopupHeightFromItemCount(int items_count)
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;
- bool has_window_size_constraint = (g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasSizeConstraint) != 0;
- g.NextWindowData.Flags &= ~ImGuiNextWindowDataFlags_HasSizeConstraint;
-
ImGuiWindow* window = GetCurrentWindow();
+
+ ImGuiNextWindowDataFlags backup_next_window_data_flags = g.NextWindowData.Flags;
+ g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values
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);
+ IM_ASSERT((flags & (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)) != (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)); // Can't use both flags together
const float arrow_size = (flags & ImGuiComboFlags_NoArrowButton) ? 0.0f : GetFrameHeight();
const ImVec2 label_size = CalcTextSize(label, NULL, true);
- const float expected_w = CalcItemWidth();
- const float w = (flags & ImGuiComboFlags_NoPreview) ? arrow_size : expected_w;
- 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));
+ const float w = (flags & ImGuiComboFlags_NoPreview) ? arrow_size : CalcItemWidth();
+ const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f));
+ const ImRect total_bb(bb.Min, 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))
+ if (!ItemAdd(total_bb, id, &bb))
return false;
+ // Open on click
bool hovered, held;
- bool pressed = ButtonBehavior(frame_bb, id, &hovered, &held);
- bool popup_open = IsPopupOpen(id);
+ bool pressed = ButtonBehavior(bb, id, &hovered, &held);
+ const ImGuiID popup_id = ImHashStr("##ComboPopup", 0, id);
+ bool popup_open = IsPopupOpen(popup_id, ImGuiPopupFlags_None);
+ if (pressed && !popup_open)
+ {
+ OpenPopupEx(popup_id, ImGuiPopupFlags_None);
+ popup_open = true;
+ }
+ // Render shape
const ImU32 frame_col = GetColorU32(hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
- const float value_x2 = ImMax(frame_bb.Min.x, frame_bb.Max.x - arrow_size);
- RenderNavHighlight(frame_bb, id);
+ const float value_x2 = ImMax(bb.Min.x, bb.Max.x - arrow_size);
+ RenderNavHighlight(bb, id);
if (!(flags & ImGuiComboFlags_NoPreview))
- window->DrawList->AddRectFilled(frame_bb.Min, ImVec2(value_x2, frame_bb.Max.y), frame_col, style.FrameRounding, (flags & ImGuiComboFlags_NoArrowButton) ? ImDrawCornerFlags_All : ImDrawCornerFlags_Left);
+ window->DrawList->AddRectFilled(bb.Min, ImVec2(value_x2, bb.Max.y), frame_col, style.FrameRounding, (flags & ImGuiComboFlags_NoArrowButton) ? ImDrawFlags_RoundCornersAll : ImDrawFlags_RoundCornersLeft);
if (!(flags & ImGuiComboFlags_NoArrowButton))
{
ImU32 bg_col = GetColorU32((popup_open || hovered) ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
ImU32 text_col = GetColorU32(ImGuiCol_Text);
- window->DrawList->AddRectFilled(ImVec2(value_x2, frame_bb.Min.y), frame_bb.Max, bg_col, style.FrameRounding, (w <= arrow_size) ? ImDrawCornerFlags_All : ImDrawCornerFlags_Right);
- if (value_x2 + arrow_size - style.FramePadding.x <= frame_bb.Max.x)
- RenderArrow(window->DrawList, ImVec2(value_x2 + style.FramePadding.y, frame_bb.Min.y + style.FramePadding.y), text_col, ImGuiDir_Down, 1.0f);
+ window->DrawList->AddRectFilled(ImVec2(value_x2, bb.Min.y), bb.Max, bg_col, style.FrameRounding, (w <= arrow_size) ? ImDrawFlags_RoundCornersAll : ImDrawFlags_RoundCornersRight);
+ if (value_x2 + arrow_size - style.FramePadding.x <= bb.Max.x)
+ RenderArrow(window->DrawList, ImVec2(value_x2 + style.FramePadding.y, bb.Min.y + style.FramePadding.y), text_col, ImGuiDir_Down, 1.0f);
}
- RenderFrameBorder(frame_bb.Min, frame_bb.Max, style.FrameRounding);
- if (preview_value != NULL && !(flags & ImGuiComboFlags_NoPreview))
- RenderTextClipped(frame_bb.Min + style.FramePadding, ImVec2(value_x2, frame_bb.Max.y), 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);
+ RenderFrameBorder(bb.Min, bb.Max, style.FrameRounding);
- if ((pressed || g.NavActivateId == id) && !popup_open)
+ // Custom preview
+ if (flags & ImGuiComboFlags_CustomPreview)
{
- if (window->DC.NavLayerCurrent == 0)
- window->NavLastIds[0] = id;
- OpenPopupEx(id);
- popup_open = true;
+ g.ComboPreviewData.PreviewRect = ImRect(bb.Min.x, bb.Min.y, value_x2, bb.Max.y);
+ IM_ASSERT(preview_value == NULL || preview_value[0] == 0);
+ preview_value = NULL;
}
+ // Render preview and label
+ if (preview_value != NULL && !(flags & ImGuiComboFlags_NoPreview))
+ {
+ if (g.LogEnabled)
+ LogSetNextTextDecoration("{", "}");
+ RenderTextClipped(bb.Min + style.FramePadding, ImVec2(value_x2, bb.Max.y), preview_value, NULL, NULL);
+ }
+ if (label_size.x > 0)
+ RenderText(ImVec2(bb.Max.x + style.ItemInnerSpacing.x, bb.Min.y + style.FramePadding.y), label);
+
if (!popup_open)
return false;
- if (has_window_size_constraint)
+ g.NextWindowData.Flags = backup_next_window_data_flags;
+ return BeginComboPopup(popup_id, bb, flags);
+}
+
+bool ImGui::BeginComboPopup(ImGuiID popup_id, const ImRect& bb, ImGuiComboFlags flags)
+{
+ ImGuiContext& g = *GImGui;
+ if (!IsPopupOpen(popup_id, ImGuiPopupFlags_None))
+ {
+ g.NextWindowData.ClearFlags();
+ return false;
+ }
+
+ // Set popup size
+ float w = bb.GetWidth();
+ if (g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasSizeConstraint)
{
- g.NextWindowData.Flags |= ImGuiNextWindowDataFlags_HasSizeConstraint;
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
+ 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;
@@ -1491,26 +1702,27 @@ bool ImGui::BeginCombo(const char* label, const char* preview_value, ImGuiComboF
SetNextWindowSizeConstraints(ImVec2(w, 0.0f), ImVec2(FLT_MAX, CalcMaxPopupHeightFromItemCount(popup_max_height_in_items)));
}
+ // This is essentially a specialized version of BeginPopupEx()
char name[16];
ImFormatString(name, IM_ARRAYSIZE(name), "##Combo_%02d", g.BeginPopupStack.Size); // Recycle windows based on depth
- // Peak into expected window size so we can position it
+ // Set position given a custom constraint (peak into expected window size so we can position it)
+ // FIXME: This might be easier to express with an hypothetical SetNextWindowPosConstraints() function?
+ // FIXME: This might be moved to Begin() or at least around the same spot where Tooltips and other Popups are calling FindBestWindowPosForPopupEx()?
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);
+ // Always override 'AutoPosLastDirection' to not leave a chance for a past value to affect us.
+ ImVec2 size_expected = CalcWindowNextAutoFitSize(popup_window);
+ popup_window->AutoPosLastDirection = (flags & ImGuiComboFlags_PopupAlignLeft) ? ImGuiDir_Left : ImGuiDir_Down; // Left = "Below, Toward Left", Down = "Below, Toward Right (default)"
+ ImRect r_outer = GetPopupAllowedExtentRect(popup_window);
+ ImVec2 pos = FindBestWindowPosForPopupEx(bb.GetBL(), size_expected, &popup_window->AutoPosLastDirection, r_outer, bb, ImGuiPopupPositionPolicy_ComboBox);
SetNextWindowPos(pos);
}
// We don't use BeginPopupEx() solely because we have a custom name string, which we could make an argument to BeginPopupEx()
ImGuiWindowFlags window_flags = ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_Popup | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoMove;
-
- // Horizontally align ourselves with the framed text
- PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(style.FramePadding.x, style.WindowPadding.y));
+ PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(g.Style.FramePadding.x, g.Style.WindowPadding.y)); // Horizontally align ourselves with the framed text
bool ret = Begin(name, NULL, window_flags);
PopStyleVar();
if (!ret)
@@ -1527,6 +1739,59 @@ void ImGui::EndCombo()
EndPopup();
}
+// Call directly after the BeginCombo/EndCombo block. The preview is designed to only host non-interactive elements
+// (Experimental, see GitHub issues: #1658, #4168)
+bool ImGui::BeginComboPreview()
+{
+ ImGuiContext& g = *GImGui;
+ ImGuiWindow* window = g.CurrentWindow;
+ ImGuiComboPreviewData* preview_data = &g.ComboPreviewData;
+
+ if (window->SkipItems || !(g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible))
+ return false;
+ IM_ASSERT(g.LastItemData.Rect.Min.x == preview_data->PreviewRect.Min.x && g.LastItemData.Rect.Min.y == preview_data->PreviewRect.Min.y); // Didn't call after BeginCombo/EndCombo block or forgot to pass ImGuiComboFlags_CustomPreview flag?
+ if (!window->ClipRect.Contains(preview_data->PreviewRect)) // Narrower test (optional)
+ return false;
+
+ // FIXME: This could be contained in a PushWorkRect() api
+ preview_data->BackupCursorPos = window->DC.CursorPos;
+ preview_data->BackupCursorMaxPos = window->DC.CursorMaxPos;
+ preview_data->BackupCursorPosPrevLine = window->DC.CursorPosPrevLine;
+ preview_data->BackupPrevLineTextBaseOffset = window->DC.PrevLineTextBaseOffset;
+ preview_data->BackupLayout = window->DC.LayoutType;
+ window->DC.CursorPos = preview_data->PreviewRect.Min + g.Style.FramePadding;
+ window->DC.CursorMaxPos = window->DC.CursorPos;
+ window->DC.LayoutType = ImGuiLayoutType_Horizontal;
+ window->DC.IsSameLine = false;
+ PushClipRect(preview_data->PreviewRect.Min, preview_data->PreviewRect.Max, true);
+
+ return true;
+}
+
+void ImGui::EndComboPreview()
+{
+ ImGuiContext& g = *GImGui;
+ ImGuiWindow* window = g.CurrentWindow;
+ ImGuiComboPreviewData* preview_data = &g.ComboPreviewData;
+
+ // FIXME: Using CursorMaxPos approximation instead of correct AABB which we will store in ImDrawCmd in the future
+ ImDrawList* draw_list = window->DrawList;
+ if (window->DC.CursorMaxPos.x < preview_data->PreviewRect.Max.x && window->DC.CursorMaxPos.y < preview_data->PreviewRect.Max.y)
+ if (draw_list->CmdBuffer.Size > 1) // Unlikely case that the PushClipRect() didn't create a command
+ {
+ draw_list->_CmdHeader.ClipRect = draw_list->CmdBuffer[draw_list->CmdBuffer.Size - 1].ClipRect = draw_list->CmdBuffer[draw_list->CmdBuffer.Size - 2].ClipRect;
+ draw_list->_TryMergeDrawCmds();
+ }
+ PopClipRect();
+ window->DC.CursorPos = preview_data->BackupCursorPos;
+ window->DC.CursorMaxPos = ImMax(window->DC.CursorMaxPos, preview_data->BackupCursorMaxPos);
+ window->DC.CursorPosPrevLine = preview_data->BackupCursorPosPrevLine;
+ window->DC.PrevLineTextBaseOffset = preview_data->BackupPrevLineTextBaseOffset;
+ window->DC.LayoutType = preview_data->BackupLayout;
+ window->DC.IsSameLine = false;
+ preview_data->PreviewRect = ImRect();
+}
+
// Getter for the old Combo() API: const char*[]
static bool Items_ArrayGetter(void* data, int idx, const char** out_text)
{
@@ -1569,7 +1834,7 @@ bool ImGui::Combo(const char* label, int* current_item, bool (*items_getter)(voi
// 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.Flags & ImGuiNextWindowDataFlags_HasSizeConstraint))
- SetNextWindowSizeConstraints(ImVec2(0,0), ImVec2(FLT_MAX, CalcMaxPopupHeightFromItemCount(popup_max_height_in_items)));
+ SetNextWindowSizeConstraints(ImVec2(0, 0), ImVec2(FLT_MAX, CalcMaxPopupHeightFromItemCount(popup_max_height_in_items)));
if (!BeginCombo(label, preview_value, ImGuiComboFlags_None))
return false;
@@ -1579,7 +1844,7 @@ bool ImGui::Combo(const char* label, int* current_item, bool (*items_getter)(voi
bool value_changed = false;
for (int i = 0; i < items_count; i++)
{
- PushID((void*)(intptr_t)i);
+ PushID(i);
const bool item_selected = (i == *current_item);
const char* item_text;
if (!items_getter(data, i, &item_text))
@@ -1595,6 +1860,10 @@ bool ImGui::Combo(const char* label, int* current_item, bool (*items_getter)(voi
}
EndCombo();
+
+ if (value_changed)
+ MarkItemEdited(g.LastItemData.ID);
+
return value_changed;
}
@@ -1622,59 +1891,36 @@ bool ImGui::Combo(const char* label, int* current_item, const char* items_separa
//-------------------------------------------------------------------------
// [SECTION] Data Type and Data Formatting Helpers [Internal]
//-------------------------------------------------------------------------
-// - PatchFormatStringFloatToInt()
// - DataTypeGetInfo()
// - DataTypeFormatString()
// - DataTypeApplyOp()
// - DataTypeApplyOpFromText()
+// - DataTypeCompare()
+// - DataTypeClamp()
// - GetMinimumStepAtDecimalPrecision
// - RoundScalarWithFormat<>()
//-------------------------------------------------------------------------
static const ImGuiDataTypeInfo GDataTypeInfo[] =
{
- { sizeof(char), "%d", "%d" }, // ImGuiDataType_S8
- { sizeof(unsigned char), "%u", "%u" },
- { sizeof(short), "%d", "%d" }, // ImGuiDataType_S16
- { sizeof(unsigned short), "%u", "%u" },
- { sizeof(int), "%d", "%d" }, // ImGuiDataType_S32
- { sizeof(unsigned int), "%u", "%u" },
+ { sizeof(char), "S8", "%d", "%d" }, // ImGuiDataType_S8
+ { sizeof(unsigned char), "U8", "%u", "%u" },
+ { sizeof(short), "S16", "%d", "%d" }, // ImGuiDataType_S16
+ { sizeof(unsigned short), "U16", "%u", "%u" },
+ { sizeof(int), "S32", "%d", "%d" }, // ImGuiDataType_S32
+ { sizeof(unsigned int), "U32", "%u", "%u" },
#ifdef _MSC_VER
- { sizeof(ImS64), "%I64d","%I64d" }, // ImGuiDataType_S64
- { sizeof(ImU64), "%I64u","%I64u" },
+ { sizeof(ImS64), "S64", "%I64d","%I64d" }, // ImGuiDataType_S64
+ { sizeof(ImU64), "U64", "%I64u","%I64u" },
#else
- { sizeof(ImS64), "%lld", "%lld" }, // ImGuiDataType_S64
- { sizeof(ImU64), "%llu", "%llu" },
+ { sizeof(ImS64), "S64", "%lld", "%lld" }, // ImGuiDataType_S64
+ { sizeof(ImU64), "U64", "%llu", "%llu" },
#endif
- { sizeof(float), "%f", "%f" }, // ImGuiDataType_Float (float are promoted to double in va_arg)
- { sizeof(double), "%f", "%lf" }, // ImGuiDataType_Double
+ { sizeof(float), "float", "%.3f","%f" }, // ImGuiDataType_Float (float are promoted to double in va_arg)
+ { sizeof(double), "double","%f", "%lf" }, // ImGuiDataType_Double
};
IM_STATIC_ASSERT(IM_ARRAYSIZE(GDataTypeInfo) == ImGuiDataType_COUNT);
-// FIXME-LEGACY: Prior to 1.61 our DragInt() function internally used floats and because of this the compile-time default value for format was "%.0f".
-// Even though we changed the compile-time default, we expect users to have carried %f around, which would break the display of DragInt() calls.
-// To honor backward compatibility we are rewriting the format string, unless IMGUI_DISABLE_OBSOLETE_FUNCTIONS is enabled. What could possibly go wrong?!
-static const char* PatchFormatStringFloatToInt(const char* fmt)
-{
- if (fmt[0] == '%' && fmt[1] == '.' && fmt[2] == '0' && fmt[3] == 'f' && fmt[4] == 0) // Fast legacy path for "%.0f" which is expected to be the most common case.
- return "%d";
- const char* fmt_start = ImParseFormatFindStart(fmt); // Find % (if any, and ignore %%)
- const char* fmt_end = ImParseFormatFindEnd(fmt_start); // Find end of format specifier, which itself is an exercise of confidence/recklessness (because snprintf is dependent on libc or user).
- if (fmt_end > fmt_start && fmt_end[-1] == 'f')
- {
-#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
- if (fmt_start == fmt && fmt_end[0] == 0)
- return "%d";
- ImGuiContext& g = *GImGui;
- ImFormatString(g.TempBuffer, IM_ARRAYSIZE(g.TempBuffer), "%.*s%%d%s", (int)(fmt_start - fmt), fmt, fmt_end); // Honor leading and trailing decorations, but lose alignment/precision.
- return g.TempBuffer;
-#else
- IM_ASSERT(0 && "DragInt(): Invalid format string!"); // Old versions used a default parameter of "%.0f", please replace with e.g. "%d"
-#endif
- }
- return fmt;
-}
-
const ImGuiDataTypeInfo* ImGui::DataTypeGetInfo(ImGuiDataType data_type)
{
IM_ASSERT(data_type >= 0 && data_type < ImGuiDataType_COUNT);
@@ -1704,7 +1950,7 @@ int ImGui::DataTypeFormatString(char* buf, int buf_size, ImGuiDataType data_type
return 0;
}
-void ImGui::DataTypeApplyOp(ImGuiDataType data_type, int op, void* output, void* arg1, const void* arg2)
+void ImGui::DataTypeApplyOp(ImGuiDataType data_type, int op, void* output, const void* arg1, const void* arg2)
{
IM_ASSERT(op == '+' || op == '-');
switch (data_type)
@@ -1756,92 +2002,32 @@ void ImGui::DataTypeApplyOp(ImGuiDataType data_type, int op, void* output, void*
// User can input math operators (e.g. +100) to edit a numerical values.
// NB: This is _not_ a full expression evaluator. We should probably add one and replace this dumb mess..
-bool ImGui::DataTypeApplyOpFromText(const char* buf, const char* initial_value_buf, ImGuiDataType data_type, void* p_data, const char* format)
+bool ImGui::DataTypeApplyFromText(const char* buf, ImGuiDataType data_type, void* p_data, const char* format)
{
while (ImCharIsBlankA(*buf))
buf++;
-
- // We don't support '-' op because it would conflict with inputing negative value.
- // Instead you can use +-100 to subtract from an existing value
- char op = buf[0];
- if (op == '+' || op == '*' || op == '/')
- {
- buf++;
- while (ImCharIsBlankA(*buf))
- buf++;
- }
- else
- {
- op = 0;
- }
if (!buf[0])
return false;
// Copy the value in an opaque buffer so we can compare at the end of the function if it changed at all.
- IM_ASSERT(data_type < ImGuiDataType_COUNT);
- int data_backup[2];
- const ImGuiDataTypeInfo* type_info = ImGui::DataTypeGetInfo(data_type);
- IM_ASSERT(type_info->Size <= sizeof(data_backup));
- memcpy(data_backup, p_data, type_info->Size);
+ const ImGuiDataTypeInfo* type_info = DataTypeGetInfo(data_type);
+ ImGuiDataTypeTempStorage data_backup;
+ memcpy(&data_backup, p_data, type_info->Size);
- if (format == NULL)
+ // Sanitize format
+ // For float/double we have to ignore format with precision (e.g. "%.2f") because sscanf doesn't take them in, so force them into %f and %lf
+ char format_sanitized[32];
+ if (data_type == ImGuiDataType_Float || data_type == ImGuiDataType_Double)
format = type_info->ScanFmt;
-
- // FIXME-LEGACY: The aim is to remove those operators and write a proper expression evaluator at some point..
- int arg1i = 0;
- if (data_type == ImGuiDataType_S32)
- {
- int* v = (int*)p_data;
- int arg0i = *v;
- float arg1f = 0.0f;
- if (op && sscanf(initial_value_buf, format, &arg0i) < 1)
- return false;
- // Store operand in a float so we can use fractional value for multipliers (*1.1), but constant always parsed as integer so we can fit big integers (e.g. 2000000003) past float precision
- if (op == '+') { if (sscanf(buf, "%d", &arg1i)) *v = (int)(arg0i + arg1i); } // Add (use "+-" to subtract)
- else if (op == '*') { if (sscanf(buf, "%f", &arg1f)) *v = (int)(arg0i * arg1f); } // Multiply
- else if (op == '/') { if (sscanf(buf, "%f", &arg1f) && arg1f != 0.0f) *v = (int)(arg0i / arg1f); } // Divide
- else { if (sscanf(buf, format, &arg1i) == 1) *v = arg1i; } // Assign constant
- }
- else if (data_type == ImGuiDataType_Float)
- {
- // For floats we have to ignore format with precision (e.g. "%.2f") because sscanf doesn't take them in
- format = "%f";
- float* v = (float*)p_data;
- float arg0f = *v, arg1f = 0.0f;
- if (op && sscanf(initial_value_buf, format, &arg0f) < 1)
- return false;
- if (sscanf(buf, format, &arg1f) < 1)
- return false;
- if (op == '+') { *v = arg0f + arg1f; } // Add (use "+-" to subtract)
- else if (op == '*') { *v = arg0f * arg1f; } // Multiply
- else if (op == '/') { if (arg1f != 0.0f) *v = arg0f / arg1f; } // Divide
- else { *v = arg1f; } // Assign constant
- }
- else if (data_type == ImGuiDataType_Double)
- {
- format = "%lf"; // scanf differentiate float/double unlike printf which forces everything to double because of ellipsis
- double* v = (double*)p_data;
- double arg0f = *v, arg1f = 0.0;
- if (op && sscanf(initial_value_buf, format, &arg0f) < 1)
- return false;
- if (sscanf(buf, format, &arg1f) < 1)
- return false;
- if (op == '+') { *v = arg0f + arg1f; } // Add (use "+-" to subtract)
- else if (op == '*') { *v = arg0f * arg1f; } // Multiply
- else if (op == '/') { if (arg1f != 0.0f) *v = arg0f / arg1f; } // Divide
- else { *v = arg1f; } // Assign constant
- }
- else if (data_type == ImGuiDataType_U32 || data_type == ImGuiDataType_S64 || data_type == ImGuiDataType_U64)
- {
- // All other types assign constant
- // We don't bother handling support for legacy operators since they are a little too crappy. Instead we will later implement a proper expression evaluator in the future.
- sscanf(buf, format, p_data);
- }
else
+ format = ImParseFormatSanitizeForScanning(format, format_sanitized, IM_ARRAYSIZE(format_sanitized));
+
+ // Small types need a 32-bit buffer to receive the result from scanf()
+ int v32 = 0;
+ if (sscanf(buf, format, type_info->Size >= 4 ? p_data : &v32) < 1)
+ return false;
+ if (type_info->Size < 4)
{
- // Small types need a 32-bit buffer to receive the result from scanf()
- int v32;
- sscanf(buf, format, &v32);
if (data_type == ImGuiDataType_S8)
*(ImS8*)p_data = (ImS8)ImClamp(v32, (int)IM_S8_MIN, (int)IM_S8_MAX);
else if (data_type == ImGuiDataType_U8)
@@ -1854,7 +2040,64 @@ bool ImGui::DataTypeApplyOpFromText(const char* buf, const char* initial_value_b
IM_ASSERT(0);
}
- return memcmp(data_backup, p_data, type_info->Size) != 0;
+ return memcmp(&data_backup, p_data, type_info->Size) != 0;
+}
+
+template<typename T>
+static int DataTypeCompareT(const T* lhs, const T* rhs)
+{
+ if (*lhs < *rhs) return -1;
+ if (*lhs > *rhs) return +1;
+ return 0;
+}
+
+int ImGui::DataTypeCompare(ImGuiDataType data_type, const void* arg_1, const void* arg_2)
+{
+ switch (data_type)
+ {
+ case ImGuiDataType_S8: return DataTypeCompareT<ImS8 >((const ImS8* )arg_1, (const ImS8* )arg_2);
+ case ImGuiDataType_U8: return DataTypeCompareT<ImU8 >((const ImU8* )arg_1, (const ImU8* )arg_2);
+ case ImGuiDataType_S16: return DataTypeCompareT<ImS16 >((const ImS16* )arg_1, (const ImS16* )arg_2);
+ case ImGuiDataType_U16: return DataTypeCompareT<ImU16 >((const ImU16* )arg_1, (const ImU16* )arg_2);
+ case ImGuiDataType_S32: return DataTypeCompareT<ImS32 >((const ImS32* )arg_1, (const ImS32* )arg_2);
+ case ImGuiDataType_U32: return DataTypeCompareT<ImU32 >((const ImU32* )arg_1, (const ImU32* )arg_2);
+ case ImGuiDataType_S64: return DataTypeCompareT<ImS64 >((const ImS64* )arg_1, (const ImS64* )arg_2);
+ case ImGuiDataType_U64: return DataTypeCompareT<ImU64 >((const ImU64* )arg_1, (const ImU64* )arg_2);
+ case ImGuiDataType_Float: return DataTypeCompareT<float >((const float* )arg_1, (const float* )arg_2);
+ case ImGuiDataType_Double: return DataTypeCompareT<double>((const double*)arg_1, (const double*)arg_2);
+ case ImGuiDataType_COUNT: break;
+ }
+ IM_ASSERT(0);
+ return 0;
+}
+
+template<typename T>
+static bool DataTypeClampT(T* v, const T* v_min, const T* v_max)
+{
+ // Clamp, both sides are optional, return true if modified
+ if (v_min && *v < *v_min) { *v = *v_min; return true; }
+ if (v_max && *v > *v_max) { *v = *v_max; return true; }
+ return false;
+}
+
+bool ImGui::DataTypeClamp(ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max)
+{
+ switch (data_type)
+ {
+ case ImGuiDataType_S8: return DataTypeClampT<ImS8 >((ImS8* )p_data, (const ImS8* )p_min, (const ImS8* )p_max);
+ case ImGuiDataType_U8: return DataTypeClampT<ImU8 >((ImU8* )p_data, (const ImU8* )p_min, (const ImU8* )p_max);
+ case ImGuiDataType_S16: return DataTypeClampT<ImS16 >((ImS16* )p_data, (const ImS16* )p_min, (const ImS16* )p_max);
+ case ImGuiDataType_U16: return DataTypeClampT<ImU16 >((ImU16* )p_data, (const ImU16* )p_min, (const ImU16* )p_max);
+ case ImGuiDataType_S32: return DataTypeClampT<ImS32 >((ImS32* )p_data, (const ImS32* )p_min, (const ImS32* )p_max);
+ case ImGuiDataType_U32: return DataTypeClampT<ImU32 >((ImU32* )p_data, (const ImU32* )p_min, (const ImU32* )p_max);
+ case ImGuiDataType_S64: return DataTypeClampT<ImS64 >((ImS64* )p_data, (const ImS64* )p_min, (const ImS64* )p_max);
+ case ImGuiDataType_U64: return DataTypeClampT<ImU64 >((ImU64* )p_data, (const ImU64* )p_min, (const ImU64* )p_max);
+ case ImGuiDataType_Float: return DataTypeClampT<float >((float* )p_data, (const float* )p_min, (const float* )p_max);
+ case ImGuiDataType_Double: return DataTypeClampT<double>((double*)p_data, (const double*)p_min, (const double*)p_max);
+ case ImGuiDataType_COUNT: break;
+ }
+ IM_ASSERT(0);
+ return false;
}
static float GetMinimumStepAtDecimalPrecision(int decimal_precision)
@@ -1866,33 +2109,27 @@ static float GetMinimumStepAtDecimalPrecision(int decimal_precision)
}
template<typename TYPE>
-static const char* ImAtoi(const char* src, TYPE* output)
-{
- int negative = 0;
- if (*src == '-') { negative = 1; src++; }
- if (*src == '+') { src++; }
- TYPE v = 0;
- while (*src >= '0' && *src <= '9')
- v = (v * 10) + (*src++ - '0');
- *output = negative ? -v : v;
- return src;
-}
-
-template<typename TYPE, typename SIGNEDTYPE>
TYPE ImGui::RoundScalarWithFormatT(const char* format, ImGuiDataType data_type, TYPE v)
{
+ IM_UNUSED(data_type);
+ IM_ASSERT(data_type == ImGuiDataType_Float || data_type == ImGuiDataType_Double);
const char* fmt_start = ImParseFormatFindStart(format);
if (fmt_start[0] != '%' || fmt_start[1] == '%') // Don't apply if the value is not visible in the format string
return v;
+
+ // Sanitize format
+ char fmt_sanitized[32];
+ ImParseFormatSanitizeForPrinting(fmt_start, fmt_sanitized, IM_ARRAYSIZE(fmt_sanitized));
+ fmt_start = fmt_sanitized;
+
+ // Format value with our rounding, and read back
char v_str[64];
ImFormatString(v_str, IM_ARRAYSIZE(v_str), fmt_start, v);
const char* p = v_str;
while (*p == ' ')
p++;
- if (data_type == ImGuiDataType_Float || data_type == ImGuiDataType_Double)
- v = (TYPE)ImAtof(p);
- else
- ImAtoi(p, (SIGNEDTYPE*)&v);
+ v = (TYPE)ImAtof(p);
+
return v;
}
@@ -1917,16 +2154,13 @@ TYPE ImGui::RoundScalarWithFormatT(const char* format, ImGuiDataType data_type,
// This is called by DragBehavior() when the widget is active (held by mouse or being manipulated with Nav controls)
template<typename TYPE, typename SIGNEDTYPE, typename FLOATTYPE>
-bool ImGui::DragBehaviorT(ImGuiDataType data_type, TYPE* v, float v_speed, const TYPE v_min, const TYPE v_max, const char* format, float power, ImGuiDragFlags flags)
+bool ImGui::DragBehaviorT(ImGuiDataType data_type, TYPE* v, float v_speed, const TYPE v_min, const TYPE v_max, const char* format, ImGuiSliderFlags flags)
{
ImGuiContext& g = *GImGui;
- const ImGuiAxis axis = (flags & ImGuiDragFlags_Vertical) ? ImGuiAxis_Y : ImGuiAxis_X;
- const bool is_decimal = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double);
+ const ImGuiAxis axis = (flags & ImGuiSliderFlags_Vertical) ? ImGuiAxis_Y : ImGuiAxis_X;
const bool is_clamped = (v_min < v_max);
- const bool is_power = (power != 1.0f && is_decimal && is_clamped && (v_max - v_min < FLT_MAX));
- const bool is_locked = (v_min > v_max);
- if (is_locked)
- return false;
+ const bool is_logarithmic = (flags & ImGuiSliderFlags_Logarithmic) != 0;
+ const bool is_floating_point = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double);
// Default tweak speed
if (v_speed == 0.0f && is_clamped && (v_max - v_min < FLT_MAX))
@@ -1934,7 +2168,7 @@ bool ImGui::DragBehaviorT(ImGuiDataType data_type, TYPE* v, float v_speed, const
// Inputs accumulates into g.DragCurrentAccum, which is flushed into the current value as soon as it makes a difference with our precision settings
float adjust_delta = 0.0f;
- if (g.ActiveIdSource == ImGuiInputSource_Mouse && IsMousePosValid() && g.IO.MouseDragMaxDistanceSqr[0] > 1.0f*1.0f)
+ if (g.ActiveIdSource == ImGuiInputSource_Mouse && IsMousePosValid() && IsMouseDragPastThreshold(0, g.IO.MouseDragThreshold * DRAG_MOUSE_THRESHOLD_FACTOR))
{
adjust_delta = g.IO.MouseDelta[axis];
if (g.IO.KeyAlt)
@@ -1944,8 +2178,11 @@ bool ImGui::DragBehaviorT(ImGuiDataType data_type, TYPE* v, float v_speed, const
}
else if (g.ActiveIdSource == ImGuiInputSource_Nav)
{
- int decimal_precision = is_decimal ? ImParseFormatPrecision(format, 3) : 0;
- adjust_delta = GetNavInputAmount2d(ImGuiNavDirSourceFlags_Keyboard | ImGuiNavDirSourceFlags_PadDPad, ImGuiInputReadMode_RepeatFast, 1.0f / 10.0f, 10.0f)[axis];
+ const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, 3) : 0;
+ const bool tweak_slow = IsKeyDown((g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakSlow : ImGuiKey_NavKeyboardTweakSlow);
+ const bool tweak_fast = IsKeyDown((g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakFast : ImGuiKey_NavKeyboardTweakFast);
+ const float tweak_factor = tweak_slow ? 1.0f / 1.0f : tweak_fast ? 10.0f : 1.0f;
+ adjust_delta = GetNavTweakPressedAmount(axis) * tweak_factor;
v_speed = ImMax(v_speed, GetMinimumStepAtDecimalPrecision(decimal_precision));
}
adjust_delta *= v_speed;
@@ -1954,12 +2191,15 @@ bool ImGui::DragBehaviorT(ImGuiDataType data_type, TYPE* v, float v_speed, const
if (axis == ImGuiAxis_Y)
adjust_delta = -adjust_delta;
+ // For logarithmic use our range is effectively 0..1 so scale the delta into that range
+ if (is_logarithmic && (v_max - v_min < FLT_MAX) && ((v_max - v_min) > 0.000001f)) // Epsilon to avoid /0
+ adjust_delta /= (float)(v_max - v_min);
+
// Clear current value on activation
// Avoid altering values and clamping when we are _already_ past the limits and heading in the same direction, so e.g. if range is 0..255, current value is 300 and we are pushing to the right side, keep the 300.
bool is_just_activated = g.ActiveIdIsJustActivated;
bool is_already_past_limits_and_pushing_outward = is_clamped && ((*v >= v_max && adjust_delta > 0.0f) || (*v <= v_min && adjust_delta < 0.0f));
- bool is_drag_direction_change_with_power = is_power && ((adjust_delta < 0 && g.DragCurrentAccum > 0) || (adjust_delta > 0 && g.DragCurrentAccum < 0));
- if (is_just_activated || is_already_past_limits_and_pushing_outward || is_drag_direction_change_with_power)
+ if (is_just_activated || is_already_past_limits_and_pushing_outward)
{
g.DragCurrentAccum = 0.0f;
g.DragCurrentAccumDirty = false;
@@ -1976,13 +2216,19 @@ bool ImGui::DragBehaviorT(ImGuiDataType data_type, TYPE* v, float v_speed, const
TYPE v_cur = *v;
FLOATTYPE v_old_ref_for_accum_remainder = (FLOATTYPE)0.0f;
- if (is_power)
+ float logarithmic_zero_epsilon = 0.0f; // Only valid when is_logarithmic is true
+ const float zero_deadzone_halfsize = 0.0f; // Drag widgets have no deadzone (as it doesn't make sense)
+ if (is_logarithmic)
{
- // Offset + round to user desired precision, with a curve on the v_min..v_max range to get more precision on one side of the range
- FLOATTYPE v_old_norm_curved = ImPow((FLOATTYPE)(v_cur - v_min) / (FLOATTYPE)(v_max - v_min), (FLOATTYPE)1.0f / power);
- FLOATTYPE v_new_norm_curved = v_old_norm_curved + (g.DragCurrentAccum / (v_max - v_min));
- v_cur = v_min + (SIGNEDTYPE)ImPow(ImSaturate((float)v_new_norm_curved), power) * (v_max - v_min);
- v_old_ref_for_accum_remainder = v_old_norm_curved;
+ // When using logarithmic sliders, we need to clamp to avoid hitting zero, but our choice of clamp value greatly affects slider precision. We attempt to use the specified precision to estimate a good lower bound.
+ const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, 3) : 1;
+ logarithmic_zero_epsilon = ImPow(0.1f, (float)decimal_precision);
+
+ // Convert to parametric space, apply delta, convert back
+ float v_old_parametric = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, v_cur, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
+ float v_new_parametric = v_old_parametric + g.DragCurrentAccum;
+ v_cur = ScaleValueFromRatioT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, v_new_parametric, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
+ v_old_ref_for_accum_remainder = v_old_parametric;
}
else
{
@@ -1990,14 +2236,16 @@ bool ImGui::DragBehaviorT(ImGuiDataType data_type, TYPE* v, float v_speed, const
}
// Round to user desired precision based on format string
- v_cur = RoundScalarWithFormatT<TYPE, SIGNEDTYPE>(format, data_type, v_cur);
+ if (is_floating_point && !(flags & ImGuiSliderFlags_NoRoundToFormat))
+ v_cur = RoundScalarWithFormatT<TYPE>(format, data_type, v_cur);
// Preserve remainder after rounding has been applied. This also allow slow tweaking of values.
g.DragCurrentAccumDirty = false;
- if (is_power)
+ if (is_logarithmic)
{
- FLOATTYPE v_cur_norm_curved = ImPow((FLOATTYPE)(v_cur - v_min) / (FLOATTYPE)(v_max - v_min), (FLOATTYPE)1.0f / power);
- g.DragCurrentAccum -= (float)(v_cur_norm_curved - v_old_ref_for_accum_remainder);
+ // Convert to parametric space, apply delta, convert back
+ float v_new_parametric = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, v_cur, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
+ g.DragCurrentAccum -= (float)(v_new_parametric - v_old_ref_for_accum_remainder);
}
else
{
@@ -2011,9 +2259,9 @@ bool ImGui::DragBehaviorT(ImGuiDataType data_type, TYPE* v, float v_speed, const
// Clamp values (+ handle overflow/wrap-around for integer types)
if (*v != v_cur && is_clamped)
{
- if (v_cur < v_min || (v_cur > *v && adjust_delta < 0.0f && !is_decimal))
+ if (v_cur < v_min || (v_cur > *v && adjust_delta < 0.0f && !is_floating_point))
v_cur = v_min;
- if (v_cur > v_max || (v_cur < *v && adjust_delta > 0.0f && !is_decimal))
+ if (v_cur > v_max || (v_cur < *v && adjust_delta > 0.0f && !is_floating_point))
v_cur = v_max;
}
@@ -2024,11 +2272,15 @@ bool ImGui::DragBehaviorT(ImGuiDataType data_type, TYPE* v, float v_speed, const
return true;
}
-bool ImGui::DragBehavior(ImGuiID id, ImGuiDataType data_type, void* p_v, float v_speed, const void* p_min, const void* p_max, const char* format, float power, ImGuiDragFlags flags)
+bool ImGui::DragBehavior(ImGuiID id, ImGuiDataType data_type, void* p_v, float v_speed, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags)
{
+ // Read imgui.cpp "API BREAKING CHANGES" section for 1.78 if you hit this assert.
+ IM_ASSERT((flags == 1 || (flags & ImGuiSliderFlags_InvalidMask_) == 0) && "Invalid ImGuiSliderFlags flags! Has the 'float power' argument been mistakenly cast to flags? Call function with ImGuiSliderFlags_Logarithmic flags instead.");
+
ImGuiContext& g = *GImGui;
if (g.ActiveId == id)
{
+ // Those are the things we can do easily outside the DragBehaviorT<> template, saves code generation.
if (g.ActiveIdSource == ImGuiInputSource_Mouse && !g.IO.MouseDown[0])
ClearActiveID();
else if (g.ActiveIdSource == ImGuiInputSource_Nav && g.NavActivatePressedId == id && !g.ActiveIdIsJustActivated)
@@ -2036,19 +2288,21 @@ bool ImGui::DragBehavior(ImGuiID id, ImGuiDataType data_type, void* p_v, float v
}
if (g.ActiveId != id)
return false;
+ if ((g.LastItemData.InFlags & ImGuiItemFlags_ReadOnly) || (flags & ImGuiSliderFlags_ReadOnly))
+ return false;
switch (data_type)
{
- case ImGuiDataType_S8: { ImS32 v32 = (ImS32)*(ImS8*)p_v; bool r = DragBehaviorT<ImS32, ImS32, float>(ImGuiDataType_S32, &v32, v_speed, p_min ? *(const ImS8*) p_min : IM_S8_MIN, p_max ? *(const ImS8*)p_max : IM_S8_MAX, format, power, flags); if (r) *(ImS8*)p_v = (ImS8)v32; return r; }
- case ImGuiDataType_U8: { ImU32 v32 = (ImU32)*(ImU8*)p_v; bool r = DragBehaviorT<ImU32, ImS32, float>(ImGuiDataType_U32, &v32, v_speed, p_min ? *(const ImU8*) p_min : IM_U8_MIN, p_max ? *(const ImU8*)p_max : IM_U8_MAX, format, power, flags); if (r) *(ImU8*)p_v = (ImU8)v32; return r; }
- case ImGuiDataType_S16: { ImS32 v32 = (ImS32)*(ImS16*)p_v; bool r = DragBehaviorT<ImS32, ImS32, float>(ImGuiDataType_S32, &v32, v_speed, p_min ? *(const ImS16*)p_min : IM_S16_MIN, p_max ? *(const ImS16*)p_max : IM_S16_MAX, format, power, flags); if (r) *(ImS16*)p_v = (ImS16)v32; return r; }
- case ImGuiDataType_U16: { ImU32 v32 = (ImU32)*(ImU16*)p_v; bool r = DragBehaviorT<ImU32, ImS32, float>(ImGuiDataType_U32, &v32, v_speed, p_min ? *(const ImU16*)p_min : IM_U16_MIN, p_max ? *(const ImU16*)p_max : IM_U16_MAX, format, power, flags); if (r) *(ImU16*)p_v = (ImU16)v32; return r; }
- case ImGuiDataType_S32: return DragBehaviorT<ImS32, ImS32, float >(data_type, (ImS32*)p_v, v_speed, p_min ? *(const ImS32* )p_min : IM_S32_MIN, p_max ? *(const ImS32* )p_max : IM_S32_MAX, format, power, flags);
- case ImGuiDataType_U32: return DragBehaviorT<ImU32, ImS32, float >(data_type, (ImU32*)p_v, v_speed, p_min ? *(const ImU32* )p_min : IM_U32_MIN, p_max ? *(const ImU32* )p_max : IM_U32_MAX, format, power, flags);
- case ImGuiDataType_S64: return DragBehaviorT<ImS64, ImS64, double>(data_type, (ImS64*)p_v, v_speed, p_min ? *(const ImS64* )p_min : IM_S64_MIN, p_max ? *(const ImS64* )p_max : IM_S64_MAX, format, power, flags);
- case ImGuiDataType_U64: return DragBehaviorT<ImU64, ImS64, double>(data_type, (ImU64*)p_v, v_speed, p_min ? *(const ImU64* )p_min : IM_U64_MIN, p_max ? *(const ImU64* )p_max : IM_U64_MAX, format, power, flags);
- case ImGuiDataType_Float: return DragBehaviorT<float, float, float >(data_type, (float*)p_v, v_speed, p_min ? *(const float* )p_min : -FLT_MAX, p_max ? *(const float* )p_max : FLT_MAX, format, power, flags);
- case ImGuiDataType_Double: return DragBehaviorT<double,double,double>(data_type, (double*)p_v, v_speed, p_min ? *(const double*)p_min : -DBL_MAX, p_max ? *(const double*)p_max : DBL_MAX, format, power, flags);
+ case ImGuiDataType_S8: { ImS32 v32 = (ImS32)*(ImS8*)p_v; bool r = DragBehaviorT<ImS32, ImS32, float>(ImGuiDataType_S32, &v32, v_speed, p_min ? *(const ImS8*) p_min : IM_S8_MIN, p_max ? *(const ImS8*)p_max : IM_S8_MAX, format, flags); if (r) *(ImS8*)p_v = (ImS8)v32; return r; }
+ case ImGuiDataType_U8: { ImU32 v32 = (ImU32)*(ImU8*)p_v; bool r = DragBehaviorT<ImU32, ImS32, float>(ImGuiDataType_U32, &v32, v_speed, p_min ? *(const ImU8*) p_min : IM_U8_MIN, p_max ? *(const ImU8*)p_max : IM_U8_MAX, format, flags); if (r) *(ImU8*)p_v = (ImU8)v32; return r; }
+ case ImGuiDataType_S16: { ImS32 v32 = (ImS32)*(ImS16*)p_v; bool r = DragBehaviorT<ImS32, ImS32, float>(ImGuiDataType_S32, &v32, v_speed, p_min ? *(const ImS16*)p_min : IM_S16_MIN, p_max ? *(const ImS16*)p_max : IM_S16_MAX, format, flags); if (r) *(ImS16*)p_v = (ImS16)v32; return r; }
+ case ImGuiDataType_U16: { ImU32 v32 = (ImU32)*(ImU16*)p_v; bool r = DragBehaviorT<ImU32, ImS32, float>(ImGuiDataType_U32, &v32, v_speed, p_min ? *(const ImU16*)p_min : IM_U16_MIN, p_max ? *(const ImU16*)p_max : IM_U16_MAX, format, flags); if (r) *(ImU16*)p_v = (ImU16)v32; return r; }
+ case ImGuiDataType_S32: return DragBehaviorT<ImS32, ImS32, float >(data_type, (ImS32*)p_v, v_speed, p_min ? *(const ImS32* )p_min : IM_S32_MIN, p_max ? *(const ImS32* )p_max : IM_S32_MAX, format, flags);
+ case ImGuiDataType_U32: return DragBehaviorT<ImU32, ImS32, float >(data_type, (ImU32*)p_v, v_speed, p_min ? *(const ImU32* )p_min : IM_U32_MIN, p_max ? *(const ImU32* )p_max : IM_U32_MAX, format, flags);
+ case ImGuiDataType_S64: return DragBehaviorT<ImS64, ImS64, double>(data_type, (ImS64*)p_v, v_speed, p_min ? *(const ImS64* )p_min : IM_S64_MIN, p_max ? *(const ImS64* )p_max : IM_S64_MAX, format, flags);
+ case ImGuiDataType_U64: return DragBehaviorT<ImU64, ImS64, double>(data_type, (ImU64*)p_v, v_speed, p_min ? *(const ImU64* )p_min : IM_U64_MIN, p_max ? *(const ImU64* )p_max : IM_U64_MAX, format, flags);
+ case ImGuiDataType_Float: return DragBehaviorT<float, float, float >(data_type, (float*)p_v, v_speed, p_min ? *(const float* )p_min : -FLT_MAX, p_max ? *(const float* )p_max : FLT_MAX, format, flags);
+ case ImGuiDataType_Double: return DragBehaviorT<double,double,double>(data_type, (double*)p_v, v_speed, p_min ? *(const double*)p_min : -DBL_MAX, p_max ? *(const double*)p_max : DBL_MAX, format, flags);
case ImGuiDataType_COUNT: break;
}
IM_ASSERT(0);
@@ -2056,82 +2310,96 @@ bool ImGui::DragBehavior(ImGuiID id, ImGuiDataType data_type, void* p_v, float v
}
// Note: p_data, p_min and p_max are _pointers_ to a memory address holding the data. For a Drag widget, p_min and p_max are optional.
-// Read code of e.g. SliderFloat(), SliderInt() etc. or examples in 'Demo->Widgets->Data Types' to understand how to use this function directly.
-bool ImGui::DragScalar(const char* label, ImGuiDataType data_type, void* p_data, float v_speed, const void* p_min, const void* p_max, const char* format, float power)
+// Read code of e.g. DragFloat(), DragInt() etc. or examples in 'Demo->Widgets->Data Types' to understand how to use this function directly.
+bool ImGui::DragScalar(const char* label, ImGuiDataType data_type, void* p_data, float v_speed, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags)
{
ImGuiWindow* window = GetCurrentWindow();
if (window->SkipItems)
return false;
- if (power != 1.0f)
- IM_ASSERT(p_min != NULL && p_max != NULL); // When using a power curve the drag needs to have known bounds
-
ImGuiContext& g = *GImGui;
const ImGuiStyle& style = g.Style;
const ImGuiID id = window->GetID(label);
const float w = CalcItemWidth();
+
const ImVec2 label_size = CalcTextSize(label, NULL, true);
- const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y*2.0f));
+ 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));
+ const bool temp_input_allowed = (flags & ImGuiSliderFlags_NoInput) == 0;
ItemSize(total_bb, style.FramePadding.y);
- if (!ItemAdd(total_bb, id, &frame_bb))
+ if (!ItemAdd(total_bb, id, &frame_bb, temp_input_allowed ? ImGuiItemFlags_Inputable : 0))
return false;
// Default format string when passing NULL
if (format == NULL)
format = DataTypeGetInfo(data_type)->PrintFmt;
- else if (data_type == ImGuiDataType_S32 && strcmp(format, "%d") != 0) // (FIXME-LEGACY: Patch old "%.0f" format string to use "%d", read function more details.)
- format = PatchFormatStringFloatToInt(format);
- // Tabbing or CTRL-clicking on Drag turns it into an input box
const bool hovered = ItemHoverable(frame_bb, id);
- bool temp_input_is_active = TempInputTextIsActive(id);
- bool temp_input_start = false;
+ bool temp_input_is_active = temp_input_allowed && TempInputIsActive(id);
if (!temp_input_is_active)
{
- const bool focus_requested = FocusableItemRegister(window, id);
- const bool clicked = (hovered && g.IO.MouseClicked[0]);
- const bool double_clicked = (hovered && g.IO.MouseDoubleClicked[0]);
- if (focus_requested || clicked || double_clicked || g.NavActivateId == id || g.NavInputId == id)
+ // Tabbing or CTRL-clicking on Drag turns it into an InputText
+ const bool input_requested_by_tabbing = temp_input_allowed && (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_FocusedByTabbing) != 0;
+ const bool clicked = hovered && IsMouseClicked(0, id);
+ const bool double_clicked = (hovered && g.IO.MouseClickedCount[0] == 2 && TestKeyOwner(ImGuiKey_MouseLeft, id));
+ const bool make_active = (input_requested_by_tabbing || clicked || double_clicked || g.NavActivateId == id || g.NavActivateInputId == id);
+ if (make_active && (clicked || double_clicked))
+ SetKeyOwner(ImGuiKey_MouseLeft, id);
+ if (make_active && temp_input_allowed)
+ if (input_requested_by_tabbing || (clicked && g.IO.KeyCtrl) || double_clicked || g.NavActivateInputId == id)
+ temp_input_is_active = true;
+
+ // (Optional) simple click (without moving) turns Drag into an InputText
+ if (g.IO.ConfigDragClickToInputText && temp_input_allowed && !temp_input_is_active)
+ if (g.ActiveId == id && hovered && g.IO.MouseReleased[0] && !IsMouseDragPastThreshold(0, g.IO.MouseDragThreshold * DRAG_MOUSE_THRESHOLD_FACTOR))
+ {
+ g.NavActivateId = g.NavActivateInputId = id;
+ g.NavActivateFlags = ImGuiActivateFlags_PreferInput;
+ temp_input_is_active = true;
+ }
+
+ if (make_active && !temp_input_is_active)
{
SetActiveID(id, window);
SetFocusID(id, window);
FocusWindow(window);
g.ActiveIdUsingNavDirMask = (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right);
- if (focus_requested || (clicked && g.IO.KeyCtrl) || double_clicked || g.NavInputId == id)
- {
- temp_input_start = true;
- FocusableItemUnregister(window);
- }
}
}
- if (temp_input_is_active || temp_input_start)
- return TempInputTextScalar(frame_bb, id, label, data_type, p_data, format);
+
+ if (temp_input_is_active)
+ {
+ // Only clamp CTRL+Click input when ImGuiSliderFlags_AlwaysClamp is set
+ const bool is_clamp_input = (flags & ImGuiSliderFlags_AlwaysClamp) != 0 && (p_min == NULL || p_max == NULL || DataTypeCompare(data_type, p_min, p_max) < 0);
+ return TempInputScalar(frame_bb, id, label, data_type, p_data, format, is_clamp_input ? p_min : NULL, is_clamp_input ? p_max : NULL);
+ }
// Draw frame
- const ImU32 frame_col = GetColorU32(g.ActiveId == id ? ImGuiCol_FrameBgActive : g.HoveredId == id ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
+ const ImU32 frame_col = GetColorU32(g.ActiveId == id ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
RenderNavHighlight(frame_bb, id);
RenderFrame(frame_bb.Min, frame_bb.Max, frame_col, true, style.FrameRounding);
// Drag behavior
- const bool value_changed = DragBehavior(id, data_type, p_data, v_speed, p_min, p_max, format, power, ImGuiDragFlags_None);
+ const bool value_changed = DragBehavior(id, data_type, p_data, v_speed, p_min, p_max, format, flags);
if (value_changed)
MarkItemEdited(id);
// Display value using user-provided display format so user can add prefix/suffix/decorations to the value.
char value_buf[64];
const char* value_buf_end = value_buf + DataTypeFormatString(value_buf, IM_ARRAYSIZE(value_buf), data_type, p_data, format);
+ if (g.LogEnabled)
+ LogSetNextTextDecoration("{", "}");
RenderTextClipped(frame_bb.Min, frame_bb.Max, value_buf, value_buf_end, NULL, ImVec2(0.5f, 0.5f));
if (label_size.x > 0.0f)
RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label);
- IMGUI_TEST_ENGINE_ITEM_INFO(id, label, window->DC.ItemFlags);
+ IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
return value_changed;
}
-bool ImGui::DragScalarN(const char* label, ImGuiDataType data_type, void* p_data, int components, float v_speed, const void* p_min, const void* p_max, const char* format, float power)
+bool ImGui::DragScalarN(const char* label, ImGuiDataType data_type, void* p_data, int components, float v_speed, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags)
{
ImGuiWindow* window = GetCurrentWindow();
if (window->SkipItems)
@@ -2148,7 +2416,7 @@ bool ImGui::DragScalarN(const char* label, ImGuiDataType data_type, void* p_data
PushID(i);
if (i > 0)
SameLine(0, g.Style.ItemInnerSpacing.x);
- value_changed |= DragScalar("", data_type, p_data, v_speed, p_min, p_max, format, power);
+ value_changed |= DragScalar("", data_type, p_data, v_speed, p_min, p_max, format, flags);
PopID();
PopItemWidth();
p_data = (void*)((char*)p_data + type_size);
@@ -2166,27 +2434,28 @@ bool ImGui::DragScalarN(const char* label, ImGuiDataType data_type, void* p_data
return value_changed;
}
-bool ImGui::DragFloat(const char* label, float* v, float v_speed, float v_min, float v_max, const char* format, float power)
+bool ImGui::DragFloat(const char* label, float* v, float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
{
- return DragScalar(label, ImGuiDataType_Float, v, v_speed, &v_min, &v_max, format, power);
+ return DragScalar(label, ImGuiDataType_Float, v, v_speed, &v_min, &v_max, format, flags);
}
-bool ImGui::DragFloat2(const char* label, float v[2], float v_speed, float v_min, float v_max, const char* format, float power)
+bool ImGui::DragFloat2(const char* label, float v[2], float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
{
- return DragScalarN(label, ImGuiDataType_Float, v, 2, v_speed, &v_min, &v_max, format, power);
+ return DragScalarN(label, ImGuiDataType_Float, v, 2, v_speed, &v_min, &v_max, format, flags);
}
-bool ImGui::DragFloat3(const char* label, float v[3], float v_speed, float v_min, float v_max, const char* format, float power)
+bool ImGui::DragFloat3(const char* label, float v[3], float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
{
- return DragScalarN(label, ImGuiDataType_Float, v, 3, v_speed, &v_min, &v_max, format, power);
+ return DragScalarN(label, ImGuiDataType_Float, v, 3, v_speed, &v_min, &v_max, format, flags);
}
-bool ImGui::DragFloat4(const char* label, float v[4], float v_speed, float v_min, float v_max, const char* format, float power)
+bool ImGui::DragFloat4(const char* label, float v[4], float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
{
- return DragScalarN(label, ImGuiDataType_Float, v, 4, v_speed, &v_min, &v_max, format, power);
+ return DragScalarN(label, ImGuiDataType_Float, v, 4, v_speed, &v_min, &v_max, format, flags);
}
-bool ImGui::DragFloatRange2(const char* label, float* v_current_min, float* v_current_max, float v_speed, float v_min, float v_max, const char* format, const char* format_max, float power)
+// NB: You likely want to specify the ImGuiSliderFlags_AlwaysClamp when using this.
+bool ImGui::DragFloatRange2(const char* label, float* v_current_min, float* v_current_max, float v_speed, float v_min, float v_max, const char* format, const char* format_max, ImGuiSliderFlags flags)
{
ImGuiWindow* window = GetCurrentWindow();
if (window->SkipItems)
@@ -2197,41 +2466,50 @@ bool ImGui::DragFloatRange2(const char* label, float* v_current_min, float* v_cu
BeginGroup();
PushMultiItemsWidths(2, CalcItemWidth());
- bool value_changed = DragFloat("##min", v_current_min, v_speed, (v_min >= v_max) ? -FLT_MAX : v_min, (v_min >= v_max) ? *v_current_max : ImMin(v_max, *v_current_max), format, power);
+ float min_min = (v_min >= v_max) ? -FLT_MAX : v_min;
+ float min_max = (v_min >= v_max) ? *v_current_max : ImMin(v_max, *v_current_max);
+ ImGuiSliderFlags min_flags = flags | ((min_min == min_max) ? ImGuiSliderFlags_ReadOnly : 0);
+ bool value_changed = DragScalar("##min", ImGuiDataType_Float, v_current_min, v_speed, &min_min, &min_max, format, min_flags);
PopItemWidth();
SameLine(0, g.Style.ItemInnerSpacing.x);
- value_changed |= DragFloat("##max", v_current_max, v_speed, (v_min >= v_max) ? *v_current_min : ImMax(v_min, *v_current_min), (v_min >= v_max) ? FLT_MAX : v_max, format_max ? format_max : format, power);
+
+ float max_min = (v_min >= v_max) ? *v_current_min : ImMax(v_min, *v_current_min);
+ float max_max = (v_min >= v_max) ? FLT_MAX : v_max;
+ ImGuiSliderFlags max_flags = flags | ((max_min == max_max) ? ImGuiSliderFlags_ReadOnly : 0);
+ value_changed |= DragScalar("##max", ImGuiDataType_Float, v_current_max, v_speed, &max_min, &max_max, format_max ? format_max : format, max_flags);
PopItemWidth();
SameLine(0, g.Style.ItemInnerSpacing.x);
TextEx(label, FindRenderedTextEnd(label));
EndGroup();
PopID();
+
return value_changed;
}
// NB: v_speed is float to allow adjusting the drag speed with more precision
-bool ImGui::DragInt(const char* label, int* v, float v_speed, int v_min, int v_max, const char* format)
+bool ImGui::DragInt(const char* label, int* v, float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
{
- return DragScalar(label, ImGuiDataType_S32, v, v_speed, &v_min, &v_max, format);
+ return DragScalar(label, ImGuiDataType_S32, v, v_speed, &v_min, &v_max, format, flags);
}
-bool ImGui::DragInt2(const char* label, int v[2], float v_speed, int v_min, int v_max, const char* format)
+bool ImGui::DragInt2(const char* label, int v[2], float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
{
- return DragScalarN(label, ImGuiDataType_S32, v, 2, v_speed, &v_min, &v_max, format);
+ return DragScalarN(label, ImGuiDataType_S32, v, 2, v_speed, &v_min, &v_max, format, flags);
}
-bool ImGui::DragInt3(const char* label, int v[3], float v_speed, int v_min, int v_max, const char* format)
+bool ImGui::DragInt3(const char* label, int v[3], float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
{
- return DragScalarN(label, ImGuiDataType_S32, v, 3, v_speed, &v_min, &v_max, format);
+ return DragScalarN(label, ImGuiDataType_S32, v, 3, v_speed, &v_min, &v_max, format, flags);
}
-bool ImGui::DragInt4(const char* label, int v[4], float v_speed, int v_min, int v_max, const char* format)
+bool ImGui::DragInt4(const char* label, int v[4], float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
{
- return DragScalarN(label, ImGuiDataType_S32, v, 4, v_speed, &v_min, &v_max, format);
+ return DragScalarN(label, ImGuiDataType_S32, v, 4, v_speed, &v_min, &v_max, format, flags);
}
-bool ImGui::DragIntRange2(const char* label, int* v_current_min, int* v_current_max, float v_speed, int v_min, int v_max, const char* format, const char* format_max)
+// NB: You likely want to specify the ImGuiSliderFlags_AlwaysClamp when using this.
+bool ImGui::DragIntRange2(const char* label, int* v_current_min, int* v_current_max, float v_speed, int v_min, int v_max, const char* format, const char* format_max, ImGuiSliderFlags flags)
{
ImGuiWindow* window = GetCurrentWindow();
if (window->SkipItems)
@@ -2242,10 +2520,17 @@ bool ImGui::DragIntRange2(const char* label, int* v_current_min, int* v_current_
BeginGroup();
PushMultiItemsWidths(2, CalcItemWidth());
- bool value_changed = DragInt("##min", v_current_min, v_speed, (v_min >= v_max) ? INT_MIN : v_min, (v_min >= v_max) ? *v_current_max : ImMin(v_max, *v_current_max), format);
+ int min_min = (v_min >= v_max) ? INT_MIN : v_min;
+ int min_max = (v_min >= v_max) ? *v_current_max : ImMin(v_max, *v_current_max);
+ ImGuiSliderFlags min_flags = flags | ((min_min == min_max) ? ImGuiSliderFlags_ReadOnly : 0);
+ bool value_changed = DragInt("##min", v_current_min, v_speed, min_min, min_max, format, min_flags);
PopItemWidth();
SameLine(0, g.Style.ItemInnerSpacing.x);
- value_changed |= DragInt("##max", v_current_max, v_speed, (v_min >= v_max) ? *v_current_min : ImMax(v_min, *v_current_min), (v_min >= v_max) ? INT_MAX : v_max, format_max ? format_max : format);
+
+ int max_min = (v_min >= v_max) ? *v_current_min : ImMax(v_min, *v_current_min);
+ int max_max = (v_min >= v_max) ? INT_MAX : v_max;
+ ImGuiSliderFlags max_flags = flags | ((max_min == max_max) ? ImGuiSliderFlags_ReadOnly : 0);
+ value_changed |= DragInt("##max", v_current_max, v_speed, max_min, max_max, format_max ? format_max : format, max_flags);
PopItemWidth();
SameLine(0, g.Style.ItemInnerSpacing.x);
@@ -2259,6 +2544,8 @@ bool ImGui::DragIntRange2(const char* label, int* v_current_min, int* v_current_
//-------------------------------------------------------------------------
// [SECTION] Widgets: SliderScalar, SliderFloat, SliderInt, etc.
//-------------------------------------------------------------------------
+// - ScaleRatioFromValueT<> [Internal]
+// - ScaleValueFromRatioT<> [Internal]
// - SliderBehaviorT<>() [Internal]
// - SliderBehavior() [Internal]
// - SliderScalar()
@@ -2277,30 +2564,128 @@ bool ImGui::DragIntRange2(const char* label, int* v_current_min, int* v_current_
// - VSliderInt()
//-------------------------------------------------------------------------
-template<typename TYPE, typename FLOATTYPE>
-float ImGui::SliderCalcRatioFromValueT(ImGuiDataType data_type, TYPE v, TYPE v_min, TYPE v_max, float power, float linear_zero_pos)
+// Convert a value v in the output space of a slider into a parametric position on the slider itself (the logical opposite of ScaleValueFromRatioT)
+template<typename TYPE, typename SIGNEDTYPE, typename FLOATTYPE>
+float ImGui::ScaleRatioFromValueT(ImGuiDataType data_type, TYPE v, TYPE v_min, TYPE v_max, bool is_logarithmic, float logarithmic_zero_epsilon, float zero_deadzone_halfsize)
{
if (v_min == v_max)
return 0.0f;
+ IM_UNUSED(data_type);
- const bool is_power = (power != 1.0f) && (data_type == ImGuiDataType_Float || data_type == ImGuiDataType_Double);
const TYPE v_clamped = (v_min < v_max) ? ImClamp(v, v_min, v_max) : ImClamp(v, v_max, v_min);
- if (is_power)
+ if (is_logarithmic)
{
- if (v_clamped < 0.0f)
+ bool flipped = v_max < v_min;
+
+ if (flipped) // Handle the case where the range is backwards
+ ImSwap(v_min, v_max);
+
+ // Fudge min/max to avoid getting close to log(0)
+ FLOATTYPE v_min_fudged = (ImAbs((FLOATTYPE)v_min) < logarithmic_zero_epsilon) ? ((v_min < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_min;
+ FLOATTYPE v_max_fudged = (ImAbs((FLOATTYPE)v_max) < logarithmic_zero_epsilon) ? ((v_max < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_max;
+
+ // Awkward special cases - we need ranges of the form (-100 .. 0) to convert to (-100 .. -epsilon), not (-100 .. epsilon)
+ if ((v_min == 0.0f) && (v_max < 0.0f))
+ v_min_fudged = -logarithmic_zero_epsilon;
+ else if ((v_max == 0.0f) && (v_min < 0.0f))
+ v_max_fudged = -logarithmic_zero_epsilon;
+
+ float result;
+ if (v_clamped <= v_min_fudged)
+ result = 0.0f; // Workaround for values that are in-range but below our fudge
+ else if (v_clamped >= v_max_fudged)
+ result = 1.0f; // Workaround for values that are in-range but above our fudge
+ else if ((v_min * v_max) < 0.0f) // Range crosses zero, so split into two portions
{
- const float f = 1.0f - (float)((v_clamped - v_min) / (ImMin((TYPE)0, v_max) - v_min));
- return (1.0f - ImPow(f, 1.0f/power)) * linear_zero_pos;
+ float zero_point_center = (-(float)v_min) / ((float)v_max - (float)v_min); // The zero point in parametric space. There's an argument we should take the logarithmic nature into account when calculating this, but for now this should do (and the most common case of a symmetrical range works fine)
+ float zero_point_snap_L = zero_point_center - zero_deadzone_halfsize;
+ float zero_point_snap_R = zero_point_center + zero_deadzone_halfsize;
+ if (v == 0.0f)
+ result = zero_point_center; // Special case for exactly zero
+ else if (v < 0.0f)
+ result = (1.0f - (float)(ImLog(-(FLOATTYPE)v_clamped / logarithmic_zero_epsilon) / ImLog(-v_min_fudged / logarithmic_zero_epsilon))) * zero_point_snap_L;
+ else
+ result = zero_point_snap_R + ((float)(ImLog((FLOATTYPE)v_clamped / logarithmic_zero_epsilon) / ImLog(v_max_fudged / logarithmic_zero_epsilon)) * (1.0f - zero_point_snap_R));
}
+ else if ((v_min < 0.0f) || (v_max < 0.0f)) // Entirely negative slider
+ result = 1.0f - (float)(ImLog(-(FLOATTYPE)v_clamped / -v_max_fudged) / ImLog(-v_min_fudged / -v_max_fudged));
else
+ result = (float)(ImLog((FLOATTYPE)v_clamped / v_min_fudged) / ImLog(v_max_fudged / v_min_fudged));
+
+ return flipped ? (1.0f - result) : result;
+ }
+ else
+ {
+ // Linear slider
+ return (float)((FLOATTYPE)(SIGNEDTYPE)(v_clamped - v_min) / (FLOATTYPE)(SIGNEDTYPE)(v_max - v_min));
+ }
+}
+
+// Convert a parametric position on a slider into a value v in the output space (the logical opposite of ScaleRatioFromValueT)
+template<typename TYPE, typename SIGNEDTYPE, typename FLOATTYPE>
+TYPE ImGui::ScaleValueFromRatioT(ImGuiDataType data_type, float t, TYPE v_min, TYPE v_max, bool is_logarithmic, float logarithmic_zero_epsilon, float zero_deadzone_halfsize)
+{
+ // We special-case the extents because otherwise our logarithmic fudging can lead to "mathematically correct"
+ // but non-intuitive behaviors like a fully-left slider not actually reaching the minimum value. Also generally simpler.
+ if (t <= 0.0f || v_min == v_max)
+ return v_min;
+ if (t >= 1.0f)
+ return v_max;
+
+ TYPE result = (TYPE)0;
+ if (is_logarithmic)
+ {
+ // Fudge min/max to avoid getting silly results close to zero
+ FLOATTYPE v_min_fudged = (ImAbs((FLOATTYPE)v_min) < logarithmic_zero_epsilon) ? ((v_min < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_min;
+ FLOATTYPE v_max_fudged = (ImAbs((FLOATTYPE)v_max) < logarithmic_zero_epsilon) ? ((v_max < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_max;
+
+ const bool flipped = v_max < v_min; // Check if range is "backwards"
+ if (flipped)
+ ImSwap(v_min_fudged, v_max_fudged);
+
+ // Awkward special case - we need ranges of the form (-100 .. 0) to convert to (-100 .. -epsilon), not (-100 .. epsilon)
+ if ((v_max == 0.0f) && (v_min < 0.0f))
+ v_max_fudged = -logarithmic_zero_epsilon;
+
+ float t_with_flip = flipped ? (1.0f - t) : t; // t, but flipped if necessary to account for us flipping the range
+
+ if ((v_min * v_max) < 0.0f) // Range crosses zero, so we have to do this in two parts
+ {
+ float zero_point_center = (-(float)ImMin(v_min, v_max)) / ImAbs((float)v_max - (float)v_min); // The zero point in parametric space
+ float zero_point_snap_L = zero_point_center - zero_deadzone_halfsize;
+ float zero_point_snap_R = zero_point_center + zero_deadzone_halfsize;
+ if (t_with_flip >= zero_point_snap_L && t_with_flip <= zero_point_snap_R)
+ result = (TYPE)0.0f; // Special case to make getting exactly zero possible (the epsilon prevents it otherwise)
+ else if (t_with_flip < zero_point_center)
+ result = (TYPE)-(logarithmic_zero_epsilon * ImPow(-v_min_fudged / logarithmic_zero_epsilon, (FLOATTYPE)(1.0f - (t_with_flip / zero_point_snap_L))));
+ else
+ result = (TYPE)(logarithmic_zero_epsilon * ImPow(v_max_fudged / logarithmic_zero_epsilon, (FLOATTYPE)((t_with_flip - zero_point_snap_R) / (1.0f - zero_point_snap_R))));
+ }
+ else if ((v_min < 0.0f) || (v_max < 0.0f)) // Entirely negative slider
+ result = (TYPE)-(-v_max_fudged * ImPow(-v_min_fudged / -v_max_fudged, (FLOATTYPE)(1.0f - t_with_flip)));
+ else
+ result = (TYPE)(v_min_fudged * ImPow(v_max_fudged / v_min_fudged, (FLOATTYPE)t_with_flip));
+ }
+ else
+ {
+ // Linear slider
+ const bool is_floating_point = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double);
+ if (is_floating_point)
+ {
+ result = ImLerp(v_min, v_max, t);
+ }
+ else if (t < 1.0)
{
- const float f = (float)((v_clamped - ImMax((TYPE)0, v_min)) / (v_max - ImMax((TYPE)0, v_min)));
- return linear_zero_pos + ImPow(f, 1.0f/power) * (1.0f - linear_zero_pos);
+ // - For integer values we want the clicking position to match the grab box so we round above
+ // This code is carefully tuned to work with large values (e.g. high ranges of U64) while preserving this property..
+ // - Not doing a *1.0 multiply at the end of a range as it tends to be lossy. While absolute aiming at a large s64/u64
+ // range is going to be imprecise anyway, with this check we at least make the edge values matches expected limits.
+ FLOATTYPE v_new_off_f = (SIGNEDTYPE)(v_max - v_min) * t;
+ result = (TYPE)((SIGNEDTYPE)v_min + (SIGNEDTYPE)(v_new_off_f + (FLOATTYPE)(v_min > v_max ? -0.5 : 0.5)));
}
}
- // Linear slider
- return (float)((FLOATTYPE)(v_clamped - v_min) / (FLOATTYPE)(v_max - v_min));
+ return result;
}
namespace ImGui
@@ -2308,51 +2693,47 @@ namespace ImGui
float RoundScalarWithFormatFloat(const char* format, ImGuiDataType data_type, float v)
{
- return RoundScalarWithFormatT<float, float>(format, data_type, v);
+ return RoundScalarWithFormatT<float>(format, data_type, v);
}
float SliderCalcRatioFromValueFloat(ImGuiDataType data_type, float v, float v_min, float v_max, float power, float linear_zero_pos)
{
- return SliderCalcRatioFromValueT<float, float>(data_type, v, v_min, v_max, power, linear_zero_pos);
+ return ScaleRatioFromValueT<float, float, float>(data_type, v, v_min, v_max, false, power, linear_zero_pos);
}
} // namespace ImGui
-// FIXME: Move some of the code into SliderBehavior(). Current responsability is larger than what the equivalent DragBehaviorT<> does, we also do some rendering, etc.
+// FIXME: Try to move more of the code into shared SliderBehavior()
template<typename TYPE, typename SIGNEDTYPE, typename FLOATTYPE>
-bool ImGui::SliderBehaviorT(const ImRect& bb, ImGuiID id, ImGuiDataType data_type, TYPE* v, const TYPE v_min, const TYPE v_max, const char* format, float power, ImGuiSliderFlags flags, ImRect* out_grab_bb)
+bool ImGui::SliderBehaviorT(const ImRect& bb, ImGuiID id, ImGuiDataType data_type, TYPE* v, const TYPE v_min, const TYPE v_max, const char* format, ImGuiSliderFlags flags, ImRect* out_grab_bb)
{
ImGuiContext& g = *GImGui;
const ImGuiStyle& style = g.Style;
const ImGuiAxis axis = (flags & ImGuiSliderFlags_Vertical) ? ImGuiAxis_Y : ImGuiAxis_X;
- const bool is_decimal = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double);
- const bool is_power = (power != 1.0f) && is_decimal;
+ const bool is_logarithmic = (flags & ImGuiSliderFlags_Logarithmic) != 0;
+ const bool is_floating_point = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double);
+ const SIGNEDTYPE v_range = (v_min < v_max ? v_max - v_min : v_min - v_max);
- const float grab_padding = 2.0f;
+ // Calculate bounds
+ const float grab_padding = 2.0f; // FIXME: Should be part of style.
const float slider_sz = (bb.Max[axis] - bb.Min[axis]) - grab_padding * 2.0f;
float grab_sz = style.GrabMinSize;
- SIGNEDTYPE v_range = (v_min < v_max ? v_max - v_min : v_min - v_max);
- if (!is_decimal && v_range >= 0) // v_range < 0 may happen on integer overflows
- grab_sz = ImMax((float)(slider_sz / (v_range + 1)), style.GrabMinSize); // For integer sliders: if possible have the grab size represent 1 unit
+ if (!is_floating_point && v_range >= 0) // v_range < 0 may happen on integer overflows
+ grab_sz = ImMax((float)(slider_sz / (v_range + 1)), style.GrabMinSize); // For integer sliders: if possible have the grab size represent 1 unit
grab_sz = ImMin(grab_sz, slider_sz);
const float slider_usable_sz = slider_sz - grab_sz;
const float slider_usable_pos_min = bb.Min[axis] + grab_padding + grab_sz * 0.5f;
const float slider_usable_pos_max = bb.Max[axis] - grab_padding - grab_sz * 0.5f;
- // For power curve sliders that cross over sign boundary we want the curve to be symmetric around 0.0f
- float linear_zero_pos; // 0.0->1.0f
- if (is_power && v_min * v_max < 0.0f)
+ float logarithmic_zero_epsilon = 0.0f; // Only valid when is_logarithmic is true
+ float zero_deadzone_halfsize = 0.0f; // Only valid when is_logarithmic is true
+ if (is_logarithmic)
{
- // Different sign
- const FLOATTYPE linear_dist_min_to_0 = ImPow(v_min >= 0 ? (FLOATTYPE)v_min : -(FLOATTYPE)v_min, (FLOATTYPE)1.0f / power);
- const FLOATTYPE linear_dist_max_to_0 = ImPow(v_max >= 0 ? (FLOATTYPE)v_max : -(FLOATTYPE)v_max, (FLOATTYPE)1.0f / power);
- linear_zero_pos = (float)(linear_dist_min_to_0 / (linear_dist_min_to_0 + linear_dist_max_to_0));
- }
- else
- {
- // Same sign
- linear_zero_pos = v_min < 0.0f ? 1.0f : 0.0f;
+ // When using logarithmic sliders, we need to clamp to avoid hitting zero, but our choice of clamp value greatly affects slider precision. We attempt to use the specified precision to estimate a good lower bound.
+ const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, 3) : 1;
+ logarithmic_zero_epsilon = ImPow(0.1f, (float)decimal_precision);
+ zero_deadzone_halfsize = (style.LogSliderDeadzone * 0.5f) / ImMax(slider_usable_sz, 1.0f);
}
// Process interacting with the slider
@@ -2370,7 +2751,17 @@ bool ImGui::SliderBehaviorT(const ImRect& bb, ImGuiID id, ImGuiDataType data_typ
else
{
const float mouse_abs_pos = g.IO.MousePos[axis];
- clicked_t = (slider_usable_sz > 0.0f) ? ImClamp((mouse_abs_pos - slider_usable_pos_min) / slider_usable_sz, 0.0f, 1.0f) : 0.0f;
+ if (g.ActiveIdIsJustActivated)
+ {
+ float grab_t = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, *v, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
+ if (axis == ImGuiAxis_Y)
+ grab_t = 1.0f - grab_t;
+ const float grab_pos = ImLerp(slider_usable_pos_min, slider_usable_pos_max, grab_t);
+ const bool clicked_around_grab = (mouse_abs_pos >= grab_pos - grab_sz * 0.5f - 1.0f) && (mouse_abs_pos <= grab_pos + grab_sz * 0.5f + 1.0f); // No harm being extra generous here.
+ g.SliderGrabClickOffset = (clicked_around_grab && is_floating_point) ? mouse_abs_pos - grab_pos : 0.0f;
+ }
+ if (slider_usable_sz > 0.0f)
+ clicked_t = ImSaturate((mouse_abs_pos - g.SliderGrabClickOffset - slider_usable_pos_min) / slider_usable_sz);
if (axis == ImGuiAxis_Y)
clicked_t = 1.0f - clicked_t;
set_new_value = true;
@@ -2378,87 +2769,81 @@ bool ImGui::SliderBehaviorT(const ImRect& bb, ImGuiID id, ImGuiDataType data_typ
}
else if (g.ActiveIdSource == ImGuiInputSource_Nav)
{
- const ImVec2 delta2 = GetNavInputAmount2d(ImGuiNavDirSourceFlags_Keyboard | ImGuiNavDirSourceFlags_PadDPad, ImGuiInputReadMode_RepeatFast, 0.0f, 0.0f);
- float delta = (axis == ImGuiAxis_X) ? delta2.x : -delta2.y;
- if (g.NavActivatePressedId == id && !g.ActiveIdIsJustActivated)
+ if (g.ActiveIdIsJustActivated)
{
- ClearActiveID();
+ g.SliderCurrentAccum = 0.0f; // Reset any stored nav delta upon activation
+ g.SliderCurrentAccumDirty = false;
}
- else if (delta != 0.0f)
+
+ float input_delta = (axis == ImGuiAxis_X) ? GetNavTweakPressedAmount(axis) : -GetNavTweakPressedAmount(axis);
+ if (input_delta != 0.0f)
{
- clicked_t = SliderCalcRatioFromValueT<TYPE,FLOATTYPE>(data_type, *v, v_min, v_max, power, linear_zero_pos);
- const int decimal_precision = is_decimal ? ImParseFormatPrecision(format, 3) : 0;
- if ((decimal_precision > 0) || is_power)
+ const bool tweak_slow = IsKeyDown((g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakSlow : ImGuiKey_NavKeyboardTweakSlow);
+ const bool tweak_fast = IsKeyDown((g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakFast : ImGuiKey_NavKeyboardTweakFast);
+ const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, 3) : 0;
+ if (decimal_precision > 0)
{
- delta /= 100.0f; // Gamepad/keyboard tweak speeds in % of slider bounds
- if (IsNavInputDown(ImGuiNavInput_TweakSlow))
- delta /= 10.0f;
+ input_delta /= 100.0f; // Gamepad/keyboard tweak speeds in % of slider bounds
+ if (tweak_slow)
+ input_delta /= 10.0f;
}
else
{
- if ((v_range >= -100.0f && v_range <= 100.0f) || IsNavInputDown(ImGuiNavInput_TweakSlow))
- delta = ((delta < 0.0f) ? -1.0f : +1.0f) / (float)v_range; // Gamepad/keyboard tweak speeds in integer steps
+ if ((v_range >= -100.0f && v_range <= 100.0f) || tweak_slow)
+ input_delta = ((input_delta < 0.0f) ? -1.0f : +1.0f) / (float)v_range; // Gamepad/keyboard tweak speeds in integer steps
else
- delta /= 100.0f;
+ input_delta /= 100.0f;
}
- if (IsNavInputDown(ImGuiNavInput_TweakFast))
- delta *= 10.0f;
- set_new_value = true;
- if ((clicked_t >= 1.0f && delta > 0.0f) || (clicked_t <= 0.0f && delta < 0.0f)) // This is to avoid applying the saturation when already past the limits
- set_new_value = false;
- else
- clicked_t = ImSaturate(clicked_t + delta);
+ if (tweak_fast)
+ input_delta *= 10.0f;
+
+ g.SliderCurrentAccum += input_delta;
+ g.SliderCurrentAccumDirty = true;
}
- }
- if (set_new_value)
- {
- TYPE v_new;
- if (is_power)
+ float delta = g.SliderCurrentAccum;
+ if (g.NavActivatePressedId == id && !g.ActiveIdIsJustActivated)
{
- // Account for power curve scale on both sides of the zero
- if (clicked_t < linear_zero_pos)
- {
- // Negative: rescale to the negative range before powering
- float a = 1.0f - (clicked_t / linear_zero_pos);
- a = ImPow(a, power);
- v_new = ImLerp(ImMin(v_max, (TYPE)0), v_min, a);
- }
- else
- {
- // Positive: rescale to the positive range before powering
- float a;
- if (ImFabs(linear_zero_pos - 1.0f) > 1.e-6f)
- a = (clicked_t - linear_zero_pos) / (1.0f - linear_zero_pos);
- else
- a = clicked_t;
- a = ImPow(a, power);
- v_new = ImLerp(ImMax(v_min, (TYPE)0), v_max, a);
- }
+ ClearActiveID();
}
- else
+ else if (g.SliderCurrentAccumDirty)
{
- // Linear slider
- if (is_decimal)
+ clicked_t = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, *v, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
+
+ if ((clicked_t >= 1.0f && delta > 0.0f) || (clicked_t <= 0.0f && delta < 0.0f)) // This is to avoid applying the saturation when already past the limits
{
- v_new = ImLerp(v_min, v_max, clicked_t);
+ set_new_value = false;
+ g.SliderCurrentAccum = 0.0f; // If pushing up against the limits, don't continue to accumulate
}
else
{
- // For integer values we want the clicking position to match the grab box so we round above
- // This code is carefully tuned to work with large values (e.g. high ranges of U64) while preserving this property..
- FLOATTYPE v_new_off_f = (v_max - v_min) * clicked_t;
- TYPE v_new_off_floor = (TYPE)(v_new_off_f);
- TYPE v_new_off_round = (TYPE)(v_new_off_f + (FLOATTYPE)0.5);
- if (v_new_off_floor < v_new_off_round)
- v_new = v_min + v_new_off_round;
+ set_new_value = true;
+ float old_clicked_t = clicked_t;
+ clicked_t = ImSaturate(clicked_t + delta);
+
+ // Calculate what our "new" clicked_t will be, and thus how far we actually moved the slider, and subtract this from the accumulator
+ TYPE v_new = ScaleValueFromRatioT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, clicked_t, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
+ if (is_floating_point && !(flags & ImGuiSliderFlags_NoRoundToFormat))
+ v_new = RoundScalarWithFormatT<TYPE>(format, data_type, v_new);
+ float new_clicked_t = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, v_new, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
+
+ if (delta > 0)
+ g.SliderCurrentAccum -= ImMin(new_clicked_t - old_clicked_t, delta);
else
- v_new = v_min + v_new_off_floor;
+ g.SliderCurrentAccum -= ImMax(new_clicked_t - old_clicked_t, delta);
}
+
+ g.SliderCurrentAccumDirty = false;
}
+ }
+
+ if (set_new_value)
+ {
+ TYPE v_new = ScaleValueFromRatioT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, clicked_t, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
// Round to user desired precision based on format string
- v_new = RoundScalarWithFormatT<TYPE,SIGNEDTYPE>(format, data_type, v_new);
+ if (is_floating_point && !(flags & ImGuiSliderFlags_NoRoundToFormat))
+ v_new = RoundScalarWithFormatT<TYPE>(format, data_type, v_new);
// Apply result
if (*v != v_new)
@@ -2476,7 +2861,7 @@ bool ImGui::SliderBehaviorT(const ImRect& bb, ImGuiID id, ImGuiDataType data_typ
else
{
// Output grab position so it can be displayed by the caller
- float grab_t = SliderCalcRatioFromValueT<TYPE, FLOATTYPE>(data_type, *v, v_min, v_max, power, linear_zero_pos);
+ float grab_t = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, *v, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
if (axis == ImGuiAxis_Y)
grab_t = 1.0f - grab_t;
const float grab_pos = ImLerp(slider_usable_pos_min, slider_usable_pos_max, grab_t);
@@ -2489,35 +2874,43 @@ bool ImGui::SliderBehaviorT(const ImRect& bb, ImGuiID id, ImGuiDataType data_typ
return value_changed;
}
-// For 32-bits and larger types, slider bounds are limited to half the natural type range.
+// For 32-bit and larger types, slider bounds are limited to half the natural type range.
// So e.g. an integer Slider between INT_MAX-10 and INT_MAX will fail, but an integer Slider between INT_MAX/2-10 and INT_MAX/2 will be ok.
// It would be possible to lift that limitation with some work but it doesn't seem to be worth it for sliders.
-bool ImGui::SliderBehavior(const ImRect& bb, ImGuiID id, ImGuiDataType data_type, void* p_v, const void* p_min, const void* p_max, const char* format, float power, ImGuiSliderFlags flags, ImRect* out_grab_bb)
+bool ImGui::SliderBehavior(const ImRect& bb, ImGuiID id, ImGuiDataType data_type, void* p_v, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags, ImRect* out_grab_bb)
{
+ // Read imgui.cpp "API BREAKING CHANGES" section for 1.78 if you hit this assert.
+ IM_ASSERT((flags == 1 || (flags & ImGuiSliderFlags_InvalidMask_) == 0) && "Invalid ImGuiSliderFlags flag! Has the 'float power' argument been mistakenly cast to flags? Call function with ImGuiSliderFlags_Logarithmic flags instead.");
+
+ // Those are the things we can do easily outside the SliderBehaviorT<> template, saves code generation.
+ ImGuiContext& g = *GImGui;
+ if ((g.LastItemData.InFlags & ImGuiItemFlags_ReadOnly) || (flags & ImGuiSliderFlags_ReadOnly))
+ return false;
+
switch (data_type)
{
- case ImGuiDataType_S8: { ImS32 v32 = (ImS32)*(ImS8*)p_v; bool r = SliderBehaviorT<ImS32, ImS32, float>(bb, id, ImGuiDataType_S32, &v32, *(const ImS8*)p_min, *(const ImS8*)p_max, format, power, flags, out_grab_bb); if (r) *(ImS8*)p_v = (ImS8)v32; return r; }
- case ImGuiDataType_U8: { ImU32 v32 = (ImU32)*(ImU8*)p_v; bool r = SliderBehaviorT<ImU32, ImS32, float>(bb, id, ImGuiDataType_U32, &v32, *(const ImU8*)p_min, *(const ImU8*)p_max, format, power, flags, out_grab_bb); if (r) *(ImU8*)p_v = (ImU8)v32; return r; }
- case ImGuiDataType_S16: { ImS32 v32 = (ImS32)*(ImS16*)p_v; bool r = SliderBehaviorT<ImS32, ImS32, float>(bb, id, ImGuiDataType_S32, &v32, *(const ImS16*)p_min, *(const ImS16*)p_max, format, power, flags, out_grab_bb); if (r) *(ImS16*)p_v = (ImS16)v32; return r; }
- case ImGuiDataType_U16: { ImU32 v32 = (ImU32)*(ImU16*)p_v; bool r = SliderBehaviorT<ImU32, ImS32, float>(bb, id, ImGuiDataType_U32, &v32, *(const ImU16*)p_min, *(const ImU16*)p_max, format, power, flags, out_grab_bb); if (r) *(ImU16*)p_v = (ImU16)v32; return r; }
+ case ImGuiDataType_S8: { ImS32 v32 = (ImS32)*(ImS8*)p_v; bool r = SliderBehaviorT<ImS32, ImS32, float>(bb, id, ImGuiDataType_S32, &v32, *(const ImS8*)p_min, *(const ImS8*)p_max, format, flags, out_grab_bb); if (r) *(ImS8*)p_v = (ImS8)v32; return r; }
+ case ImGuiDataType_U8: { ImU32 v32 = (ImU32)*(ImU8*)p_v; bool r = SliderBehaviorT<ImU32, ImS32, float>(bb, id, ImGuiDataType_U32, &v32, *(const ImU8*)p_min, *(const ImU8*)p_max, format, flags, out_grab_bb); if (r) *(ImU8*)p_v = (ImU8)v32; return r; }
+ case ImGuiDataType_S16: { ImS32 v32 = (ImS32)*(ImS16*)p_v; bool r = SliderBehaviorT<ImS32, ImS32, float>(bb, id, ImGuiDataType_S32, &v32, *(const ImS16*)p_min, *(const ImS16*)p_max, format, flags, out_grab_bb); if (r) *(ImS16*)p_v = (ImS16)v32; return r; }
+ case ImGuiDataType_U16: { ImU32 v32 = (ImU32)*(ImU16*)p_v; bool r = SliderBehaviorT<ImU32, ImS32, float>(bb, id, ImGuiDataType_U32, &v32, *(const ImU16*)p_min, *(const ImU16*)p_max, format, flags, out_grab_bb); if (r) *(ImU16*)p_v = (ImU16)v32; return r; }
case ImGuiDataType_S32:
- IM_ASSERT(*(const ImS32*)p_min >= IM_S32_MIN/2 && *(const ImS32*)p_max <= IM_S32_MAX/2);
- return SliderBehaviorT<ImS32, ImS32, float >(bb, id, data_type, (ImS32*)p_v, *(const ImS32*)p_min, *(const ImS32*)p_max, format, power, flags, out_grab_bb);
+ IM_ASSERT(*(const ImS32*)p_min >= IM_S32_MIN / 2 && *(const ImS32*)p_max <= IM_S32_MAX / 2);
+ return SliderBehaviorT<ImS32, ImS32, float >(bb, id, data_type, (ImS32*)p_v, *(const ImS32*)p_min, *(const ImS32*)p_max, format, flags, out_grab_bb);
case ImGuiDataType_U32:
- IM_ASSERT(*(const ImU32*)p_max <= IM_U32_MAX/2);
- return SliderBehaviorT<ImU32, ImS32, float >(bb, id, data_type, (ImU32*)p_v, *(const ImU32*)p_min, *(const ImU32*)p_max, format, power, flags, out_grab_bb);
+ IM_ASSERT(*(const ImU32*)p_max <= IM_U32_MAX / 2);
+ return SliderBehaviorT<ImU32, ImS32, float >(bb, id, data_type, (ImU32*)p_v, *(const ImU32*)p_min, *(const ImU32*)p_max, format, flags, out_grab_bb);
case ImGuiDataType_S64:
- IM_ASSERT(*(const ImS64*)p_min >= IM_S64_MIN/2 && *(const ImS64*)p_max <= IM_S64_MAX/2);
- return SliderBehaviorT<ImS64, ImS64, double>(bb, id, data_type, (ImS64*)p_v, *(const ImS64*)p_min, *(const ImS64*)p_max, format, power, flags, out_grab_bb);
+ IM_ASSERT(*(const ImS64*)p_min >= IM_S64_MIN / 2 && *(const ImS64*)p_max <= IM_S64_MAX / 2);
+ return SliderBehaviorT<ImS64, ImS64, double>(bb, id, data_type, (ImS64*)p_v, *(const ImS64*)p_min, *(const ImS64*)p_max, format, flags, out_grab_bb);
case ImGuiDataType_U64:
- IM_ASSERT(*(const ImU64*)p_max <= IM_U64_MAX/2);
- return SliderBehaviorT<ImU64, ImS64, double>(bb, id, data_type, (ImU64*)p_v, *(const ImU64*)p_min, *(const ImU64*)p_max, format, power, flags, out_grab_bb);
+ IM_ASSERT(*(const ImU64*)p_max <= IM_U64_MAX / 2);
+ return SliderBehaviorT<ImU64, ImS64, double>(bb, id, data_type, (ImU64*)p_v, *(const ImU64*)p_min, *(const ImU64*)p_max, format, flags, out_grab_bb);
case ImGuiDataType_Float:
- IM_ASSERT(*(const float*)p_min >= -FLT_MAX/2.0f && *(const float*)p_max <= FLT_MAX/2.0f);
- return SliderBehaviorT<float, float, float >(bb, id, data_type, (float*)p_v, *(const float*)p_min, *(const float*)p_max, format, power, flags, out_grab_bb);
+ IM_ASSERT(*(const float*)p_min >= -FLT_MAX / 2.0f && *(const float*)p_max <= FLT_MAX / 2.0f);
+ return SliderBehaviorT<float, float, float >(bb, id, data_type, (float*)p_v, *(const float*)p_min, *(const float*)p_max, format, flags, out_grab_bb);
case ImGuiDataType_Double:
- IM_ASSERT(*(const double*)p_min >= -DBL_MAX/2.0f && *(const double*)p_max <= DBL_MAX/2.0f);
- return SliderBehaviorT<double,double,double>(bb, id, data_type, (double*)p_v, *(const double*)p_min, *(const double*)p_max, format, power, flags, out_grab_bb);
+ IM_ASSERT(*(const double*)p_min >= -DBL_MAX / 2.0f && *(const double*)p_max <= DBL_MAX / 2.0f);
+ return SliderBehaviorT<double, double, double>(bb, id, data_type, (double*)p_v, *(const double*)p_min, *(const double*)p_max, format, flags, out_grab_bb);
case ImGuiDataType_COUNT: break;
}
IM_ASSERT(0);
@@ -2526,7 +2919,7 @@ bool ImGui::SliderBehavior(const ImRect& bb, ImGuiID id, ImGuiDataType data_type
// Note: p_data, p_min and p_max are _pointers_ to a memory address holding the data. For a slider, they are all required.
// Read code of e.g. SliderFloat(), SliderInt() etc. or examples in 'Demo->Widgets->Data Types' to understand how to use this function directly.
-bool ImGui::SliderScalar(const char* label, ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max, const char* format, float power)
+bool ImGui::SliderScalar(const char* label, ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags)
{
ImGuiWindow* window = GetCurrentWindow();
if (window->SkipItems)
@@ -2538,51 +2931,56 @@ bool ImGui::SliderScalar(const char* label, ImGuiDataType data_type, void* p_dat
const float w = CalcItemWidth();
const ImVec2 label_size = CalcTextSize(label, NULL, true);
- const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y*2.0f));
+ 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));
+ const bool temp_input_allowed = (flags & ImGuiSliderFlags_NoInput) == 0;
ItemSize(total_bb, style.FramePadding.y);
- if (!ItemAdd(total_bb, id, &frame_bb))
+ if (!ItemAdd(total_bb, id, &frame_bb, temp_input_allowed ? ImGuiItemFlags_Inputable : 0))
return false;
// Default format string when passing NULL
if (format == NULL)
format = DataTypeGetInfo(data_type)->PrintFmt;
- else if (data_type == ImGuiDataType_S32 && strcmp(format, "%d") != 0) // (FIXME-LEGACY: Patch old "%.0f" format string to use "%d", read function more details.)
- format = PatchFormatStringFloatToInt(format);
- // Tabbing or CTRL-clicking on Slider turns it into an input box
const bool hovered = ItemHoverable(frame_bb, id);
- bool temp_input_is_active = TempInputTextIsActive(id);
- bool temp_input_start = false;
+ bool temp_input_is_active = temp_input_allowed && TempInputIsActive(id);
if (!temp_input_is_active)
{
- const bool focus_requested = FocusableItemRegister(window, id);
- const bool clicked = (hovered && g.IO.MouseClicked[0]);
- if (focus_requested || clicked || g.NavActivateId == id || g.NavInputId == id)
+ // Tabbing or CTRL-clicking on Slider turns it into an input box
+ const bool input_requested_by_tabbing = temp_input_allowed && (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_FocusedByTabbing) != 0;
+ const bool clicked = hovered && IsMouseClicked(0, id);
+ const bool make_active = (input_requested_by_tabbing || clicked || g.NavActivateId == id || g.NavActivateInputId == id);
+ if (make_active && clicked)
+ SetKeyOwner(ImGuiKey_MouseLeft, id);
+ if (make_active && temp_input_allowed)
+ if (input_requested_by_tabbing || (clicked && g.IO.KeyCtrl) || g.NavActivateInputId == id)
+ temp_input_is_active = true;
+
+ if (make_active && !temp_input_is_active)
{
SetActiveID(id, window);
SetFocusID(id, window);
FocusWindow(window);
g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right);
- if (focus_requested || (clicked && g.IO.KeyCtrl) || g.NavInputId == id)
- {
- temp_input_start = true;
- FocusableItemUnregister(window);
- }
}
}
- if (temp_input_is_active || temp_input_start)
- return TempInputTextScalar(frame_bb, id, label, data_type, p_data, format);
+
+ if (temp_input_is_active)
+ {
+ // Only clamp CTRL+Click input when ImGuiSliderFlags_AlwaysClamp is set
+ const bool is_clamp_input = (flags & ImGuiSliderFlags_AlwaysClamp) != 0;
+ return TempInputScalar(frame_bb, id, label, data_type, p_data, format, is_clamp_input ? p_min : NULL, is_clamp_input ? p_max : NULL);
+ }
// Draw frame
- const ImU32 frame_col = GetColorU32(g.ActiveId == id ? ImGuiCol_FrameBgActive : g.HoveredId == id ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
+ const ImU32 frame_col = GetColorU32(g.ActiveId == id ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
RenderNavHighlight(frame_bb, id);
RenderFrame(frame_bb.Min, frame_bb.Max, frame_col, true, g.Style.FrameRounding);
// Slider behavior
ImRect grab_bb;
- const bool value_changed = SliderBehavior(frame_bb, id, data_type, p_data, p_min, p_max, format, power, ImGuiSliderFlags_None, &grab_bb);
+ const bool value_changed = SliderBehavior(frame_bb, id, data_type, p_data, p_min, p_max, format, flags, &grab_bb);
if (value_changed)
MarkItemEdited(id);
@@ -2593,17 +2991,19 @@ bool ImGui::SliderScalar(const char* label, ImGuiDataType data_type, void* p_dat
// Display value using user-provided display format so user can add prefix/suffix/decorations to the value.
char value_buf[64];
const char* value_buf_end = value_buf + DataTypeFormatString(value_buf, IM_ARRAYSIZE(value_buf), data_type, p_data, format);
- RenderTextClipped(frame_bb.Min, frame_bb.Max, value_buf, value_buf_end, NULL, ImVec2(0.5f,0.5f));
+ if (g.LogEnabled)
+ LogSetNextTextDecoration("{", "}");
+ RenderTextClipped(frame_bb.Min, frame_bb.Max, value_buf, value_buf_end, NULL, ImVec2(0.5f, 0.5f));
if (label_size.x > 0.0f)
RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label);
- IMGUI_TEST_ENGINE_ITEM_INFO(id, label, window->DC.ItemFlags);
+ IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
return value_changed;
}
// Add multiple sliders on 1 line for compact edition of multiple components
-bool ImGui::SliderScalarN(const char* label, ImGuiDataType data_type, void* v, int components, const void* v_min, const void* v_max, const char* format, float power)
+bool ImGui::SliderScalarN(const char* label, ImGuiDataType data_type, void* v, int components, const void* v_min, const void* v_max, const char* format, ImGuiSliderFlags flags)
{
ImGuiWindow* window = GetCurrentWindow();
if (window->SkipItems)
@@ -2620,7 +3020,7 @@ bool ImGui::SliderScalarN(const char* label, ImGuiDataType data_type, void* v, i
PushID(i);
if (i > 0)
SameLine(0, g.Style.ItemInnerSpacing.x);
- value_changed |= SliderScalar("", data_type, v, v_min, v_max, format, power);
+ value_changed |= SliderScalar("", data_type, v, v_min, v_max, format, flags);
PopID();
PopItemWidth();
v = (void*)((char*)v + type_size);
@@ -2638,57 +3038,57 @@ bool ImGui::SliderScalarN(const char* label, ImGuiDataType data_type, void* v, i
return value_changed;
}
-bool ImGui::SliderFloat(const char* label, float* v, float v_min, float v_max, const char* format, float power)
+bool ImGui::SliderFloat(const char* label, float* v, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
{
- return SliderScalar(label, ImGuiDataType_Float, v, &v_min, &v_max, format, power);
+ return SliderScalar(label, ImGuiDataType_Float, v, &v_min, &v_max, format, flags);
}
-bool ImGui::SliderFloat2(const char* label, float v[2], float v_min, float v_max, const char* format, float power)
+bool ImGui::SliderFloat2(const char* label, float v[2], float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
{
- return SliderScalarN(label, ImGuiDataType_Float, v, 2, &v_min, &v_max, format, power);
+ return SliderScalarN(label, ImGuiDataType_Float, v, 2, &v_min, &v_max, format, flags);
}
-bool ImGui::SliderFloat3(const char* label, float v[3], float v_min, float v_max, const char* format, float power)
+bool ImGui::SliderFloat3(const char* label, float v[3], float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
{
- return SliderScalarN(label, ImGuiDataType_Float, v, 3, &v_min, &v_max, format, power);
+ return SliderScalarN(label, ImGuiDataType_Float, v, 3, &v_min, &v_max, format, flags);
}
-bool ImGui::SliderFloat4(const char* label, float v[4], float v_min, float v_max, const char* format, float power)
+bool ImGui::SliderFloat4(const char* label, float v[4], float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
{
- return SliderScalarN(label, ImGuiDataType_Float, v, 4, &v_min, &v_max, format, power);
+ return SliderScalarN(label, ImGuiDataType_Float, v, 4, &v_min, &v_max, format, flags);
}
-bool ImGui::SliderAngle(const char* label, float* v_rad, float v_degrees_min, float v_degrees_max, const char* format)
+bool ImGui::SliderAngle(const char* label, float* v_rad, float v_degrees_min, float v_degrees_max, const char* format, ImGuiSliderFlags flags)
{
if (format == NULL)
format = "%.0f deg";
- float v_deg = (*v_rad) * 360.0f / (2*IM_PI);
- bool value_changed = SliderFloat(label, &v_deg, v_degrees_min, v_degrees_max, format, 1.0f);
- *v_rad = v_deg * (2*IM_PI) / 360.0f;
+ float v_deg = (*v_rad) * 360.0f / (2 * IM_PI);
+ bool value_changed = SliderFloat(label, &v_deg, v_degrees_min, v_degrees_max, format, flags);
+ *v_rad = v_deg * (2 * IM_PI) / 360.0f;
return value_changed;
}
-bool ImGui::SliderInt(const char* label, int* v, int v_min, int v_max, const char* format)
+bool ImGui::SliderInt(const char* label, int* v, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
{
- return SliderScalar(label, ImGuiDataType_S32, v, &v_min, &v_max, format);
+ return SliderScalar(label, ImGuiDataType_S32, v, &v_min, &v_max, format, flags);
}
-bool ImGui::SliderInt2(const char* label, int v[2], int v_min, int v_max, const char* format)
+bool ImGui::SliderInt2(const char* label, int v[2], int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
{
- return SliderScalarN(label, ImGuiDataType_S32, v, 2, &v_min, &v_max, format);
+ return SliderScalarN(label, ImGuiDataType_S32, v, 2, &v_min, &v_max, format, flags);
}
-bool ImGui::SliderInt3(const char* label, int v[3], int v_min, int v_max, const char* format)
+bool ImGui::SliderInt3(const char* label, int v[3], int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
{
- return SliderScalarN(label, ImGuiDataType_S32, v, 3, &v_min, &v_max, format);
+ return SliderScalarN(label, ImGuiDataType_S32, v, 3, &v_min, &v_max, format, flags);
}
-bool ImGui::SliderInt4(const char* label, int v[4], int v_min, int v_max, const char* format)
+bool ImGui::SliderInt4(const char* label, int v[4], int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
{
- return SliderScalarN(label, ImGuiDataType_S32, v, 4, &v_min, &v_max, format);
+ return SliderScalarN(label, ImGuiDataType_S32, v, 4, &v_min, &v_max, format, flags);
}
-bool ImGui::VSliderScalar(const char* label, const ImVec2& size, ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max, const char* format, float power)
+bool ImGui::VSliderScalar(const char* label, const ImVec2& size, ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags)
{
ImGuiWindow* window = GetCurrentWindow();
if (window->SkipItems)
@@ -2709,12 +3109,13 @@ bool ImGui::VSliderScalar(const char* label, const ImVec2& size, ImGuiDataType d
// Default format string when passing NULL
if (format == NULL)
format = DataTypeGetInfo(data_type)->PrintFmt;
- else if (data_type == ImGuiDataType_S32 && strcmp(format, "%d") != 0) // (FIXME-LEGACY: Patch old "%.0f" format string to use "%d", read function more details.)
- format = PatchFormatStringFloatToInt(format);
const bool hovered = ItemHoverable(frame_bb, id);
- if ((hovered && g.IO.MouseClicked[0]) || g.NavActivateId == id || g.NavInputId == id)
+ const bool clicked = hovered && IsMouseClicked(0, id);
+ if (clicked || g.NavActivateId == id || g.NavActivateInputId == id)
{
+ if (clicked)
+ SetKeyOwner(ImGuiKey_MouseLeft, id);
SetActiveID(id, window);
SetFocusID(id, window);
FocusWindow(window);
@@ -2722,13 +3123,13 @@ bool ImGui::VSliderScalar(const char* label, const ImVec2& size, ImGuiDataType d
}
// Draw frame
- const ImU32 frame_col = GetColorU32(g.ActiveId == id ? ImGuiCol_FrameBgActive : g.HoveredId == id ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
+ const ImU32 frame_col = GetColorU32(g.ActiveId == id ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
RenderNavHighlight(frame_bb, id);
RenderFrame(frame_bb.Min, frame_bb.Max, frame_col, true, g.Style.FrameRounding);
// Slider behavior
ImRect grab_bb;
- const bool value_changed = SliderBehavior(frame_bb, id, data_type, p_data, p_min, p_max, format, power, ImGuiSliderFlags_Vertical, &grab_bb);
+ const bool value_changed = SliderBehavior(frame_bb, id, data_type, p_data, p_min, p_max, format, flags | ImGuiSliderFlags_Vertical, &grab_bb);
if (value_changed)
MarkItemEdited(id);
@@ -2740,21 +3141,21 @@ bool ImGui::VSliderScalar(const char* label, const ImVec2& size, ImGuiDataType d
// For the vertical slider we allow centered text to overlap the frame padding
char value_buf[64];
const char* value_buf_end = value_buf + DataTypeFormatString(value_buf, IM_ARRAYSIZE(value_buf), data_type, p_data, format);
- RenderTextClipped(ImVec2(frame_bb.Min.x, frame_bb.Min.y + style.FramePadding.y), frame_bb.Max, value_buf, value_buf_end, NULL, ImVec2(0.5f,0.0f));
+ RenderTextClipped(ImVec2(frame_bb.Min.x, frame_bb.Min.y + style.FramePadding.y), frame_bb.Max, value_buf, value_buf_end, NULL, ImVec2(0.5f, 0.0f));
if (label_size.x > 0.0f)
RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label);
return value_changed;
}
-bool ImGui::VSliderFloat(const char* label, const ImVec2& size, float* v, float v_min, float v_max, const char* format, float power)
+bool ImGui::VSliderFloat(const char* label, const ImVec2& size, float* v, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
{
- return VSliderScalar(label, size, ImGuiDataType_Float, v, &v_min, &v_max, format, power);
+ return VSliderScalar(label, size, ImGuiDataType_Float, v, &v_min, &v_max, format, flags);
}
-bool ImGui::VSliderInt(const char* label, const ImVec2& size, int* v, int v_min, int v_max, const char* format)
+bool ImGui::VSliderInt(const char* label, const ImVec2& size, int* v, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
{
- return VSliderScalar(label, size, ImGuiDataType_S32, v, &v_min, &v_max, format);
+ return VSliderScalar(label, size, ImGuiDataType_S32, v, &v_min, &v_max, format, flags);
}
//-------------------------------------------------------------------------
@@ -2763,6 +3164,8 @@ bool ImGui::VSliderInt(const char* label, const ImVec2& size, int* v, int v_min,
// - ImParseFormatFindStart() [Internal]
// - ImParseFormatFindEnd() [Internal]
// - ImParseFormatTrimDecorations() [Internal]
+// - ImParseFormatSanitizeForPrinting() [Internal]
+// - ImParseFormatSanitizeForScanning() [Internal]
// - ImParseFormatPrecision() [Internal]
// - TempInputTextScalar() [Internal]
// - InputScalar()
@@ -2826,6 +3229,57 @@ const char* ImParseFormatTrimDecorations(const char* fmt, char* buf, size_t buf_
return buf;
}
+// Sanitize format
+// - Zero terminate so extra characters after format (e.g. "%f123") don't confuse atof/atoi
+// - stb_sprintf.h supports several new modifiers which format numbers in a way that also makes them incompatible atof/atoi.
+void ImParseFormatSanitizeForPrinting(const char* fmt_in, char* fmt_out, size_t fmt_out_size)
+{
+ const char* fmt_end = ImParseFormatFindEnd(fmt_in);
+ IM_UNUSED(fmt_out_size);
+ IM_ASSERT((size_t)(fmt_end - fmt_in + 1) < fmt_out_size); // Format is too long, let us know if this happens to you!
+ while (fmt_in < fmt_end)
+ {
+ char c = *fmt_in++;
+ if (c != '\'' && c != '$' && c != '_') // Custom flags provided by stb_sprintf.h. POSIX 2008 also supports '.
+ *(fmt_out++) = c;
+ }
+ *fmt_out = 0; // Zero-terminate
+}
+
+// - For scanning we need to remove all width and precision fields "%3.7f" -> "%f". BUT don't strip types like "%I64d" which includes digits. ! "%07I64d" -> "%I64d"
+const char* ImParseFormatSanitizeForScanning(const char* fmt_in, char* fmt_out, size_t fmt_out_size)
+{
+ const char* fmt_end = ImParseFormatFindEnd(fmt_in);
+ const char* fmt_out_begin = fmt_out;
+ IM_UNUSED(fmt_out_size);
+ IM_ASSERT((size_t)(fmt_end - fmt_in + 1) < fmt_out_size); // Format is too long, let us know if this happens to you!
+ bool has_type = false;
+ while (fmt_in < fmt_end)
+ {
+ char c = *fmt_in++;
+ if (!has_type && ((c >= '0' && c <= '9') || c == '.'))
+ continue;
+ has_type |= ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')); // Stop skipping digits
+ if (c != '\'' && c != '$' && c != '_') // Custom flags provided by stb_sprintf.h. POSIX 2008 also supports '.
+ *(fmt_out++) = c;
+ }
+ *fmt_out = 0; // Zero-terminate
+ return fmt_out_begin;
+}
+
+template<typename TYPE>
+static const char* ImAtoi(const char* src, TYPE* output)
+{
+ int negative = 0;
+ if (*src == '-') { negative = 1; src++; }
+ if (*src == '+') { src++; }
+ TYPE v = 0;
+ while (*src >= '0' && *src <= '9')
+ v = (v * 10) + (*src++ - '0');
+ *output = negative ? -v : v;
+ return src;
+}
+
// Parse display precision back from the display format string
// FIXME: This is still used by some navigation code path to infer a minimum tweak step, but we should aim to rework widgets so it isn't needed.
int ImParseFormatPrecision(const char* fmt, int default_precision)
@@ -2852,35 +3306,67 @@ int ImParseFormatPrecision(const char* fmt, int default_precision)
// Create text input in place of another active widget (e.g. used when doing a CTRL+Click on drag/slider widgets)
// FIXME: Facilitate using this in variety of other situations.
-bool ImGui::TempInputTextScalar(const ImRect& bb, ImGuiID id, const char* label, ImGuiDataType data_type, void* p_data, const char* format)
+bool ImGui::TempInputText(const ImRect& bb, ImGuiID id, const char* label, char* buf, int buf_size, ImGuiInputTextFlags flags)
{
- ImGuiContext& g = *GImGui;
-
// On the first frame, g.TempInputTextId == 0, then on subsequent frames it becomes == id.
// We clear ActiveID on the first frame to allow the InputText() taking it back.
- const bool init = (g.TempInputTextId != id);
+ ImGuiContext& g = *GImGui;
+ const bool init = (g.TempInputId != id);
if (init)
ClearActiveID();
+ g.CurrentWindow->DC.CursorPos = bb.Min;
+ bool value_changed = InputTextEx(label, NULL, buf, buf_size, bb.GetSize(), flags | ImGuiInputTextFlags_MergedItem);
+ if (init)
+ {
+ // First frame we started displaying the InputText widget, we expect it to take the active id.
+ IM_ASSERT(g.ActiveId == id);
+ g.TempInputId = g.ActiveId;
+ }
+ return value_changed;
+}
+
+static inline ImGuiInputTextFlags InputScalar_DefaultCharsFilter(ImGuiDataType data_type, const char* format)
+{
+ if (data_type == ImGuiDataType_Float || data_type == ImGuiDataType_Double)
+ return ImGuiInputTextFlags_CharsScientific;
+ const char format_last_char = format[0] ? format[strlen(format) - 1] : 0;
+ return (format_last_char == 'x' || format_last_char == 'X') ? ImGuiInputTextFlags_CharsHexadecimal : ImGuiInputTextFlags_CharsDecimal;
+}
+
+// Note that Drag/Slider functions are only forwarding the min/max values clamping values if the ImGuiSliderFlags_AlwaysClamp flag is set!
+// This is intended: this way we allow CTRL+Click manual input to set a value out of bounds, for maximum flexibility.
+// However this may not be ideal for all uses, as some user code may break on out of bound values.
+bool ImGui::TempInputScalar(const ImRect& bb, ImGuiID id, const char* label, ImGuiDataType data_type, void* p_data, const char* format, const void* p_clamp_min, const void* p_clamp_max)
+{
char fmt_buf[32];
char data_buf[32];
format = ImParseFormatTrimDecorations(format, fmt_buf, IM_ARRAYSIZE(fmt_buf));
DataTypeFormatString(data_buf, IM_ARRAYSIZE(data_buf), data_type, p_data, format);
ImStrTrimBlanks(data_buf);
- g.CurrentWindow->DC.CursorPos = bb.Min;
ImGuiInputTextFlags flags = ImGuiInputTextFlags_AutoSelectAll | ImGuiInputTextFlags_NoMarkEdited;
- flags |= ((data_type == ImGuiDataType_Float || data_type == ImGuiDataType_Double) ? ImGuiInputTextFlags_CharsScientific : ImGuiInputTextFlags_CharsDecimal);
- bool value_changed = InputTextEx(label, NULL, data_buf, IM_ARRAYSIZE(data_buf), bb.GetSize(), flags);
- if (init)
- {
- // First frame we started displaying the InputText widget, we expect it to take the active id.
- IM_ASSERT(g.ActiveId == id);
- g.TempInputTextId = g.ActiveId;
- }
- if (value_changed)
+ flags |= InputScalar_DefaultCharsFilter(data_type, format);
+
+ bool value_changed = false;
+ if (TempInputText(bb, id, label, data_buf, IM_ARRAYSIZE(data_buf), flags))
{
- value_changed = DataTypeApplyOpFromText(data_buf, g.InputTextState.InitialTextA.Data, data_type, p_data, NULL);
+ // Backup old value
+ size_t data_type_size = DataTypeGetInfo(data_type)->Size;
+ ImGuiDataTypeTempStorage data_backup;
+ memcpy(&data_backup, p_data, data_type_size);
+
+ // Apply new value (or operations) then clamp
+ DataTypeApplyFromText(data_buf, data_type, p_data, format);
+ if (p_clamp_min || p_clamp_max)
+ {
+ if (p_clamp_min && p_clamp_max && DataTypeCompare(data_type, p_clamp_min, p_clamp_max) > 0)
+ ImSwap(p_clamp_min, p_clamp_max);
+ DataTypeClamp(data_type, p_data, p_clamp_min, p_clamp_max);
+ }
+
+ // Only mark as edited if new value is different
+ value_changed = memcmp(&data_backup, p_data, data_type_size) != 0;
if (value_changed)
MarkItemEdited(id);
}
@@ -2904,12 +3390,12 @@ bool ImGui::InputScalar(const char* label, ImGuiDataType data_type, void* p_data
char buf[64];
DataTypeFormatString(buf, IM_ARRAYSIZE(buf), data_type, p_data, format);
- bool value_changed = false;
- if ((flags & (ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_CharsScientific)) == 0)
- flags |= ImGuiInputTextFlags_CharsDecimal;
- flags |= ImGuiInputTextFlags_AutoSelectAll;
- flags |= ImGuiInputTextFlags_NoMarkEdited; // We call MarkItemEdited() ourselve by comparing the actual data rather than the string.
+ // Testing ActiveId as a minor optimization as filtering is not needed until active
+ if (g.ActiveId == 0 && (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_CharsScientific)) == 0)
+ flags |= InputScalar_DefaultCharsFilter(data_type, format);
+ flags |= ImGuiInputTextFlags_AutoSelectAll | ImGuiInputTextFlags_NoMarkEdited; // We call MarkItemEdited() ourselves by comparing the actual data rather than the string.
+ bool value_changed = false;
if (p_step != NULL)
{
const float button_size = GetFrameHeight();
@@ -2918,14 +3404,15 @@ bool ImGui::InputScalar(const char* label, ImGuiDataType data_type, void* p_data
PushID(label);
SetNextItemWidth(ImMax(1.0f, CalcItemWidth() - (button_size + style.ItemInnerSpacing.x) * 2));
if (InputText("", buf, IM_ARRAYSIZE(buf), flags)) // PushId(label) + "" gives us the expected ID from outside point of view
- value_changed = DataTypeApplyOpFromText(buf, g.InputTextState.InitialTextA.Data, data_type, p_data, format);
+ value_changed = DataTypeApplyFromText(buf, data_type, p_data, format);
+ IMGUI_TEST_ENGINE_ITEM_INFO(g.LastItemData.ID, label, g.LastItemData.StatusFlags);
// Step buttons
const ImVec2 backup_frame_padding = style.FramePadding;
style.FramePadding.x = style.FramePadding.y;
ImGuiButtonFlags button_flags = ImGuiButtonFlags_Repeat | ImGuiButtonFlags_DontClosePopups;
if (flags & ImGuiInputTextFlags_ReadOnly)
- button_flags |= ImGuiButtonFlags_Disabled;
+ BeginDisabled();
SameLine(0, style.ItemInnerSpacing.x);
if (ButtonEx("-", ImVec2(button_size, button_size), button_flags))
{
@@ -2938,6 +3425,8 @@ bool ImGui::InputScalar(const char* label, ImGuiDataType data_type, void* p_data
DataTypeApplyOp(data_type, '+', p_data, p_data, g.IO.KeyCtrl && p_step_fast ? p_step_fast : p_step);
value_changed = true;
}
+ if (flags & ImGuiInputTextFlags_ReadOnly)
+ EndDisabled();
const char* label_end = FindRenderedTextEnd(label);
if (label != label_end)
@@ -2953,10 +3442,10 @@ bool ImGui::InputScalar(const char* label, ImGuiDataType data_type, void* p_data
else
{
if (InputText(label, buf, IM_ARRAYSIZE(buf), flags))
- value_changed = DataTypeApplyOpFromText(buf, g.InputTextState.InitialTextA.Data, data_type, p_data, format);
+ value_changed = DataTypeApplyFromText(buf, data_type, p_data, format);
}
if (value_changed)
- MarkItemEdited(window->DC.LastItemId);
+ MarkItemEdited(g.LastItemData.ID);
return value_changed;
}
@@ -2999,7 +3488,7 @@ bool ImGui::InputScalarN(const char* label, ImGuiDataType data_type, void* p_dat
bool ImGui::InputFloat(const char* label, float* v, float step, float step_fast, const char* format, ImGuiInputTextFlags flags)
{
flags |= ImGuiInputTextFlags_CharsScientific;
- return InputScalar(label, ImGuiDataType_Float, (void*)v, (void*)(step>0.0f ? &step : NULL), (void*)(step_fast>0.0f ? &step_fast : NULL), format, flags);
+ return InputScalar(label, ImGuiDataType_Float, (void*)v, (void*)(step > 0.0f ? &step : NULL), (void*)(step_fast > 0.0f ? &step_fast : NULL), format, flags);
}
bool ImGui::InputFloat2(const char* label, float v[2], const char* format, ImGuiInputTextFlags flags)
@@ -3017,46 +3506,11 @@ bool ImGui::InputFloat4(const char* label, float v[4], const char* format, ImGui
return InputScalarN(label, ImGuiDataType_Float, v, 4, NULL, NULL, format, flags);
}
-// Prefer using "const char* format" directly, which is more flexible and consistent with other API.
-#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
-bool ImGui::InputFloat(const char* label, float* v, float step, float step_fast, int decimal_precision, ImGuiInputTextFlags flags)
-{
- char format[16] = "%f";
- if (decimal_precision >= 0)
- ImFormatString(format, IM_ARRAYSIZE(format), "%%.%df", decimal_precision);
- return InputFloat(label, v, step, step_fast, format, flags);
-}
-
-bool ImGui::InputFloat2(const char* label, float v[2], int decimal_precision, ImGuiInputTextFlags flags)
-{
- char format[16] = "%f";
- if (decimal_precision >= 0)
- ImFormatString(format, IM_ARRAYSIZE(format), "%%.%df", decimal_precision);
- return InputScalarN(label, ImGuiDataType_Float, v, 2, NULL, NULL, format, flags);
-}
-
-bool ImGui::InputFloat3(const char* label, float v[3], int decimal_precision, ImGuiInputTextFlags flags)
-{
- char format[16] = "%f";
- if (decimal_precision >= 0)
- ImFormatString(format, IM_ARRAYSIZE(format), "%%.%df", decimal_precision);
- return InputScalarN(label, ImGuiDataType_Float, v, 3, NULL, NULL, format, flags);
-}
-
-bool ImGui::InputFloat4(const char* label, float v[4], int decimal_precision, ImGuiInputTextFlags flags)
-{
- char format[16] = "%f";
- if (decimal_precision >= 0)
- ImFormatString(format, IM_ARRAYSIZE(format), "%%.%df", decimal_precision);
- return InputScalarN(label, ImGuiDataType_Float, v, 4, NULL, NULL, format, flags);
-}
-#endif // IMGUI_DISABLE_OBSOLETE_FUNCTIONS
-
bool ImGui::InputInt(const char* label, int* v, int step, int step_fast, ImGuiInputTextFlags flags)
{
// Hexadecimal input provided as a convenience but the flag name is awkward. Typically you'd use InputText() to parse your own data, if you want to handle prefixes.
const char* format = (flags & ImGuiInputTextFlags_CharsHexadecimal) ? "%08X" : "%d";
- return InputScalar(label, ImGuiDataType_S32, (void*)v, (void*)(step>0 ? &step : NULL), (void*)(step_fast>0 ? &step_fast : NULL), format, flags);
+ return InputScalar(label, ImGuiDataType_S32, (void*)v, (void*)(step > 0 ? &step : NULL), (void*)(step_fast > 0 ? &step_fast : NULL), format, flags);
}
bool ImGui::InputInt2(const char* label, int v[2], ImGuiInputTextFlags flags)
@@ -3077,7 +3531,7 @@ bool ImGui::InputInt4(const char* label, int v[4], ImGuiInputTextFlags flags)
bool ImGui::InputDouble(const char* label, double* v, double step, double step_fast, const char* format, ImGuiInputTextFlags flags)
{
flags |= ImGuiInputTextFlags_CharsScientific;
- return InputScalar(label, ImGuiDataType_Double, (void*)v, (void*)(step>0.0 ? &step : NULL), (void*)(step_fast>0.0 ? &step_fast : NULL), format, flags);
+ return InputScalar(label, ImGuiDataType_Double, (void*)v, (void*)(step > 0.0 ? &step : NULL), (void*)(step_fast > 0.0 ? &step_fast : NULL), format, flags);
}
//-------------------------------------------------------------------------
@@ -3086,13 +3540,17 @@ bool ImGui::InputDouble(const char* label, double* v, double step, double step_f
// - InputText()
// - InputTextWithHint()
// - InputTextMultiline()
+// - InputTextGetCharInfo() [Internal]
+// - InputTextReindexLines() [Internal]
+// - InputTextReindexLinesRange() [Internal]
// - InputTextEx() [Internal]
+// - DebugNodeInputTextState() [Internal]
//-------------------------------------------------------------------------
bool ImGui::InputText(const char* label, char* buf, size_t buf_size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data)
{
IM_ASSERT(!(flags & ImGuiInputTextFlags_Multiline)); // call InputTextMultiline()
- return InputTextEx(label, NULL, buf, (int)buf_size, ImVec2(0,0), flags, callback, user_data);
+ return InputTextEx(label, NULL, buf, (int)buf_size, ImVec2(0, 0), flags, callback, user_data);
}
bool ImGui::InputTextMultiline(const char* label, char* buf, size_t buf_size, const ImVec2& size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data)
@@ -3102,7 +3560,7 @@ bool ImGui::InputTextMultiline(const char* label, char* buf, size_t buf_size, co
bool ImGui::InputTextWithHint(const char* label, const char* hint, char* buf, size_t buf_size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data)
{
- IM_ASSERT(!(flags & ImGuiInputTextFlags_Multiline)); // call InputTextMultiline()
+ IM_ASSERT(!(flags & ImGuiInputTextFlags_Multiline)); // call InputTextMultiline() or InputTextEx() manually if you need multi-line + hint.
return InputTextEx(label, hint, buf, (int)buf_size, ImVec2(0, 0), flags, callback, user_data);
}
@@ -3127,7 +3585,7 @@ static ImVec2 InputTextCalcTextSizeW(const ImWchar* text_begin, const ImWchar* t
const float line_height = g.FontSize;
const float scale = line_height / font->FontSize;
- ImVec2 text_size = ImVec2(0,0);
+ ImVec2 text_size = ImVec2(0, 0);
float line_width = 0.0f;
const ImWchar* s = text_begin;
@@ -3169,12 +3627,12 @@ static ImVec2 InputTextCalcTextSizeW(const ImWchar* text_begin, const ImWchar* t
namespace ImStb
{
-static int STB_TEXTEDIT_STRINGLEN(const STB_TEXTEDIT_STRING* obj) { return obj->CurLenW; }
-static ImWchar STB_TEXTEDIT_GETCHAR(const STB_TEXTEDIT_STRING* obj, int idx) { return obj->TextW[idx]; }
-static float STB_TEXTEDIT_GETWIDTH(STB_TEXTEDIT_STRING* obj, int line_start_idx, int char_idx) { ImWchar c = obj->TextW[line_start_idx + char_idx]; if (c == '\n') return STB_TEXTEDIT_GETWIDTH_NEWLINE; ImGuiContext& g = *GImGui; return g.Font->GetCharAdvance(c) * (g.FontSize / g.Font->FontSize); }
-static int STB_TEXTEDIT_KEYTOTEXT(int key) { return key >= 0x10000 ? 0 : key; }
+static int STB_TEXTEDIT_STRINGLEN(const ImGuiInputTextState* obj) { return obj->CurLenW; }
+static ImWchar STB_TEXTEDIT_GETCHAR(const ImGuiInputTextState* obj, int idx) { return obj->TextW[idx]; }
+static float STB_TEXTEDIT_GETWIDTH(ImGuiInputTextState* obj, int line_start_idx, int char_idx) { ImWchar c = obj->TextW[line_start_idx + char_idx]; if (c == '\n') return STB_TEXTEDIT_GETWIDTH_NEWLINE; ImGuiContext& g = *GImGui; return g.Font->GetCharAdvance(c) * (g.FontSize / g.Font->FontSize); }
+static int STB_TEXTEDIT_KEYTOTEXT(int key) { return key >= 0x200000 ? 0 : key; }
static ImWchar STB_TEXTEDIT_NEWLINE = '\n';
-static void STB_TEXTEDIT_LAYOUTROW(StbTexteditRow* r, STB_TEXTEDIT_STRING* obj, int line_start_idx)
+static void STB_TEXTEDIT_LAYOUTROW(StbTexteditRow* r, ImGuiInputTextState* obj, int line_start_idx)
{
const ImWchar* text = obj->TextW.Data;
const ImWchar* text_remaining = NULL;
@@ -3187,23 +3645,23 @@ static void STB_TEXTEDIT_LAYOUTROW(StbTexteditRow* r, STB_TEXTEDIT_STRING* ob
r->num_chars = (int)(text_remaining - (text + line_start_idx));
}
-static bool is_separator(unsigned int c) { return ImCharIsBlankW(c) || c==',' || c==';' || c=='(' || c==')' || c=='{' || c=='}' || c=='[' || c==']' || c=='|'; }
-static int is_word_boundary_from_right(STB_TEXTEDIT_STRING* obj, int idx) { return idx > 0 ? (is_separator( obj->TextW[idx-1] ) && !is_separator( obj->TextW[idx] ) ) : 1; }
-static int STB_TEXTEDIT_MOVEWORDLEFT_IMPL(STB_TEXTEDIT_STRING* obj, int idx) { idx--; while (idx >= 0 && !is_word_boundary_from_right(obj, idx)) idx--; return idx < 0 ? 0 : idx; }
-#ifdef __APPLE__ // FIXME: Move setting to IO structure
-static int is_word_boundary_from_left(STB_TEXTEDIT_STRING* obj, int idx) { return idx > 0 ? (!is_separator( obj->TextW[idx-1] ) && is_separator( obj->TextW[idx] ) ) : 1; }
-static int STB_TEXTEDIT_MOVEWORDRIGHT_IMPL(STB_TEXTEDIT_STRING* obj, int idx) { idx++; int len = obj->CurLenW; while (idx < len && !is_word_boundary_from_left(obj, idx)) idx++; return idx > len ? len : idx; }
-#else
-static int STB_TEXTEDIT_MOVEWORDRIGHT_IMPL(STB_TEXTEDIT_STRING* obj, int idx) { idx++; int len = obj->CurLenW; while (idx < len && !is_word_boundary_from_right(obj, idx)) idx++; return idx > len ? len : idx; }
-#endif
-#define STB_TEXTEDIT_MOVEWORDLEFT STB_TEXTEDIT_MOVEWORDLEFT_IMPL // They need to be #define for stb_textedit.h
+// When ImGuiInputTextFlags_Password is set, we don't want actions such as CTRL+Arrow to leak the fact that underlying data are blanks or separators.
+static bool is_separator(unsigned int c) { return ImCharIsBlankW(c) || c==',' || c==';' || c=='(' || c==')' || c=='{' || c=='}' || c=='[' || c==']' || c=='|' || c=='\n' || c=='\r'; }
+static int is_word_boundary_from_right(ImGuiInputTextState* obj, int idx) { if (obj->Flags & ImGuiInputTextFlags_Password) return 0; return idx > 0 ? (is_separator(obj->TextW[idx - 1]) && !is_separator(obj->TextW[idx]) ) : 1; }
+static int is_word_boundary_from_left(ImGuiInputTextState* obj, int idx) { if (obj->Flags & ImGuiInputTextFlags_Password) return 0; return idx > 0 ? (!is_separator(obj->TextW[idx - 1]) && is_separator(obj->TextW[idx])) : 1; }
+static int STB_TEXTEDIT_MOVEWORDLEFT_IMPL(ImGuiInputTextState* obj, int idx) { idx--; while (idx >= 0 && !is_word_boundary_from_right(obj, idx)) idx--; return idx < 0 ? 0 : idx; }
+static int STB_TEXTEDIT_MOVEWORDRIGHT_MAC(ImGuiInputTextState* obj, int idx) { idx++; int len = obj->CurLenW; while (idx < len && !is_word_boundary_from_left(obj, idx)) idx++; return idx > len ? len : idx; }
+static int STB_TEXTEDIT_MOVEWORDRIGHT_WIN(ImGuiInputTextState* obj, int idx) { idx++; int len = obj->CurLenW; while (idx < len && !is_word_boundary_from_right(obj, idx)) idx++; return idx > len ? len : idx; }
+static int STB_TEXTEDIT_MOVEWORDRIGHT_IMPL(ImGuiInputTextState* obj, int idx) { if (ImGui::GetIO().ConfigMacOSXBehaviors) return STB_TEXTEDIT_MOVEWORDRIGHT_MAC(obj, idx); else return STB_TEXTEDIT_MOVEWORDRIGHT_WIN(obj, idx); }
+#define STB_TEXTEDIT_MOVEWORDLEFT STB_TEXTEDIT_MOVEWORDLEFT_IMPL // They need to be #define for stb_textedit.h
#define STB_TEXTEDIT_MOVEWORDRIGHT STB_TEXTEDIT_MOVEWORDRIGHT_IMPL
-static void STB_TEXTEDIT_DELETECHARS(STB_TEXTEDIT_STRING* obj, int pos, int n)
+static void STB_TEXTEDIT_DELETECHARS(ImGuiInputTextState* obj, int pos, int n)
{
ImWchar* dst = obj->TextW.Data + pos;
// We maintain our buffer length in both UTF-8 and wchar formats
+ obj->Edited = true;
obj->CurLenA -= ImTextCountUtf8BytesFromStr(dst, dst + n);
obj->CurLenW -= n;
@@ -3214,9 +3672,9 @@ static void STB_TEXTEDIT_DELETECHARS(STB_TEXTEDIT_STRING* obj, int pos, int n)
*dst = '\0';
}
-static bool STB_TEXTEDIT_INSERTCHARS(STB_TEXTEDIT_STRING* obj, int pos, const ImWchar* new_text, int new_text_len)
+static bool STB_TEXTEDIT_INSERTCHARS(ImGuiInputTextState* obj, int pos, const ImWchar* new_text, int new_text_len)
{
- const bool is_resizable = (obj->UserFlags & ImGuiInputTextFlags_CallbackResize) != 0;
+ const bool is_resizable = (obj->Flags & ImGuiInputTextFlags_CallbackResize) != 0;
const int text_len = obj->CurLenW;
IM_ASSERT(pos <= text_len);
@@ -3238,6 +3696,7 @@ static bool STB_TEXTEDIT_INSERTCHARS(STB_TEXTEDIT_STRING* obj, int pos, const Im
memmove(text + pos + new_text_len, text + pos, (size_t)(text_len - pos) * sizeof(ImWchar));
memcpy(text + pos, new_text, (size_t)new_text_len * sizeof(ImWchar));
+ obj->Edited = true;
obj->CurLenW += new_text_len;
obj->CurLenA += new_text_len_utf8;
obj->TextW[obj->CurLenW] = '\0';
@@ -3260,13 +3719,33 @@ static bool STB_TEXTEDIT_INSERTCHARS(STB_TEXTEDIT_STRING* obj, int pos, const Im
#define STB_TEXTEDIT_K_REDO 0x20000B // keyboard input to perform redo
#define STB_TEXTEDIT_K_WORDLEFT 0x20000C // keyboard input to move cursor left one word
#define STB_TEXTEDIT_K_WORDRIGHT 0x20000D // keyboard input to move cursor right one word
+#define STB_TEXTEDIT_K_PGUP 0x20000E // keyboard input to move cursor up a page
+#define STB_TEXTEDIT_K_PGDOWN 0x20000F // keyboard input to move cursor down a page
#define STB_TEXTEDIT_K_SHIFT 0x400000
#define STB_TEXTEDIT_IMPLEMENTATION
#include "imstb_textedit.h"
+// stb_textedit internally allows for a single undo record to do addition and deletion, but somehow, calling
+// the stb_textedit_paste() function creates two separate records, so we perform it manually. (FIXME: Report to nothings/stb?)
+static void stb_textedit_replace(ImGuiInputTextState* str, STB_TexteditState* state, const STB_TEXTEDIT_CHARTYPE* text, int text_len)
+{
+ stb_text_makeundo_replace(str, state, 0, str->CurLenW, text_len);
+ ImStb::STB_TEXTEDIT_DELETECHARS(str, 0, str->CurLenW);
+ state->cursor = state->select_start = state->select_end = 0;
+ if (text_len <= 0)
+ return;
+ if (ImStb::STB_TEXTEDIT_INSERTCHARS(str, 0, text, text_len))
+ {
+ state->cursor = state->select_start = state->select_end = text_len;
+ state->has_preferred_x = 0;
+ return;
+ }
+ IM_ASSERT(0); // Failed to insert character, normally shouldn't happen because of how we currently use stb_textedit_replace()
}
+} // namespace ImStb
+
void ImGuiInputTextState::OnKeyPressed(int key)
{
stb_textedit_key(this, &Stb, key);
@@ -3291,7 +3770,7 @@ void ImGuiInputTextCallbackData::DeleteChars(int pos, int bytes_count)
*dst++ = c;
*dst = '\0';
- if (CursorPos + bytes_count >= pos)
+ if (CursorPos >= pos + bytes_count)
CursorPos -= bytes_count;
else if (CursorPos >= pos)
CursorPos = pos;
@@ -3309,7 +3788,7 @@ void ImGuiInputTextCallbackData::InsertChars(int pos, const char* new_text, cons
if (!is_resizable)
return;
- // Contrary to STB_TEXTEDIT_INSERTCHARS() this is working in the UTF8 buffer, hence the midly similar code (until we remove the U16 buffer alltogether!)
+ // Contrary to STB_TEXTEDIT_INSERTCHARS() this is working in the UTF8 buffer, hence the mildly similar code (until we remove the U16 buffer altogether!)
ImGuiContext& g = *GImGui;
ImGuiInputTextState* edit_state = &g.InputTextState;
IM_ASSERT(edit_state->ID != 0 && g.ActiveId == edit_state->ID);
@@ -3333,50 +3812,82 @@ void ImGuiInputTextCallbackData::InsertChars(int pos, const char* new_text, cons
}
// Return false to discard a character.
-static bool InputTextFilterCharacter(unsigned int* p_char, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data)
+static bool InputTextFilterCharacter(unsigned int* p_char, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data, ImGuiInputSource input_source)
{
+ IM_ASSERT(input_source == ImGuiInputSource_Keyboard || input_source == ImGuiInputSource_Clipboard);
unsigned int c = *p_char;
// Filter non-printable (NB: isprint is unreliable! see #2467)
+ bool apply_named_filters = true;
if (c < 0x20)
{
bool pass = false;
- pass |= (c == '\n' && (flags & ImGuiInputTextFlags_Multiline));
+ pass |= (c == '\n' && (flags & ImGuiInputTextFlags_Multiline)); // Note that an Enter KEY will emit \r and be ignored (we poll for KEY in InputText() code)
pass |= (c == '\t' && (flags & ImGuiInputTextFlags_AllowTabInput));
if (!pass)
return false;
+ apply_named_filters = false; // Override named filters below so newline and tabs can still be inserted.
}
- // We ignore Ascii representation of delete (emitted from Backspace on OSX, see #2578, #2817)
- if (c == 127)
- return false;
+ if (input_source != ImGuiInputSource_Clipboard)
+ {
+ // We ignore Ascii representation of delete (emitted from Backspace on OSX, see #2578, #2817)
+ if (c == 127)
+ return false;
- // Filter private Unicode range. GLFW on OSX seems to send private characters for special keys like arrow keys (FIXME)
- if (c >= 0xE000 && c <= 0xF8FF)
+ // Filter private Unicode range. GLFW on OSX seems to send private characters for special keys like arrow keys (FIXME)
+ if (c >= 0xE000 && c <= 0xF8FF)
+ return false;
+ }
+
+ // Filter Unicode ranges we are not handling in this build
+ if (c > IM_UNICODE_CODEPOINT_MAX)
return false;
// Generic named filters
- if (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_CharsUppercase | ImGuiInputTextFlags_CharsNoBlank | ImGuiInputTextFlags_CharsScientific))
- {
+ if (apply_named_filters && (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_CharsUppercase | ImGuiInputTextFlags_CharsNoBlank | ImGuiInputTextFlags_CharsScientific)))
+ {
+ // The libc allows overriding locale, with e.g. 'setlocale(LC_NUMERIC, "de_DE.UTF-8");' which affect the output/input of printf/scanf to use e.g. ',' instead of '.'.
+ // The standard mandate that programs starts in the "C" locale where the decimal point is '.'.
+ // We don't really intend to provide widespread support for it, but out of empathy for people stuck with using odd API, we support the bare minimum aka overriding the decimal point.
+ // Change the default decimal_point with:
+ // ImGui::GetCurrentContext()->PlatformLocaleDecimalPoint = *localeconv()->decimal_point;
+ // Users of non-default decimal point (in particular ',') may be affected by word-selection logic (is_word_boundary_from_right/is_word_boundary_from_left) functions.
+ ImGuiContext& g = *GImGui;
+ const unsigned c_decimal_point = (unsigned int)g.PlatformLocaleDecimalPoint;
+
+ // Full-width -> half-width conversion for numeric fields (https://en.wikipedia.org/wiki/Halfwidth_and_Fullwidth_Forms_(Unicode_block)
+ // While this is mostly convenient, this has the side-effect for uninformed users accidentally inputting full-width characters that they may
+ // scratch their head as to why it works in numerical fields vs in generic text fields it would require support in the font.
+ if (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsScientific | ImGuiInputTextFlags_CharsHexadecimal))
+ if (c >= 0xFF01 && c <= 0xFF5E)
+ c = c - 0xFF01 + 0x21;
+
+ // Allow 0-9 . - + * /
if (flags & ImGuiInputTextFlags_CharsDecimal)
- if (!(c >= '0' && c <= '9') && (c != '.') && (c != '-') && (c != '+') && (c != '*') && (c != '/'))
+ if (!(c >= '0' && c <= '9') && (c != c_decimal_point) && (c != '-') && (c != '+') && (c != '*') && (c != '/'))
return false;
+ // Allow 0-9 . - + * / e E
if (flags & ImGuiInputTextFlags_CharsScientific)
- if (!(c >= '0' && c <= '9') && (c != '.') && (c != '-') && (c != '+') && (c != '*') && (c != '/') && (c != 'e') && (c != 'E'))
+ if (!(c >= '0' && c <= '9') && (c != c_decimal_point) && (c != '-') && (c != '+') && (c != '*') && (c != '/') && (c != 'e') && (c != 'E'))
return false;
+ // Allow 0-9 a-F A-F
if (flags & ImGuiInputTextFlags_CharsHexadecimal)
if (!(c >= '0' && c <= '9') && !(c >= 'a' && c <= 'f') && !(c >= 'A' && c <= 'F'))
return false;
+ // Turn a-z into A-Z
if (flags & ImGuiInputTextFlags_CharsUppercase)
if (c >= 'a' && c <= 'z')
- *p_char = (c += (unsigned int)('A'-'a'));
+ c += (unsigned int)('A' - 'a');
if (flags & ImGuiInputTextFlags_CharsNoBlank)
if (ImCharIsBlankW(c))
return false;
+
+ *p_char = c;
}
// Custom callback filter
@@ -3398,6 +3909,41 @@ static bool InputTextFilterCharacter(unsigned int* p_char, ImGuiInputTextFlags f
return true;
}
+// Find the shortest single replacement we can make to get the new text from the old text.
+// Important: needs to be run before TextW is rewritten with the new characters because calling STB_TEXTEDIT_GETCHAR() at the end.
+// FIXME: Ideally we should transition toward (1) making InsertChars()/DeleteChars() update undo-stack (2) discourage (and keep reconcile) or obsolete (and remove reconcile) accessing buffer directly.
+static void InputTextReconcileUndoStateAfterUserCallback(ImGuiInputTextState* state, const char* new_buf_a, int new_length_a)
+{
+ ImGuiContext& g = *GImGui;
+ const ImWchar* old_buf = state->TextW.Data;
+ const int old_length = state->CurLenW;
+ const int new_length = ImTextCountCharsFromUtf8(new_buf_a, new_buf_a + new_length_a);
+ g.TempBuffer.reserve_discard((new_length + 1) * sizeof(ImWchar));
+ ImWchar* new_buf = (ImWchar*)(void*)g.TempBuffer.Data;
+ ImTextStrFromUtf8(new_buf, new_length + 1, new_buf_a, new_buf_a + new_length_a);
+
+ const int shorter_length = ImMin(old_length, new_length);
+ int first_diff;
+ for (first_diff = 0; first_diff < shorter_length; first_diff++)
+ if (old_buf[first_diff] != new_buf[first_diff])
+ break;
+ if (first_diff == old_length && first_diff == new_length)
+ return;
+
+ int old_last_diff = old_length - 1;
+ int new_last_diff = new_length - 1;
+ for (; old_last_diff >= first_diff && new_last_diff >= first_diff; old_last_diff--, new_last_diff--)
+ if (old_buf[old_last_diff] != new_buf[new_last_diff])
+ break;
+
+ const int insert_len = new_last_diff - first_diff + 1;
+ const int delete_len = old_last_diff - first_diff + 1;
+ if (insert_len > 0 || delete_len > 0)
+ if (STB_TEXTEDIT_CHARTYPE* p = stb_text_createundo(&state->Stb.undostate, first_diff, delete_len, insert_len))
+ for (int i = 0; i < delete_len; i++)
+ p[i] = ImStb::STB_TEXTEDIT_GETCHAR(state, first_diff + i);
+}
+
// Edit a string of text
// - buf_size account for the zero-terminator, so a buf_size of 6 can hold "Hello" but not "Hello!".
// This is so we can easily call InputText() on static arrays using ARRAYSIZE() and to match
@@ -3412,6 +3958,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
if (window->SkipItems)
return false;
+ IM_ASSERT(buf != NULL && buf_size >= 0);
IM_ASSERT(!((flags & ImGuiInputTextFlags_CallbackHistory) && (flags & ImGuiInputTextFlags_Multiline))); // Can't use both together (they both use up/down keys)
IM_ASSERT(!((flags & ImGuiInputTextFlags_CallbackCompletion) && (flags & ImGuiInputTextFlags_AllowTabInput))); // Can't use both together (they both use tab key)
@@ -3428,11 +3975,11 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
if (is_resizable)
IM_ASSERT(callback != NULL); // Must provide a callback if you set the ImGuiInputTextFlags_CallbackResize flag!
- if (is_multiline) // Open group before calling GetID() because groups tracks id created within their scope,
+ if (is_multiline) // Open group before calling GetID() because groups tracks id created within their scope (including the scrollbar)
BeginGroup();
const ImGuiID id = window->GetID(label);
const ImVec2 label_size = CalcTextSize(label, NULL, true);
- const ImVec2 frame_size = CalcItemSize(size_arg, CalcItemWidth(), (is_multiline ? g.FontSize * 8.0f : label_size.y) + style.FramePadding.y*2.0f); // Arbitrary default of 8 lines high for multi-line
+ const ImVec2 frame_size = CalcItemSize(size_arg, CalcItemWidth(), (is_multiline ? g.FontSize * 8.0f : label_size.y) + style.FramePadding.y * 2.0f); // Arbitrary default of 8 lines high for multi-line
const ImVec2 total_size = ImVec2(frame_size.x + (label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f), frame_size.y);
const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size);
@@ -3440,54 +3987,72 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
ImGuiWindow* draw_window = window;
ImVec2 inner_size = frame_size;
+ ImGuiItemStatusFlags item_status_flags = 0;
+ ImGuiLastItemData item_data_backup;
if (is_multiline)
{
- if (!ItemAdd(total_bb, id, &frame_bb))
+ ImVec2 backup_pos = window->DC.CursorPos;
+ ItemSize(total_bb, style.FramePadding.y);
+ if (!ItemAdd(total_bb, id, &frame_bb, ImGuiItemFlags_Inputable))
{
- ItemSize(total_bb, style.FramePadding.y);
EndGroup();
return false;
}
- if (!BeginChildFrame(id, frame_bb.GetSize()))
+ item_status_flags = g.LastItemData.StatusFlags;
+ item_data_backup = g.LastItemData;
+ window->DC.CursorPos = backup_pos;
+
+ // We reproduce the contents of BeginChildFrame() in order to provide 'label' so our window internal data are easier to read/debug.
+ // FIXME-NAV: Pressing NavActivate will trigger general child activation right before triggering our own below. Harmless but bizarre.
+ PushStyleColor(ImGuiCol_ChildBg, style.Colors[ImGuiCol_FrameBg]);
+ PushStyleVar(ImGuiStyleVar_ChildRounding, style.FrameRounding);
+ PushStyleVar(ImGuiStyleVar_ChildBorderSize, style.FrameBorderSize);
+ PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); // Ensure no clip rect so mouse hover can reach FramePadding edges
+ bool child_visible = BeginChildEx(label, id, frame_bb.GetSize(), true, ImGuiWindowFlags_NoMove);
+ PopStyleVar(3);
+ PopStyleColor();
+ if (!child_visible)
{
- EndChildFrame();
+ EndChild();
EndGroup();
return false;
}
draw_window = g.CurrentWindow; // Child window
- draw_window->DC.NavLayerActiveMaskNext |= draw_window->DC.NavLayerCurrentMask; // This is to ensure that EndChild() will display a navigation highlight
+ draw_window->DC.NavLayersActiveMaskNext |= (1 << draw_window->DC.NavLayerCurrent); // This is to ensure that EndChild() will display a navigation highlight so we can "enter" into it.
+ draw_window->DC.CursorPos += style.FramePadding;
inner_size.x -= draw_window->ScrollbarSizes.x;
}
else
{
+ // Support for internal ImGuiInputTextFlags_MergedItem flag, which could be redesigned as an ItemFlags if needed (with test performed in ItemAdd)
ItemSize(total_bb, style.FramePadding.y);
- if (!ItemAdd(total_bb, id, &frame_bb))
- return false;
+ if (!(flags & ImGuiInputTextFlags_MergedItem))
+ if (!ItemAdd(total_bb, id, &frame_bb, ImGuiItemFlags_Inputable))
+ return false;
+ item_status_flags = g.LastItemData.StatusFlags;
}
const bool hovered = ItemHoverable(frame_bb, id);
if (hovered)
g.MouseCursor = ImGuiMouseCursor_TextInput;
- // NB: we are only allowed to access 'edit_state' if we are the active widget.
- ImGuiInputTextState* state = NULL;
- if (g.InputTextState.ID == id)
- state = &g.InputTextState;
+ // We are only allowed to access the state if we are already the active widget.
+ ImGuiInputTextState* state = GetInputTextState(id);
- const bool focus_requested = FocusableItemRegister(window, id);
- const bool focus_requested_by_code = focus_requested && (g.FocusRequestCurrWindow == window && g.FocusRequestCurrCounterAll == window->DC.FocusCounterAll);
- const bool focus_requested_by_tab = focus_requested && !focus_requested_by_code;
+ const bool input_requested_by_tabbing = (item_status_flags & ImGuiItemStatusFlags_FocusedByTabbing) != 0;
+ const bool input_requested_by_nav = (g.ActiveId != id) && ((g.NavActivateInputId == id) || (g.NavActivateId == id && g.NavInputSource == ImGuiInputSource_Keyboard));
const bool user_clicked = hovered && io.MouseClicked[0];
- const bool user_nav_input_start = (g.ActiveId != id) && ((g.NavInputId == id) || (g.NavActivateId == id && g.NavInputSource == ImGuiInputSource_NavKeyboard));
- const bool user_scroll_finish = is_multiline && state != NULL && g.ActiveId == 0 && g.ActiveIdPreviousFrame == GetScrollbarID(draw_window, ImGuiAxis_Y);
- const bool user_scroll_active = is_multiline && state != NULL && g.ActiveId == GetScrollbarID(draw_window, ImGuiAxis_Y);
-
+ const bool user_scroll_finish = is_multiline && state != NULL && g.ActiveId == 0 && g.ActiveIdPreviousFrame == GetWindowScrollbarID(draw_window, ImGuiAxis_Y);
+ const bool user_scroll_active = is_multiline && state != NULL && g.ActiveId == GetWindowScrollbarID(draw_window, ImGuiAxis_Y);
bool clear_active_id = false;
- bool select_all = (g.ActiveId != id) && ((flags & ImGuiInputTextFlags_AutoSelectAll) != 0 || user_nav_input_start) && (!is_multiline);
+ bool select_all = false;
- const bool init_make_active = (focus_requested || user_clicked || user_scroll_finish || user_nav_input_start);
+ float scroll_y = is_multiline ? draw_window->Scroll.y : FLT_MAX;
+
+ const bool init_changed_specs = (state != NULL && state->Stb.single_line != !is_multiline);
+ const bool init_make_active = (user_clicked || user_scroll_finish || input_requested_by_nav || input_requested_by_tabbing);
const bool init_state = (init_make_active || user_scroll_active);
- if (init_state && g.ActiveId != id)
+ if ((init_state && g.ActiveId != id) || init_changed_specs)
{
// Access state even if we don't own it yet.
state = &g.InputTextState;
@@ -3499,17 +4064,21 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
state->InitialTextA.resize(buf_len + 1); // UTF-8. we use +1 to make sure that .Data is always pointing to at least an empty string.
memcpy(state->InitialTextA.Data, buf, buf_len + 1);
+ // Preserve cursor position and undo/redo stack if we come back to same widget
+ // FIXME: Since we reworked this on 2022/06, may want to differenciate recycle_cursor vs recycle_undostate?
+ bool recycle_state = (state->ID == id && !init_changed_specs);
+ if (recycle_state && (state->CurLenA != buf_len || (state->TextAIsValid && strncmp(state->TextA.Data, buf, buf_len) != 0)))
+ recycle_state = false;
+
// Start edition
const char* buf_end = NULL;
+ state->ID = id;
state->TextW.resize(buf_size + 1); // wchar count <= UTF-8 count. we use +1 to make sure that .Data is always pointing to at least an empty string.
state->TextA.resize(0);
state->TextAIsValid = false; // TextA is not valid yet (we will display buf until then)
state->CurLenW = ImTextStrFromUtf8(state->TextW.Data, buf_size, buf, NULL, &buf_end);
state->CurLenA = (int)(buf_end - buf); // We can't get the result from ImStrncpy() above because it is not UTF-8 aware. Here we'll cut off malformed UTF-8.
- // Preserve cursor position and undo/redo stack if we come back to same widget
- // FIXME: For non-readonly widgets we might be able to require that TextAIsValid && TextA == buf ? (untested) and discard undo stack if user buffer has changed.
- const bool recycle_state = (state->ID == id);
if (recycle_state)
{
// Recycle existing cursor/selection/undo stack but clamp position
@@ -3518,16 +4087,22 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
}
else
{
- state->ID = id;
state->ScrollX = 0.0f;
stb_textedit_initialize_state(&state->Stb, !is_multiline);
- if (!is_multiline && focus_requested_by_code)
+ }
+
+ if (!is_multiline)
+ {
+ if (flags & ImGuiInputTextFlags_AutoSelectAll)
+ select_all = true;
+ if (input_requested_by_nav && (!recycle_state || !(g.NavActivateFlags & ImGuiActivateFlags_TryToPreserveState)))
+ select_all = true;
+ if (input_requested_by_tabbing || (user_clicked && io.KeyCtrl))
select_all = true;
}
- if (flags & ImGuiInputTextFlags_AlwaysInsertMode)
- state->Stb.insert_mode = 1;
- if (!is_multiline && (focus_requested_by_tab || (user_clicked && io.KeyCtrl)))
- select_all = true;
+
+ if (flags & ImGuiInputTextFlags_AlwaysOverwrite)
+ state->Stb.insert_mode = 1; // stb field name is indeed incorrect (see #2863)
}
if (g.ActiveId != id && init_make_active)
@@ -3536,18 +4111,24 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
SetActiveID(id, window);
SetFocusID(id, window);
FocusWindow(window);
-
- // Declare our inputs
- IM_ASSERT(ImGuiNavInput_COUNT < 32);
+ }
+ if (g.ActiveId == id)
+ {
+ // Declare some inputs, the other are registered and polled via Shortcut() routing system.
+ if (user_clicked)
+ SetKeyOwner(ImGuiKey_MouseLeft, id);
g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right);
if (is_multiline || (flags & ImGuiInputTextFlags_CallbackHistory))
g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Up) | (1 << ImGuiDir_Down);
- g.ActiveIdUsingNavInputMask |= (1 << ImGuiNavInput_Cancel);
- g.ActiveIdUsingKeyInputMask |= ((ImU64)1 << ImGuiKey_Home) | ((ImU64)1 << ImGuiKey_End);
+ SetKeyOwner(ImGuiKey_Home, id);
+ SetKeyOwner(ImGuiKey_End, id);
if (is_multiline)
- g.ActiveIdUsingKeyInputMask |= ((ImU64)1 << ImGuiKey_PageUp) | ((ImU64)1 << ImGuiKey_PageDown); // FIXME-NAV: Page up/down actually not supported yet by widget, but claim them ahead.
- if (flags & (ImGuiInputTextFlags_CallbackCompletion | ImGuiInputTextFlags_AllowTabInput)) // Disable keyboard tabbing out as we will use the \t character.
- g.ActiveIdUsingKeyInputMask |= ((ImU64)1 << ImGuiKey_Tab);
+ {
+ SetKeyOwner(ImGuiKey_PageUp, id);
+ SetKeyOwner(ImGuiKey_PageDown, id);
+ }
+ if (flags & (ImGuiInputTextFlags_CallbackCompletion | ImGuiInputTextFlags_AllowTabInput)) // Disable keyboard tabbing out as we will use the \t character.
+ SetKeyOwner(ImGuiKey_Tab, id);
}
// We have an edge case if ActiveId was set through another widget (e.g. widget being swapped), clear id immediately (don't wait until the end of the function)
@@ -3559,10 +4140,10 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
clear_active_id = true;
// Lock the decision of whether we are going to take the path displaying the cursor or selection
- const bool render_cursor = (g.ActiveId == id) || (state && user_scroll_active);
- bool render_selection = state && state->HasSelection() && (RENDER_SELECTION_WHEN_INACTIVE || render_cursor);
+ bool render_cursor = (g.ActiveId == id) || (state && user_scroll_active);
+ bool render_selection = state && (state->HasSelection() || select_all) && (RENDER_SELECTION_WHEN_INACTIVE || render_cursor);
bool value_changed = false;
- bool enter_pressed = false;
+ bool validated = false;
// When read-only we always use the live data passed to the function
// FIXME-OPT: Because our selection/cursor code currently needs the wide text we need to convert it when active, which is not ideal :(
@@ -3587,7 +4168,6 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
ImFont* password_font = &g.InputTextPasswordFont;
password_font->FontSize = g.Font->FontSize;
password_font->Scale = g.Font->Scale;
- password_font->DisplayOffset = g.Font->DisplayOffset;
password_font->Ascent = g.Font->Ascent;
password_font->Descent = g.Font->Descent;
password_font->ContainerAtlas = g.Font->ContainerAtlas;
@@ -3603,10 +4183,9 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
{
IM_ASSERT(state != NULL);
backup_current_text_length = state->CurLenA;
+ state->Edited = false;
state->BufCapacityA = buf_size;
- state->UserFlags = flags;
- state->UserCallback = callback;
- state->UserCallbackData = callback_user_data;
+ state->Flags = flags;
// Although we are active we don't prevent mouse from hovering other elements unless we are interacting right now with the widget.
// Down the line we should have a cleaner library-wide concept of Selected vs Active.
@@ -3615,25 +4194,57 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
// Edit in progress
const float mouse_x = (io.MousePos.x - frame_bb.Min.x - style.FramePadding.x) + state->ScrollX;
- const float mouse_y = (is_multiline ? (io.MousePos.y - draw_window->DC.CursorPos.y - style.FramePadding.y) : (g.FontSize*0.5f));
+ const float mouse_y = (is_multiline ? (io.MousePos.y - draw_window->DC.CursorPos.y) : (g.FontSize * 0.5f));
const bool is_osx = io.ConfigMacOSXBehaviors;
- if (select_all || (hovered && !is_osx && io.MouseDoubleClicked[0]))
+ if (select_all)
{
state->SelectAll();
state->SelectedAllMouseLock = true;
}
- else if (hovered && is_osx && io.MouseDoubleClicked[0])
+ else if (hovered && io.MouseClickedCount[0] >= 2 && !io.KeyShift)
{
- // Double-click select a word only, OS X style (by simulating keystrokes)
- state->OnKeyPressed(STB_TEXTEDIT_K_WORDLEFT);
- state->OnKeyPressed(STB_TEXTEDIT_K_WORDRIGHT | STB_TEXTEDIT_K_SHIFT);
+ stb_textedit_click(state, &state->Stb, mouse_x, mouse_y);
+ const int multiclick_count = (io.MouseClickedCount[0] - 2);
+ if ((multiclick_count % 2) == 0)
+ {
+ // Double-click: Select word
+ // We always use the "Mac" word advance for double-click select vs CTRL+Right which use the platform dependent variant:
+ // FIXME: There are likely many ways to improve this behavior, but there's no "right" behavior (depends on use-case, software, OS)
+ const bool is_bol = (state->Stb.cursor == 0) || ImStb::STB_TEXTEDIT_GETCHAR(state, state->Stb.cursor - 1) == '\n';
+ if (STB_TEXT_HAS_SELECTION(&state->Stb) || !is_bol)
+ state->OnKeyPressed(STB_TEXTEDIT_K_WORDLEFT);
+ //state->OnKeyPressed(STB_TEXTEDIT_K_WORDRIGHT | STB_TEXTEDIT_K_SHIFT);
+ if (!STB_TEXT_HAS_SELECTION(&state->Stb))
+ ImStb::stb_textedit_prep_selection_at_cursor(&state->Stb);
+ state->Stb.cursor = ImStb::STB_TEXTEDIT_MOVEWORDRIGHT_MAC(state, state->Stb.cursor);
+ state->Stb.select_end = state->Stb.cursor;
+ ImStb::stb_textedit_clamp(state, &state->Stb);
+ }
+ else
+ {
+ // Triple-click: Select line
+ const bool is_eol = ImStb::STB_TEXTEDIT_GETCHAR(state, state->Stb.cursor) == '\n';
+ state->OnKeyPressed(STB_TEXTEDIT_K_LINESTART);
+ state->OnKeyPressed(STB_TEXTEDIT_K_LINEEND | STB_TEXTEDIT_K_SHIFT);
+ state->OnKeyPressed(STB_TEXTEDIT_K_RIGHT | STB_TEXTEDIT_K_SHIFT);
+ if (!is_eol && is_multiline)
+ {
+ ImSwap(state->Stb.select_start, state->Stb.select_end);
+ state->Stb.cursor = state->Stb.select_end;
+ }
+ state->CursorFollow = false;
+ }
+ state->CursorAnimReset();
}
else if (io.MouseClicked[0] && !state->SelectedAllMouseLock)
{
if (hovered)
{
- stb_textedit_click(state, &state->Stb, mouse_x, mouse_y);
+ if (io.KeyShift)
+ stb_textedit_drag(state, &state->Stb, mouse_x, mouse_y);
+ else
+ stb_textedit_click(state, &state->Stb, mouse_x, mouse_y);
state->CursorAnimReset();
}
}
@@ -3646,29 +4257,28 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
if (state->SelectedAllMouseLock && !io.MouseDown[0])
state->SelectedAllMouseLock = false;
- // It is ill-defined whether the back-end needs to send a \t character when pressing the TAB keys.
- // Win32 and GLFW naturally do it but not SDL.
+ // We expect backends to emit a Tab key but some also emit a Tab character which we ignore (#2467, #1336)
+ // (For Tab and Enter: Win32/SFML/Allegro are sending both keys and chars, GLFW and SDL are only sending keys. For Space they all send all threes)
const bool ignore_char_inputs = (io.KeyCtrl && !io.KeyAlt) || (is_osx && io.KeySuper);
- if ((flags & ImGuiInputTextFlags_AllowTabInput) && IsKeyPressedMap(ImGuiKey_Tab) && !ignore_char_inputs && !io.KeyShift && !is_readonly)
- if (!io.InputQueueCharacters.contains('\t'))
- {
- unsigned int c = '\t'; // Insert TAB
- if (InputTextFilterCharacter(&c, flags, callback, callback_user_data))
- state->OnKeyPressed((int)c);
- }
+ if ((flags & ImGuiInputTextFlags_AllowTabInput) && IsKeyPressed(ImGuiKey_Tab) && !ignore_char_inputs && !io.KeyShift && !is_readonly)
+ {
+ unsigned int c = '\t'; // Insert TAB
+ if (InputTextFilterCharacter(&c, flags, callback, callback_user_data, ImGuiInputSource_Keyboard))
+ state->OnKeyPressed((int)c);
+ }
// Process regular text input (before we check for Return because using some IME will effectively send a Return?)
// We ignore CTRL inputs, but need to allow ALT+CTRL as some keyboards (e.g. German) use AltGR (which _is_ Alt+Ctrl) to input certain characters.
if (io.InputQueueCharacters.Size > 0)
{
- if (!ignore_char_inputs && !is_readonly && !user_nav_input_start)
+ if (!ignore_char_inputs && !is_readonly && !input_requested_by_nav)
for (int n = 0; n < io.InputQueueCharacters.Size; n++)
{
// Insert character if they pass filtering
unsigned int c = (unsigned int)io.InputQueueCharacters[n];
- if (c == '\t' && io.KeyShift)
+ if (c == '\t') // Skip Tab, see above.
continue;
- if (InputTextFilterCharacter(&c, flags, callback, callback_user_data))
+ if (InputTextFilterCharacter(&c, flags, callback, callback_user_data, ImGuiInputSource_Keyboard))
state->OnKeyPressed((int)c);
}
@@ -3678,67 +4288,101 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
}
// Process other shortcuts/key-presses
- bool cancel_edit = false;
+ bool revert_edit = false;
if (g.ActiveId == id && !g.ActiveIdIsJustActivated && !clear_active_id)
{
IM_ASSERT(state != NULL);
+
+ const int row_count_per_page = ImMax((int)((inner_size.y - style.FramePadding.y) / g.FontSize), 1);
+ state->Stb.row_count_per_page = row_count_per_page;
+
const int k_mask = (io.KeyShift ? STB_TEXTEDIT_K_SHIFT : 0);
const bool is_osx = io.ConfigMacOSXBehaviors;
- const bool is_shortcut_key = (is_osx ? (io.KeySuper && !io.KeyCtrl) : (io.KeyCtrl && !io.KeySuper)) && !io.KeyAlt && !io.KeyShift; // OS X style: Shortcuts using Cmd/Super instead of Ctrl
- const bool is_osx_shift_shortcut = is_osx && io.KeySuper && io.KeyShift && !io.KeyCtrl && !io.KeyAlt;
const bool is_wordmove_key_down = is_osx ? io.KeyAlt : io.KeyCtrl; // OS X style: Text editing cursor movement using Alt instead of Ctrl
const bool is_startend_key_down = is_osx && io.KeySuper && !io.KeyCtrl && !io.KeyAlt; // OS X style: Line/Text Start and End using Cmd+Arrows instead of Home/End
- const bool is_ctrl_key_only = io.KeyCtrl && !io.KeyShift && !io.KeyAlt && !io.KeySuper;
- const bool is_shift_key_only = io.KeyShift && !io.KeyCtrl && !io.KeyAlt && !io.KeySuper;
-
- const bool is_cut = ((is_shortcut_key && IsKeyPressedMap(ImGuiKey_X)) || (is_shift_key_only && IsKeyPressedMap(ImGuiKey_Delete))) && !is_readonly && !is_password && (!is_multiline || state->HasSelection());
- const bool is_copy = ((is_shortcut_key && IsKeyPressedMap(ImGuiKey_C)) || (is_ctrl_key_only && IsKeyPressedMap(ImGuiKey_Insert))) && !is_password && (!is_multiline || state->HasSelection());
- const bool is_paste = ((is_shortcut_key && IsKeyPressedMap(ImGuiKey_V)) || (is_shift_key_only && IsKeyPressedMap(ImGuiKey_Insert))) && !is_readonly;
- const bool is_undo = ((is_shortcut_key && IsKeyPressedMap(ImGuiKey_Z)) && !is_readonly && is_undoable);
- const bool is_redo = ((is_shortcut_key && IsKeyPressedMap(ImGuiKey_Y)) || (is_osx_shift_shortcut && IsKeyPressedMap(ImGuiKey_Z))) && !is_readonly && is_undoable;
-
- if (IsKeyPressedMap(ImGuiKey_LeftArrow)) { state->OnKeyPressed((is_startend_key_down ? STB_TEXTEDIT_K_LINESTART : is_wordmove_key_down ? STB_TEXTEDIT_K_WORDLEFT : STB_TEXTEDIT_K_LEFT) | k_mask); }
- else if (IsKeyPressedMap(ImGuiKey_RightArrow)) { state->OnKeyPressed((is_startend_key_down ? STB_TEXTEDIT_K_LINEEND : is_wordmove_key_down ? STB_TEXTEDIT_K_WORDRIGHT : STB_TEXTEDIT_K_RIGHT) | k_mask); }
- else if (IsKeyPressedMap(ImGuiKey_UpArrow) && is_multiline) { if (io.KeyCtrl) SetScrollY(draw_window, ImMax(draw_window->Scroll.y - g.FontSize, 0.0f)); else state->OnKeyPressed((is_startend_key_down ? STB_TEXTEDIT_K_TEXTSTART : STB_TEXTEDIT_K_UP) | k_mask); }
- else if (IsKeyPressedMap(ImGuiKey_DownArrow) && is_multiline) { if (io.KeyCtrl) SetScrollY(draw_window, ImMin(draw_window->Scroll.y + g.FontSize, GetScrollMaxY())); else state->OnKeyPressed((is_startend_key_down ? STB_TEXTEDIT_K_TEXTEND : STB_TEXTEDIT_K_DOWN) | k_mask); }
- else if (IsKeyPressedMap(ImGuiKey_Home)) { state->OnKeyPressed(io.KeyCtrl ? STB_TEXTEDIT_K_TEXTSTART | k_mask : STB_TEXTEDIT_K_LINESTART | k_mask); }
- else if (IsKeyPressedMap(ImGuiKey_End)) { state->OnKeyPressed(io.KeyCtrl ? STB_TEXTEDIT_K_TEXTEND | k_mask : STB_TEXTEDIT_K_LINEEND | k_mask); }
- else if (IsKeyPressedMap(ImGuiKey_Delete) && !is_readonly) { state->OnKeyPressed(STB_TEXTEDIT_K_DELETE | k_mask); }
- else if (IsKeyPressedMap(ImGuiKey_Backspace) && !is_readonly)
+
+ // Using Shortcut() with ImGuiInputFlags_RouteFocused (default policy) to allow routing operations for other code (e.g. calling window trying to use CTRL+A and CTRL+B: formet would be handled by InputText)
+ // Otherwise we could simply assume that we own the keys as we are active.
+ const ImGuiInputFlags f_repeat = ImGuiInputFlags_Repeat;
+ const bool is_cut = (Shortcut(ImGuiMod_Shortcut | ImGuiKey_X, id, f_repeat) || Shortcut(ImGuiMod_Shift | ImGuiKey_Delete, id, f_repeat)) && !is_readonly && !is_password && (!is_multiline || state->HasSelection());
+ const bool is_copy = (Shortcut(ImGuiMod_Shortcut | ImGuiKey_C, id) || Shortcut(ImGuiMod_Ctrl | ImGuiKey_Insert, id)) && !is_password && (!is_multiline || state->HasSelection());
+ const bool is_paste = (Shortcut(ImGuiMod_Shortcut | ImGuiKey_V, id, f_repeat) || Shortcut(ImGuiMod_Shift | ImGuiKey_Insert, id, f_repeat)) && !is_readonly;
+ const bool is_undo = (Shortcut(ImGuiMod_Shortcut | ImGuiKey_Z, id, f_repeat)) && !is_readonly && is_undoable;
+ const bool is_redo = (Shortcut(ImGuiMod_Shortcut | ImGuiKey_Y, id, f_repeat) || (is_osx && Shortcut(ImGuiMod_Shortcut | ImGuiMod_Shift | ImGuiKey_Z, id, f_repeat))) && !is_readonly && is_undoable;
+ const bool is_select_all = Shortcut(ImGuiMod_Shortcut | ImGuiKey_A, id);
+
+ // We allow validate/cancel with Nav source (gamepad) to makes it easier to undo an accidental NavInput press with no keyboard wired, but otherwise it isn't very useful.
+ const bool nav_gamepad_active = (io.ConfigFlags & ImGuiConfigFlags_NavEnableGamepad) != 0 && (io.BackendFlags & ImGuiBackendFlags_HasGamepad) != 0;
+ const bool is_enter_pressed = IsKeyPressed(ImGuiKey_Enter, true) || IsKeyPressed(ImGuiKey_KeypadEnter, true);
+ const bool is_gamepad_validate = nav_gamepad_active && (IsKeyPressed(ImGuiKey_NavGamepadActivate, false) || IsKeyPressed(ImGuiKey_NavGamepadInput, false));
+ const bool is_cancel = Shortcut(ImGuiKey_Escape, id, f_repeat) || (nav_gamepad_active && Shortcut(ImGuiKey_NavGamepadCancel, id, f_repeat));
+
+ // FIXME: Should use more Shortcut() and reduce IsKeyPressed()+SetKeyOwner(), but requires modifiers combination to be taken account of.
+ if (IsKeyPressed(ImGuiKey_LeftArrow)) { state->OnKeyPressed((is_startend_key_down ? STB_TEXTEDIT_K_LINESTART : is_wordmove_key_down ? STB_TEXTEDIT_K_WORDLEFT : STB_TEXTEDIT_K_LEFT) | k_mask); }
+ else if (IsKeyPressed(ImGuiKey_RightArrow)) { state->OnKeyPressed((is_startend_key_down ? STB_TEXTEDIT_K_LINEEND : is_wordmove_key_down ? STB_TEXTEDIT_K_WORDRIGHT : STB_TEXTEDIT_K_RIGHT) | k_mask); }
+ else if (IsKeyPressed(ImGuiKey_UpArrow) && is_multiline) { if (io.KeyCtrl) SetScrollY(draw_window, ImMax(draw_window->Scroll.y - g.FontSize, 0.0f)); else state->OnKeyPressed((is_startend_key_down ? STB_TEXTEDIT_K_TEXTSTART : STB_TEXTEDIT_K_UP) | k_mask); }
+ else if (IsKeyPressed(ImGuiKey_DownArrow) && is_multiline) { if (io.KeyCtrl) SetScrollY(draw_window, ImMin(draw_window->Scroll.y + g.FontSize, GetScrollMaxY())); else state->OnKeyPressed((is_startend_key_down ? STB_TEXTEDIT_K_TEXTEND : STB_TEXTEDIT_K_DOWN) | k_mask); }
+ else if (IsKeyPressed(ImGuiKey_PageUp) && is_multiline) { state->OnKeyPressed(STB_TEXTEDIT_K_PGUP | k_mask); scroll_y -= row_count_per_page * g.FontSize; }
+ else if (IsKeyPressed(ImGuiKey_PageDown) && is_multiline) { state->OnKeyPressed(STB_TEXTEDIT_K_PGDOWN | k_mask); scroll_y += row_count_per_page * g.FontSize; }
+ else if (IsKeyPressed(ImGuiKey_Home)) { state->OnKeyPressed(io.KeyCtrl ? STB_TEXTEDIT_K_TEXTSTART | k_mask : STB_TEXTEDIT_K_LINESTART | k_mask); }
+ else if (IsKeyPressed(ImGuiKey_End)) { state->OnKeyPressed(io.KeyCtrl ? STB_TEXTEDIT_K_TEXTEND | k_mask : STB_TEXTEDIT_K_LINEEND | k_mask); }
+ else if (IsKeyPressed(ImGuiKey_Delete) && !is_readonly && !is_cut) { state->OnKeyPressed(STB_TEXTEDIT_K_DELETE | k_mask); }
+ else if (IsKeyPressed(ImGuiKey_Backspace) && !is_readonly)
{
if (!state->HasSelection())
{
if (is_wordmove_key_down)
- state->OnKeyPressed(STB_TEXTEDIT_K_WORDLEFT|STB_TEXTEDIT_K_SHIFT);
+ state->OnKeyPressed(STB_TEXTEDIT_K_WORDLEFT | STB_TEXTEDIT_K_SHIFT);
else if (is_osx && io.KeySuper && !io.KeyAlt && !io.KeyCtrl)
- state->OnKeyPressed(STB_TEXTEDIT_K_LINESTART|STB_TEXTEDIT_K_SHIFT);
+ state->OnKeyPressed(STB_TEXTEDIT_K_LINESTART | STB_TEXTEDIT_K_SHIFT);
}
state->OnKeyPressed(STB_TEXTEDIT_K_BACKSPACE | k_mask);
}
- else if (IsKeyPressedMap(ImGuiKey_Enter) || IsKeyPressedMap(ImGuiKey_KeyPadEnter))
+ else if (is_enter_pressed || is_gamepad_validate)
{
+ // Determine if we turn Enter into a \n character
bool ctrl_enter_for_new_line = (flags & ImGuiInputTextFlags_CtrlEnterForNewLine) != 0;
- if (!is_multiline || (ctrl_enter_for_new_line && !io.KeyCtrl) || (!ctrl_enter_for_new_line && io.KeyCtrl))
+ if (!is_multiline || is_gamepad_validate || (ctrl_enter_for_new_line && !io.KeyCtrl) || (!ctrl_enter_for_new_line && io.KeyCtrl))
{
- enter_pressed = clear_active_id = true;
+ validated = true;
+ if (io.ConfigInputTextEnterKeepActive && !is_multiline)
+ state->SelectAll(); // No need to scroll
+ else
+ clear_active_id = true;
}
else if (!is_readonly)
{
unsigned int c = '\n'; // Insert new line
- if (InputTextFilterCharacter(&c, flags, callback, callback_user_data))
+ if (InputTextFilterCharacter(&c, flags, callback, callback_user_data, ImGuiInputSource_Keyboard))
state->OnKeyPressed((int)c);
}
}
- else if (IsKeyPressedMap(ImGuiKey_Escape))
+ else if (is_cancel)
{
- clear_active_id = cancel_edit = true;
+ if (flags & ImGuiInputTextFlags_EscapeClearsAll)
+ {
+ if (state->CurLenA > 0)
+ {
+ revert_edit = true;
+ }
+ else
+ {
+ render_cursor = render_selection = false;
+ clear_active_id = true;
+ }
+ }
+ else
+ {
+ clear_active_id = revert_edit = true;
+ render_cursor = render_selection = false;
+ }
}
else if (is_undo || is_redo)
{
state->OnKeyPressed(is_undo ? STB_TEXTEDIT_K_UNDO : STB_TEXTEDIT_K_REDO);
state->ClearSelection();
}
- else if (is_shortcut_key && IsKeyPressedMap(ImGuiKey_A))
+ else if (is_select_all)
{
state->SelectAll();
state->CursorFollow = true;
@@ -3770,7 +4414,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
{
// Filter pasted buffer
const int clipboard_len = (int)strlen(clipboard);
- ImWchar* clipboard_filtered = (ImWchar*)IM_ALLOC((clipboard_len+1) * sizeof(ImWchar));
+ ImWchar* clipboard_filtered = (ImWchar*)IM_ALLOC((clipboard_len + 1) * sizeof(ImWchar));
int clipboard_filtered_len = 0;
for (const char* s = clipboard; *s; )
{
@@ -3778,7 +4422,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
s += ImTextCharFromUtf8(&c, s, NULL);
if (c == 0)
break;
- if (c >= 0x10000 || !InputTextFilterCharacter(&c, flags, callback, callback_user_data))
+ if (!InputTextFilterCharacter(&c, flags, callback, callback_user_data, ImGuiInputSource_Clipboard))
continue;
clipboard_filtered[clipboard_filtered_len++] = (ImWchar)c;
}
@@ -3797,62 +4441,87 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
}
// Process callbacks and apply result back to user's buffer.
+ const char* apply_new_text = NULL;
+ int apply_new_text_length = 0;
if (g.ActiveId == id)
{
IM_ASSERT(state != NULL);
- const char* apply_new_text = NULL;
- int apply_new_text_length = 0;
- if (cancel_edit)
+ if (revert_edit && !is_readonly)
{
- // Restore initial value. Only return true if restoring to the initial value changes the current buffer contents.
- if (!is_readonly && strcmp(buf, state->InitialTextA.Data) != 0)
+ if (flags & ImGuiInputTextFlags_EscapeClearsAll)
+ {
+ // Clear input
+ apply_new_text = "";
+ apply_new_text_length = 0;
+ STB_TEXTEDIT_CHARTYPE empty_string;
+ stb_textedit_replace(state, &state->Stb, &empty_string, 0);
+ }
+ else if (strcmp(buf, state->InitialTextA.Data) != 0)
{
+ // Restore initial value. Only return true if restoring to the initial value changes the current buffer contents.
+ // Push records into the undo stack so we can CTRL+Z the revert operation itself
apply_new_text = state->InitialTextA.Data;
apply_new_text_length = state->InitialTextA.Size - 1;
+ ImVector<ImWchar> w_text;
+ if (apply_new_text_length > 0)
+ {
+ w_text.resize(ImTextCountCharsFromUtf8(apply_new_text, apply_new_text + apply_new_text_length) + 1);
+ ImTextStrFromUtf8(w_text.Data, w_text.Size, apply_new_text, apply_new_text + apply_new_text_length);
+ }
+ stb_textedit_replace(state, &state->Stb, w_text.Data, (apply_new_text_length > 0) ? (w_text.Size - 1) : 0);
}
}
+ // Apply ASCII value
+ if (!is_readonly)
+ {
+ state->TextAIsValid = true;
+ state->TextA.resize(state->TextW.Size * 4 + 1);
+ ImTextStrToUtf8(state->TextA.Data, state->TextA.Size, state->TextW.Data, NULL);
+ }
+
// When using 'ImGuiInputTextFlags_EnterReturnsTrue' as a special case we reapply the live buffer back to the input buffer before clearing ActiveId, even though strictly speaking it wasn't modified on this frame.
- // If we didn't do that, code like InputInt() with ImGuiInputTextFlags_EnterReturnsTrue would fail. Also this allows the user to use InputText() with ImGuiInputTextFlags_EnterReturnsTrue without maintaining any user-side storage.
- bool apply_edit_back_to_user_buffer = !cancel_edit || (enter_pressed && (flags & ImGuiInputTextFlags_EnterReturnsTrue) != 0);
+ // If we didn't do that, code like InputInt() with ImGuiInputTextFlags_EnterReturnsTrue would fail.
+ // This also allows the user to use InputText() with ImGuiInputTextFlags_EnterReturnsTrue without maintaining any user-side storage (please note that if you use this property along ImGuiInputTextFlags_CallbackResize you can end up with your temporary string object unnecessarily allocating once a frame, either store your string data, either if you don't then don't use ImGuiInputTextFlags_CallbackResize).
+ const bool apply_edit_back_to_user_buffer = !revert_edit || (validated && (flags & ImGuiInputTextFlags_EnterReturnsTrue) != 0);
if (apply_edit_back_to_user_buffer)
{
// Apply new value immediately - copy modified buffer back
// Note that as soon as the input box is active, the in-widget value gets priority over any underlying modification of the input buffer
// FIXME: We actually always render 'buf' when calling DrawList->AddText, making the comment above incorrect.
// FIXME-OPT: CPU waste to do this every time the widget is active, should mark dirty state from the stb_textedit callbacks.
- if (!is_readonly)
- {
- state->TextAIsValid = true;
- state->TextA.resize(state->TextW.Size * 4 + 1);
- ImTextStrToUtf8(state->TextA.Data, state->TextA.Size, state->TextW.Data, NULL);
- }
// User callback
- if ((flags & (ImGuiInputTextFlags_CallbackCompletion | ImGuiInputTextFlags_CallbackHistory | ImGuiInputTextFlags_CallbackAlways)) != 0)
+ if ((flags & (ImGuiInputTextFlags_CallbackCompletion | ImGuiInputTextFlags_CallbackHistory | ImGuiInputTextFlags_CallbackEdit | ImGuiInputTextFlags_CallbackAlways)) != 0)
{
IM_ASSERT(callback != NULL);
// The reason we specify the usage semantic (Completion/History) is that Completion needs to disable keyboard TABBING at the moment.
ImGuiInputTextFlags event_flag = 0;
- ImGuiKey event_key = ImGuiKey_COUNT;
- if ((flags & ImGuiInputTextFlags_CallbackCompletion) != 0 && IsKeyPressedMap(ImGuiKey_Tab))
+ ImGuiKey event_key = ImGuiKey_None;
+ if ((flags & ImGuiInputTextFlags_CallbackCompletion) != 0 && IsKeyPressed(ImGuiKey_Tab))
{
event_flag = ImGuiInputTextFlags_CallbackCompletion;
event_key = ImGuiKey_Tab;
}
- else if ((flags & ImGuiInputTextFlags_CallbackHistory) != 0 && IsKeyPressedMap(ImGuiKey_UpArrow))
+ else if ((flags & ImGuiInputTextFlags_CallbackHistory) != 0 && IsKeyPressed(ImGuiKey_UpArrow))
{
event_flag = ImGuiInputTextFlags_CallbackHistory;
event_key = ImGuiKey_UpArrow;
}
- else if ((flags & ImGuiInputTextFlags_CallbackHistory) != 0 && IsKeyPressedMap(ImGuiKey_DownArrow))
+ else if ((flags & ImGuiInputTextFlags_CallbackHistory) != 0 && IsKeyPressed(ImGuiKey_DownArrow))
{
event_flag = ImGuiInputTextFlags_CallbackHistory;
event_key = ImGuiKey_DownArrow;
}
+ else if ((flags & ImGuiInputTextFlags_CallbackEdit) && state->Edited)
+ {
+ event_flag = ImGuiInputTextFlags_CallbackEdit;
+ }
else if (flags & ImGuiInputTextFlags_CallbackAlways)
+ {
event_flag = ImGuiInputTextFlags_CallbackAlways;
+ }
if (event_flag)
{
@@ -3862,8 +4531,9 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
callback_data.Flags = flags;
callback_data.UserData = callback_user_data;
+ char* callback_buf = is_readonly ? buf : state->TextA.Data;
callback_data.EventKey = event_key;
- callback_data.Buf = state->TextA.Data;
+ callback_data.Buf = callback_buf;
callback_data.BufTextLen = state->CurLenA;
callback_data.BufSize = state->BufCapacityA;
callback_data.BufDirty = false;
@@ -3878,17 +4548,21 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
callback(&callback_data);
// Read back what user may have modified
- IM_ASSERT(callback_data.Buf == state->TextA.Data); // Invalid to modify those fields
+ callback_buf = is_readonly ? buf : state->TextA.Data; // Pointer may have been invalidated by a resize callback
+ IM_ASSERT(callback_data.Buf == callback_buf); // Invalid to modify those fields
IM_ASSERT(callback_data.BufSize == state->BufCapacityA);
IM_ASSERT(callback_data.Flags == flags);
- if (callback_data.CursorPos != utf8_cursor_pos) { state->Stb.cursor = ImTextCountCharsFromUtf8(callback_data.Buf, callback_data.Buf + callback_data.CursorPos); state->CursorFollow = true; }
- if (callback_data.SelectionStart != utf8_selection_start) { state->Stb.select_start = ImTextCountCharsFromUtf8(callback_data.Buf, callback_data.Buf + callback_data.SelectionStart); }
- if (callback_data.SelectionEnd != utf8_selection_end) { state->Stb.select_end = ImTextCountCharsFromUtf8(callback_data.Buf, callback_data.Buf + callback_data.SelectionEnd); }
- if (callback_data.BufDirty)
+ const bool buf_dirty = callback_data.BufDirty;
+ if (callback_data.CursorPos != utf8_cursor_pos || buf_dirty) { state->Stb.cursor = ImTextCountCharsFromUtf8(callback_data.Buf, callback_data.Buf + callback_data.CursorPos); state->CursorFollow = true; }
+ if (callback_data.SelectionStart != utf8_selection_start || buf_dirty) { state->Stb.select_start = (callback_data.SelectionStart == callback_data.CursorPos) ? state->Stb.cursor : ImTextCountCharsFromUtf8(callback_data.Buf, callback_data.Buf + callback_data.SelectionStart); }
+ if (callback_data.SelectionEnd != utf8_selection_end || buf_dirty) { state->Stb.select_end = (callback_data.SelectionEnd == callback_data.SelectionStart) ? state->Stb.select_start : ImTextCountCharsFromUtf8(callback_data.Buf, callback_data.Buf + callback_data.SelectionEnd); }
+ if (buf_dirty)
{
+ IM_ASSERT((flags & ImGuiInputTextFlags_ReadOnly) == 0);
IM_ASSERT(callback_data.BufTextLen == (int)strlen(callback_data.Buf)); // You need to maintain BufTextLen if you change the text!
+ InputTextReconcileUndoStateAfterUserCallback(state, callback_data.Buf, callback_data.BufTextLen); // FIXME: Move the rest of this block inside function and rename to InputTextReconcileStateAfterUserCallback() ?
if (callback_data.BufTextLen > backup_current_text_length && is_resizable)
- state->TextW.resize(state->TextW.Size + (callback_data.BufTextLen - backup_current_text_length));
+ state->TextW.resize(state->TextW.Size + (callback_data.BufTextLen - backup_current_text_length)); // Worse case scenario resize
state->CurLenW = ImTextStrFromUtf8(state->TextW.Data, state->TextW.Size, callback_data.Buf, NULL);
state->CurLenA = callback_data.BufTextLen; // Assume correct length and valid UTF-8 from user, saves us an extra strlen()
state->CursorAnimReset();
@@ -3903,36 +4577,35 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
apply_new_text_length = state->CurLenA;
}
}
+ }
- // Copy result to user buffer
- if (apply_new_text)
+ // Copy result to user buffer. This can currently only happen when (g.ActiveId == id)
+ if (apply_new_text != NULL)
+ {
+ // We cannot test for 'backup_current_text_length != apply_new_text_length' here because we have no guarantee that the size
+ // of our owned buffer matches the size of the string object held by the user, and by design we allow InputText() to be used
+ // without any storage on user's side.
+ IM_ASSERT(apply_new_text_length >= 0);
+ if (is_resizable)
{
- IM_ASSERT(apply_new_text_length >= 0);
- if (backup_current_text_length != apply_new_text_length && is_resizable)
- {
- ImGuiInputTextCallbackData callback_data;
- callback_data.EventFlag = ImGuiInputTextFlags_CallbackResize;
- callback_data.Flags = flags;
- callback_data.Buf = buf;
- callback_data.BufTextLen = apply_new_text_length;
- callback_data.BufSize = ImMax(buf_size, apply_new_text_length + 1);
- callback_data.UserData = callback_user_data;
- callback(&callback_data);
- buf = callback_data.Buf;
- buf_size = callback_data.BufSize;
- apply_new_text_length = ImMin(callback_data.BufTextLen, buf_size - 1);
- IM_ASSERT(apply_new_text_length <= buf_size);
- }
-
- // If the underlying buffer resize was denied or not carried to the next frame, apply_new_text_length+1 may be >= buf_size.
- ImStrncpy(buf, apply_new_text, ImMin(apply_new_text_length + 1, buf_size));
- value_changed = true;
+ ImGuiInputTextCallbackData callback_data;
+ callback_data.EventFlag = ImGuiInputTextFlags_CallbackResize;
+ callback_data.Flags = flags;
+ callback_data.Buf = buf;
+ callback_data.BufTextLen = apply_new_text_length;
+ callback_data.BufSize = ImMax(buf_size, apply_new_text_length + 1);
+ callback_data.UserData = callback_user_data;
+ callback(&callback_data);
+ buf = callback_data.Buf;
+ buf_size = callback_data.BufSize;
+ apply_new_text_length = ImMin(callback_data.BufTextLen, buf_size - 1);
+ IM_ASSERT(apply_new_text_length <= buf_size);
}
+ //IMGUI_DEBUG_PRINT("InputText(\"%s\"): apply_new_text length %d\n", label, apply_new_text_length);
- // Clear temporary user storage
- state->UserFlags = 0;
- state->UserCallback = NULL;
- state->UserCallbackData = NULL;
+ // If the underlying buffer resize was denied or not carried to the next frame, apply_new_text_length+1 may be >= buf_size.
+ ImStrncpy(buf, apply_new_text, ImMin(apply_new_text_length + 1, buf_size));
+ value_changed = true;
}
// Release active ID at the end of the function (so e.g. pressing Return still does a final application of the value)
@@ -4002,7 +4675,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
// In multi-line mode, we never exit the loop until all lines are counted, so add one extra to the searches_remaining counter.
searches_remaining += is_multiline ? 1 : 0;
int line_count = 0;
- //for (const ImWchar* s = text_begin; (s = (const ImWchar*)wcschr((const wchar_t*)s, (wchar_t)'\n')) != NULL; s++) // FIXME-OPT: Could use this when wchar_t are 16-bits
+ //for (const ImWchar* s = text_begin; (s = (const ImWchar*)wcschr((const wchar_t*)s, (wchar_t)'\n')) != NULL; s++) // FIXME-OPT: Could use this when wchar_t are 16-bit
for (const ImWchar* s = text_begin; *s != 0; s++)
if (*s == '\n')
{
@@ -4037,10 +4710,11 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
if (!(flags & ImGuiInputTextFlags_NoHorizontalScroll))
{
const float scroll_increment_x = inner_size.x * 0.25f;
+ const float visible_width = inner_size.x - style.FramePadding.x;
if (cursor_offset.x < state->ScrollX)
state->ScrollX = IM_FLOOR(ImMax(0.0f, cursor_offset.x - scroll_increment_x));
- else if (cursor_offset.x - inner_size.x >= state->ScrollX)
- state->ScrollX = IM_FLOOR(cursor_offset.x - inner_size.x + scroll_increment_x);
+ else if (cursor_offset.x - visible_width >= state->ScrollX)
+ state->ScrollX = IM_FLOOR(cursor_offset.x - visible_width + scroll_increment_x);
}
else
{
@@ -4050,11 +4724,13 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
// Vertical scroll
if (is_multiline)
{
- float scroll_y = draw_window->Scroll.y;
+ // Test if cursor is vertically visible
if (cursor_offset.y - g.FontSize < scroll_y)
scroll_y = ImMax(0.0f, cursor_offset.y - g.FontSize);
- else if (cursor_offset.y - inner_size.y >= scroll_y)
- scroll_y = cursor_offset.y - inner_size.y;
+ else if (cursor_offset.y - (inner_size.y - style.FramePadding.y * 2.0f) >= scroll_y)
+ scroll_y = cursor_offset.y - inner_size.y + style.FramePadding.y * 2.0f;
+ const float scroll_max_y = ImMax((text_size.y + style.FramePadding.y * 2.0f) - inner_size.y, 0.0f);
+ scroll_y = ImClamp(scroll_y, 0.0f, scroll_max_y);
draw_pos.y += (draw_window->Scroll.y - scroll_y); // Manipulate cursor pos immediately avoid a frame of lag
draw_window->Scroll.y = scroll_y;
}
@@ -4079,7 +4755,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
break;
if (rect_pos.y < clip_rect.y)
{
- //p = (const ImWchar*)wmemchr((const wchar_t*)p, '\n', text_selected_end - p); // FIXME-OPT: Could use this when wchar_t are 16-bits
+ //p = (const ImWchar*)wmemchr((const wchar_t*)p, '\n', text_selected_end - p); // FIXME-OPT: Could use this when wchar_t are 16-bit
//p = p ? p + 1 : text_selected_end;
while (p < text_selected_end)
if (*p++ == '\n')
@@ -4089,7 +4765,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
{
ImVec2 rect_size = InputTextCalcTextSizeW(p, text_selected_end, &p, NULL, true);
if (rect_size.x <= 0.0f) rect_size.x = IM_FLOOR(g.Font->GetCharAdvance((ImWchar)' ') * 0.50f); // So we can see selected empty lines
- ImRect rect(rect_pos + ImVec2(0.0f, bg_offy_up - g.FontSize), rect_pos +ImVec2(rect_size.x, bg_offy_dn));
+ ImRect rect(rect_pos + ImVec2(0.0f, bg_offy_up - g.FontSize), rect_pos + ImVec2(rect_size.x, bg_offy_dn));
rect.ClipWith(clip_rect);
if (rect.Overlaps(clip_rect))
draw_window->DrawList->AddRectFilled(rect.Min, rect.Max, bg_color);
@@ -4111,14 +4787,18 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
{
state->CursorAnim += io.DeltaTime;
bool cursor_is_visible = (!g.IO.ConfigInputTextCursorBlink) || (state->CursorAnim <= 0.0f) || ImFmod(state->CursorAnim, 1.20f) <= 0.80f;
- ImVec2 cursor_screen_pos = draw_pos + cursor_offset - draw_scroll;
+ ImVec2 cursor_screen_pos = ImFloor(draw_pos + cursor_offset - draw_scroll);
ImRect cursor_screen_rect(cursor_screen_pos.x, cursor_screen_pos.y - g.FontSize + 0.5f, cursor_screen_pos.x + 1.0f, cursor_screen_pos.y - 1.5f);
if (cursor_is_visible && cursor_screen_rect.Overlaps(clip_rect))
draw_window->DrawList->AddLine(cursor_screen_rect.Min, cursor_screen_rect.GetBL(), GetColorU32(ImGuiCol_Text));
// Notify OS of text input position for advanced IME (-1 x offset so that Windows IME can cover our cursor. Bit of an extra nicety.)
if (!is_readonly)
- g.PlatformImePos = ImVec2(cursor_screen_pos.x - 1.0f, cursor_screen_pos.y - g.FontSize);
+ {
+ g.PlatformImeData.WantVisible = true;
+ g.PlatformImeData.InputPos = ImVec2(cursor_screen_pos.x - 1.0f, cursor_screen_pos.y - g.FontSize);
+ g.PlatformImeData.InputLineHeight = g.FontSize;
+ }
}
}
else
@@ -4138,19 +4818,36 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
}
}
+ if (is_password && !is_displaying_hint)
+ PopFont();
+
if (is_multiline)
{
- Dummy(text_size + ImVec2(0.0f, g.FontSize)); // Always add room to scroll an extra line
- EndChildFrame();
+ // For focus requests to work on our multiline we need to ensure our child ItemAdd() call specifies the ImGuiItemFlags_Inputable (ref issue #4761)...
+ Dummy(ImVec2(text_size.x, text_size.y + style.FramePadding.y));
+ ImGuiItemFlags backup_item_flags = g.CurrentItemFlags;
+ g.CurrentItemFlags |= ImGuiItemFlags_Inputable | ImGuiItemFlags_NoTabStop;
+ EndChild();
+ item_data_backup.StatusFlags |= (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredWindow);
+ g.CurrentItemFlags = backup_item_flags;
+
+ // ...and then we need to undo the group overriding last item data, which gets a bit messy as EndGroup() tries to forward scrollbar being active...
+ // FIXME: This quite messy/tricky, should attempt to get rid of the child window.
EndGroup();
+ if (g.LastItemData.ID == 0)
+ {
+ g.LastItemData.ID = id;
+ g.LastItemData.InFlags = item_data_backup.InFlags;
+ g.LastItemData.StatusFlags = item_data_backup.StatusFlags;
+ }
}
- if (is_password && !is_displaying_hint)
- PopFont();
-
// Log as text
- if (g.LogEnabled && !(is_password && !is_displaying_hint))
+ if (g.LogEnabled && (!is_password || is_displaying_hint))
+ {
+ LogSetNextTextDecoration("{", "}");
LogRenderedText(&draw_pos, buf_display, buf_display_end);
+ }
if (label_size.x > 0)
RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label);
@@ -4158,13 +4855,48 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
if (value_changed && !(flags & ImGuiInputTextFlags_NoMarkEdited))
MarkItemEdited(id);
- IMGUI_TEST_ENGINE_ITEM_INFO(id, label, window->DC.ItemFlags);
+ IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
if ((flags & ImGuiInputTextFlags_EnterReturnsTrue) != 0)
- return enter_pressed;
+ return validated;
else
return value_changed;
}
+void ImGui::DebugNodeInputTextState(ImGuiInputTextState* state)
+{
+#ifndef IMGUI_DISABLE_DEBUG_TOOLS
+ ImGuiContext& g = *GImGui;
+ ImStb::STB_TexteditState* stb_state = &state->Stb;
+ ImStb::StbUndoState* undo_state = &stb_state->undostate;
+ Text("ID: 0x%08X, ActiveID: 0x%08X", state->ID, g.ActiveId);
+ DebugLocateItemOnHover(state->ID);
+ Text("CurLenW: %d, CurLenA: %d, Cursor: %d, Selection: %d..%d", state->CurLenA, state->CurLenW, stb_state->cursor, stb_state->select_start, stb_state->select_end);
+ Text("undo_point: %d, redo_point: %d, undo_char_point: %d, redo_char_point: %d", undo_state->undo_point, undo_state->redo_point, undo_state->undo_char_point, undo_state->redo_char_point);
+ if (BeginChild("undopoints", ImVec2(0.0f, GetTextLineHeight() * 15), true)) // Visualize undo state
+ {
+ PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0));
+ for (int n = 0; n < STB_TEXTEDIT_UNDOSTATECOUNT; n++)
+ {
+ ImStb::StbUndoRecord* undo_rec = &undo_state->undo_rec[n];
+ const char undo_rec_type = (n < undo_state->undo_point) ? 'u' : (n >= undo_state->redo_point) ? 'r' : ' ';
+ if (undo_rec_type == ' ')
+ BeginDisabled();
+ char buf[64] = "";
+ if (undo_rec_type != ' ' && undo_rec->char_storage != -1)
+ ImTextStrToUtf8(buf, IM_ARRAYSIZE(buf), undo_state->undo_char + undo_rec->char_storage, undo_state->undo_char + undo_rec->char_storage + undo_rec->insert_length);
+ Text("%c [%02d] where %03d, insert %03d, delete %03d, char_storage %03d \"%s\"",
+ undo_rec_type, n, undo_rec->where, undo_rec->insert_length, undo_rec->delete_length, undo_rec->char_storage, buf);
+ if (undo_rec_type == ' ')
+ EndDisabled();
+ }
+ PopStyleVar();
+ }
+ EndChild();
+#else
+ IM_UNUSED(state);
+#endif
+}
+
//-------------------------------------------------------------------------
// [SECTION] Widgets: ColorEdit, ColorPicker, ColorButton, etc.
//-------------------------------------------------------------------------
@@ -4185,9 +4917,33 @@ bool ImGui::ColorEdit3(const char* label, float col[3], ImGuiColorEditFlags flag
return ColorEdit4(label, col, flags | ImGuiColorEditFlags_NoAlpha);
}
+// ColorEdit supports RGB and HSV inputs. In case of RGB input resulting color may have undefined hue and/or saturation.
+// Since widget displays both RGB and HSV values we must preserve hue and saturation to prevent these values resetting.
+static void ColorEditRestoreHS(const float* col, float* H, float* S, float* V)
+{
+ // This check is optional. Suppose we have two color widgets side by side, both widgets display different colors, but both colors have hue and/or saturation undefined.
+ // With color check: hue/saturation is preserved in one widget. Editing color in one widget would reset hue/saturation in another one.
+ // Without color check: common hue/saturation would be displayed in all widgets that have hue/saturation undefined.
+ // g.ColorEditLastColor is stored as ImU32 RGB value: this essentially gives us color equality check with reduced precision.
+ // Tiny external color changes would not be detected and this check would still pass. This is OK, since we only restore hue/saturation _only_ if they are undefined,
+ // therefore this change flipping hue/saturation from undefined to a very tiny value would still be represented in color picker.
+ ImGuiContext& g = *GImGui;
+ if (g.ColorEditLastColor != ImGui::ColorConvertFloat4ToU32(ImVec4(col[0], col[1], col[2], 0)))
+ return;
+
+ // When S == 0, H is undefined.
+ // When H == 1 it wraps around to 0.
+ if (*S == 0.0f || (*H == 0.0f && g.ColorEditLastHue == 1))
+ *H = g.ColorEditLastHue;
+
+ // When V == 0, S is undefined.
+ if (*V == 0.0f)
+ *S = g.ColorEditLastSat;
+}
+
// 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.
+// With typical options: Left-click on color 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();
@@ -4209,24 +4965,24 @@ bool ImGui::ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flag
// 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__DisplayMask)) | ImGuiColorEditFlags_DisplayRGB | ImGuiColorEditFlags_NoOptions;
+ flags = (flags & (~ImGuiColorEditFlags_DisplayMask_)) | ImGuiColorEditFlags_DisplayRGB | 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__DisplayMask))
- flags |= (g.ColorEditOptions & ImGuiColorEditFlags__DisplayMask);
- if (!(flags & ImGuiColorEditFlags__DataTypeMask))
- flags |= (g.ColorEditOptions & ImGuiColorEditFlags__DataTypeMask);
- if (!(flags & ImGuiColorEditFlags__PickerMask))
- flags |= (g.ColorEditOptions & ImGuiColorEditFlags__PickerMask);
- if (!(flags & ImGuiColorEditFlags__InputMask))
- flags |= (g.ColorEditOptions & ImGuiColorEditFlags__InputMask);
- flags |= (g.ColorEditOptions & ~(ImGuiColorEditFlags__DisplayMask | ImGuiColorEditFlags__DataTypeMask | ImGuiColorEditFlags__PickerMask | ImGuiColorEditFlags__InputMask));
- IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags__DisplayMask)); // Check that only 1 is selected
- IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags__InputMask)); // Check that only 1 is selected
+ if (!(flags & ImGuiColorEditFlags_DisplayMask_))
+ flags |= (g.ColorEditOptions & ImGuiColorEditFlags_DisplayMask_);
+ if (!(flags & ImGuiColorEditFlags_DataTypeMask_))
+ flags |= (g.ColorEditOptions & ImGuiColorEditFlags_DataTypeMask_);
+ if (!(flags & ImGuiColorEditFlags_PickerMask_))
+ flags |= (g.ColorEditOptions & ImGuiColorEditFlags_PickerMask_);
+ if (!(flags & ImGuiColorEditFlags_InputMask_))
+ flags |= (g.ColorEditOptions & ImGuiColorEditFlags_InputMask_);
+ flags |= (g.ColorEditOptions & ~(ImGuiColorEditFlags_DisplayMask_ | ImGuiColorEditFlags_DataTypeMask_ | ImGuiColorEditFlags_PickerMask_ | ImGuiColorEditFlags_InputMask_));
+ IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_DisplayMask_)); // Check that only 1 is selected
+ IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_InputMask_)); // Check that only 1 is selected
const bool alpha = (flags & ImGuiColorEditFlags_NoAlpha) == 0;
const bool hdr = (flags & ImGuiColorEditFlags_HDR) != 0;
@@ -4240,8 +4996,7 @@ bool ImGui::ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flag
{
// Hue is lost when converting from greyscale rgb (saturation=0). Restore it.
ColorConvertRGBtoHSV(f[0], f[1], f[2], f[0], f[1], f[2]);
- if (f[1] == 0 && memcmp(g.ColorEditLastColor, col, sizeof(float) * 3) == 0)
- f[0] = g.ColorEditLastHue;
+ ColorEditRestoreHS(col, &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]) };
@@ -4255,8 +5010,8 @@ bool ImGui::ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flag
if ((flags & (ImGuiColorEditFlags_DisplayRGB | ImGuiColorEditFlags_DisplayHSV)) != 0 && (flags & ImGuiColorEditFlags_NoInputs) == 0)
{
// RGB/HSV 0..255 Sliders
- const float w_item_one = ImMax(1.0f, IM_FLOOR((w_inputs - (style.ItemInnerSpacing.x) * (components-1)) / (float)components));
- const float w_item_last = ImMax(1.0f, IM_FLOOR(w_inputs - (w_item_one + style.ItemInnerSpacing.x) * (components-1)));
+ const float w_item_one = ImMax(1.0f, IM_FLOOR((w_inputs - (style.ItemInnerSpacing.x) * (components - 1)) / (float)components));
+ const float w_item_last = ImMax(1.0f, IM_FLOOR(w_inputs - (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);
static const char* ids[4] = { "##X", "##Y", "##Z", "##W" };
@@ -4280,19 +5035,18 @@ bool ImGui::ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flag
SameLine(0, style.ItemInnerSpacing.x);
SetNextItemWidth((n + 1 < components) ? w_item_one : w_item_last);
- // Disable Hue edit when Saturation is zero
- const bool disable_hue_edit = (n == 0 && (flags & ImGuiColorEditFlags_DisplayHSV) && i[1] == 0);
+ // FIXME: When ImGuiColorEditFlags_HDR flag is passed HS values snap in weird ways when SV values go below 0.
if (flags & ImGuiColorEditFlags_Float)
{
- value_changed |= DragFloat(ids[n], &f[n], 1.0f/255.0f, disable_hue_edit ? +FLT_MAX : 0.0f, disable_hue_edit ? -FLT_MAX : hdr ? 0.0f : 1.0f, fmt_table_float[fmt_idx][n]);
+ value_changed |= DragFloat(ids[n], &f[n], 1.0f / 255.0f, 0.0f, hdr ? 0.0f : 1.0f, fmt_table_float[fmt_idx][n]);
value_changed_as_float |= value_changed;
}
else
{
- value_changed |= DragInt(ids[n], &i[n], 1.0f, disable_hue_edit ? INT_MAX : 0, disable_hue_edit ? INT_MIN : hdr ? 0 : 255, fmt_table_int[fmt_idx][n]);
+ 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");
+ OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight);
}
}
else if ((flags & ImGuiColorEditFlags_DisplayHex) != 0 && (flags & ImGuiColorEditFlags_NoInputs) == 0)
@@ -4300,9 +5054,9 @@ bool ImGui::ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flag
// 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));
+ 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));
+ ImFormatString(buf, IM_ARRAYSIZE(buf), "#%02X%02X%02X", ImClamp(i[0], 0, 255), ImClamp(i[1], 0, 255), ImClamp(i[2], 0, 255));
SetNextItemWidth(w_inputs);
if (InputText("##Text", buf, IM_ARRAYSIZE(buf), ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_CharsUppercase))
{
@@ -4310,14 +5064,17 @@ bool ImGui::ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flag
char* p = buf;
while (*p == '#' || ImCharIsBlankA(*p))
p++;
- i[0] = i[1] = i[2] = i[3] = 0;
+ i[0] = i[1] = i[2] = 0;
+ i[3] = 0xFF; // alpha default to 255 is not parsed by scanf (e.g. inputting #FFFFFF omitting alpha)
+ int r;
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)
+ r = 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]);
+ r = sscanf(p, "%02X%02X%02X", (unsigned int*)&i[0], (unsigned int*)&i[1], (unsigned int*)&i[2]);
+ IM_UNUSED(r); // Fixes C6031: Return value ignored: 'sscanf'.
}
if (!(flags & ImGuiColorEditFlags_NoOptions))
- OpenPopupOnItemClick("context");
+ OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight);
}
ImGuiWindow* picker_active_window = NULL;
@@ -4334,31 +5091,37 @@ bool ImGui::ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flag
// Store current color and open a picker
g.ColorPickerRef = col_v4;
OpenPopup("picker");
- SetNextWindowPos(window->DC.LastItemRect.GetBL() + ImVec2(-1,style.ItemSpacing.y));
+ SetNextWindowPos(g.LastItemData.Rect.GetBL() + ImVec2(0.0f, style.ItemSpacing.y));
}
}
if (!(flags & ImGuiColorEditFlags_NoOptions))
- OpenPopupOnItemClick("context");
+ OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight);
if (BeginPopup("picker"))
{
- picker_active_window = g.CurrentWindow;
- if (label != label_display_end)
+ if (g.CurrentWindow->BeginCount == 1)
{
- TextEx(label, label_display_end);
- Spacing();
+ picker_active_window = g.CurrentWindow;
+ if (label != label_display_end)
+ {
+ TextEx(label, label_display_end);
+ Spacing();
+ }
+ ImGuiColorEditFlags picker_flags_to_forward = ImGuiColorEditFlags_DataTypeMask_ | ImGuiColorEditFlags_PickerMask_ | ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaBar;
+ ImGuiColorEditFlags picker_flags = (flags_untouched & picker_flags_to_forward) | ImGuiColorEditFlags_DisplayMask_ | ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_AlphaPreviewHalf;
+ SetNextItemWidth(square_sz * 12.0f); // Use 256 + bar sizes?
+ value_changed |= ColorPicker4("##picker", col, picker_flags, &g.ColorPickerRef.x);
}
- ImGuiColorEditFlags picker_flags_to_forward = ImGuiColorEditFlags__DataTypeMask | ImGuiColorEditFlags__PickerMask | ImGuiColorEditFlags__InputMask | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaBar;
- ImGuiColorEditFlags picker_flags = (flags_untouched & picker_flags_to_forward) | ImGuiColorEditFlags__DisplayMask | ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_AlphaPreviewHalf;
- SetNextItemWidth(square_sz * 12.0f); // Use 256 + bar sizes?
- value_changed |= ColorPicker4("##picker", col, picker_flags, &g.ColorPickerRef.x);
EndPopup();
}
}
if (label != label_display_end && !(flags & ImGuiColorEditFlags_NoLabel))
{
- window->DC.CursorPos = ImVec2(pos.x + w_full + style.ItemInnerSpacing.x, pos.y + style.FramePadding.y);
+ // Position not necessarily next to last submitted button (e.g. if style.ColorButtonPosition == ImGuiDir_Left),
+ // but we need to use SameLine() to setup baseline correctly. Might want to refactor SameLine() to simplify this.
+ SameLine(0.0f, style.ItemInnerSpacing.x);
+ window->DC.CursorPos.x = pos.x + ((flags & ImGuiColorEditFlags_NoInputs) ? w_button : w_full + style.ItemInnerSpacing.x);
TextEx(label, label_display_end);
}
@@ -4371,8 +5134,9 @@ bool ImGui::ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flag
if ((flags & ImGuiColorEditFlags_DisplayHSV) && (flags & ImGuiColorEditFlags_InputRGB))
{
g.ColorEditLastHue = f[0];
+ g.ColorEditLastSat = f[1];
ColorConvertHSVtoRGB(f[0], f[1], f[2], f[0], f[1], f[2]);
- memcpy(g.ColorEditLastColor, f, sizeof(float) * 3);
+ g.ColorEditLastColor = ColorConvertFloat4ToU32(ImVec4(f[0], f[1], f[2], 0));
}
if ((flags & ImGuiColorEditFlags_DisplayRGB) && (flags & ImGuiColorEditFlags_InputHSV))
ColorConvertRGBtoHSV(f[0], f[1], f[2], f[0], f[1], f[2]);
@@ -4389,12 +5153,12 @@ bool ImGui::ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flag
// 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 ((g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredRect) && !(flags & ImGuiColorEditFlags_NoDragDrop) && BeginDragDropTarget())
{
bool accepted_drag_drop = false;
if (const ImGuiPayload* payload = AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_3F))
{
- memcpy((float*)col, payload->Data, sizeof(float) * 3); // Preserve alpha if any //-V512
+ memcpy((float*)col, payload->Data, sizeof(float) * 3); // Preserve alpha if any //-V512 //-V1086
value_changed = accepted_drag_drop = true;
}
if (const ImGuiPayload* payload = AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_4F))
@@ -4411,10 +5175,10 @@ bool ImGui::ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flag
// 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;
+ g.LastItemData.ID = g.ActiveId;
- if (value_changed)
- MarkItemEdited(window->DC.LastItemId);
+ if (value_changed && g.LastItemData.ID != 0) // In case of ID collision, the second EndGroup() won't catch g.ActiveId
+ MarkItemEdited(g.LastItemData.ID);
return value_changed;
}
@@ -4428,52 +5192,6 @@ bool ImGui::ColorPicker3(const char* label, float col[3], ImGuiColorEditFlags fl
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, float alpha)
{
@@ -4513,12 +5231,12 @@ bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags fl
ColorPickerOptionsPopup(col, flags);
// Read stored options
- if (!(flags & ImGuiColorEditFlags__PickerMask))
- flags |= ((g.ColorEditOptions & ImGuiColorEditFlags__PickerMask) ? g.ColorEditOptions : ImGuiColorEditFlags__OptionsDefault) & ImGuiColorEditFlags__PickerMask;
- if (!(flags & ImGuiColorEditFlags__InputMask))
- flags |= ((g.ColorEditOptions & ImGuiColorEditFlags__InputMask) ? g.ColorEditOptions : ImGuiColorEditFlags__OptionsDefault) & ImGuiColorEditFlags__InputMask;
- IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags__PickerMask)); // Check that only 1 is selected
- IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags__InputMask)); // Check that only 1 is selected
+ if (!(flags & ImGuiColorEditFlags_PickerMask_))
+ flags |= ((g.ColorEditOptions & ImGuiColorEditFlags_PickerMask_) ? g.ColorEditOptions : ImGuiColorEditFlags_DefaultOptions_) & ImGuiColorEditFlags_PickerMask_;
+ if (!(flags & ImGuiColorEditFlags_InputMask_))
+ flags |= ((g.ColorEditOptions & ImGuiColorEditFlags_InputMask_) ? g.ColorEditOptions : ImGuiColorEditFlags_DefaultOptions_) & ImGuiColorEditFlags_InputMask_;
+ IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_PickerMask_)); // Check that only 1 is selected
+ IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_InputMask_)); // Check that only 1 is selected
if (!(flags & ImGuiColorEditFlags_NoOptions))
flags |= (g.ColorEditOptions & ImGuiColorEditFlags_AlphaBar);
@@ -4539,7 +5257,7 @@ bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags fl
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);
+ 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);
@@ -4553,8 +5271,7 @@ bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags fl
{
// Hue is lost when converting from greyscale rgb (saturation=0). Restore it.
ColorConvertRGBtoHSV(R, G, B, H, S, V);
- if (S == 0 && memcmp(g.ColorEditLastColor, col, sizeof(float) * 3) == 0)
- H = g.ColorEditLastHue;
+ ColorEditRestoreHS(col, &H, &S, &V);
}
else if (flags & ImGuiColorEditFlags_InputHSV)
{
@@ -4573,10 +5290,10 @@ bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags fl
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))
+ 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;
+ 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;
@@ -4597,7 +5314,7 @@ bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags fl
}
}
if (!(flags & ImGuiColorEditFlags_NoOptions))
- OpenPopupOnItemClick("context");
+ OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight);
}
else if (flags & ImGuiColorEditFlags_PickerHueBar)
{
@@ -4605,19 +5322,23 @@ bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags fl
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));
+ 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));
+
+ // Greatly reduces hue jitter and reset to 0 when hue == 255 and color is rapidly modified using SV square.
+ if (g.ColorEditLastColor == ColorConvertFloat4ToU32(ImVec4(col[0], col[1], col[2], 0)))
+ H = g.ColorEditLastHue;
value_changed = value_changed_sv = true;
}
if (!(flags & ImGuiColorEditFlags_NoOptions))
- OpenPopupOnItemClick("context");
+ OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight);
// 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));
+ H = ImSaturate((io.MousePos.y - picker_pos.y) / (sv_picker_size - 1));
value_changed = value_changed_h = true;
}
}
@@ -4629,7 +5350,7 @@ bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags fl
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));
+ col[3] = 1.0f - ImSaturate((io.MousePos.y - picker_pos.y) / (sv_picker_size - 1));
value_changed = true;
}
}
@@ -4659,7 +5380,7 @@ bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags fl
if ((flags & ImGuiColorEditFlags_NoLabel))
Text("Current");
- ImGuiColorEditFlags sub_flags_to_forward = ImGuiColorEditFlags__InputMask | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf | ImGuiColorEditFlags_NoTooltip;
+ ImGuiColorEditFlags sub_flags_to_forward = ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf | ImGuiColorEditFlags_NoTooltip;
ColorButton("##current", col_v4, (flags & sub_flags_to_forward), ImVec2(square_sz * 3, square_sz * 2));
if (ref_col != NULL)
{
@@ -4680,9 +5401,10 @@ bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags fl
{
if (flags & ImGuiColorEditFlags_InputRGB)
{
- 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]);
+ ColorConvertHSVtoRGB(H, S, V, col[0], col[1], col[2]);
g.ColorEditLastHue = H;
- memcpy(g.ColorEditLastColor, col, sizeof(float) * 3);
+ g.ColorEditLastSat = S;
+ g.ColorEditLastColor = ColorConvertFloat4ToU32(ImVec4(col[0], col[1], col[2], 0));
}
else if (flags & ImGuiColorEditFlags_InputHSV)
{
@@ -4697,19 +5419,19 @@ bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags fl
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__InputMask | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_NoOptions | ImGuiColorEditFlags_NoSmallPreview | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf;
+ ImGuiColorEditFlags sub_flags_to_forward = ImGuiColorEditFlags_DataTypeMask_ | ImGuiColorEditFlags_InputMask_ | 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_DisplayRGB || (flags & ImGuiColorEditFlags__DisplayMask) == 0)
+ if (flags & ImGuiColorEditFlags_DisplayRGB || (flags & ImGuiColorEditFlags_DisplayMask_) == 0)
if (ColorEdit4("##rgb", col, sub_flags | ImGuiColorEditFlags_DisplayRGB))
{
- // FIXME: Hackily differenciating using the DragInt (ActiveId != 0 && !ActiveIdAllowOverlap) vs. using the InputText or DropTarget.
+ // FIXME: Hackily differentiating 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_DisplayHSV || (flags & ImGuiColorEditFlags__DisplayMask) == 0)
+ if (flags & ImGuiColorEditFlags_DisplayHSV || (flags & ImGuiColorEditFlags_DisplayMask_) == 0)
value_changed |= ColorEdit4("##hsv", col, sub_flags | ImGuiColorEditFlags_DisplayHSV);
- if (flags & ImGuiColorEditFlags_DisplayHex || (flags & ImGuiColorEditFlags__DisplayMask) == 0)
+ if (flags & ImGuiColorEditFlags_DisplayHex || (flags & ImGuiColorEditFlags_DisplayMask_) == 0)
value_changed |= ColorEdit4("##hex", col, sub_flags | ImGuiColorEditFlags_DisplayHex);
PopItemWidth();
}
@@ -4736,8 +5458,7 @@ bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags fl
G = col[1];
B = col[2];
ColorConvertRGBtoHSV(R, G, B, H, S, V);
- if (S == 0 && memcmp(g.ColorEditLastColor, col, sizeof(float) * 3) == 0) // Fix local Hue as display below will use it immediately.
- H = g.ColorEditLastHue;
+ ColorEditRestoreHS(col, &H, &S, &V); // Fix local Hue as display below will use it immediately.
}
else if (flags & ImGuiColorEditFlags_InputHSV)
{
@@ -4771,23 +5492,23 @@ bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags fl
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(col_white, false, wheel_thickness);
+ draw_list->PathStroke(col_white, 0, 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, col_hues[n], col_hues[n+1]);
+ ShadeVertsLinearColorGradientKeepAlpha(draw_list, vert_start_idx, vert_end_idx, gradient_p0, gradient_p1, col_hues[n], col_hues[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);
+ 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, col_midgrey, hue_cursor_segments);
+ draw_list->AddCircle(hue_cursor_pos, hue_cursor_rad + 1, col_midgrey, hue_cursor_segments);
draw_list->AddCircle(hue_cursor_pos, hue_cursor_rad, col_white, hue_cursor_segments);
// Render SV triangle (rotated according to hue)
@@ -4825,7 +5546,7 @@ bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags fl
// 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, user_col32_striped_of_alpha, 12);
- draw_list->AddCircle(sv_cursor_pos, sv_cursor_rad+1, col_midgrey, 12);
+ draw_list->AddCircle(sv_cursor_pos, sv_cursor_rad + 1, col_midgrey, 12);
draw_list->AddCircle(sv_cursor_pos, sv_cursor_rad, col_white, 12);
// Render alpha bar
@@ -4833,7 +5554,7 @@ bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags fl
{
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, 0, bar1_bb.GetWidth() / 2.0f, ImVec2(0.0f, 0.0f));
+ RenderColorRectWithAlphaCheckerboard(draw_list, bar1_bb.Min, bar1_bb.Max, 0, bar1_bb.GetWidth() / 2.0f, ImVec2(0.0f, 0.0f));
draw_list->AddRectFilledMultiColor(bar1_bb.Min, bar1_bb.Max, user_col32_striped_of_alpha, user_col32_striped_of_alpha, user_col32_striped_of_alpha & ~IM_COL32_A_MASK, user_col32_striped_of_alpha & ~IM_COL32_A_MASK);
float bar1_line_y = IM_ROUND(picker_pos.y + (1.0f - alpha) * sv_picker_size);
RenderFrameBorder(bar1_bb.Min, bar1_bb.Max, 0.0f);
@@ -4844,19 +5565,19 @@ bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags fl
if (value_changed && memcmp(backup_initial_col, col, components * sizeof(float)) == 0)
value_changed = false;
- if (value_changed)
- MarkItemEdited(window->DC.LastItemId);
+ if (value_changed && g.LastItemData.ID != 0) // In case of ID collision, the second EndGroup() won't catch g.ActiveId
+ MarkItemEdited(g.LastItemData.ID);
PopID();
return value_changed;
}
-// A little colored square. Return true when clicked.
+// A little color 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.
// Note that 'col' may be encoded in HSV if ImGuiColorEditFlags_InputHSV is set.
-bool ImGui::ColorButton(const char* desc_id, const ImVec4& col, ImGuiColorEditFlags flags, ImVec2 size)
+bool ImGui::ColorButton(const char* desc_id, const ImVec4& col, ImGuiColorEditFlags flags, const ImVec2& size_arg)
{
ImGuiWindow* window = GetCurrentWindow();
if (window->SkipItems)
@@ -4864,11 +5585,8 @@ bool ImGui::ColorButton(const char* desc_id, const ImVec4& col, ImGuiColorEditFl
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 float default_size = GetFrameHeight();
+ const ImVec2 size(size_arg.x == 0.0f ? default_size : size_arg.x, size_arg.y == 0.0f ? default_size : size_arg.y);
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))
@@ -4888,28 +5606,35 @@ bool ImGui::ColorButton(const char* desc_id, const ImVec4& col, ImGuiColorEditFl
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);
+ float off = 0.0f;
+ if ((flags & ImGuiColorEditFlags_NoBorder) == 0)
+ {
+ 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_rgb.w < 1.0f)
{
float mid_x = IM_ROUND((bb_inner.Min.x + bb_inner.Max.x) * 0.5f);
- RenderColorRectWithAlphaCheckerboard(ImVec2(bb_inner.Min.x + grid_step, bb_inner.Min.y), bb_inner.Max, GetColorU32(col_rgb), 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_rgb_without_alpha), rounding, ImDrawCornerFlags_TopLeft|ImDrawCornerFlags_BotLeft);
+ RenderColorRectWithAlphaCheckerboard(window->DrawList, ImVec2(bb_inner.Min.x + grid_step, bb_inner.Min.y), bb_inner.Max, GetColorU32(col_rgb), grid_step, ImVec2(-grid_step + off, off), rounding, ImDrawFlags_RoundCornersRight);
+ window->DrawList->AddRectFilled(bb_inner.Min, ImVec2(mid_x, bb_inner.Max.y), GetColorU32(col_rgb_without_alpha), rounding, ImDrawFlags_RoundCornersLeft);
}
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_rgb : col_rgb_without_alpha;
if (col_source.w < 1.0f)
- RenderColorRectWithAlphaCheckerboard(bb_inner.Min, bb_inner.Max, GetColorU32(col_source), grid_step, ImVec2(off, off), rounding);
+ RenderColorRectWithAlphaCheckerboard(window->DrawList, 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);
+ window->DrawList->AddRectFilled(bb_inner.Min, bb_inner.Max, GetColorU32(col_source), rounding);
}
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
+ if ((flags & ImGuiColorEditFlags_NoBorder) == 0)
+ {
+ 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.
@@ -4927,7 +5652,7 @@ bool ImGui::ColorButton(const char* desc_id, const ImVec4& col, ImGuiColorEditFl
// Tooltip
if (!(flags & ImGuiColorEditFlags_NoTooltip) && hovered)
- ColorTooltip(desc_id, &col.x, flags & (ImGuiColorEditFlags__InputMask | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf));
+ ColorTooltip(desc_id, &col.x, flags & (ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf));
return pressed;
}
@@ -4936,18 +5661,18 @@ bool ImGui::ColorButton(const char* desc_id, const ImVec4& col, ImGuiColorEditFl
void ImGui::SetColorEditOptions(ImGuiColorEditFlags flags)
{
ImGuiContext& g = *GImGui;
- if ((flags & ImGuiColorEditFlags__DisplayMask) == 0)
- flags |= ImGuiColorEditFlags__OptionsDefault & ImGuiColorEditFlags__DisplayMask;
- if ((flags & ImGuiColorEditFlags__DataTypeMask) == 0)
- flags |= ImGuiColorEditFlags__OptionsDefault & ImGuiColorEditFlags__DataTypeMask;
- if ((flags & ImGuiColorEditFlags__PickerMask) == 0)
- flags |= ImGuiColorEditFlags__OptionsDefault & ImGuiColorEditFlags__PickerMask;
- if ((flags & ImGuiColorEditFlags__InputMask) == 0)
- flags |= ImGuiColorEditFlags__OptionsDefault & ImGuiColorEditFlags__InputMask;
- IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags__DisplayMask)); // Check only 1 option is selected
- IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags__DataTypeMask)); // Check only 1 option is selected
- IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags__PickerMask)); // Check only 1 option is selected
- IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags__InputMask)); // Check only 1 option is selected
+ if ((flags & ImGuiColorEditFlags_DisplayMask_) == 0)
+ flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_DisplayMask_;
+ if ((flags & ImGuiColorEditFlags_DataTypeMask_) == 0)
+ flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_DataTypeMask_;
+ if ((flags & ImGuiColorEditFlags_PickerMask_) == 0)
+ flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_PickerMask_;
+ if ((flags & ImGuiColorEditFlags_InputMask_) == 0)
+ flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_InputMask_;
+ IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_DisplayMask_)); // Check only 1 option is selected
+ IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_DataTypeMask_)); // Check only 1 option is selected
+ IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_PickerMask_)); // Check only 1 option is selected
+ IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_InputMask_)); // Check only 1 option is selected
g.ColorEditOptions = flags;
}
@@ -4956,7 +5681,7 @@ void ImGui::ColorTooltip(const char* text, const float* col, ImGuiColorEditFlags
{
ImGuiContext& g = *GImGui;
- BeginTooltipEx(0, true);
+ BeginTooltipEx(ImGuiTooltipFlags_OverridePreviousTooltip, ImGuiWindowFlags_None);
const char* text_end = text ? FindRenderedTextEnd(text, NULL) : text;
if (text_end > text)
{
@@ -4967,9 +5692,9 @@ void ImGui::ColorTooltip(const char* text, const float* col, ImGuiColorEditFlags
ImVec2 sz(g.FontSize * 3 + g.Style.FramePadding.y * 2, g.FontSize * 3 + g.Style.FramePadding.y * 2);
ImVec4 cf(col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]);
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]);
- ColorButton("##preview", cf, (flags & (ImGuiColorEditFlags__InputMask | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf)) | ImGuiColorEditFlags_NoTooltip, sz);
+ ColorButton("##preview", cf, (flags & (ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf)) | ImGuiColorEditFlags_NoTooltip, sz);
SameLine();
- if ((flags & ImGuiColorEditFlags_InputRGB) || !(flags & ImGuiColorEditFlags__InputMask))
+ if ((flags & ImGuiColorEditFlags_InputRGB) || !(flags & ImGuiColorEditFlags_InputMask_))
{
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]);
@@ -4988,28 +5713,28 @@ void ImGui::ColorTooltip(const char* text, const float* col, ImGuiColorEditFlags
void ImGui::ColorEditOptionsPopup(const float* col, ImGuiColorEditFlags flags)
{
- bool allow_opt_inputs = !(flags & ImGuiColorEditFlags__DisplayMask);
- bool allow_opt_datatype = !(flags & ImGuiColorEditFlags__DataTypeMask);
+ bool allow_opt_inputs = !(flags & ImGuiColorEditFlags_DisplayMask_);
+ 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_DisplayRGB) != 0)) opts = (opts & ~ImGuiColorEditFlags__DisplayMask) | ImGuiColorEditFlags_DisplayRGB;
- if (RadioButton("HSV", (opts & ImGuiColorEditFlags_DisplayHSV) != 0)) opts = (opts & ~ImGuiColorEditFlags__DisplayMask) | ImGuiColorEditFlags_DisplayHSV;
- if (RadioButton("Hex", (opts & ImGuiColorEditFlags_DisplayHex) != 0)) opts = (opts & ~ImGuiColorEditFlags__DisplayMask) | ImGuiColorEditFlags_DisplayHex;
+ if (RadioButton("RGB", (opts & ImGuiColorEditFlags_DisplayRGB) != 0)) opts = (opts & ~ImGuiColorEditFlags_DisplayMask_) | ImGuiColorEditFlags_DisplayRGB;
+ if (RadioButton("HSV", (opts & ImGuiColorEditFlags_DisplayHSV) != 0)) opts = (opts & ~ImGuiColorEditFlags_DisplayMask_) | ImGuiColorEditFlags_DisplayHSV;
+ if (RadioButton("Hex", (opts & ImGuiColorEditFlags_DisplayHex) != 0)) opts = (opts & ~ImGuiColorEditFlags_DisplayMask_) | ImGuiColorEditFlags_DisplayHex;
}
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 (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)))
+ if (Button("Copy as..", ImVec2(-1, 0)))
OpenPopup("Copy");
if (BeginPopup("Copy"))
{
@@ -5021,12 +5746,15 @@ void ImGui::ColorEditOptionsPopup(const float* col, ImGuiColorEditFlags flags)
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);
+ ImFormatString(buf, IM_ARRAYSIZE(buf), "#%02X%02X%02X", cr, cg, cb);
if (Selectable(buf))
SetClipboardText(buf);
+ if (!(flags & ImGuiColorEditFlags_NoAlpha))
+ {
+ ImFormatString(buf, IM_ARRAYSIZE(buf), "#%02X%02X%02X%02X", cr, cg, cb, ca);
+ if (Selectable(buf))
+ SetClipboardText(buf);
+ }
EndPopup();
}
@@ -5036,7 +5764,7 @@ void ImGui::ColorEditOptionsPopup(const float* col, ImGuiColorEditFlags flags)
void ImGui::ColorPickerOptionsPopup(const float* ref_col, ImGuiColorEditFlags flags)
{
- bool allow_opt_picker = !(flags & ImGuiColorEditFlags__PickerMask);
+ 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) || !BeginPopup("context"))
return;
@@ -5050,16 +5778,16 @@ void ImGui::ColorPickerOptionsPopup(const float* ref_col, ImGuiColorEditFlags fl
// Draw small/thumbnail version of each picker type (over an invisible button for selection)
if (picker_type > 0) Separator();
PushID(picker_type);
- ImGuiColorEditFlags picker_flags = ImGuiColorEditFlags_NoInputs|ImGuiColorEditFlags_NoOptions|ImGuiColorEditFlags_NoLabel|ImGuiColorEditFlags_NoSidePreview|(flags & ImGuiColorEditFlags_NoAlpha);
+ 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 = GetCursorScreenPos();
if (Selectable("##selectable", false, 0, picker_size)) // By default, Selectable() is closing popup
- g.ColorEditOptions = (g.ColorEditOptions & ~ImGuiColorEditFlags__PickerMask) | (picker_flags & ImGuiColorEditFlags__PickerMask);
+ g.ColorEditOptions = (g.ColorEditOptions & ~ImGuiColorEditFlags_PickerMask_) | (picker_flags & ImGuiColorEditFlags_PickerMask_);
SetCursorScreenPos(backup_pos);
- ImVec4 dummy_ref_col;
- memcpy(&dummy_ref_col, ref_col, sizeof(float) * ((picker_flags & ImGuiColorEditFlags_NoAlpha) ? 3 : 4));
- ColorPicker4("##dummypicker", &dummy_ref_col.x, picker_flags);
+ ImVec4 previewing_ref_col;
+ memcpy(&previewing_ref_col, ref_col, sizeof(float) * ((picker_flags & ImGuiColorEditFlags_NoAlpha) ? 3 : 4));
+ ColorPicker4("##previewing_picker", &previewing_ref_col.x, picker_flags);
PopID();
}
PopItemWidth();
@@ -5067,7 +5795,7 @@ void ImGui::ColorPickerOptionsPopup(const float* ref_col, ImGuiColorEditFlags fl
if (allow_opt_alpha_bar)
{
if (allow_opt_picker) Separator();
- CheckboxFlags("Alpha Bar", (unsigned int*)&g.ColorEditOptions, ImGuiColorEditFlags_AlphaBar);
+ CheckboxFlags("Alpha Bar", &g.ColorEditOptions, ImGuiColorEditFlags_AlphaBar);
}
EndPopup();
}
@@ -5156,9 +5884,9 @@ bool ImGui::TreeNodeExV(const char* str_id, ImGuiTreeNodeFlags flags, const char
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);
+ const char* label, *label_end;
+ ImFormatStringToTempBufferV(&label, &label_end, fmt, args);
+ return TreeNodeBehavior(window->GetID(str_id), flags, label, label_end);
}
bool ImGui::TreeNodeExV(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args)
@@ -5167,12 +5895,19 @@ bool ImGui::TreeNodeExV(const void* ptr_id, ImGuiTreeNodeFlags flags, const char
if (window->SkipItems)
return false;
+ const char* label, *label_end;
+ ImFormatStringToTempBufferV(&label, &label_end, fmt, args);
+ return TreeNodeBehavior(window->GetID(ptr_id), flags, label, label_end);
+}
+
+void ImGui::TreeNodeSetOpen(ImGuiID id, bool open)
+{
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);
+ ImGuiStorage* storage = g.CurrentWindow->DC.StateStorage;
+ storage->SetInt(id, open ? 1 : 0);
}
-bool ImGui::TreeNodeBehaviorIsOpen(ImGuiID id, ImGuiTreeNodeFlags flags)
+bool ImGui::TreeNodeUpdateNextOpen(ImGuiID id, ImGuiTreeNodeFlags flags)
{
if (flags & ImGuiTreeNodeFlags_Leaf)
return true;
@@ -5188,7 +5923,7 @@ bool ImGui::TreeNodeBehaviorIsOpen(ImGuiID id, ImGuiTreeNodeFlags flags)
if (g.NextItemData.OpenCond & ImGuiCond_Always)
{
is_open = g.NextItemData.OpenVal;
- storage->SetInt(id, is_open);
+ TreeNodeSetOpen(id, is_open);
}
else
{
@@ -5197,7 +5932,7 @@ bool ImGui::TreeNodeBehaviorIsOpen(ImGuiID id, ImGuiTreeNodeFlags flags)
if (stored_value == -1)
{
is_open = g.NextItemData.OpenVal;
- storage->SetInt(id, is_open);
+ TreeNodeSetOpen(id, is_open);
}
else
{
@@ -5234,7 +5969,7 @@ bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* l
const ImVec2 label_size = CalcTextSize(label, label_end, false);
// We vertically grow up to current line height up the typical widget height.
- const float frame_height = ImMax(ImMin(window->DC.CurrLineSize.y, g.FontSize + style.FramePadding.y*2), label_size.y + padding.y*2);
+ const float frame_height = ImMax(ImMin(window->DC.CurrLineSize.y, g.FontSize + style.FramePadding.y * 2), label_size.y + padding.y * 2);
ImRect frame_bb;
frame_bb.Min.x = (flags & ImGuiTreeNodeFlags_SpanFullWidth) ? window->WorkRect.Min.x : window->DC.CursorPos.x;
frame_bb.Min.y = window->DC.CursorPos.y;
@@ -5248,9 +5983,9 @@ bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* l
frame_bb.Max.x += IM_FLOOR(window->WindowPadding.x * 0.5f);
}
- const float text_offset_x = g.FontSize + (display_frame ? padding.x*3 : padding.x*2); // Collapser arrow width + Spacing
+ const float text_offset_x = g.FontSize + (display_frame ? padding.x * 3 : padding.x * 2); // Collapser arrow width + Spacing
const float text_offset_y = ImMax(padding.y, window->DC.CurrLineTextBaseOffset); // Latch before ItemSize changes it
- const float text_width = g.FontSize + (label_size.x > 0.0f ? label_size.x + padding.x*2 : 0.0f); // Include collapser
+ const float text_width = g.FontSize + (label_size.x > 0.0f ? label_size.x + padding.x * 2 : 0.0f); // Include collapser
ImVec2 text_pos(window->DC.CursorPos.x + text_offset_x, window->DC.CursorPos.y + text_offset_y);
ItemSize(ImVec2(text_width, frame_height), padding.y);
@@ -5263,35 +5998,53 @@ bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* l
// 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.
const bool is_leaf = (flags & ImGuiTreeNodeFlags_Leaf) != 0;
- bool is_open = TreeNodeBehaviorIsOpen(id, flags);
+ bool is_open = TreeNodeUpdateNextOpen(id, flags);
if (is_open && !g.NavIdIsAlive && (flags & ImGuiTreeNodeFlags_NavLeftJumpsBackHere) && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen))
- window->DC.TreeMayJumpToParentOnPopMask |= (1 << window->DC.TreeDepth);
+ window->DC.TreeJumpToParentOnPopMask |= (1 << window->DC.TreeDepth);
bool item_add = ItemAdd(interact_bb, id);
- window->DC.LastItemStatusFlags |= ImGuiItemStatusFlags_HasDisplayRect;
- window->DC.LastItemDisplayRect = frame_bb;
+ g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasDisplayRect;
+ g.LastItemData.DisplayRect = frame_bb;
if (!item_add)
{
if (is_open && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen))
TreePushOverrideID(id);
- IMGUI_TEST_ENGINE_ITEM_INFO(window->DC.LastItemId, label, window->DC.ItemFlags | (is_leaf ? 0 : ImGuiItemStatusFlags_Openable) | (is_open ? ImGuiItemStatusFlags_Opened : 0));
+ IMGUI_TEST_ENGINE_ITEM_INFO(g.LastItemData.ID, label, g.LastItemData.StatusFlags | (is_leaf ? 0 : ImGuiItemStatusFlags_Openable) | (is_open ? ImGuiItemStatusFlags_Opened : 0));
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;
+ ImGuiButtonFlags button_flags = ImGuiTreeNodeFlags_None;
if (flags & ImGuiTreeNodeFlags_AllowItemOverlap)
button_flags |= ImGuiButtonFlags_AllowItemOverlap;
- if (flags & ImGuiTreeNodeFlags_OpenOnDoubleClick)
- button_flags |= ImGuiButtonFlags_PressedOnDoubleClick | ((flags & ImGuiTreeNodeFlags_OpenOnArrow) ? ImGuiButtonFlags_PressedOnClickRelease : 0);
if (!is_leaf)
button_flags |= ImGuiButtonFlags_PressedOnDragDropHold;
+ // We allow clicking on the arrow section with keyboard modifiers held, in order to easily
+ // allow browsing a tree while preserving selection with code implementing multi-selection patterns.
+ // When clicking on the rest of the tree node we always disallow keyboard modifiers.
+ const float arrow_hit_x1 = (text_pos.x - text_offset_x) - style.TouchExtraPadding.x;
+ const float arrow_hit_x2 = (text_pos.x - text_offset_x) + (g.FontSize + padding.x * 2.0f) + style.TouchExtraPadding.x;
+ const bool is_mouse_x_over_arrow = (g.IO.MousePos.x >= arrow_hit_x1 && g.IO.MousePos.x < arrow_hit_x2);
+ if (window != g.HoveredWindow || !is_mouse_x_over_arrow)
+ button_flags |= ImGuiButtonFlags_NoKeyModifiers;
+
+ // Open behaviors can be altered with the _OpenOnArrow and _OnOnDoubleClick flags.
+ // Some alteration have subtle effects (e.g. toggle on MouseUp vs MouseDown events) due to requirements for multi-selection and drag and drop support.
+ // - Single-click on label = Toggle on MouseUp (default, when _OpenOnArrow=0)
+ // - Single-click on arrow = Toggle on MouseDown (when _OpenOnArrow=0)
+ // - Single-click on arrow = Toggle on MouseDown (when _OpenOnArrow=1)
+ // - Double-click on label = Toggle on MouseDoubleClick (when _OpenOnDoubleClick=1)
+ // - Double-click on arrow = Toggle on MouseDoubleClick (when _OpenOnDoubleClick=1 and _OpenOnArrow=0)
+ // It is rather standard that arrow click react on Down rather than Up.
+ // We set ImGuiButtonFlags_PressedOnClickRelease on OpenOnDoubleClick because we want the item to be active on the initial MouseDown in order for drag and drop to work.
+ if (is_mouse_x_over_arrow)
+ button_flags |= ImGuiButtonFlags_PressedOnClick;
+ else if (flags & ImGuiTreeNodeFlags_OpenOnDoubleClick)
+ button_flags |= ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnDoubleClick;
+ else
+ button_flags |= ImGuiButtonFlags_PressedOnClickRelease;
+
bool selected = (flags & ImGuiTreeNodeFlags_Selected) != 0;
const bool was_selected = selected;
@@ -5300,25 +6053,28 @@ bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* l
bool toggled = false;
if (!is_leaf)
{
- if (pressed)
+ if (pressed && g.DragDropHoldJustPressedId != id)
{
- const float arrow_x1 = text_pos.x - text_offset_x;
- const float arrow_x2 = arrow_x1 + g.FontSize + padding.x * 2.0f;
- toggled = !(flags & (ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) || (g.NavActivateId == id);
+ if ((flags & (ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) == 0 || (g.NavActivateId == id))
+ toggled = true;
if (flags & ImGuiTreeNodeFlags_OpenOnArrow)
- toggled |= IsMouseHoveringRect(ImVec2(arrow_x1, interact_bb.Min.y), ImVec2(arrow_x2, 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;
+ toggled |= is_mouse_x_over_arrow && !g.NavDisableMouseHover; // Lightweight equivalent of IsMouseHoveringRect() since ButtonBehavior() already did the job
+ if ((flags & ImGuiTreeNodeFlags_OpenOnDoubleClick) && g.IO.MouseClickedCount[0] == 2)
+ toggled = true;
+ }
+ else if (pressed && g.DragDropHoldJustPressedId == id)
+ {
+ IM_ASSERT(button_flags & ImGuiButtonFlags_PressedOnDragDropHold);
+ if (!is_open) // When using Drag and Drop "hold to open" we keep the node highlighted after opening, but never close it again.
+ toggled = true;
}
- if (g.NavId == id && g.NavMoveRequest && g.NavMoveDir == ImGuiDir_Left && is_open)
+ if (g.NavId == id && 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?
+ if (g.NavId == id && 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();
@@ -5328,6 +6084,7 @@ bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* l
{
is_open = !is_open;
window->DC.StateStorage->SetInt(id, is_open);
+ g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledOpen;
}
}
if (flags & ImGuiTreeNodeFlags_AllowItemOverlap)
@@ -5335,7 +6092,7 @@ bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* l
// In this branch, TreeNodeBehavior() cannot toggle the selection so this will never trigger.
if (selected != was_selected) //-V547
- window->DC.LastItemStatusFlags |= ImGuiItemStatusFlags_ToggledSelection;
+ g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledSelection;
// Render
const ImU32 text_col = GetColorU32(ImGuiCol_Text);
@@ -5354,19 +6111,10 @@ bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* l
text_pos.x -= text_offset_x;
if (flags & ImGuiTreeNodeFlags_ClipLabelForTrailingButton)
frame_bb.Max.x -= g.FontSize + style.FramePadding.x;
+
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, log_suffix+2);
- }
- else
- {
- RenderTextClipped(text_pos, frame_bb.Max, label, label_end, &label_size);
- }
+ LogSetNextTextDecoration("###", "###");
+ RenderTextClipped(text_pos, frame_bb.Max, label, label_end, &label_size);
}
else
{
@@ -5375,20 +6123,20 @@ bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* l
{
const ImU32 bg_col = GetColorU32((held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header);
RenderFrame(frame_bb.Min, frame_bb.Max, bg_col, false);
- RenderNavHighlight(frame_bb, id, nav_highlight_flags);
}
+ RenderNavHighlight(frame_bb, id, nav_highlight_flags);
if (flags & ImGuiTreeNodeFlags_Bullet)
RenderBullet(window->DrawList, ImVec2(text_pos.x - text_offset_x * 0.5f, text_pos.y + g.FontSize * 0.5f), text_col);
else if (!is_leaf)
RenderArrow(window->DrawList, ImVec2(text_pos.x - text_offset_x + padding.x, text_pos.y + g.FontSize * 0.15f), text_col, is_open ? ImGuiDir_Down : ImGuiDir_Right, 0.70f);
if (g.LogEnabled)
- LogRenderedText(&text_pos, ">");
+ LogSetNextTextDecoration(">", NULL);
RenderText(text_pos, label, label_end, false);
}
if (is_open && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen))
TreePushOverrideID(id);
- IMGUI_TEST_ENGINE_ITEM_INFO(id, label, window->DC.ItemFlags | (is_leaf ? 0 : ImGuiItemStatusFlags_Openable) | (is_open ? ImGuiItemStatusFlags_Opened : 0));
+ IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | (is_leaf ? 0 : ImGuiItemStatusFlags_Openable) | (is_open ? ImGuiItemStatusFlags_Opened : 0));
return is_open;
}
@@ -5397,7 +6145,7 @@ void ImGui::TreePush(const char* str_id)
ImGuiWindow* window = GetCurrentWindow();
Indent();
window->DC.TreeDepth++;
- PushID(str_id ? str_id : "#TreePush");
+ PushID(str_id);
}
void ImGui::TreePush(const void* ptr_id)
@@ -5405,15 +6153,16 @@ void ImGui::TreePush(const void* ptr_id)
ImGuiWindow* window = GetCurrentWindow();
Indent();
window->DC.TreeDepth++;
- PushID(ptr_id ? ptr_id : (const void*)"#TreePush");
+ PushID(ptr_id);
}
void ImGui::TreePushOverrideID(ImGuiID id)
{
- ImGuiWindow* window = GetCurrentWindow();
+ ImGuiContext& g = *GImGui;
+ ImGuiWindow* window = g.CurrentWindow;
Indent();
window->DC.TreeDepth++;
- window->IDStack.push_back(id);
+ PushOverrideID(id);
}
void ImGui::TreePop()
@@ -5427,12 +6176,12 @@ void ImGui::TreePop()
// Handle Left arrow to move to parent tree node (when ImGuiTreeNodeFlags_NavLeftJumpsBackHere is enabled)
if (g.NavMoveDir == ImGuiDir_Left && g.NavWindow == window && NavMoveRequestButNoResultYet())
- if (g.NavIdIsAlive && (window->DC.TreeMayJumpToParentOnPopMask & tree_depth_mask))
+ if (g.NavIdIsAlive && (window->DC.TreeJumpToParentOnPopMask & tree_depth_mask))
{
- SetNavID(window->IDStack.back(), g.NavLayer);
+ SetNavID(window->IDStack.back(), g.NavLayer, 0, ImRect());
NavMoveRequestCancel();
}
- window->DC.TreeMayJumpToParentOnPopMask &= tree_depth_mask - 1;
+ window->DC.TreeJumpToParentOnPopMask &= tree_depth_mask - 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();
@@ -5467,31 +6216,38 @@ bool ImGui::CollapsingHeader(const char* label, ImGuiTreeNodeFlags flags)
return TreeNodeBehavior(window->GetID(label), flags | ImGuiTreeNodeFlags_CollapsingHeader, label);
}
-bool ImGui::CollapsingHeader(const char* label, bool* p_open, ImGuiTreeNodeFlags flags)
+// p_visible == NULL : regular collapsing header
+// p_visible != NULL && *p_visible == true : show a small close button on the corner of the header, clicking the button will set *p_visible = false
+// p_visible != NULL && *p_visible == false : do not show the header at all
+// Do not mistake this with the Open state of the header itself, which you can adjust with SetNextItemOpen() or ImGuiTreeNodeFlags_DefaultOpen.
+bool ImGui::CollapsingHeader(const char* label, bool* p_visible, ImGuiTreeNodeFlags flags)
{
ImGuiWindow* window = GetCurrentWindow();
if (window->SkipItems)
return false;
- if (p_open && !*p_open)
+ if (p_visible && !*p_visible)
return false;
ImGuiID id = window->GetID(label);
- flags |= ImGuiTreeNodeFlags_CollapsingHeader | (p_open ? ImGuiTreeNodeFlags_AllowItemOverlap | ImGuiTreeNodeFlags_ClipLabelForTrailingButton : 0);
+ flags |= ImGuiTreeNodeFlags_CollapsingHeader;
+ if (p_visible)
+ flags |= ImGuiTreeNodeFlags_AllowItemOverlap | ImGuiTreeNodeFlags_ClipLabelForTrailingButton;
bool is_open = TreeNodeBehavior(id, flags, label);
- if (p_open)
+ if (p_visible != NULL)
{
// Create a small overlapping close button
// FIXME: We can evolve this into user accessible helpers to add extra buttons on title bars, headers, etc.
// FIXME: CloseButton can overlap into text, need find a way to clip the text somehow.
ImGuiContext& g = *GImGui;
- ImGuiItemHoveredDataBackup last_item_backup;
+ ImGuiLastItemData last_item_backup = g.LastItemData;
float button_size = g.FontSize;
- float button_x = ImMax(window->DC.LastItemRect.Min.x, window->DC.LastItemRect.Max.x - g.Style.FramePadding.x * 2.0f - button_size);
- float button_y = window->DC.LastItemRect.Min.y;
- if (CloseButton(window->GetID((void*)((intptr_t)id + 1)), ImVec2(button_x, button_y)))
- *p_open = false;
- last_item_backup.Restore();
+ float button_x = ImMax(g.LastItemData.Rect.Min.x, g.LastItemData.Rect.Max.x - g.Style.FramePadding.x * 2.0f - button_size);
+ float button_y = g.LastItemData.Rect.Min.y;
+ ImGuiID close_button_id = GetIDWithSeed("#CLOSE", NULL, id);
+ if (CloseButton(close_button_id, ImVec2(button_x, button_y)))
+ *p_visible = false;
+ g.LastItemData = last_item_backup;
}
return is_open;
@@ -5503,8 +6259,10 @@ bool ImGui::CollapsingHeader(const char* label, bool* p_open, ImGuiTreeNodeFlags
// - Selectable()
//-------------------------------------------------------------------------
-// Tip: pass a non-visible label (e.g. "##dummy") then you can use the space to draw other text or image.
+// Tip: pass a non-visible label (e.g. "##hello") 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.
+// With this scheme, ImGuiSelectableFlags_SpanAllColumns and ImGuiSelectableFlags_AllowItemOverlap are also frequently used flags.
+// FIXME: Selectable() with (size.x == 0.0f) and (SelectableTextAlign.x > 0.0f) followed by SameLine() is currently not supported.
bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags flags, const ImVec2& size_arg)
{
ImGuiWindow* window = GetCurrentWindow();
@@ -5514,78 +6272,103 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl
ImGuiContext& g = *GImGui;
const ImGuiStyle& style = g.Style;
- if ((flags & ImGuiSelectableFlags_SpanAllColumns) && window->DC.CurrentColumns) // FIXME-OPT: Avoid if vertically clipped.
- PushColumnsBackground();
-
+ // Submit label or explicit size to ItemSize(), whereas ItemAdd() will submit a larger/spanning rectangle.
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.CurrLineTextBaseOffset;
- ImRect bb_inner(pos, pos + size);
ItemSize(size, 0.0f);
- // 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 - pos.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 so we extend the box to cover spacing between selectable.
- const float spacing_x = style.ItemSpacing.x;
- const float spacing_y = style.ItemSpacing.y;
- const float spacing_L = IM_FLOOR(spacing_x * 0.50f);
- const float spacing_U = IM_FLOOR(spacing_y * 0.50f);
- bb.Min.x -= spacing_L;
- bb.Min.y -= spacing_U;
- bb.Max.x += (spacing_x - spacing_L);
- bb.Max.y += (spacing_y - spacing_U);
-
- bool item_add;
- if (flags & ImGuiSelectableFlags_Disabled)
- {
- ImGuiItemFlags backup_item_flags = window->DC.ItemFlags;
- window->DC.ItemFlags |= ImGuiItemFlags_Disabled | ImGuiItemFlags_NoNavDefaultFocus;
- item_add = ItemAdd(bb, id);
- window->DC.ItemFlags = backup_item_flags;
+ // Fill horizontal space
+ // We don't support (size < 0.0f) in Selectable() because the ItemSpacing extension would make explicitly right-aligned sizes not visibly match other widgets.
+ const bool span_all_columns = (flags & ImGuiSelectableFlags_SpanAllColumns) != 0;
+ const float min_x = span_all_columns ? window->ParentWorkRect.Min.x : pos.x;
+ const float max_x = span_all_columns ? window->ParentWorkRect.Max.x : window->WorkRect.Max.x;
+ if (size_arg.x == 0.0f || (flags & ImGuiSelectableFlags_SpanAvailWidth))
+ size.x = ImMax(label_size.x, max_x - min_x);
+
+ // Text stays at the submission position, but bounding box may be extended on both sides
+ const ImVec2 text_min = pos;
+ const ImVec2 text_max(min_x + size.x, pos.y + size.y);
+
+ // Selectables are meant to be tightly packed together with no click-gap, so we extend their box to cover spacing between selectable.
+ ImRect bb(min_x, pos.y, text_max.x, text_max.y);
+ if ((flags & ImGuiSelectableFlags_NoPadWithHalfSpacing) == 0)
+ {
+ const float spacing_x = span_all_columns ? 0.0f : style.ItemSpacing.x;
+ const float spacing_y = style.ItemSpacing.y;
+ const float spacing_L = IM_FLOOR(spacing_x * 0.50f);
+ const float spacing_U = IM_FLOOR(spacing_y * 0.50f);
+ bb.Min.x -= spacing_L;
+ bb.Min.y -= spacing_U;
+ bb.Max.x += (spacing_x - spacing_L);
+ bb.Max.y += (spacing_y - spacing_U);
+ }
+ //if (g.IO.KeyCtrl) { GetForegroundDrawList()->AddRect(bb.Min, bb.Max, IM_COL32(0, 255, 0, 255)); }
+
+ // Modify ClipRect for the ItemAdd(), faster than doing a PushColumnsBackground/PushTableBackground for every Selectable..
+ const float backup_clip_rect_min_x = window->ClipRect.Min.x;
+ const float backup_clip_rect_max_x = window->ClipRect.Max.x;
+ if (span_all_columns)
+ {
+ window->ClipRect.Min.x = window->ParentWorkRect.Min.x;
+ window->ClipRect.Max.x = window->ParentWorkRect.Max.x;
}
- else
+
+ const bool disabled_item = (flags & ImGuiSelectableFlags_Disabled) != 0;
+ const bool item_add = ItemAdd(bb, id, NULL, disabled_item ? ImGuiItemFlags_Disabled : ImGuiItemFlags_None);
+ if (span_all_columns)
{
- item_add = ItemAdd(bb, id);
+ window->ClipRect.Min.x = backup_clip_rect_min_x;
+ window->ClipRect.Max.x = backup_clip_rect_max_x;
}
+
if (!item_add)
- {
- if ((flags & ImGuiSelectableFlags_SpanAllColumns) && window->DC.CurrentColumns)
- PopColumnsBackground();
return false;
- }
+
+ const bool disabled_global = (g.CurrentItemFlags & ImGuiItemFlags_Disabled) != 0;
+ if (disabled_item && !disabled_global) // Only testing this as an optimization
+ BeginDisabled();
+
+ // FIXME: We can standardize the behavior of those two, we could also keep the fast path of override ClipRect + full push on render only,
+ // which would be advantageous since most selectable are not selected.
+ if (span_all_columns && window->DC.CurrentColumns)
+ PushColumnsBackground();
+ else if (span_all_columns && g.CurrentTable)
+ TablePushBackgroundChannel();
// 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;
- if (flags & ImGuiSelectableFlags_AllowItemOverlap) button_flags |= ImGuiButtonFlags_AllowItemOverlap;
-
- if (flags & ImGuiSelectableFlags_Disabled)
- selected = false;
+ if (flags & ImGuiSelectableFlags_NoHoldingActiveID) { button_flags |= ImGuiButtonFlags_NoHoldingActiveId; }
+ if (flags & ImGuiSelectableFlags_NoSetKeyOwner) { button_flags |= ImGuiButtonFlags_NoSetKeyOwner; }
+ if (flags & ImGuiSelectableFlags_SelectOnClick) { button_flags |= ImGuiButtonFlags_PressedOnClick; }
+ if (flags & ImGuiSelectableFlags_SelectOnRelease) { button_flags |= ImGuiButtonFlags_PressedOnRelease; }
+ if (flags & ImGuiSelectableFlags_AllowDoubleClick) { button_flags |= ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnDoubleClick; }
+ if (flags & ImGuiSelectableFlags_AllowItemOverlap) { button_flags |= ImGuiButtonFlags_AllowItemOverlap; }
const bool was_selected = selected;
bool hovered, held;
bool pressed = ButtonBehavior(bb, id, &hovered, &held, button_flags);
+ // Auto-select when moved into
+ // - This will be more fully fleshed in the range-select branch
+ // - This is not exposed as it won't nicely work with some user side handling of shift/control
+ // - We cannot do 'if (g.NavJustMovedToId != id) { selected = false; pressed = was_selected; }' for two reasons
+ // - (1) it would require focus scope to be set, need exposing PushFocusScope() or equivalent (e.g. BeginSelection() calling PushFocusScope())
+ // - (2) usage will fail with clipped items
+ // The multi-select API aim to fix those issues, e.g. may be replaced with a BeginSelection() API.
+ if ((flags & ImGuiSelectableFlags_SelectOnNav) && g.NavJustMovedToId != 0 && g.NavJustMovedToFocusScopeId == g.CurrentFocusScopeId)
+ if (g.NavJustMovedToId == id)
+ selected = pressed = true;
+
// Update NavId when clicking or when Hovering (this doesn't happen on most widgets), so navigation can be resumed with gamepad/keyboard
if (pressed || (hovered && (flags & ImGuiSelectableFlags_SetNavIdOnHover)))
{
if (!g.NavDisableMouseHover && g.NavWindow == window && g.NavLayer == window->DC.NavLayerCurrent)
{
+ SetNavID(id, window->DC.NavLayerCurrent, g.CurrentFocusScopeId, WindowRectAbsToRel(window, bb)); // (bb == NavRect)
g.NavDisableHighlight = true;
- SetNavID(id, window->DC.NavLayerCurrent);
}
}
if (pressed)
@@ -5596,7 +6379,7 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl
// In this branch, Selectable() cannot toggle the selection so this will never trigger.
if (selected != was_selected) //-V547
- window->DC.LastItemStatusFlags |= ImGuiItemStatusFlags_ToggledSelection;
+ g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledSelection;
// Render
if (held && (flags & ImGuiSelectableFlags_DrawHoveredWhenHeld))
@@ -5605,25 +6388,25 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl
{
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);
}
+ RenderNavHighlight(bb, id, ImGuiNavHighlightFlags_TypeThin | ImGuiNavHighlightFlags_NoRounding);
- if ((flags & ImGuiSelectableFlags_SpanAllColumns) && window->DC.CurrentColumns)
- {
+ if (span_all_columns && window->DC.CurrentColumns)
PopColumnsBackground();
- bb.Max.x -= (GetContentRegionMax().x - max_x);
- }
+ else if (span_all_columns && g.CurrentTable)
+ TablePopBackgroundChannel();
- if (flags & ImGuiSelectableFlags_Disabled) PushStyleColor(ImGuiCol_Text, style.Colors[ImGuiCol_TextDisabled]);
- RenderTextClipped(bb_inner.Min, bb_inner.Max, label, NULL, &label_size, style.SelectableTextAlign, &bb);
- if (flags & ImGuiSelectableFlags_Disabled) PopStyleColor();
+ RenderTextClipped(text_min, text_max, label, NULL, &label_size, style.SelectableTextAlign, &bb);
// Automatically close popups
- if (pressed && (window->Flags & ImGuiWindowFlags_Popup) && !(flags & ImGuiSelectableFlags_DontClosePopups) && !(window->DC.ItemFlags & ImGuiItemFlags_SelectableDontClosePopup))
+ if (pressed && (window->Flags & ImGuiWindowFlags_Popup) && !(flags & ImGuiSelectableFlags_DontClosePopups) && !(g.LastItemData.InFlags & ImGuiItemFlags_SelectableDontClosePopup))
CloseCurrentPopup();
- IMGUI_TEST_ENGINE_ITEM_INFO(id, label, window->DC.ItemFlags);
- return pressed;
+ if (disabled_item && !disabled_global)
+ EndDisabled();
+
+ IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
+ return pressed; //-V1020
}
bool ImGui::Selectable(const char* label, bool* p_selected, ImGuiSelectableFlags flags, const ImVec2& size_arg)
@@ -5639,18 +6422,14 @@ bool ImGui::Selectable(const char* label, bool* p_selected, ImGuiSelectableFlags
//-------------------------------------------------------------------------
// [SECTION] Widgets: ListBox
//-------------------------------------------------------------------------
+// - BeginListBox()
+// - EndListBox()
// - ListBox()
-// - ListBoxHeader()
-// - ListBoxFooter()
-//-------------------------------------------------------------------------
-// FIXME: This is an old API. We should redesign some of it, rename ListBoxHeader->BeginListBox, ListBoxFooter->EndListBox
-// and promote using them over existing ListBox() functions, similarly to change with combo boxes.
//-------------------------------------------------------------------------
-// FIXME: In principle this function should be called BeginListBox(). We should rename it after re-evaluating if we want to keep the same signature.
-// 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 non-visible label e.g. "##empty"
-bool ImGui::ListBoxHeader(const char* label, const ImVec2& size_arg)
+// Tip: To have a list filling the entire window width, use size.x = -FLT_MIN and pass an non-visible label e.g. "##empty"
+// Tip: If your vertical size is calculated from an item count (e.g. 10 * item_height) consider adding a fractional part to facilitate seeing scrolling boundaries (e.g. 10.25 * item_height).
+bool ImGui::BeginListBox(const char* label, const ImVec2& size_arg)
{
ImGuiContext& g = *GImGui;
ImGuiWindow* window = GetCurrentWindow();
@@ -5661,12 +6440,12 @@ bool ImGui::ListBoxHeader(const char* label, const ImVec2& size_arg)
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);
+ // Size default to hold ~7.25 items.
+ // Fractional number of items helps seeing that we can scroll down/up without looking at scrollbar.
+ ImVec2 size = ImFloor(CalcItemSize(size_arg, CalcItemWidth(), GetTextLineHeightWithSpacing() * 7.25f + style.FramePadding.y * 2.0f));
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.
g.NextItemData.ClearFlags();
if (!IsRectVisible(bb.Min, bb.Max))
@@ -5676,48 +6455,42 @@ bool ImGui::ListBoxHeader(const char* label, const ImVec2& size_arg)
return false;
}
+ // FIXME-OPT: We could omit the BeginGroup() if label_size.x but would need to omit the EndGroup() as well.
BeginGroup();
- if (label_size.x > 0)
- RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label);
+ if (label_size.x > 0.0f)
+ {
+ ImVec2 label_pos = ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y);
+ RenderText(label_pos, label);
+ window->DC.CursorMaxPos = ImMax(window->DC.CursorMaxPos, label_pos + label_size);
+ }
BeginChildFrame(id, frame_bb.GetSize());
return true;
}
-// FIXME: In principle this function should be called EndListBox(). We should rename it after re-evaluating if we want to keep the same signature.
+#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
+// OBSOLETED in 1.81 (from February 2021)
bool ImGui::ListBoxHeader(const char* label, int items_count, int height_in_items)
{
- // Size default to hold ~7.25 items.
- // We add +25% worth of item height to allow the user to see at a glance if there are more items up/down, without looking at the scrollbar.
- // We don't add this extra bit 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);
- const ImGuiStyle& style = GetStyle();
- float height_in_items_f = (height_in_items < items_count) ? (height_in_items + 0.25f) : (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().
+ // If height_in_items == -1, default height is maximum 7.
+ ImGuiContext& g = *GImGui;
+ float height_in_items_f = (height_in_items < 0 ? ImMin(items_count, 7) : height_in_items) + 0.25f;
ImVec2 size;
size.x = 0.0f;
- size.y = ImFloor(GetTextLineHeightWithSpacing() * height_in_items_f + style.FramePadding.y * 2.0f);
- return ListBoxHeader(label, size);
+ size.y = GetTextLineHeightWithSpacing() * height_in_items_f + g.Style.FramePadding.y * 2.0f;
+ return BeginListBox(label, size);
}
+#endif
-// FIXME: In principle this function should be called EndListBox(). We should rename it after re-evaluating if we want to keep the same signature.
-void ImGui::ListBoxFooter()
+void ImGui::EndListBox()
{
- ImGuiWindow* parent_window = GetCurrentWindow()->ParentWindow;
- const ImRect bb = parent_window->DC.LastItemRect;
- const ImGuiStyle& style = GetStyle();
+ ImGuiContext& g = *GImGui;
+ ImGuiWindow* window = g.CurrentWindow;
+ IM_ASSERT((window->Flags & ImGuiWindowFlags_ChildWindow) && "Mismatched BeginListBox/EndListBox calls. Did you test the return value of BeginListBox?");
+ IM_UNUSED(window);
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();
+ EndGroup(); // This is only required to be able to do IsItemXXX query on the whole ListBox including label
}
bool ImGui::ListBox(const char* label, int* current_item, const char* const items[], int items_count, int height_items)
@@ -5726,24 +6499,35 @@ bool ImGui::ListBox(const char* label, int* current_item, const char* const item
return value_changed;
}
+// This is merely a helper around BeginListBox(), EndListBox().
+// Considering using those directly to submit custom data or store selection differently.
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))
+ ImGuiContext& g = *GImGui;
+
+ // Calculate size from "height_in_items"
+ if (height_in_items < 0)
+ height_in_items = ImMin(items_count, 7);
+ float height_in_items_f = height_in_items + 0.25f;
+ ImVec2 size(0.0f, ImFloor(GetTextLineHeightWithSpacing() * height_in_items_f + g.Style.FramePadding.y * 2.0f));
+
+ if (!BeginListBox(label, size))
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;
+ // Assume all items have even height (= 1 line of text). If you need items of different height,
+ // you can create a custom version of ListBox() in your code without using the clipper.
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.
+ ImGuiListClipper clipper;
+ clipper.Begin(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);
+ const bool item_selected = (i == *current_item);
if (Selectable(item_text, item_selected))
{
*current_item = i;
@@ -5753,9 +6537,10 @@ bool ImGui::ListBox(const char* label, int* current_item, bool (*items_getter)(v
SetItemDefaultFocus();
PopID();
}
- ListBoxFooter();
+ EndListBox();
+
if (value_changed)
- MarkItemEdited(g.CurrentWindow->DC.LastItemId);
+ MarkItemEdited(g.LastItemData.ID);
return value_changed;
}
@@ -5767,14 +6552,19 @@ bool ImGui::ListBox(const char* label, int* current_item, bool (*items_getter)(v
// - PlotLines()
// - PlotHistogram()
//-------------------------------------------------------------------------
+// Plot/Graph widgets are not very good.
+// Consider writing your own, or using a third-party one, see:
+// - ImPlot https://github.com/epezent/implot
+// - others https://github.com/ocornut/imgui/wiki/Useful-Extensions
+//-------------------------------------------------------------------------
-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 frame_size)
+int 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 frame_size)
{
+ ImGuiContext& g = *GImGui;
ImGuiWindow* window = GetCurrentWindow();
if (window->SkipItems)
- return;
+ return -1;
- ImGuiContext& g = *GImGui;
const ImGuiStyle& style = g.Style;
const ImGuiID id = window->GetID(label);
@@ -5789,7 +6579,7 @@ void ImGui::PlotEx(ImGuiPlotType plot_type, const char* label, float (*values_ge
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;
+ return -1;
const bool hovered = ItemHoverable(frame_bb, id);
// Determine scale from values if not specified
@@ -5814,13 +6604,13 @@ void ImGui::PlotEx(ImGuiPlotType plot_type, const char* label, float (*values_ge
RenderFrame(frame_bb.Min, frame_bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding);
const int values_count_min = (plot_type == ImGuiPlotType_Lines) ? 2 : 1;
+ int idx_hovered = -1;
if (values_count >= values_count_min)
{
int res_w = ImMin((int)frame_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 && inner_bb.Contains(g.IO.MousePos))
{
const float t = ImClamp((g.IO.MousePos.x - inner_bb.Min.x) / (inner_bb.Max.x - inner_bb.Min.x), 0.0f, 0.9999f);
@@ -5830,10 +6620,10 @@ void ImGui::PlotEx(ImGuiPlotType plot_type, const char* label, float (*values_ge
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);
+ 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;
+ idx_hovered = v_idx;
}
const float t_step = 1.0f / (float)res_w;
@@ -5842,7 +6632,7 @@ void ImGui::PlotEx(ImGuiPlotType plot_type, const char* label, float (*values_ge
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
+ float histogram_zero_line_t = (scale_min * scale_max < 0.0f) ? (1 + 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);
@@ -5860,13 +6650,13 @@ void ImGui::PlotEx(ImGuiPlotType plot_type, const char* label, float (*values_ge
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);
+ window->DrawList->AddLine(pos0, pos1, idx_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);
+ window->DrawList->AddRectFilled(pos0, pos1, idx_hovered == v1_idx ? col_hovered : col_base);
}
t0 = t1;
@@ -5876,10 +6666,14 @@ void ImGui::PlotEx(ImGuiPlotType plot_type, const char* label, float (*values_ge
// 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));
+ 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);
+
+ // Return hovered index or -1 if none are hovered.
+ // This is currently not exposed in the public API because we need a larger redesign of the whole thing, but in the short-term we are making it available in PlotEx().
+ return idx_hovered;
}
struct ImGuiPlotArrayGetterData
@@ -5965,49 +6759,51 @@ void ImGui::Value(const char* prefix, float v, const char* float_format)
// - EndMainMenuBar()
// - BeginMenu()
// - EndMenu()
+// - MenuItemEx() [Internal]
// - MenuItem()
//-------------------------------------------------------------------------
// Helpers for internal use
-ImGuiMenuColumns::ImGuiMenuColumns()
+void ImGuiMenuColumns::Update(float spacing, bool window_reappearing)
{
- Spacing = Width = NextWidth = 0.0f;
- memset(Pos, 0, sizeof(Pos));
- memset(NextWidths, 0, sizeof(NextWidths));
+ if (window_reappearing)
+ memset(Widths, 0, sizeof(Widths));
+ Spacing = (ImU16)spacing;
+ CalcNextTotalWidth(true);
+ memset(Widths, 0, sizeof(Widths));
+ TotalWidth = NextTotalWidth;
+ NextTotalWidth = 0;
}
-void ImGuiMenuColumns::Update(int count, float spacing, bool clear)
+void ImGuiMenuColumns::CalcNextTotalWidth(bool update_offsets)
{
- IM_ASSERT(count == IM_ARRAYSIZE(Pos));
- IM_UNUSED(count);
- Width = NextWidth = 0.0f;
- Spacing = spacing;
- if (clear)
- memset(NextWidths, 0, sizeof(NextWidths));
- for (int i = 0; i < IM_ARRAYSIZE(Pos); i++)
+ ImU16 offset = 0;
+ bool want_spacing = false;
+ for (int i = 0; i < IM_ARRAYSIZE(Widths); i++)
{
- if (i > 0 && NextWidths[i] > 0.0f)
- Width += Spacing;
- Pos[i] = IM_FLOOR(Width);
- Width += NextWidths[i];
- NextWidths[i] = 0.0f;
+ ImU16 width = Widths[i];
+ if (want_spacing && width > 0)
+ offset += Spacing;
+ want_spacing |= (width > 0);
+ if (update_offsets)
+ {
+ if (i == 1) { OffsetLabel = offset; }
+ if (i == 2) { OffsetShortcut = offset; }
+ if (i == 3) { OffsetMark = offset; }
+ }
+ offset += width;
}
+ NextTotalWidth = offset;
}
-float ImGuiMenuColumns::DeclColumns(float w0, float w1, float w2) // not using va_arg because they promote float to double
+float ImGuiMenuColumns::DeclColumns(float w_icon, float w_label, float w_shortcut, float w_mark)
{
- 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 < IM_ARRAYSIZE(Pos); i++)
- NextWidth += NextWidths[i] + ((i > 0 && NextWidths[i] > 0.0f) ? Spacing : 0.0f);
- return ImMax(Width, NextWidth);
-}
-
-float ImGuiMenuColumns::CalcExtraSpace(float avail_w) const
-{
- return ImMax(0.0f, avail_w - Width);
+ Widths[0] = ImMax(Widths[0], (ImU16)w_icon);
+ Widths[1] = ImMax(Widths[1], (ImU16)w_label);
+ Widths[2] = ImMax(Widths[2], (ImU16)w_shortcut);
+ Widths[3] = ImMax(Widths[3], (ImU16)w_mark);
+ CalcNextTotalWidth(false);
+ return (float)ImMax(TotalWidth, NextTotalWidth);
}
// FIXME: Provided a rectangle perhaps e.g. a BeginMenuBarEx() could be used anywhere..
@@ -6029,14 +6825,15 @@ bool ImGui::BeginMenuBar()
// 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(IM_ROUND(bar_rect.Min.x), IM_ROUND(bar_rect.Min.y + window->WindowBorderSize), IM_ROUND(ImMax(bar_rect.Min.x, bar_rect.Max.x - window->WindowRounding)), IM_ROUND(bar_rect.Max.y));
+ ImRect clip_rect(IM_ROUND(bar_rect.Min.x + window->WindowBorderSize), IM_ROUND(bar_rect.Min.y + window->WindowBorderSize), IM_ROUND(ImMax(bar_rect.Min.x, bar_rect.Max.x - ImMax(window->WindowRounding, window->WindowBorderSize))), IM_ROUND(bar_rect.Max.y));
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);
+ // We overwrite CursorMaxPos because BeginGroup sets it to CursorPos (essentially the .EmitItem hack in EndMenuBar() would need something analogous here, maybe a BeginGroupEx() with flags).
+ window->DC.CursorPos = window->DC.CursorMaxPos = ImVec2(bar_rect.Min.x + window->DC.MenuBarOffset.x, bar_rect.Min.y + window->DC.MenuBarOffset.y);
window->DC.LayoutType = ImGuiLayoutType_Horizontal;
+ window->DC.IsSameLine = false;
window->DC.NavLayerCurrent = ImGuiNavLayer_Menu;
- window->DC.NavLayerCurrentMask = (1 << ImGuiNavLayer_Menu);
window->DC.MenuBarAppending = true;
AlignTextToFramePadding();
return true;
@@ -6052,56 +6849,95 @@ void ImGui::EndMenuBar()
// 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))
{
+ // Try to find out if the request is for one of our child menu
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)
+ if (nav_earliest_child->ParentWindow == window && nav_earliest_child->DC.ParentLayoutType == ImGuiLayoutType_Horizontal && (g.NavMoveFlags & ImGuiNavMoveFlags_Forwarded) == 0)
{
// 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)
+ // 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 bothering)
const ImGuiNavLayer layer = ImGuiNavLayer_Menu;
- IM_ASSERT(window->DC.NavLayerActiveMaskNext & (1 << layer)); // Sanity check
+ IM_ASSERT(window->DC.NavLayersActiveMaskNext & (1 << layer)); // Sanity check (FIXME: Seems unnecessary)
FocusWindow(window);
- SetNavIDWithRectRel(window->NavLastIds[layer], layer, window->NavRectRel[layer]);
- g.NavLayer = layer;
+ SetNavID(window->NavLastIds[layer], layer, 0, window->NavRectRel[layer]);
g.NavDisableHighlight = true; // Hide highlight for the current frame so we don't see the intermediary selection.
- g.NavMoveRequestForward = ImGuiNavForward_ForwardQueued;
- NavMoveRequestCancel();
+ g.NavDisableMouseHover = g.NavMousePosDirty = true;
+ NavMoveRequestForward(g.NavMoveDir, g.NavMoveClipDir, g.NavMoveFlags, g.NavMoveScrollFlags); // Repeat
}
}
+ IM_MSVC_WARNING_SUPPRESS(6011); // Static Analysis false positive "warning C6011: Dereferencing NULL pointer 'window'"
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().EmitItem = false;
+ window->DC.MenuBarOffset.x = window->DC.CursorPos.x - window->Pos.x; // Save horizontal position so next append can reuse it. This is kinda equivalent to a per-layer CursorPos.
+ g.GroupStack.back().EmitItem = false;
EndGroup(); // Restore position on layer 0
window->DC.LayoutType = ImGuiLayoutType_Vertical;
+ window->DC.IsSameLine = false;
window->DC.NavLayerCurrent = ImGuiNavLayer_Main;
- window->DC.NavLayerCurrentMask = (1 << ImGuiNavLayer_Main);
window->DC.MenuBarAppending = false;
}
-// For the main menu bar, which cannot be moved, we honor g.Style.DisplaySafeAreaPadding to ensure text can be visible on a TV set.
+// Important: calling order matters!
+// FIXME: Somehow overlapping with docking tech.
+// FIXME: The "rect-cut" aspect of this could be formalized into a lower-level helper (rect-cut: https://halt.software/dead-simple-layouts)
+bool ImGui::BeginViewportSideBar(const char* name, ImGuiViewport* viewport_p, ImGuiDir dir, float axis_size, ImGuiWindowFlags window_flags)
+{
+ IM_ASSERT(dir != ImGuiDir_None);
+
+ ImGuiWindow* bar_window = FindWindowByName(name);
+ if (bar_window == NULL || bar_window->BeginCount == 0)
+ {
+ // Calculate and set window size/position
+ ImGuiViewportP* viewport = (ImGuiViewportP*)(void*)(viewport_p ? viewport_p : GetMainViewport());
+ ImRect avail_rect = viewport->GetBuildWorkRect();
+ ImGuiAxis axis = (dir == ImGuiDir_Up || dir == ImGuiDir_Down) ? ImGuiAxis_Y : ImGuiAxis_X;
+ ImVec2 pos = avail_rect.Min;
+ if (dir == ImGuiDir_Right || dir == ImGuiDir_Down)
+ pos[axis] = avail_rect.Max[axis] - axis_size;
+ ImVec2 size = avail_rect.GetSize();
+ size[axis] = axis_size;
+ SetNextWindowPos(pos);
+ SetNextWindowSize(size);
+
+ // Report our size into work area (for next frame) using actual window size
+ if (dir == ImGuiDir_Up || dir == ImGuiDir_Left)
+ viewport->BuildWorkOffsetMin[axis] += axis_size;
+ else if (dir == ImGuiDir_Down || dir == ImGuiDir_Right)
+ viewport->BuildWorkOffsetMax[axis] -= axis_size;
+ }
+
+ window_flags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove;
+ PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
+ PushStyleVar(ImGuiStyleVar_WindowMinSize, ImVec2(0, 0)); // Lift normal size constraint
+ bool is_open = Begin(name, NULL, window_flags);
+ PopStyleVar(2);
+
+ return is_open;
+}
+
bool ImGui::BeginMainMenuBar()
{
ImGuiContext& g = *GImGui;
+ ImGuiViewportP* viewport = (ImGuiViewportP*)(void*)GetMainViewport();
+
+ // For the main menu bar, which cannot be moved, we honor g.Style.DisplaySafeAreaPadding to ensure text can be visible on a TV set.
+ // FIXME: This could be generalized as an opt-in way to clamp window->DC.CursorStartPos to avoid SafeArea?
+ // FIXME: Consider removing support for safe area down the line... it's messy. Nowadays consoles have support for TV calibration in OS settings.
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);
+ ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_MenuBar;
+ float height = GetFrameHeight();
+ bool is_open = BeginViewportSideBar("##MainMenuBar", viewport, ImGuiDir_Up, height, window_flags);
g.NextWindowData.MenuBarOffsetMinVal = ImVec2(0.0f, 0.0f);
- if (!is_open)
- {
+
+ if (is_open)
+ BeginMenuBar();
+ else
End();
- return false;
- }
- return true; //-V1020
+ return is_open;
}
void ImGui::EndMainMenuBar()
@@ -6111,13 +6947,35 @@ void ImGui::EndMainMenuBar()
// When the user has left the menu layer (typically: closed menus through activation of an item), we restore focus to the previous window
// FIXME: With this strategy we won't be able to restore a NULL focus.
ImGuiContext& g = *GImGui;
- if (g.CurrentWindow == g.NavWindow && g.NavLayer == 0 && !g.NavAnyRequest)
+ if (g.CurrentWindow == g.NavWindow && g.NavLayer == ImGuiNavLayer_Main && !g.NavAnyRequest)
FocusTopMostWindowUnderOne(g.NavWindow, NULL);
End();
}
-bool ImGui::BeginMenu(const char* label, bool enabled)
+static bool IsRootOfOpenMenuSet()
+{
+ ImGuiContext& g = *GImGui;
+ ImGuiWindow* window = g.CurrentWindow;
+ if ((g.OpenPopupStack.Size <= g.BeginPopupStack.Size) || (window->Flags & ImGuiWindowFlags_ChildMenu))
+ return false;
+
+ // Initially we used 'upper_popup->OpenParentId == window->IDStack.back()' to differentiate multiple menu sets from each others
+ // (e.g. inside menu bar vs loose menu items) based on parent ID.
+ // This would however prevent the use of e.g. PuhsID() user code submitting menus.
+ // Previously this worked between popup and a first child menu because the first child menu always had the _ChildWindow flag,
+ // making hovering on parent popup possible while first child menu was focused - but this was generally a bug with other side effects.
+ // Instead we don't treat Popup specifically (in order to consistently support menu features in them), maybe the first child menu of a Popup
+ // doesn't have the _ChildWindow flag, and we rely on this IsRootOfOpenMenuSet() check to allow hovering between root window/popup and first child menu.
+ // In the end, lack of ID check made it so we could no longer differentiate between separate menu sets. To compensate for that, we at least check parent window nav layer.
+ // This fixes the most common case of menu opening on hover when moving between window content and menu bar. Multiple different menu sets in same nav layer would still
+ // open on hover, but that should be a lesser problem, because if such menus are close in proximity in window content then it won't feel weird and if they are far apart
+ // it likely won't be a problem anyone runs into.
+ const ImGuiPopupData* upper_popup = &g.OpenPopupStack[g.BeginPopupStack.Size];
+ return (window->DC.NavLayerCurrent == upper_popup->ParentNavLayer && upper_popup->Window && (upper_popup->Window->Flags & ImGuiWindowFlags_ChildMenu));
+}
+
+bool ImGui::BeginMenuEx(const char* label, const char* icon, bool enabled)
{
ImGuiWindow* window = GetCurrentWindow();
if (window->SkipItems)
@@ -6126,20 +6984,49 @@ bool ImGui::BeginMenu(const char* label, bool enabled)
ImGuiContext& g = *GImGui;
const ImGuiStyle& style = g.Style;
const ImGuiID id = window->GetID(label);
+ bool menu_is_open = IsPopupOpen(id, ImGuiPopupFlags_None);
+
+ // 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)
+ // The first menu in a hierarchy isn't so hovering doesn't get across (otherwise e.g. resizing borders with ImGuiButtonFlags_FlattenChildren would react), but top-most BeginMenu() will bypass that limitation.
+ ImGuiWindowFlags window_flags = ImGuiWindowFlags_ChildMenu | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoNavFocus;
+ if (window->Flags & ImGuiWindowFlags_ChildMenu)
+ window_flags |= ImGuiWindowFlags_ChildWindow;
+
+ // If a menu with same the ID was already submitted, we will append to it, matching the behavior of Begin().
+ // We are relying on a O(N) search - so O(N log N) over the frame - which seems like the most efficient for the expected small amount of BeginMenu() calls per frame.
+ // If somehow this is ever becoming a problem we can switch to use e.g. ImGuiStorage mapping key to last frame used.
+ if (g.MenusIdSubmittedThisFrame.contains(id))
+ {
+ if (menu_is_open)
+ menu_is_open = BeginPopupEx(id, window_flags); // menu_is_open can be 'false' when the popup is completely clipped (e.g. zero size display)
+ else
+ g.NextWindowData.ClearFlags(); // we behave like Begin() and need to consume those values
+ return menu_is_open;
+ }
+
+ // Tag menu as used. Next time BeginMenu() with same ID is called it will append to existing menu
+ g.MenusIdSubmittedThisFrame.push_back(id);
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.BeginPopupStack.Size && g.OpenPopupStack[g.BeginPopupStack.Size].OpenParentId == window->IDStack.back());
- ImGuiWindow* backed_nav_window = g.NavWindow;
+ // Odd hack to allow hovering across menus of a same menu-set (otherwise we wouldn't be able to hover parent without always being a Child window)
+ // This is only done for items for the menu set and not the full parent window.
+ const bool menuset_is_open = IsRootOfOpenMenuSet();
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)
+ PushItemFlag(ImGuiItemFlags_NoWindowHoverableCheck, true);
// The reference position stored in popup_pos will be used by Begin() to find a suitable position for the child menu,
- // However the final position is going to be different! It is choosen by FindBestWindowPosForPopup().
+ // However the final position is going to be different! It is chosen by FindBestWindowPosForPopup().
// e.g. Menus tend to overlap each other horizontally to amplify relative Z-ordering.
ImVec2 popup_pos, pos = window->DC.CursorPos;
+ PushID(label);
+ if (!enabled)
+ BeginDisabled();
+ const ImGuiMenuColumns* offsets = &window->DC.MenuColumns;
+ bool pressed;
+
+ // We use ImGuiSelectableFlags_NoSetKeyOwner to allow down on one menu item, move, up on another.
+ const ImGuiSelectableFlags selectable_flags = ImGuiSelectableFlags_NoHoldingActiveID | ImGuiSelectableFlags_NoSetKeyOwner | ImGuiSelectableFlags_SelectOnClick | ImGuiSelectableFlags_DontClosePopups;
if (window->DC.LayoutType == ImGuiLayoutType_Horizontal)
{
// Menu inside an horizontal menu bar
@@ -6149,24 +7036,35 @@ bool ImGui::BeginMenu(const char* label, bool enabled)
window->DC.CursorPos.x += IM_FLOOR(style.ItemSpacing.x * 0.5f);
PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(style.ItemSpacing.x * 2.0f, style.ItemSpacing.y));
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));
+ ImVec2 text_pos(window->DC.CursorPos.x + offsets->OffsetLabel, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
+ pressed = Selectable("", menu_is_open, selectable_flags, ImVec2(w, 0.0f));
+ RenderText(text_pos, label);
PopStyleVar();
window->DC.CursorPos.x += IM_FLOOR(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
+ // Menu inside a regular/vertical menu
+ // (In a typical menu window where all items are BeginMenu() or MenuItem() calls, extra_w will always be 0.0f.
+ // Only when they are other items sticking out we're going to add spacing, yet only register minimum width into the layout system.
popup_pos = ImVec2(pos.x, pos.y - style.WindowPadding.y);
- float w = window->MenuColumns.DeclColumns(label_size.x, 0.0f, IM_FLOOR(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));
- ImU32 text_col = GetColorU32(enabled ? ImGuiCol_Text : ImGuiCol_TextDisabled);
- RenderArrow(window->DrawList, pos + ImVec2(window->MenuColumns.Pos[2] + extra_w + g.FontSize * 0.30f, 0.0f), text_col, ImGuiDir_Right);
- }
-
- const bool hovered = enabled && ItemHoverable(window->DC.LastItemRect, id);
+ float icon_w = (icon && icon[0]) ? CalcTextSize(icon, NULL).x : 0.0f;
+ float checkmark_w = IM_FLOOR(g.FontSize * 1.20f);
+ float min_w = window->DC.MenuColumns.DeclColumns(icon_w, label_size.x, 0.0f, checkmark_w); // Feedback to next frame
+ float extra_w = ImMax(0.0f, GetContentRegionAvail().x - min_w);
+ ImVec2 text_pos(window->DC.CursorPos.x + offsets->OffsetLabel, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
+ pressed = Selectable("", menu_is_open, selectable_flags | ImGuiSelectableFlags_SpanAvailWidth, ImVec2(min_w, 0.0f));
+ RenderText(text_pos, label);
+ if (icon_w > 0.0f)
+ RenderText(pos + ImVec2(offsets->OffsetIcon, 0.0f), icon);
+ RenderArrow(window->DrawList, pos + ImVec2(offsets->OffsetMark + extra_w + g.FontSize * 0.30f, 0.0f), GetColorU32(ImGuiCol_Text), ImGuiDir_Right);
+ }
+ if (!enabled)
+ EndDisabled();
+
+ const bool hovered = (g.HoveredId == id) && enabled && !g.NavDisableMouseHover;
if (menuset_is_open)
- g.NavWindow = backed_nav_window;
+ PopItemFlag();
bool want_open = false;
bool want_close = false;
@@ -6174,37 +7072,39 @@ bool ImGui::BeginMenu(const char* label, bool enabled)
{
// Close menu when not hovering it anymore unless we are moving roughly in the direction of the menu
// Implement http://bjk5.com/post/44698559168/breaking-down-amazons-mega-dropdown to avoid using timers, so menus feels more reactive.
- bool moving_toward_other_child_menu = false;
-
- ImGuiWindow* child_menu_window = (g.BeginPopupStack.Size < g.OpenPopupStack.Size && g.OpenPopupStack[g.BeginPopupStack.Size].SourceWindow == window) ? g.OpenPopupStack[g.BeginPopupStack.Size].Window : NULL;
- if (g.HoveredWindow == window && child_menu_window != NULL && !(window->Flags & ImGuiWindowFlags_MenuBar))
+ bool moving_toward_child_menu = false;
+ ImGuiPopupData* child_popup = (g.BeginPopupStack.Size < g.OpenPopupStack.Size) ? &g.OpenPopupStack[g.BeginPopupStack.Size] : NULL; // Popup candidate (testing below)
+ ImGuiWindow* child_menu_window = (child_popup && child_popup->Window && child_popup->Window->ParentWindow == window) ? child_popup->Window : NULL;
+ if (g.HoveredWindow == window && child_menu_window != NULL)
{
- // FIXME-DPI: Values should be derived from a master "scale" factor.
+ float ref_unit = g.FontSize; // FIXME-DPI
+ float child_dir = (window->Pos.x < child_menu_window->Pos.x) ? 1.0f : -1.0f;
ImRect next_window_rect = child_menu_window->Rect();
- ImVec2 ta = g.IO.MousePos - g.IO.MouseDelta;
- ImVec2 tb = (window->Pos.x < child_menu_window->Pos.x) ? next_window_rect.GetTL() : next_window_rect.GetTR();
- ImVec2 tc = (window->Pos.x < child_menu_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 < child_menu_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_toward_other_child_menu = ImTriangleContainsPoint(ta, tb, tc, g.IO.MousePos);
- //GetForegroundDrawList()->AddTriangleFilled(ta, tb, tc, moving_within_opened_triangle ? IM_COL32(0,128,0,128) : IM_COL32(128,0,0,128)); // [DEBUG]
+ ImVec2 ta = (g.IO.MousePos - g.IO.MouseDelta);
+ ImVec2 tb = (child_dir > 0.0f) ? next_window_rect.GetTL() : next_window_rect.GetTR();
+ ImVec2 tc = (child_dir > 0.0f) ? next_window_rect.GetBL() : next_window_rect.GetBR();
+ float extra = ImClamp(ImFabs(ta.x - tb.x) * 0.30f, ref_unit * 0.5f, ref_unit * 2.5f); // add a bit of extra slack.
+ ta.x += child_dir * -0.5f;
+ tb.x += child_dir * ref_unit;
+ tc.x += child_dir * ref_unit;
+ tb.y = ta.y + ImMax((tb.y - extra) - ta.y, -ref_unit * 8.0f); // triangle has maximum height to limit the slope and the bias toward large sub-menus
+ tc.y = ta.y + ImMin((tc.y + extra) - ta.y, +ref_unit * 8.0f);
+ moving_toward_child_menu = ImTriangleContainsPoint(ta, tb, tc, g.IO.MousePos);
+ //GetForegroundDrawList()->AddTriangleFilled(ta, tb, tc, moving_toward_child_menu ? IM_COL32(0,128,0,128) : IM_COL32(128,0,0,128)); // [DEBUG]
}
- if (menu_is_open && !hovered && g.HoveredWindow == window && g.HoveredIdPreviousFrame != 0 && g.HoveredIdPreviousFrame != id && !moving_toward_other_child_menu)
+
+ // The 'HovereWindow == window' check creates an inconsistency (e.g. moving away from menu slowly tends to hit same window, whereas moving away fast does not)
+ // But we also need to not close the top-menu menu when moving over void. Perhaps we should extend the triangle check to a larger polygon.
+ // (Remember to test this on BeginPopup("A")->BeginMenu("B") sequence which behaves slightly differently as B isn't a Child of A and hovering isn't shared.)
+ if (menu_is_open && !hovered && g.HoveredWindow == window && !moving_toward_child_menu && !g.NavDisableMouseHover)
want_close = true;
- if (!menu_is_open && hovered && pressed) // Click to open
+ // Open
+ if (!menu_is_open && pressed) // Click/activate to open
want_open = true;
- else if (!menu_is_open && hovered && !moving_toward_other_child_menu) // Hover to open
+ else if (!menu_is_open && hovered && !moving_toward_child_menu) // Hover to open
want_open = true;
-
- 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
+ if (g.NavId == id && g.NavMoveDir == ImGuiDir_Right) // Nav-Right to open
{
want_open = true;
NavMoveRequestCancel();
@@ -6222,7 +7122,7 @@ bool ImGui::BeginMenu(const char* label, bool enabled)
{
want_open = true;
}
- else if (g.NavId == id && g.NavMoveRequest && g.NavMoveDir == ImGuiDir_Down) // Nav-Down to open
+ else if (g.NavId == id && g.NavMoveDir == ImGuiDir_Down) // Nav-Down to open
{
want_open = true;
NavMoveRequestCancel();
@@ -6231,52 +7131,71 @@ bool ImGui::BeginMenu(const char* label, bool enabled)
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))
+ if (want_close && IsPopupOpen(id, ImGuiPopupFlags_None))
ClosePopupToLevel(g.BeginPopupStack.Size, true);
- IMGUI_TEST_ENGINE_ITEM_INFO(id, label, window->DC.ItemFlags | ImGuiItemStatusFlags_Openable | (menu_is_open ? ImGuiItemStatusFlags_Opened : 0));
+ IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Openable | (menu_is_open ? ImGuiItemStatusFlags_Opened : 0));
+ PopID();
- if (!menu_is_open && want_open && g.OpenPopupStack.Size > g.BeginPopupStack.Size)
+ if (want_open && !menu_is_open && g.OpenPopupStack.Size > g.BeginPopupStack.Size)
{
- // Don't recycle same menu level in the same frame, first close the other menu and yield for a frame.
+ // Don't reopen/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)
+ else if (want_open)
+ {
+ menu_is_open = true;
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)
+ ImGuiLastItemData last_item_in_parent = g.LastItemData;
+ SetNextWindowPos(popup_pos, ImGuiCond_Always); // Note: misleading: the value will serve as reference for FindBestWindowPosForPopup(), not actual pos.
+ PushStyleVar(ImGuiStyleVar_ChildRounding, style.PopupRounding); // First level will use _PopupRounding, subsequent will use _ChildRounding
+ menu_is_open = BeginPopupEx(id, window_flags); // menu_is_open can be 'false' when the popup is completely clipped (e.g. zero size display)
+ PopStyleVar();
+ if (menu_is_open)
+ {
+ // Restore LastItemData so IsItemXXXX functions can work after BeginMenu()/EndMenu()
+ // (This fixes using IsItemClicked() and IsItemHovered(), but IsItemHovered() also relies on its support for ImGuiItemFlags_NoWindowHoverableCheck)
+ g.LastItemData = last_item_in_parent;
+ if (g.HoveredWindow == window)
+ g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredWindow;
+ }
+ }
+ else
+ {
+ g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values
}
return menu_is_open;
}
+bool ImGui::BeginMenu(const char* label, bool enabled)
+{
+ return BeginMenuEx(label, NULL, enabled);
+}
+
void ImGui::EndMenu()
{
- // Nav: When a left move request _within our child menu_ failed, close ourselves (the _parent_ 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.
+ // Nav: When a left move request our menu failed, close ourselves.
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.BeginPopupStack.Size, true);
- NavMoveRequestCancel();
- }
+ IM_ASSERT(window->Flags & ImGuiWindowFlags_Popup); // Mismatched BeginMenu()/EndMenu() calls
+ ImGuiWindow* parent_window = window->ParentWindow; // Should always be != NULL is we passed assert.
+ if (window->BeginCount == window->BeginCountPreviousFrame)
+ if (g.NavMoveDir == ImGuiDir_Left && NavMoveRequestButNoResultYet())
+ if (g.NavWindow && (g.NavWindow->RootWindowForNav == window) && parent_window->DC.LayoutType == ImGuiLayoutType_Vertical)
+ {
+ ClosePopupToLevel(g.BeginPopupStack.Size - 1, true);
+ NavMoveRequestCancel();
+ }
EndPopup();
}
-bool ImGui::MenuItem(const char* label, const char* shortcut, bool selected, bool enabled)
+bool ImGui::MenuItemEx(const char* label, const char* icon, const char* shortcut, bool selected, bool enabled)
{
ImGuiWindow* window = GetCurrentWindow();
if (window->SkipItems)
@@ -6287,44 +7206,79 @@ bool ImGui::MenuItem(const char* label, const char* shortcut, bool selected, boo
ImVec2 pos = window->DC.CursorPos;
ImVec2 label_size = CalcTextSize(label, NULL, true);
+ // See BeginMenuEx() for comments about this.
+ const bool menuset_is_open = IsRootOfOpenMenuSet();
+ if (menuset_is_open)
+ PushItemFlag(ImGuiItemFlags_NoWindowHoverableCheck, true);
+
// We've been using the equivalent of ImGuiSelectableFlags_SetNavIdOnHover on all Selectable() since early Nav system days (commit 43ee5d73),
// but I am unsure whether this should be kept at all. For now moved it to be an opt-in feature used by menus only.
- ImGuiSelectableFlags flags = ImGuiSelectableFlags_PressedOnRelease | ImGuiSelectableFlags_SetNavIdOnHover | (enabled ? 0 : ImGuiSelectableFlags_Disabled);
bool pressed;
+ PushID(label);
+ if (!enabled)
+ BeginDisabled();
+
+ // We use ImGuiSelectableFlags_NoSetKeyOwner to allow down on one menu item, move, up on another.
+ const ImGuiSelectableFlags selectable_flags = ImGuiSelectableFlags_SelectOnRelease | ImGuiSelectableFlags_NoSetKeyOwner | ImGuiSelectableFlags_SetNavIdOnHover;
+ const ImGuiMenuColumns* offsets = &window->DC.MenuColumns;
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
+ // Note that in this situation: we don't render the shortcut, we render a highlight instead of the selected tick mark.
float w = label_size.x;
window->DC.CursorPos.x += IM_FLOOR(style.ItemSpacing.x * 0.5f);
+ ImVec2 text_pos(window->DC.CursorPos.x + offsets->OffsetLabel, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(style.ItemSpacing.x * 2.0f, style.ItemSpacing.y));
- pressed = Selectable(label, false, flags, ImVec2(w, 0.0f));
+ pressed = Selectable("", selected, selectable_flags, ImVec2(w, 0.0f));
PopStyleVar();
+ if (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible)
+ RenderText(text_pos, label);
window->DC.CursorPos.x += IM_FLOOR(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, IM_FLOOR(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)
+ // Menu item inside a vertical menu
+ // (In a typical menu window where all items are BeginMenu() or MenuItem() calls, extra_w will always be 0.0f.
+ // Only when they are other items sticking out we're going to add spacing, yet only register minimum width into the layout system.
+ float icon_w = (icon && icon[0]) ? CalcTextSize(icon, NULL).x : 0.0f;
+ float shortcut_w = (shortcut && shortcut[0]) ? CalcTextSize(shortcut, NULL).x : 0.0f;
+ float checkmark_w = IM_FLOOR(g.FontSize * 1.20f);
+ float min_w = window->DC.MenuColumns.DeclColumns(icon_w, label_size.x, shortcut_w, checkmark_w); // Feedback for next frame
+ float stretch_w = ImMax(0.0f, GetContentRegionAvail().x - min_w);
+ pressed = Selectable("", false, selectable_flags | ImGuiSelectableFlags_SpanAvailWidth, ImVec2(min_w, 0.0f));
+ if (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible)
{
- PushStyleColor(ImGuiCol_Text, g.Style.Colors[ImGuiCol_TextDisabled]);
- RenderText(pos + ImVec2(window->MenuColumns.Pos[1] + extra_w, 0.0f), shortcut, NULL, false);
- PopStyleColor();
+ RenderText(pos + ImVec2(offsets->OffsetLabel, 0.0f), label);
+ if (icon_w > 0.0f)
+ RenderText(pos + ImVec2(offsets->OffsetIcon, 0.0f), icon);
+ if (shortcut_w > 0.0f)
+ {
+ PushStyleColor(ImGuiCol_Text, style.Colors[ImGuiCol_TextDisabled]);
+ RenderText(pos + ImVec2(offsets->OffsetShortcut + stretch_w, 0.0f), shortcut, NULL, false);
+ PopStyleColor();
+ }
+ if (selected)
+ RenderCheckMark(window->DrawList, pos + ImVec2(offsets->OffsetMark + stretch_w + g.FontSize * 0.40f, g.FontSize * 0.134f * 0.5f), GetColorU32(ImGuiCol_Text), g.FontSize * 0.866f);
}
- 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);
}
+ IMGUI_TEST_ENGINE_ITEM_INFO(g.LastItemData.ID, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (selected ? ImGuiItemStatusFlags_Checked : 0));
+ if (!enabled)
+ EndDisabled();
+ PopID();
+ if (menuset_is_open)
+ PopItemFlag();
- IMGUI_TEST_ENGINE_ITEM_INFO(window->DC.LastItemId, label, window->DC.ItemFlags | ImGuiItemStatusFlags_Checkable | (selected ? ImGuiItemStatusFlags_Checked : 0));
return pressed;
}
+bool ImGui::MenuItem(const char* label, const char* shortcut, bool selected, bool enabled)
+{
+ return MenuItemEx(label, NULL, shortcut, selected, enabled);
+}
+
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 (MenuItemEx(label, NULL, shortcut, p_selected ? *p_selected : false, enabled))
{
if (p_selected)
*p_selected = !*p_selected;
@@ -6336,9 +7290,6 @@ bool ImGui::MenuItem(const char* label, const char* shortcut, bool* p_selected,
//-------------------------------------------------------------------------
// [SECTION] Widgets: BeginTabBar, EndTabBar, etc.
//-------------------------------------------------------------------------
-// [BETA API] API may evolve! This code has been extracted out of the Docking branch,
-// and some of the construct which are not used in Master may be left here to facilitate merging.
-//-------------------------------------------------------------------------
// - BeginTabBar()
// - BeginTabBarEx() [Internal]
// - EndTabBar()
@@ -6348,44 +7299,61 @@ bool ImGui::MenuItem(const char* label, const char* shortcut, bool* p_selected,
// - TabBarFindTabById() [Internal]
// - TabBarRemoveTab() [Internal]
// - TabBarCloseTab() [Internal]
-// - TabBarScrollClamp()v
+// - TabBarScrollClamp() [Internal]
// - TabBarScrollToTab() [Internal]
// - TabBarQueueChangeTabOrder() [Internal]
// - TabBarScrollingButtons() [Internal]
// - TabBarTabListPopupButton() [Internal]
//-------------------------------------------------------------------------
+struct ImGuiTabBarSection
+{
+ int TabCount; // Number of tabs in this section.
+ float Width; // Sum of width of tabs in this section (after shrinking down)
+ float Spacing; // Horizontal spacing at the end of the section.
+
+ ImGuiTabBarSection() { memset(this, 0, sizeof(*this)); }
+};
+
namespace ImGui
{
static void TabBarLayout(ImGuiTabBar* tab_bar);
static ImU32 TabBarCalcTabID(ImGuiTabBar* tab_bar, const char* label);
static float TabBarCalcMaxTabWidth();
static float TabBarScrollClamp(ImGuiTabBar* tab_bar, float scrolling);
- static void TabBarScrollToTab(ImGuiTabBar* tab_bar, ImGuiTabItem* tab);
+ static void TabBarScrollToTab(ImGuiTabBar* tab_bar, ImGuiID tab_id, ImGuiTabBarSection* sections);
static ImGuiTabItem* TabBarScrollingButtons(ImGuiTabBar* tab_bar);
static ImGuiTabItem* TabBarTabListPopupButton(ImGuiTabBar* tab_bar);
}
ImGuiTabBar::ImGuiTabBar()
{
- ID = 0;
- SelectedTabId = NextSelectedTabId = VisibleTabId = 0;
+ memset(this, 0, sizeof(*this));
CurrFrameVisible = PrevFrameVisible = -1;
- LastTabContentHeight = 0.0f;
- OffsetMax = OffsetMaxIdeal = OffsetNextTab = 0.0f;
- ScrollingAnim = ScrollingTarget = ScrollingTargetDistToVisibility = ScrollingSpeed = 0.0f;
- Flags = ImGuiTabBarFlags_None;
- ReorderRequestTabId = 0;
- ReorderRequestDir = 0;
- WantLayout = VisibleTabWasSubmitted = false;
LastTabItemIdx = -1;
}
-static int IMGUI_CDECL TabItemComparerByVisibleOffset(const void* lhs, const void* rhs)
+static inline int TabItemGetSectionIdx(const ImGuiTabItem* tab)
+{
+ return (tab->Flags & ImGuiTabItemFlags_Leading) ? 0 : (tab->Flags & ImGuiTabItemFlags_Trailing) ? 2 : 1;
+}
+
+static int IMGUI_CDECL TabItemComparerBySection(const void* lhs, const void* rhs)
{
const ImGuiTabItem* a = (const ImGuiTabItem*)lhs;
const ImGuiTabItem* b = (const ImGuiTabItem*)rhs;
- return (int)(a->Offset - b->Offset);
+ const int a_section = TabItemGetSectionIdx(a);
+ const int b_section = TabItemGetSectionIdx(b);
+ if (a_section != b_section)
+ return a_section - b_section;
+ return (int)(a->IndexDuringLayout - b->IndexDuringLayout);
+}
+
+static int IMGUI_CDECL TabItemComparerByBeginOrder(const void* lhs, const void* rhs)
+{
+ const ImGuiTabItem* a = (const ImGuiTabItem*)lhs;
+ const ImGuiTabItem* b = (const ImGuiTabItem*)rhs;
+ return (int)(a->BeginOrder - b->BeginOrder);
}
static ImGuiTabBar* GetTabBarFromTabBarRef(const ImGuiPtrOrIndex& ref)
@@ -6430,17 +7398,19 @@ bool ImGui::BeginTabBarEx(ImGuiTabBar* tab_bar, const ImRect& tab_bar_bb, ImG
g.CurrentTabBarStack.push_back(GetTabBarRefFromTabBar(tab_bar));
g.CurrentTabBar = tab_bar;
+ // Append with multiple BeginTabBar()/EndTabBar() pairs.
+ tab_bar->BackupCursorPos = window->DC.CursorPos;
if (tab_bar->CurrFrameVisible == g.FrameCount)
{
- //IMGUI_DEBUG_LOG("BeginTabBarEx already called this frame\n", g.FrameCount);
- IM_ASSERT(0);
+ window->DC.CursorPos = ImVec2(tab_bar->BarRect.Min.x, tab_bar->BarRect.Max.y + tab_bar->ItemSpacingY);
+ tab_bar->BeginCount++;
return true;
}
- // When toggling back from ordered to manually-reorderable, shuffle tabs to enforce the last visible order.
- // Otherwise, the most recently inserted tabs would move at the end of visible list which can be a little too confusing or magic for the user.
- if ((flags & ImGuiTabBarFlags_Reorderable) && !(tab_bar->Flags & ImGuiTabBarFlags_Reorderable) && tab_bar->Tabs.Size > 1 && tab_bar->PrevFrameVisible != -1)
- ImQsort(tab_bar->Tabs.Data, tab_bar->Tabs.Size, sizeof(ImGuiTabItem), TabItemComparerByVisibleOffset);
+ // Ensure correct ordering when toggling ImGuiTabBarFlags_Reorderable flag, or when a new tab was added while being not reorderable
+ if ((flags & ImGuiTabBarFlags_Reorderable) != (tab_bar->Flags & ImGuiTabBarFlags_Reorderable) || (tab_bar->TabsAddedNew && !(flags & ImGuiTabBarFlags_Reorderable)))
+ ImQsort(tab_bar->Tabs.Data, tab_bar->Tabs.Size, sizeof(ImGuiTabItem), TabItemComparerByBeginOrder);
+ tab_bar->TabsAddedNew = false;
// Flags
if ((flags & ImGuiTabBarFlags_FittingPolicyMask_) == 0)
@@ -6451,11 +7421,15 @@ bool ImGui::BeginTabBarEx(ImGuiTabBar* tab_bar, const ImRect& tab_bar_bb, ImG
tab_bar->WantLayout = true; // Layout will be done on the first call to ItemTab()
tab_bar->PrevFrameVisible = tab_bar->CurrFrameVisible;
tab_bar->CurrFrameVisible = g.FrameCount;
+ tab_bar->PrevTabsContentsHeight = tab_bar->CurrTabsContentsHeight;
+ tab_bar->CurrTabsContentsHeight = 0.0f;
+ tab_bar->ItemSpacingY = g.Style.ItemSpacing.y;
tab_bar->FramePadding = g.Style.FramePadding;
+ tab_bar->TabsActiveCount = 0;
+ tab_bar->BeginCount = 1;
- // Layout
- ItemSize(ImVec2(tab_bar->OffsetMaxIdeal, tab_bar->BarRect.GetHeight()), tab_bar->FramePadding.y);
- window->DC.CursorPos.x = tab_bar->BarRect.Min.x;
+ // Set cursor pos in a way which only be used in the off-chance the user erroneously submits item before BeginTabItem(): items will overlap
+ window->DC.CursorPos = ImVec2(tab_bar->BarRect.Min.x, tab_bar->BarRect.Max.y + tab_bar->ItemSpacingY);
// Draw separator
const ImU32 col = GetColorU32((flags & ImGuiTabBarFlags_IsFocused) ? ImGuiCol_TabActive : ImGuiCol_TabUnfocusedActive);
@@ -6478,18 +7452,27 @@ void ImGui::EndTabBar()
ImGuiTabBar* tab_bar = g.CurrentTabBar;
if (tab_bar == NULL)
{
- IM_ASSERT(tab_bar != NULL && "Mismatched BeginTabBar()/EndTabBar()!");
- return; // FIXME-ERRORHANDLING
+ IM_ASSERT_USER_ERROR(tab_bar != NULL, "Mismatched BeginTabBar()/EndTabBar()!");
+ return;
}
+
+ // Fallback in case no TabItem have been submitted
if (tab_bar->WantLayout)
TabBarLayout(tab_bar);
// Restore the last visible height if no tab is visible, this reduce vertical flicker/movement when a tabs gets removed without calling SetTabItemClosed().
const bool tab_bar_appearing = (tab_bar->PrevFrameVisible + 1 < g.FrameCount);
if (tab_bar->VisibleTabWasSubmitted || tab_bar->VisibleTabId == 0 || tab_bar_appearing)
- tab_bar->LastTabContentHeight = ImMax(window->DC.CursorPos.y - tab_bar->BarRect.Max.y, 0.0f);
+ {
+ tab_bar->CurrTabsContentsHeight = ImMax(window->DC.CursorPos.y - tab_bar->BarRect.Max.y, tab_bar->CurrTabsContentsHeight);
+ window->DC.CursorPos.y = tab_bar->BarRect.Max.y + tab_bar->CurrTabsContentsHeight;
+ }
else
- window->DC.CursorPos.y = tab_bar->BarRect.Max.y + tab_bar->LastTabContentHeight;
+ {
+ window->DC.CursorPos.y = tab_bar->BarRect.Max.y + tab_bar->PrevTabsContentsHeight;
+ }
+ if (tab_bar->BeginCount > 1)
+ window->DC.CursorPos = tab_bar->BackupCursorPos;
if ((tab_bar->Flags & ImGuiTabBarFlags_DockNode) == 0)
PopID();
@@ -6505,149 +7488,200 @@ static void ImGui::TabBarLayout(ImGuiTabBar* tab_bar)
ImGuiContext& g = *GImGui;
tab_bar->WantLayout = false;
- // Garbage collect
+ // Garbage collect by compacting list
+ // Detect if we need to sort out tab list (e.g. in rare case where a tab changed section)
int tab_dst_n = 0;
+ bool need_sort_by_section = false;
+ ImGuiTabBarSection sections[3]; // Layout sections: Leading, Central, Trailing
for (int tab_src_n = 0; tab_src_n < tab_bar->Tabs.Size; tab_src_n++)
{
ImGuiTabItem* tab = &tab_bar->Tabs[tab_src_n];
- if (tab->LastFrameVisible < tab_bar->PrevFrameVisible)
+ if (tab->LastFrameVisible < tab_bar->PrevFrameVisible || tab->WantClose)
{
- if (tab->ID == tab_bar->SelectedTabId)
- tab_bar->SelectedTabId = 0;
+ // Remove tab
+ if (tab_bar->VisibleTabId == tab->ID) { tab_bar->VisibleTabId = 0; }
+ if (tab_bar->SelectedTabId == tab->ID) { tab_bar->SelectedTabId = 0; }
+ if (tab_bar->NextSelectedTabId == tab->ID) { tab_bar->NextSelectedTabId = 0; }
continue;
}
if (tab_dst_n != tab_src_n)
tab_bar->Tabs[tab_dst_n] = tab_bar->Tabs[tab_src_n];
+
+ tab = &tab_bar->Tabs[tab_dst_n];
+ tab->IndexDuringLayout = (ImS16)tab_dst_n;
+
+ // We will need sorting if tabs have changed section (e.g. moved from one of Leading/Central/Trailing to another)
+ int curr_tab_section_n = TabItemGetSectionIdx(tab);
+ if (tab_dst_n > 0)
+ {
+ ImGuiTabItem* prev_tab = &tab_bar->Tabs[tab_dst_n - 1];
+ int prev_tab_section_n = TabItemGetSectionIdx(prev_tab);
+ if (curr_tab_section_n == 0 && prev_tab_section_n != 0)
+ need_sort_by_section = true;
+ if (prev_tab_section_n == 2 && curr_tab_section_n != 2)
+ need_sort_by_section = true;
+ }
+
+ sections[curr_tab_section_n].TabCount++;
tab_dst_n++;
}
if (tab_bar->Tabs.Size != tab_dst_n)
tab_bar->Tabs.resize(tab_dst_n);
+ if (need_sort_by_section)
+ ImQsort(tab_bar->Tabs.Data, tab_bar->Tabs.Size, sizeof(ImGuiTabItem), TabItemComparerBySection);
+
+ // Calculate spacing between sections
+ sections[0].Spacing = sections[0].TabCount > 0 && (sections[1].TabCount + sections[2].TabCount) > 0 ? g.Style.ItemInnerSpacing.x : 0.0f;
+ sections[1].Spacing = sections[1].TabCount > 0 && sections[2].TabCount > 0 ? g.Style.ItemInnerSpacing.x : 0.0f;
+
// Setup next selected tab
- ImGuiID scroll_track_selected_tab_id = 0;
+ ImGuiID scroll_to_tab_id = 0;
if (tab_bar->NextSelectedTabId)
{
tab_bar->SelectedTabId = tab_bar->NextSelectedTabId;
tab_bar->NextSelectedTabId = 0;
- scroll_track_selected_tab_id = tab_bar->SelectedTabId;
+ scroll_to_tab_id = tab_bar->SelectedTabId;
}
// Process order change request (we could probably process it when requested but it's just saner to do it in a single spot).
if (tab_bar->ReorderRequestTabId != 0)
{
- if (ImGuiTabItem* tab1 = TabBarFindTabByID(tab_bar, tab_bar->ReorderRequestTabId))
- {
- //IM_ASSERT(tab_bar->Flags & ImGuiTabBarFlags_Reorderable); // <- this may happen when using debug tools
- int tab2_order = tab_bar->GetTabOrder(tab1) + tab_bar->ReorderRequestDir;
- if (tab2_order >= 0 && tab2_order < tab_bar->Tabs.Size)
- {
- ImGuiTabItem* tab2 = &tab_bar->Tabs[tab2_order];
- ImGuiTabItem item_tmp = *tab1;
- *tab1 = *tab2;
- *tab2 = item_tmp;
- if (tab2->ID == tab_bar->SelectedTabId)
- scroll_track_selected_tab_id = tab2->ID;
- tab1 = tab2 = NULL;
- }
- if (tab_bar->Flags & ImGuiTabBarFlags_SaveSettings)
- MarkIniSettingsDirty();
- }
+ if (TabBarProcessReorder(tab_bar))
+ if (tab_bar->ReorderRequestTabId == tab_bar->SelectedTabId)
+ scroll_to_tab_id = tab_bar->ReorderRequestTabId;
tab_bar->ReorderRequestTabId = 0;
}
// Tab List Popup (will alter tab_bar->BarRect and therefore the available width!)
const bool tab_list_popup_button = (tab_bar->Flags & ImGuiTabBarFlags_TabListPopupButton) != 0;
if (tab_list_popup_button)
- if (ImGuiTabItem* tab_to_select = TabBarTabListPopupButton(tab_bar)) // NB: Will alter BarRect.Max.x!
- scroll_track_selected_tab_id = tab_bar->SelectedTabId = tab_to_select->ID;
+ if (ImGuiTabItem* tab_to_select = TabBarTabListPopupButton(tab_bar)) // NB: Will alter BarRect.Min.x!
+ scroll_to_tab_id = tab_bar->SelectedTabId = tab_to_select->ID;
- // Compute ideal widths
+ // Leading/Trailing tabs will be shrink only if central one aren't visible anymore, so layout the shrink data as: leading, trailing, central
+ // (whereas our tabs are stored as: leading, central, trailing)
+ int shrink_buffer_indexes[3] = { 0, sections[0].TabCount + sections[2].TabCount, sections[0].TabCount };
g.ShrinkWidthBuffer.resize(tab_bar->Tabs.Size);
- float width_total_contents = 0.0f;
+
+ // Compute ideal tabs widths + store them into shrink buffer
ImGuiTabItem* most_recently_selected_tab = NULL;
+ int curr_section_n = -1;
bool found_selected_tab_id = false;
for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++)
{
ImGuiTabItem* tab = &tab_bar->Tabs[tab_n];
IM_ASSERT(tab->LastFrameVisible >= tab_bar->PrevFrameVisible);
- if (most_recently_selected_tab == NULL || most_recently_selected_tab->LastFrameSelected < tab->LastFrameSelected)
+ if ((most_recently_selected_tab == NULL || most_recently_selected_tab->LastFrameSelected < tab->LastFrameSelected) && !(tab->Flags & ImGuiTabItemFlags_Button))
most_recently_selected_tab = tab;
if (tab->ID == tab_bar->SelectedTabId)
found_selected_tab_id = true;
+ if (scroll_to_tab_id == 0 && g.NavJustMovedToId == tab->ID)
+ scroll_to_tab_id = tab->ID;
// Refresh tab width immediately, otherwise changes of style e.g. style.FramePadding.x would noticeably lag in the tab bar.
// Additionally, when using TabBarAddTab() to manipulate tab bar order we occasionally insert new tabs that don't have a width yet,
// and we cannot wait for the next BeginTabItem() call. We cannot compute this width within TabBarAddTab() because font size depends on the active window.
const char* tab_name = tab_bar->GetTabName(tab);
- const bool has_close_button = (tab->Flags & ImGuiTabItemFlags_NoCloseButton) ? false : true;
- tab->ContentWidth = TabItemCalcSize(tab_name, has_close_button).x;
+ const bool has_close_button_or_unsaved_marker = (tab->Flags & ImGuiTabItemFlags_NoCloseButton) == 0 || (tab->Flags & ImGuiTabItemFlags_UnsavedDocument);
+ tab->ContentWidth = (tab->RequestedWidth >= 0.0f) ? tab->RequestedWidth : TabItemCalcSize(tab_name, has_close_button_or_unsaved_marker).x;
- width_total_contents += (tab_n > 0 ? g.Style.ItemInnerSpacing.x : 0.0f) + tab->ContentWidth;
+ int section_n = TabItemGetSectionIdx(tab);
+ ImGuiTabBarSection* section = &sections[section_n];
+ section->Width += tab->ContentWidth + (section_n == curr_section_n ? g.Style.ItemInnerSpacing.x : 0.0f);
+ curr_section_n = section_n;
// Store data so we can build an array sorted by width if we need to shrink tabs down
- g.ShrinkWidthBuffer[tab_n].Index = tab_n;
- g.ShrinkWidthBuffer[tab_n].Width = tab->ContentWidth;
+ IM_MSVC_WARNING_SUPPRESS(6385);
+ ImGuiShrinkWidthItem* shrink_width_item = &g.ShrinkWidthBuffer[shrink_buffer_indexes[section_n]++];
+ shrink_width_item->Index = tab_n;
+ shrink_width_item->Width = shrink_width_item->InitialWidth = tab->ContentWidth;
+ tab->Width = ImMax(tab->ContentWidth, 1.0f);
}
- // Compute width
- const float initial_offset_x = 0.0f; // g.Style.ItemInnerSpacing.x;
- const float width_avail = ImMax(tab_bar->BarRect.GetWidth() - initial_offset_x, 0.0f);
- float width_excess = (width_avail < width_total_contents) ? (width_total_contents - width_avail) : 0.0f;
- if (width_excess > 0.0f && (tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyResizeDown))
- {
- // If we don't have enough room, resize down the largest tabs first
- ShrinkWidths(g.ShrinkWidthBuffer.Data, g.ShrinkWidthBuffer.Size, width_excess);
- for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++)
- tab_bar->Tabs[g.ShrinkWidthBuffer[tab_n].Index].Width = IM_FLOOR(g.ShrinkWidthBuffer[tab_n].Width);
- }
+ // Compute total ideal width (used for e.g. auto-resizing a window)
+ tab_bar->WidthAllTabsIdeal = 0.0f;
+ for (int section_n = 0; section_n < 3; section_n++)
+ tab_bar->WidthAllTabsIdeal += sections[section_n].Width + sections[section_n].Spacing;
+
+ // Horizontal scrolling buttons
+ // (note that TabBarScrollButtons() will alter BarRect.Max.x)
+ if ((tab_bar->WidthAllTabsIdeal > tab_bar->BarRect.GetWidth() && tab_bar->Tabs.Size > 1) && !(tab_bar->Flags & ImGuiTabBarFlags_NoTabListScrollingButtons) && (tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyScroll))
+ if (ImGuiTabItem* scroll_and_select_tab = TabBarScrollingButtons(tab_bar))
+ {
+ scroll_to_tab_id = scroll_and_select_tab->ID;
+ if ((scroll_and_select_tab->Flags & ImGuiTabItemFlags_Button) == 0)
+ tab_bar->SelectedTabId = scroll_to_tab_id;
+ }
+
+ // Shrink widths if full tabs don't fit in their allocated space
+ float section_0_w = sections[0].Width + sections[0].Spacing;
+ float section_1_w = sections[1].Width + sections[1].Spacing;
+ float section_2_w = sections[2].Width + sections[2].Spacing;
+ bool central_section_is_visible = (section_0_w + section_2_w) < tab_bar->BarRect.GetWidth();
+ float width_excess;
+ if (central_section_is_visible)
+ width_excess = ImMax(section_1_w - (tab_bar->BarRect.GetWidth() - section_0_w - section_2_w), 0.0f); // Excess used to shrink central section
else
+ width_excess = (section_0_w + section_2_w) - tab_bar->BarRect.GetWidth(); // Excess used to shrink leading/trailing section
+
+ // With ImGuiTabBarFlags_FittingPolicyScroll policy, we will only shrink leading/trailing if the central section is not visible anymore
+ if (width_excess >= 1.0f && ((tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyResizeDown) || !central_section_is_visible))
{
- const float tab_max_width = TabBarCalcMaxTabWidth();
- for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++)
+ int shrink_data_count = (central_section_is_visible ? sections[1].TabCount : sections[0].TabCount + sections[2].TabCount);
+ int shrink_data_offset = (central_section_is_visible ? sections[0].TabCount + sections[2].TabCount : 0);
+ ShrinkWidths(g.ShrinkWidthBuffer.Data + shrink_data_offset, shrink_data_count, width_excess);
+
+ // Apply shrunk values into tabs and sections
+ for (int tab_n = shrink_data_offset; tab_n < shrink_data_offset + shrink_data_count; tab_n++)
{
- ImGuiTabItem* tab = &tab_bar->Tabs[tab_n];
- tab->Width = ImMin(tab->ContentWidth, tab_max_width);
- IM_ASSERT(tab->Width > 0.0f);
+ ImGuiTabItem* tab = &tab_bar->Tabs[g.ShrinkWidthBuffer[tab_n].Index];
+ float shrinked_width = IM_FLOOR(g.ShrinkWidthBuffer[tab_n].Width);
+ if (shrinked_width < 0.0f)
+ continue;
+
+ shrinked_width = ImMax(1.0f, shrinked_width);
+ int section_n = TabItemGetSectionIdx(tab);
+ sections[section_n].Width -= (tab->Width - shrinked_width);
+ tab->Width = shrinked_width;
}
}
// Layout all active tabs
- float offset_x = initial_offset_x;
- float offset_x_ideal = offset_x;
- tab_bar->OffsetNextTab = offset_x; // This is used by non-reorderable tab bar where the submission order is always honored.
- for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++)
+ int section_tab_index = 0;
+ float tab_offset = 0.0f;
+ tab_bar->WidthAllTabs = 0.0f;
+ for (int section_n = 0; section_n < 3; section_n++)
{
- ImGuiTabItem* tab = &tab_bar->Tabs[tab_n];
- tab->Offset = offset_x;
- if (scroll_track_selected_tab_id == 0 && g.NavJustMovedToId == tab->ID)
- scroll_track_selected_tab_id = tab->ID;
- offset_x += tab->Width + g.Style.ItemInnerSpacing.x;
- offset_x_ideal += tab->ContentWidth + g.Style.ItemInnerSpacing.x;
- }
- tab_bar->OffsetMax = ImMax(offset_x - g.Style.ItemInnerSpacing.x, 0.0f);
- tab_bar->OffsetMaxIdeal = ImMax(offset_x_ideal - g.Style.ItemInnerSpacing.x, 0.0f);
+ ImGuiTabBarSection* section = &sections[section_n];
+ if (section_n == 2)
+ tab_offset = ImMin(ImMax(0.0f, tab_bar->BarRect.GetWidth() - section->Width), tab_offset);
- // Horizontal scrolling buttons
- const bool scrolling_buttons = (tab_bar->OffsetMax > tab_bar->BarRect.GetWidth() && tab_bar->Tabs.Size > 1) && !(tab_bar->Flags & ImGuiTabBarFlags_NoTabListScrollingButtons) && (tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyScroll);
- if (scrolling_buttons)
- if (ImGuiTabItem* tab_to_select = TabBarScrollingButtons(tab_bar)) // NB: Will alter BarRect.Max.x!
- scroll_track_selected_tab_id = tab_bar->SelectedTabId = tab_to_select->ID;
+ for (int tab_n = 0; tab_n < section->TabCount; tab_n++)
+ {
+ ImGuiTabItem* tab = &tab_bar->Tabs[section_tab_index + tab_n];
+ tab->Offset = tab_offset;
+ tab_offset += tab->Width + (tab_n < section->TabCount - 1 ? g.Style.ItemInnerSpacing.x : 0.0f);
+ }
+ tab_bar->WidthAllTabs += ImMax(section->Width + section->Spacing, 0.0f);
+ tab_offset += section->Spacing;
+ section_tab_index += section->TabCount;
+ }
// If we have lost the selected tab, select the next most recently active one
if (found_selected_tab_id == false)
tab_bar->SelectedTabId = 0;
if (tab_bar->SelectedTabId == 0 && tab_bar->NextSelectedTabId == 0 && most_recently_selected_tab != NULL)
- scroll_track_selected_tab_id = tab_bar->SelectedTabId = most_recently_selected_tab->ID;
+ scroll_to_tab_id = tab_bar->SelectedTabId = most_recently_selected_tab->ID;
// Lock in visible tab
tab_bar->VisibleTabId = tab_bar->SelectedTabId;
tab_bar->VisibleTabWasSubmitted = false;
// Update scrolling
- if (scroll_track_selected_tab_id)
- if (ImGuiTabItem* scroll_track_selected_tab = TabBarFindTabByID(tab_bar, scroll_track_selected_tab_id))
- TabBarScrollToTab(tab_bar, scroll_track_selected_tab);
+ if (scroll_to_tab_id != 0)
+ TabBarScrollToTab(tab_bar, scroll_to_tab_id, sections);
tab_bar->ScrollingAnim = TabBarScrollClamp(tab_bar, tab_bar->ScrollingAnim);
tab_bar->ScrollingTarget = TabBarScrollClamp(tab_bar, tab_bar->ScrollingTarget);
if (tab_bar->ScrollingAnim != tab_bar->ScrollingTarget)
@@ -6663,10 +7697,18 @@ static void ImGui::TabBarLayout(ImGuiTabBar* tab_bar)
{
tab_bar->ScrollingSpeed = 0.0f;
}
+ tab_bar->ScrollingRectMinX = tab_bar->BarRect.Min.x + sections[0].Width + sections[0].Spacing;
+ tab_bar->ScrollingRectMaxX = tab_bar->BarRect.Max.x - sections[2].Width - sections[1].Spacing;
// Clear name buffers
if ((tab_bar->Flags & ImGuiTabBarFlags_DockNode) == 0)
tab_bar->TabsNames.Buf.resize(0);
+
+ // Actual layout in host window (we don't do it in BeginTabBar() so as not to waste an extra frame)
+ ImGuiWindow* window = g.CurrentWindow;
+ window->DC.CursorPos = tab_bar->BarRect.Min;
+ ItemSize(ImVec2(tab_bar->WidthAllTabs, tab_bar->BarRect.GetHeight()), tab_bar->FramePadding.y);
+ window->DC.IdealMaxPos.x = ImMax(window->DC.IdealMaxPos.x, tab_bar->BarRect.Min.x + tab_bar->WidthAllTabsIdeal);
}
// Dockables uses Name/ID in the global namespace. Non-dockable items use the ID stack.
@@ -6713,52 +7755,142 @@ void ImGui::TabBarRemoveTab(ImGuiTabBar* tab_bar, ImGuiID tab_id)
// Called on manual closure attempt
void ImGui::TabBarCloseTab(ImGuiTabBar* tab_bar, ImGuiTabItem* tab)
{
- if ((tab_bar->VisibleTabId == tab->ID) && !(tab->Flags & ImGuiTabItemFlags_UnsavedDocument))
+ if (tab->Flags & ImGuiTabItemFlags_Button)
+ return; // A button appended with TabItemButton().
+
+ if (!(tab->Flags & ImGuiTabItemFlags_UnsavedDocument))
{
// This will remove a frame of lag for selecting another tab on closure.
// However we don't run it in the case where the 'Unsaved' flag is set, so user gets a chance to fully undo the closure
- tab->LastFrameVisible = -1;
- tab_bar->SelectedTabId = tab_bar->NextSelectedTabId = 0;
+ tab->WantClose = true;
+ if (tab_bar->VisibleTabId == tab->ID)
+ {
+ tab->LastFrameVisible = -1;
+ tab_bar->SelectedTabId = tab_bar->NextSelectedTabId = 0;
+ }
}
- else if ((tab_bar->VisibleTabId != tab->ID) && (tab->Flags & ImGuiTabItemFlags_UnsavedDocument))
+ else
{
- // Actually select before expecting closure
- tab_bar->NextSelectedTabId = tab->ID;
+ // Actually select before expecting closure attempt (on an UnsavedDocument tab user is expect to e.g. show a popup)
+ if (tab_bar->VisibleTabId != tab->ID)
+ tab_bar->NextSelectedTabId = tab->ID;
}
}
static float ImGui::TabBarScrollClamp(ImGuiTabBar* tab_bar, float scrolling)
{
- scrolling = ImMin(scrolling, tab_bar->OffsetMax - tab_bar->BarRect.GetWidth());
+ scrolling = ImMin(scrolling, tab_bar->WidthAllTabs - tab_bar->BarRect.GetWidth());
return ImMax(scrolling, 0.0f);
}
-static void ImGui::TabBarScrollToTab(ImGuiTabBar* tab_bar, ImGuiTabItem* tab)
+// Note: we may scroll to tab that are not selected! e.g. using keyboard arrow keys
+static void ImGui::TabBarScrollToTab(ImGuiTabBar* tab_bar, ImGuiID tab_id, ImGuiTabBarSection* sections)
{
+ ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_id);
+ if (tab == NULL)
+ return;
+ if (tab->Flags & ImGuiTabItemFlags_SectionMask_)
+ return;
+
ImGuiContext& g = *GImGui;
float margin = g.FontSize * 1.0f; // When to scroll to make Tab N+1 visible always make a bit of N visible to suggest more scrolling area (since we don't have a scrollbar)
int order = tab_bar->GetTabOrder(tab);
- float tab_x1 = tab->Offset + (order > 0 ? -margin : 0.0f);
- float tab_x2 = tab->Offset + tab->Width + (order + 1 < tab_bar->Tabs.Size ? margin : 1.0f);
+
+ // Scrolling happens only in the central section (leading/trailing sections are not scrolling)
+ // FIXME: This is all confusing.
+ float scrollable_width = tab_bar->BarRect.GetWidth() - sections[0].Width - sections[2].Width - sections[1].Spacing;
+
+ // We make all tabs positions all relative Sections[0].Width to make code simpler
+ float tab_x1 = tab->Offset - sections[0].Width + (order > sections[0].TabCount - 1 ? -margin : 0.0f);
+ float tab_x2 = tab->Offset - sections[0].Width + tab->Width + (order + 1 < tab_bar->Tabs.Size - sections[2].TabCount ? margin : 1.0f);
tab_bar->ScrollingTargetDistToVisibility = 0.0f;
- if (tab_bar->ScrollingTarget > tab_x1 || (tab_x2 - tab_x1 >= tab_bar->BarRect.GetWidth()))
+ if (tab_bar->ScrollingTarget > tab_x1 || (tab_x2 - tab_x1 >= scrollable_width))
{
+ // Scroll to the left
tab_bar->ScrollingTargetDistToVisibility = ImMax(tab_bar->ScrollingAnim - tab_x2, 0.0f);
tab_bar->ScrollingTarget = tab_x1;
}
- else if (tab_bar->ScrollingTarget < tab_x2 - tab_bar->BarRect.GetWidth())
+ else if (tab_bar->ScrollingTarget < tab_x2 - scrollable_width)
{
- tab_bar->ScrollingTargetDistToVisibility = ImMax((tab_x1 - tab_bar->BarRect.GetWidth()) - tab_bar->ScrollingAnim, 0.0f);
- tab_bar->ScrollingTarget = tab_x2 - tab_bar->BarRect.GetWidth();
+ // Scroll to the right
+ tab_bar->ScrollingTargetDistToVisibility = ImMax((tab_x1 - scrollable_width) - tab_bar->ScrollingAnim, 0.0f);
+ tab_bar->ScrollingTarget = tab_x2 - scrollable_width;
}
}
-void ImGui::TabBarQueueChangeTabOrder(ImGuiTabBar* tab_bar, const ImGuiTabItem* tab, int dir)
+void ImGui::TabBarQueueReorder(ImGuiTabBar* tab_bar, const ImGuiTabItem* tab, int offset)
{
- IM_ASSERT(dir == -1 || dir == +1);
+ IM_ASSERT(offset != 0);
IM_ASSERT(tab_bar->ReorderRequestTabId == 0);
tab_bar->ReorderRequestTabId = tab->ID;
- tab_bar->ReorderRequestDir = (ImS8)dir;
+ tab_bar->ReorderRequestOffset = (ImS16)offset;
+}
+
+void ImGui::TabBarQueueReorderFromMousePos(ImGuiTabBar* tab_bar, const ImGuiTabItem* src_tab, ImVec2 mouse_pos)
+{
+ ImGuiContext& g = *GImGui;
+ IM_ASSERT(tab_bar->ReorderRequestTabId == 0);
+ if ((tab_bar->Flags & ImGuiTabBarFlags_Reorderable) == 0)
+ return;
+
+ const bool is_central_section = (src_tab->Flags & ImGuiTabItemFlags_SectionMask_) == 0;
+ const float bar_offset = tab_bar->BarRect.Min.x - (is_central_section ? tab_bar->ScrollingTarget : 0);
+
+ // Count number of contiguous tabs we are crossing over
+ const int dir = (bar_offset + src_tab->Offset) > mouse_pos.x ? -1 : +1;
+ const int src_idx = tab_bar->Tabs.index_from_ptr(src_tab);
+ int dst_idx = src_idx;
+ for (int i = src_idx; i >= 0 && i < tab_bar->Tabs.Size; i += dir)
+ {
+ // Reordered tabs must share the same section
+ const ImGuiTabItem* dst_tab = &tab_bar->Tabs[i];
+ if (dst_tab->Flags & ImGuiTabItemFlags_NoReorder)
+ break;
+ if ((dst_tab->Flags & ImGuiTabItemFlags_SectionMask_) != (src_tab->Flags & ImGuiTabItemFlags_SectionMask_))
+ break;
+ dst_idx = i;
+
+ // Include spacing after tab, so when mouse cursor is between tabs we would not continue checking further tabs that are not hovered.
+ const float x1 = bar_offset + dst_tab->Offset - g.Style.ItemInnerSpacing.x;
+ const float x2 = bar_offset + dst_tab->Offset + dst_tab->Width + g.Style.ItemInnerSpacing.x;
+ //GetForegroundDrawList()->AddRect(ImVec2(x1, tab_bar->BarRect.Min.y), ImVec2(x2, tab_bar->BarRect.Max.y), IM_COL32(255, 0, 0, 255));
+ if ((dir < 0 && mouse_pos.x > x1) || (dir > 0 && mouse_pos.x < x2))
+ break;
+ }
+
+ if (dst_idx != src_idx)
+ TabBarQueueReorder(tab_bar, src_tab, dst_idx - src_idx);
+}
+
+bool ImGui::TabBarProcessReorder(ImGuiTabBar* tab_bar)
+{
+ ImGuiTabItem* tab1 = TabBarFindTabByID(tab_bar, tab_bar->ReorderRequestTabId);
+ if (tab1 == NULL || (tab1->Flags & ImGuiTabItemFlags_NoReorder))
+ return false;
+
+ //IM_ASSERT(tab_bar->Flags & ImGuiTabBarFlags_Reorderable); // <- this may happen when using debug tools
+ int tab2_order = tab_bar->GetTabOrder(tab1) + tab_bar->ReorderRequestOffset;
+ if (tab2_order < 0 || tab2_order >= tab_bar->Tabs.Size)
+ return false;
+
+ // Reordered tabs must share the same section
+ // (Note: TabBarQueueReorderFromMousePos() also has a similar test but since we allow direct calls to TabBarQueueReorder() we do it here too)
+ ImGuiTabItem* tab2 = &tab_bar->Tabs[tab2_order];
+ if (tab2->Flags & ImGuiTabItemFlags_NoReorder)
+ return false;
+ if ((tab1->Flags & ImGuiTabItemFlags_SectionMask_) != (tab2->Flags & ImGuiTabItemFlags_SectionMask_))
+ return false;
+
+ ImGuiTabItem item_tmp = *tab1;
+ ImGuiTabItem* src_tab = (tab_bar->ReorderRequestOffset > 0) ? tab1 + 1 : tab2;
+ ImGuiTabItem* dst_tab = (tab_bar->ReorderRequestOffset > 0) ? tab1 : tab2 + 1;
+ const int move_count = (tab_bar->ReorderRequestOffset > 0) ? tab_bar->ReorderRequestOffset : -tab_bar->ReorderRequestOffset;
+ memmove(dst_tab, src_tab, move_count * sizeof(ImGuiTabItem));
+ *tab2 = item_tmp;
+
+ if (tab_bar->Flags & ImGuiTabBarFlags_SaveSettings)
+ MarkIniSettingsDirty();
+ return true;
}
static ImGuiTabItem* ImGui::TabBarScrollingButtons(ImGuiTabBar* tab_bar)
@@ -6772,13 +7904,6 @@ static ImGuiTabItem* ImGui::TabBarScrollingButtons(ImGuiTabBar* tab_bar)
const ImVec2 backup_cursor_pos = window->DC.CursorPos;
//window->DrawList->AddRect(ImVec2(tab_bar->BarRect.Max.x - scrolling_buttons_width, tab_bar->BarRect.Min.y), ImVec2(tab_bar->BarRect.Max.x, tab_bar->BarRect.Max.y), IM_COL32(255,0,0,255));
- const ImRect avail_bar_rect = tab_bar->BarRect;
- bool want_clip_rect = !avail_bar_rect.Contains(ImRect(window->DC.CursorPos, window->DC.CursorPos + ImVec2(scrolling_buttons_width, 0.0f)));
- if (want_clip_rect)
- PushClipRect(tab_bar->BarRect.Min, tab_bar->BarRect.Max + ImVec2(g.Style.ItemInnerSpacing.x, 0.0f), true);
-
- ImGuiTabItem* tab_to_select = NULL;
-
int select_dir = 0;
ImVec4 arrow_col = g.Style.Colors[ImGuiCol_Text];
arrow_col.w *= 0.5f;
@@ -6789,30 +7914,44 @@ static ImGuiTabItem* ImGui::TabBarScrollingButtons(ImGuiTabBar* tab_bar)
const float backup_repeat_rate = g.IO.KeyRepeatRate;
g.IO.KeyRepeatDelay = 0.250f;
g.IO.KeyRepeatRate = 0.200f;
- window->DC.CursorPos = ImVec2(tab_bar->BarRect.Max.x - scrolling_buttons_width, tab_bar->BarRect.Min.y);
+ float x = ImMax(tab_bar->BarRect.Min.x, tab_bar->BarRect.Max.x - scrolling_buttons_width);
+ window->DC.CursorPos = ImVec2(x, tab_bar->BarRect.Min.y);
if (ArrowButtonEx("##<", ImGuiDir_Left, arrow_button_size, ImGuiButtonFlags_PressedOnClick | ImGuiButtonFlags_Repeat))
select_dir = -1;
- window->DC.CursorPos = ImVec2(tab_bar->BarRect.Max.x - scrolling_buttons_width + arrow_button_size.x, tab_bar->BarRect.Min.y);
+ window->DC.CursorPos = ImVec2(x + arrow_button_size.x, tab_bar->BarRect.Min.y);
if (ArrowButtonEx("##>", ImGuiDir_Right, arrow_button_size, ImGuiButtonFlags_PressedOnClick | ImGuiButtonFlags_Repeat))
select_dir = +1;
PopStyleColor(2);
g.IO.KeyRepeatRate = backup_repeat_rate;
g.IO.KeyRepeatDelay = backup_repeat_delay;
- if (want_clip_rect)
- PopClipRect();
-
+ ImGuiTabItem* tab_to_scroll_to = NULL;
if (select_dir != 0)
if (ImGuiTabItem* tab_item = TabBarFindTabByID(tab_bar, tab_bar->SelectedTabId))
{
int selected_order = tab_bar->GetTabOrder(tab_item);
int target_order = selected_order + select_dir;
- tab_to_select = &tab_bar->Tabs[(target_order >= 0 && target_order < tab_bar->Tabs.Size) ? target_order : selected_order]; // If we are at the end of the list, still scroll to make our tab visible
+
+ // Skip tab item buttons until another tab item is found or end is reached
+ while (tab_to_scroll_to == NULL)
+ {
+ // If we are at the end of the list, still scroll to make our tab visible
+ tab_to_scroll_to = &tab_bar->Tabs[(target_order >= 0 && target_order < tab_bar->Tabs.Size) ? target_order : selected_order];
+
+ // Cross through buttons
+ // (even if first/last item is a button, return it so we can update the scroll)
+ if (tab_to_scroll_to->Flags & ImGuiTabItemFlags_Button)
+ {
+ target_order += select_dir;
+ selected_order += select_dir;
+ tab_to_scroll_to = (target_order < 0 || target_order >= tab_bar->Tabs.Size) ? tab_to_scroll_to : NULL;
+ }
+ }
}
window->DC.CursorPos = backup_cursor_pos;
tab_bar->BarRect.Max.x -= scrolling_buttons_width + 1.0f;
- return tab_to_select;
+ return tab_to_scroll_to;
}
static ImGuiTabItem* ImGui::TabBarTabListPopupButton(ImGuiTabBar* tab_bar)
@@ -6830,7 +7969,7 @@ static ImGuiTabItem* ImGui::TabBarTabListPopupButton(ImGuiTabBar* tab_bar)
arrow_col.w *= 0.5f;
PushStyleColor(ImGuiCol_Text, arrow_col);
PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
- bool open = BeginCombo("##v", NULL, ImGuiComboFlags_NoPreview);
+ bool open = BeginCombo("##v", NULL, ImGuiComboFlags_NoPreview | ImGuiComboFlags_HeightLargest);
PopStyleColor(2);
ImGuiTabItem* tab_to_select = NULL;
@@ -6839,6 +7978,9 @@ static ImGuiTabItem* ImGui::TabBarTabListPopupButton(ImGuiTabBar* tab_bar)
for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++)
{
ImGuiTabItem* tab = &tab_bar->Tabs[tab_n];
+ if (tab->Flags & ImGuiTabItemFlags_Button)
+ continue;
+
const char* tab_name = tab_bar->GetTabName(tab);
if (Selectable(tab_name, tab_bar->SelectedTabId == tab->ID))
tab_to_select = tab;
@@ -6853,11 +7995,9 @@ static ImGuiTabItem* ImGui::TabBarTabListPopupButton(ImGuiTabBar* tab_bar)
//-------------------------------------------------------------------------
// [SECTION] Widgets: BeginTabItem, EndTabItem, etc.
//-------------------------------------------------------------------------
-// [BETA API] API may evolve! This code has been extracted out of the Docking branch,
-// and some of the construct which are not used in Master may be left here to facilitate merging.
-//-------------------------------------------------------------------------
// - BeginTabItem()
// - EndTabItem()
+// - TabItemButton()
// - TabItemEx() [Internal]
// - SetTabItemClosed()
// - TabItemCalcSize() [Internal]
@@ -6875,9 +8015,11 @@ bool ImGui::BeginTabItem(const char* label, bool* p_open, ImGuiTabItemFlags f
ImGuiTabBar* tab_bar = g.CurrentTabBar;
if (tab_bar == NULL)
{
- IM_ASSERT(tab_bar && "Needs to be called between BeginTabBar() and EndTabBar()!");
- return false; // FIXME-ERRORHANDLING
+ IM_ASSERT_USER_ERROR(tab_bar, "Needs to be called between BeginTabBar() and EndTabBar()!");
+ return false;
}
+ IM_ASSERT(!(flags & ImGuiTabItemFlags_Button)); // BeginTabItem() Can't be used with button flags, use TabItemButton() instead!
+
bool ret = TabItemEx(tab_bar, label, p_open, flags);
if (ret && !(flags & ImGuiTabItemFlags_NoPushId))
{
@@ -6897,22 +8039,41 @@ void ImGui::EndTabItem()
ImGuiTabBar* tab_bar = g.CurrentTabBar;
if (tab_bar == NULL)
{
- IM_ASSERT(tab_bar != NULL && "Needs to be called between BeginTabBar() and EndTabBar()!");
+ IM_ASSERT_USER_ERROR(tab_bar != NULL, "Needs to be called between BeginTabBar() and EndTabBar()!");
return;
}
IM_ASSERT(tab_bar->LastTabItemIdx >= 0);
ImGuiTabItem* tab = &tab_bar->Tabs[tab_bar->LastTabItemIdx];
if (!(tab->Flags & ImGuiTabItemFlags_NoPushId))
- window->IDStack.pop_back();
+ PopID();
+}
+
+bool ImGui::TabItemButton(const char* label, ImGuiTabItemFlags flags)
+{
+ ImGuiContext& g = *GImGui;
+ ImGuiWindow* window = g.CurrentWindow;
+ if (window->SkipItems)
+ return false;
+
+ ImGuiTabBar* tab_bar = g.CurrentTabBar;
+ if (tab_bar == NULL)
+ {
+ IM_ASSERT_USER_ERROR(tab_bar != NULL, "Needs to be called between BeginTabBar() and EndTabBar()!");
+ return false;
+ }
+ return TabItemEx(tab_bar, label, NULL, flags | ImGuiTabItemFlags_Button | ImGuiTabItemFlags_NoReorder);
}
bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open, ImGuiTabItemFlags flags)
{
// Layout whole tab bar if not already done
+ ImGuiContext& g = *GImGui;
if (tab_bar->WantLayout)
+ {
+ ImGuiNextItemData backup_next_item_data = g.NextItemData;
TabBarLayout(tab_bar);
-
- ImGuiContext& g = *GImGui;
+ g.NextItemData = backup_next_item_data;
+ }
ImGuiWindow* window = g.CurrentWindow;
if (window->SkipItems)
return false;
@@ -6920,17 +8081,23 @@ bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open,
const ImGuiStyle& style = g.Style;
const ImGuiID id = TabBarCalcTabID(tab_bar, label);
- // If the user called us with *p_open == false, we early out and don't render. We make a dummy call to ItemAdd() so that attempts to use a contextual popup menu with an implicit ID won't use an older ID.
+ // If the user called us with *p_open == false, we early out and don't render.
+ // We make a call to ItemAdd() so that attempts to use a contextual popup menu with an implicit ID won't use an older ID.
+ IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
if (p_open && !*p_open)
{
- PushItemFlag(ImGuiItemFlags_NoNav | ImGuiItemFlags_NoNavDefaultFocus, true);
- ItemAdd(ImRect(), id);
- PopItemFlag();
+ ItemAdd(ImRect(), id, NULL, ImGuiItemFlags_NoNav);
return false;
}
- // Calculate tab contents size
- ImVec2 size = TabItemCalcSize(label, p_open != NULL);
+ IM_ASSERT(!p_open || !(flags & ImGuiTabItemFlags_Button));
+ IM_ASSERT((flags & (ImGuiTabItemFlags_Leading | ImGuiTabItemFlags_Trailing)) != (ImGuiTabItemFlags_Leading | ImGuiTabItemFlags_Trailing)); // Can't use both Leading and Trailing
+
+ // Store into ImGuiTabItemFlags_NoCloseButton, also honor ImGuiTabItemFlags_NoCloseButton passed by user (although not documented)
+ if (flags & ImGuiTabItemFlags_NoCloseButton)
+ p_open = NULL;
+ else if (p_open == NULL)
+ flags |= ImGuiTabItemFlags_NoCloseButton;
// Acquire tab data
ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, id);
@@ -6940,41 +8107,44 @@ bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open,
tab_bar->Tabs.push_back(ImGuiTabItem());
tab = &tab_bar->Tabs.back();
tab->ID = id;
- tab->Width = size.x;
- tab_is_new = true;
+ tab_bar->TabsAddedNew = tab_is_new = true;
}
- tab_bar->LastTabItemIdx = (short)tab_bar->Tabs.index_from_ptr(tab);
- tab->ContentWidth = size.x;
+ tab_bar->LastTabItemIdx = (ImS16)tab_bar->Tabs.index_from_ptr(tab);
- if (p_open == NULL)
- flags |= ImGuiTabItemFlags_NoCloseButton;
+ // Calculate tab contents size
+ ImVec2 size = TabItemCalcSize(label, (p_open != NULL) || (flags & ImGuiTabItemFlags_UnsavedDocument));
+ tab->RequestedWidth = -1.0f;
+ if (g.NextItemData.Flags & ImGuiNextItemDataFlags_HasWidth)
+ size.x = tab->RequestedWidth = g.NextItemData.Width;
+ if (tab_is_new)
+ tab->Width = ImMax(1.0f, size.x);
+ tab->ContentWidth = size.x;
+ tab->BeginOrder = tab_bar->TabsActiveCount++;
const bool tab_bar_appearing = (tab_bar->PrevFrameVisible + 1 < g.FrameCount);
const bool tab_bar_focused = (tab_bar->Flags & ImGuiTabBarFlags_IsFocused) != 0;
const bool tab_appearing = (tab->LastFrameVisible + 1 < g.FrameCount);
+ const bool tab_just_unsaved = (flags & ImGuiTabItemFlags_UnsavedDocument) && !(tab->Flags & ImGuiTabItemFlags_UnsavedDocument);
+ const bool is_tab_button = (flags & ImGuiTabItemFlags_Button) != 0;
tab->LastFrameVisible = g.FrameCount;
tab->Flags = flags;
// Append name with zero-terminator
- tab->NameOffset = tab_bar->TabsNames.size();
+ tab->NameOffset = (ImS32)tab_bar->TabsNames.size();
tab_bar->TabsNames.append(label, label + strlen(label) + 1);
- // If we are not reorderable, always reset offset based on submission order.
- // (We already handled layout and sizing using the previous known order, but sizing is not affected by order!)
- if (!tab_appearing && !(tab_bar->Flags & ImGuiTabBarFlags_Reorderable))
+ // Update selected tab
+ if (!is_tab_button)
{
- tab->Offset = tab_bar->OffsetNextTab;
- tab_bar->OffsetNextTab += tab->Width + g.Style.ItemInnerSpacing.x;
+ if (tab_appearing && (tab_bar->Flags & ImGuiTabBarFlags_AutoSelectNewTabs) && tab_bar->NextSelectedTabId == 0)
+ if (!tab_bar_appearing || tab_bar->SelectedTabId == 0)
+ tab_bar->NextSelectedTabId = id; // New tabs gets activated
+ if ((flags & ImGuiTabItemFlags_SetSelected) && (tab_bar->SelectedTabId != id)) // _SetSelected can only be passed on explicit tab bar
+ tab_bar->NextSelectedTabId = id;
}
- // Update selected tab
- if (tab_appearing && (tab_bar->Flags & ImGuiTabBarFlags_AutoSelectNewTabs) && tab_bar->NextSelectedTabId == 0)
- if (!tab_bar_appearing || tab_bar->SelectedTabId == 0)
- tab_bar->NextSelectedTabId = id; // New tabs gets activated
- if ((flags & ImGuiTabItemFlags_SetSelected) && (tab_bar->SelectedTabId != id)) // SetSelected can only be passed on explicit tab bar
- tab_bar->NextSelectedTabId = id;
-
// Lock visibility
+ // (Note: tab_contents_visible != tab_selected... because CTRL+TAB operations may preview some tabs without selecting them!)
bool tab_contents_visible = (tab_bar->VisibleTabId == id);
if (tab_contents_visible)
tab_bar->VisibleTabWasSubmitted = true;
@@ -6984,11 +8154,13 @@ bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open,
if (tab_bar->Tabs.Size == 1 && !(tab_bar->Flags & ImGuiTabBarFlags_AutoSelectNewTabs))
tab_contents_visible = true;
- if (tab_appearing && !(tab_bar_appearing && !tab_is_new))
+ // Note that tab_is_new is not necessarily the same as tab_appearing! When a tab bar stops being submitted
+ // and then gets submitted again, the tabs will have 'tab_appearing=true' but 'tab_is_new=false'.
+ if (tab_appearing && (!tab_bar_appearing || tab_is_new))
{
- PushItemFlag(ImGuiItemFlags_NoNav | ImGuiItemFlags_NoNavDefaultFocus, true);
- ItemAdd(ImRect(), id);
- PopItemFlag();
+ ItemAdd(ImRect(), id, NULL, ImGuiItemFlags_NoNav);
+ if (is_tab_button)
+ return false;
return tab_contents_visible;
}
@@ -6999,15 +8171,19 @@ bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open,
const ImVec2 backup_main_cursor_pos = window->DC.CursorPos;
// Layout
+ const bool is_central_section = (tab->Flags & ImGuiTabItemFlags_SectionMask_) == 0;
size.x = tab->Width;
- window->DC.CursorPos = tab_bar->BarRect.Min + ImVec2(IM_FLOOR(tab->Offset - tab_bar->ScrollingAnim), 0.0f);
+ if (is_central_section)
+ window->DC.CursorPos = tab_bar->BarRect.Min + ImVec2(IM_FLOOR(tab->Offset - tab_bar->ScrollingAnim), 0.0f);
+ else
+ window->DC.CursorPos = tab_bar->BarRect.Min + ImVec2(tab->Offset, 0.0f);
ImVec2 pos = window->DC.CursorPos;
ImRect bb(pos, pos + size);
// We don't have CPU clipping primitives to clip the CloseButton (until it becomes a texture), so need to add an extra draw call (temporary in the case of vertical animation)
- bool want_clip_rect = (bb.Min.x < tab_bar->BarRect.Min.x) || (bb.Max.x > tab_bar->BarRect.Max.x);
+ const bool want_clip_rect = is_central_section && (bb.Min.x < tab_bar->ScrollingRectMinX || bb.Max.x > tab_bar->ScrollingRectMaxX);
if (want_clip_rect)
- PushClipRect(ImVec2(ImMax(bb.Min.x, tab_bar->BarRect.Min.x), bb.Min.y - 1), ImVec2(tab_bar->BarRect.Max.x, bb.Max.y), true);
+ PushClipRect(ImVec2(ImMax(bb.Min.x, tab_bar->ScrollingRectMinX), bb.Min.y - 1), ImVec2(tab_bar->ScrollingRectMaxX, bb.Max.y), true);
ImVec2 backup_cursor_max_pos = window->DC.CursorMaxPos;
ItemSize(bb.GetSize(), style.FramePadding.y);
@@ -7022,17 +8198,16 @@ bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open,
}
// Click to Select a tab
- ImGuiButtonFlags button_flags = (ImGuiButtonFlags_PressedOnClick | ImGuiButtonFlags_AllowItemOverlap);
+ ImGuiButtonFlags button_flags = ((is_tab_button ? ImGuiButtonFlags_PressedOnClickRelease : ImGuiButtonFlags_PressedOnClick) | ImGuiButtonFlags_AllowItemOverlap);
if (g.DragDropActive)
button_flags |= ImGuiButtonFlags_PressedOnDragDropHold;
bool hovered, held;
bool pressed = ButtonBehavior(bb, id, &hovered, &held, button_flags);
- if (pressed)
+ if (pressed && !is_tab_button)
tab_bar->NextSelectedTabId = id;
- hovered |= (g.HoveredId == id);
// Allow the close button to overlap unless we are dragging (in which case we don't want any overlapping tabs to be hovered)
- if (!held)
+ if (g.ActiveId != id)
SetItemAllowOverlap();
// Drag and drop: re-order tabs
@@ -7043,19 +8218,17 @@ bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open,
// While moving a tab it will jump on the other side of the mouse, so we also test for MouseDelta.x
if (g.IO.MouseDelta.x < 0.0f && g.IO.MousePos.x < bb.Min.x)
{
- if (tab_bar->Flags & ImGuiTabBarFlags_Reorderable)
- TabBarQueueChangeTabOrder(tab_bar, tab, -1);
+ TabBarQueueReorderFromMousePos(tab_bar, tab, g.IO.MousePos);
}
else if (g.IO.MouseDelta.x > 0.0f && g.IO.MousePos.x > bb.Max.x)
{
- if (tab_bar->Flags & ImGuiTabBarFlags_Reorderable)
- TabBarQueueChangeTabOrder(tab_bar, tab, +1);
+ TabBarQueueReorderFromMousePos(tab_bar, tab, g.IO.MousePos);
}
}
}
#if 0
- if (hovered && g.HoveredIdNotActiveTimer > 0.50f && bb.GetWidth() < tab->ContentWidth)
+ if (hovered && g.HoveredIdNotActiveTimer > TOOLTIP_DELAY && bb.GetWidth() < tab->ContentWidth)
{
// Enlarge tab display when hovering
bb.Max.x = bb.Min.x + IM_FLOOR(ImLerp(bb.GetWidth(), tab->ContentWidth, ImSaturate((g.HoveredIdNotActiveTimer - 0.40f) * 6.0f)));
@@ -7073,14 +8246,17 @@ bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open,
// Select with right mouse button. This is so the common idiom for context menu automatically highlight the current widget.
const bool hovered_unblocked = IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup);
if (hovered_unblocked && (IsMouseClicked(1) || IsMouseReleased(1)))
- tab_bar->NextSelectedTabId = id;
+ if (!is_tab_button)
+ tab_bar->NextSelectedTabId = id;
if (tab_bar->Flags & ImGuiTabBarFlags_NoCloseWithMiddleMouseButton)
flags |= ImGuiTabItemFlags_NoCloseWithMiddleMouseButton;
// Render tab label, process close button
- const ImGuiID close_button_id = p_open ? window->GetID((void*)((intptr_t)id + 1)) : 0;
- bool just_closed = TabItemLabelAndCloseButton(display_draw_list, bb, flags, tab_bar->FramePadding, label, id, close_button_id);
+ const ImGuiID close_button_id = p_open ? GetIDWithSeed("#CLOSE", NULL, id) : 0;
+ bool just_closed;
+ bool text_clipped;
+ TabItemLabelAndCloseButton(display_draw_list, bb, tab_just_unsaved ? (flags & ~ImGuiTabItemFlags_UnsavedDocument) : flags, tab_bar->FramePadding, label, id, close_button_id, tab_contents_visible, &just_closed, &text_clipped);
if (just_closed && p_open != NULL)
{
*p_open = false;
@@ -7092,17 +8268,25 @@ bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open,
PopClipRect();
window->DC.CursorPos = backup_main_cursor_pos;
- // Tooltip (FIXME: Won't work over the close button because ItemOverlap systems messes up with HoveredIdTimer)
- // We test IsItemHovered() to discard e.g. when another item is active or drag and drop over the tab bar (which g.HoveredId ignores)
- if (g.HoveredId == id && !held && g.HoveredIdNotActiveTimer > 0.50f && IsItemHovered())
- if (!(tab_bar->Flags & ImGuiTabBarFlags_NoTooltip))
- SetTooltip("%.*s", (int)(FindRenderedTextEnd(label) - label), label);
-
+ // Tooltip
+ // (Won't work over the close button because ItemOverlap systems messes up with HoveredIdTimer-> seems ok)
+ // (We test IsItemHovered() to discard e.g. when another item is active or drag and drop over the tab bar, which g.HoveredId ignores)
+ // FIXME: This is a mess.
+ // FIXME: We may want disabled tab to still display the tooltip?
+ if (text_clipped && g.HoveredId == id && !held)
+ if (!(tab_bar->Flags & ImGuiTabBarFlags_NoTooltip) && !(tab->Flags & ImGuiTabItemFlags_NoTooltip))
+ if (IsItemHovered(ImGuiHoveredFlags_DelayNormal))
+ SetTooltip("%.*s", (int)(FindRenderedTextEnd(label) - label), label);
+
+ IM_ASSERT(!is_tab_button || !(tab_bar->SelectedTabId == tab->ID && is_tab_button)); // TabItemButton should not be selected
+ if (is_tab_button)
+ return pressed;
return tab_contents_visible;
}
// [Public] This is call is 100% optional but it allows to remove some one-frame glitches when a tab has been unexpectedly removed.
-// To use it to need to call the function SetTabItemClosed() after BeginTabBar() and before any call to BeginTabItem()
+// To use it to need to call the function SetTabItemClosed() between BeginTabBar() and EndTabBar().
+// Tabs closed by the close button will automatically be flagged to avoid this issue.
void ImGui::SetTabItemClosed(const char* label)
{
ImGuiContext& g = *GImGui;
@@ -7110,18 +8294,18 @@ void ImGui::SetTabItemClosed(const char* label)
if (is_within_manual_tab_bar)
{
ImGuiTabBar* tab_bar = g.CurrentTabBar;
- IM_ASSERT(tab_bar->WantLayout); // Needs to be called AFTER BeginTabBar() and BEFORE the first call to BeginTabItem()
ImGuiID tab_id = TabBarCalcTabID(tab_bar, label);
- TabBarRemoveTab(tab_bar, tab_id);
+ if (ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_id))
+ tab->WantClose = true; // Will be processed by next call to TabBarLayout()
}
}
-ImVec2 ImGui::TabItemCalcSize(const char* label, bool has_close_button)
+ImVec2 ImGui::TabItemCalcSize(const char* label, bool has_close_button_or_unsaved_marker)
{
ImGuiContext& g = *GImGui;
ImVec2 label_size = CalcTextSize(label, NULL, true);
ImVec2 size = ImVec2(label_size.x + g.Style.FramePadding.x, label_size.y + g.Style.FramePadding.y * 2.0f);
- if (has_close_button)
+ if (has_close_button_or_unsaved_marker)
size.x += g.Style.FramePadding.x + (g.Style.ItemInnerSpacing.x + g.FontSize); // We use Y intentionally to fit the close button circle.
else
size.x += g.Style.FramePadding.x + 1.0f;
@@ -7135,7 +8319,7 @@ void ImGui::TabItemBackground(ImDrawList* draw_list, const ImRect& bb, ImGuiTabI
const float width = bb.GetWidth();
IM_UNUSED(flags);
IM_ASSERT(width > 0.0f);
- const float rounding = ImMax(0.0f, ImMin(g.Style.TabRounding, width * 0.5f - 1.0f));
+ const float rounding = ImMax(0.0f, ImMin((flags & ImGuiTabItemFlags_Button) ? g.Style.FrameRounding : g.Style.TabRounding, width * 0.5f - 1.0f));
const float y1 = bb.Min.y + 1.0f;
const float y2 = bb.Max.y - 1.0f;
draw_list->PathLineTo(ImVec2(bb.Min.x, y2));
@@ -7149,31 +8333,48 @@ void ImGui::TabItemBackground(ImDrawList* draw_list, const ImRect& bb, ImGuiTabI
draw_list->PathArcToFast(ImVec2(bb.Min.x + rounding + 0.5f, y1 + rounding + 0.5f), rounding, 6, 9);
draw_list->PathArcToFast(ImVec2(bb.Max.x - rounding - 0.5f, y1 + rounding + 0.5f), rounding, 9, 12);
draw_list->PathLineTo(ImVec2(bb.Max.x - 0.5f, y2));
- draw_list->PathStroke(GetColorU32(ImGuiCol_Border), false, g.Style.TabBorderSize);
+ draw_list->PathStroke(GetColorU32(ImGuiCol_Border), 0, g.Style.TabBorderSize);
}
}
// Render text label (with custom clipping) + Unsaved Document marker + Close Button logic
// We tend to lock style.FramePadding for a given tab-bar, hence the 'frame_padding' parameter.
-bool ImGui::TabItemLabelAndCloseButton(ImDrawList* draw_list, const ImRect& bb, ImGuiTabItemFlags flags, ImVec2 frame_padding, const char* label, ImGuiID tab_id, ImGuiID close_button_id)
+void ImGui::TabItemLabelAndCloseButton(ImDrawList* draw_list, const ImRect& bb, ImGuiTabItemFlags flags, ImVec2 frame_padding, const char* label, ImGuiID tab_id, ImGuiID close_button_id, bool is_contents_visible, bool* out_just_closed, bool* out_text_clipped)
{
ImGuiContext& g = *GImGui;
ImVec2 label_size = CalcTextSize(label, NULL, true);
+
+ if (out_just_closed)
+ *out_just_closed = false;
+ if (out_text_clipped)
+ *out_text_clipped = false;
+
if (bb.GetWidth() <= 1.0f)
- return false;
+ return;
+
+ // In Style V2 we'll have full override of all colors per state (e.g. focused, selected)
+ // But right now if you want to alter text color of tabs this is what you need to do.
+#if 0
+ const float backup_alpha = g.Style.Alpha;
+ if (!is_contents_visible)
+ g.Style.Alpha *= 0.7f;
+#endif
// Render text label (with clipping + alpha gradient) + unsaved marker
- const char* TAB_UNSAVED_MARKER = "*";
ImRect text_pixel_clip_bb(bb.Min.x + frame_padding.x, bb.Min.y + frame_padding.y, bb.Max.x - frame_padding.x, bb.Max.y);
- if (flags & ImGuiTabItemFlags_UnsavedDocument)
+ ImRect text_ellipsis_clip_bb = text_pixel_clip_bb;
+
+ // Return clipped state ignoring the close button
+ if (out_text_clipped)
{
- text_pixel_clip_bb.Max.x -= CalcTextSize(TAB_UNSAVED_MARKER, NULL, false).x;
- ImVec2 unsaved_marker_pos(ImMin(bb.Min.x + frame_padding.x + label_size.x + 2, text_pixel_clip_bb.Max.x), bb.Min.y + frame_padding.y + IM_FLOOR(-g.FontSize * 0.25f));
- RenderTextClippedEx(draw_list, unsaved_marker_pos, bb.Max - frame_padding, TAB_UNSAVED_MARKER, NULL, NULL);
+ *out_text_clipped = (text_ellipsis_clip_bb.Min.x + label_size.x) > text_pixel_clip_bb.Max.x;
+ //draw_list->AddCircle(text_ellipsis_clip_bb.Min, 3.0f, *out_text_clipped ? IM_COL32(255, 0, 0, 255) : IM_COL32(0, 255, 0, 255));
}
- ImRect text_ellipsis_clip_bb = text_pixel_clip_bb;
- // Close Button
+ const float button_sz = g.FontSize;
+ const ImVec2 button_pos(ImMax(bb.Min.x, bb.Max.x - frame_padding.x * 2.0f - button_sz), bb.Min.y);
+
+ // Close Button & Unsaved Marker
// We are relying on a subtle and confusing distinction between 'hovered' and 'g.HoveredId' which happens because we are using ImGuiButtonFlags_AllowOverlapMode + SetItemAllowOverlap()
// 'hovered' will be true when hovering the Tab but NOT when hovering the close button
// 'g.HoveredId==id' will be true when hovering the Tab including when hovering the close button
@@ -7181,449 +8382,50 @@ bool ImGui::TabItemLabelAndCloseButton(ImDrawList* draw_list, const ImRect& bb,
bool close_button_pressed = false;
bool close_button_visible = false;
if (close_button_id != 0)
- if (g.HoveredId == tab_id || g.HoveredId == close_button_id || g.ActiveId == close_button_id)
- close_button_visible = true;
+ if (is_contents_visible || bb.GetWidth() >= ImMax(button_sz, g.Style.TabMinWidthForCloseButton))
+ if (g.HoveredId == tab_id || g.HoveredId == close_button_id || g.ActiveId == tab_id || g.ActiveId == close_button_id)
+ close_button_visible = true;
+ bool unsaved_marker_visible = (flags & ImGuiTabItemFlags_UnsavedDocument) != 0 && (button_pos.x + button_sz <= bb.Max.x);
+
if (close_button_visible)
{
- ImGuiItemHoveredDataBackup last_item_backup;
- const float close_button_sz = g.FontSize;
+ ImGuiLastItemData last_item_backup = g.LastItemData;
PushStyleVar(ImGuiStyleVar_FramePadding, frame_padding);
- if (CloseButton(close_button_id, ImVec2(bb.Max.x - frame_padding.x * 2.0f - close_button_sz, bb.Min.y)))
+ if (CloseButton(close_button_id, button_pos))
close_button_pressed = true;
PopStyleVar();
- last_item_backup.Restore();
+ g.LastItemData = last_item_backup;
// Close with middle mouse button
if (!(flags & ImGuiTabItemFlags_NoCloseWithMiddleMouseButton) && IsMouseClicked(2))
close_button_pressed = true;
-
- text_pixel_clip_bb.Max.x -= close_button_sz;
- }
-
- float ellipsis_max_x = close_button_visible ? text_pixel_clip_bb.Max.x : bb.Max.x - 1.0f;
- RenderTextEllipsis(draw_list, text_ellipsis_clip_bb.Min, text_ellipsis_clip_bb.Max, text_pixel_clip_bb.Max.x, ellipsis_max_x, label, NULL, &label_size);
-
- return close_button_pressed;
-}
-
-
-//-------------------------------------------------------------------------
-// [SECTION] Widgets: Columns, BeginColumns, EndColumns, etc.
-// In the current version, Columns are very weak. Needs to be replaced with a more full-featured system.
-//-------------------------------------------------------------------------
-// - GetColumnIndex()
-// - GetColumnCount()
-// - GetColumnOffset()
-// - GetColumnWidth()
-// - SetColumnOffset()
-// - SetColumnWidth()
-// - PushColumnClipRect() [Internal]
-// - PushColumnsBackground() [Internal]
-// - PopColumnsBackground() [Internal]
-// - FindOrCreateColumns() [Internal]
-// - GetColumnsID() [Internal]
-// - BeginColumns()
-// - NextColumn()
-// - EndColumns()
-// - Columns()
-//-------------------------------------------------------------------------
-
-int ImGui::GetColumnIndex()
-{
- ImGuiWindow* window = GetCurrentWindowRead();
- return window->DC.CurrentColumns ? window->DC.CurrentColumns->Current : 0;
-}
-
-int ImGui::GetColumnsCount()
-{
- ImGuiWindow* window = GetCurrentWindowRead();
- return window->DC.CurrentColumns ? window->DC.CurrentColumns->Count : 1;
-}
-
-float ImGui::GetColumnOffsetFromNorm(const ImGuiColumns* columns, float offset_norm)
-{
- return offset_norm * (columns->OffMaxX - columns->OffMinX);
-}
-
-float ImGui::GetColumnNormFromOffset(const ImGuiColumns* columns, float offset)
-{
- return offset / (columns->OffMaxX - columns->OffMinX);
-}
-
-static const float COLUMNS_HIT_RECT_HALF_WIDTH = 4.0f;
-
-static float GetDraggedColumnOffset(ImGuiColumns* columns, int column_index)
-{
- // Active (dragged) column always follow mouse. The reason we need this is that dragging a column to the right edge of an auto-resizing
- // window creates a feedback loop because we store normalized positions. So while dragging we enforce absolute positioning.
- ImGuiContext& g = *GImGui;
- ImGuiWindow* window = g.CurrentWindow;
- IM_ASSERT(column_index > 0); // We are not supposed to drag column 0.
- IM_ASSERT(g.ActiveId == columns->ID + ImGuiID(column_index));
-
- float x = g.IO.MousePos.x - g.ActiveIdClickOffset.x + COLUMNS_HIT_RECT_HALF_WIDTH - window->Pos.x;
- x = ImMax(x, ImGui::GetColumnOffset(column_index - 1) + g.Style.ColumnsMinSpacing);
- if ((columns->Flags & ImGuiColumnsFlags_NoPreserveWidths))
- x = ImMin(x, ImGui::GetColumnOffset(column_index + 1) - g.Style.ColumnsMinSpacing);
-
- return x;
-}
-
-float ImGui::GetColumnOffset(int column_index)
-{
- ImGuiWindow* window = GetCurrentWindowRead();
- ImGuiColumns* columns = window->DC.CurrentColumns;
- if (columns == NULL)
- return 0.0f;
-
- if (column_index < 0)
- column_index = columns->Current;
- IM_ASSERT(column_index < columns->Columns.Size);
-
- const float t = columns->Columns[column_index].OffsetNorm;
- const float x_offset = ImLerp(columns->OffMinX, columns->OffMaxX, t);
- return x_offset;
-}
-
-static float GetColumnWidthEx(ImGuiColumns* columns, int column_index, bool before_resize = false)
-{
- if (column_index < 0)
- column_index = columns->Current;
-
- float offset_norm;
- if (before_resize)
- offset_norm = columns->Columns[column_index + 1].OffsetNormBeforeResize - columns->Columns[column_index].OffsetNormBeforeResize;
- else
- offset_norm = columns->Columns[column_index + 1].OffsetNorm - columns->Columns[column_index].OffsetNorm;
- return ImGui::GetColumnOffsetFromNorm(columns, offset_norm);
-}
-
-float ImGui::GetColumnWidth(int column_index)
-{
- ImGuiContext& g = *GImGui;
- ImGuiWindow* window = g.CurrentWindow;
- ImGuiColumns* columns = window->DC.CurrentColumns;
- if (columns == NULL)
- return GetContentRegionAvail().x;
-
- if (column_index < 0)
- column_index = columns->Current;
- return GetColumnOffsetFromNorm(columns, columns->Columns[column_index + 1].OffsetNorm - columns->Columns[column_index].OffsetNorm);
-}
-
-void ImGui::SetColumnOffset(int column_index, float offset)
-{
- ImGuiContext& g = *GImGui;
- ImGuiWindow* window = g.CurrentWindow;
- ImGuiColumns* columns = window->DC.CurrentColumns;
- IM_ASSERT(columns != NULL);
-
- if (column_index < 0)
- column_index = columns->Current;
- IM_ASSERT(column_index < columns->Columns.Size);
-
- const bool preserve_width = !(columns->Flags & ImGuiColumnsFlags_NoPreserveWidths) && (column_index < columns->Count-1);
- const float width = preserve_width ? GetColumnWidthEx(columns, column_index, columns->IsBeingResized) : 0.0f;
-
- if (!(columns->Flags & ImGuiColumnsFlags_NoForceWithinWindow))
- offset = ImMin(offset, columns->OffMaxX - g.Style.ColumnsMinSpacing * (columns->Count - column_index));
- columns->Columns[column_index].OffsetNorm = GetColumnNormFromOffset(columns, offset - columns->OffMinX);
-
- if (preserve_width)
- SetColumnOffset(column_index + 1, offset + ImMax(g.Style.ColumnsMinSpacing, width));
-}
-
-void ImGui::SetColumnWidth(int column_index, float width)
-{
- ImGuiWindow* window = GetCurrentWindowRead();
- ImGuiColumns* columns = window->DC.CurrentColumns;
- IM_ASSERT(columns != NULL);
-
- if (column_index < 0)
- column_index = columns->Current;
- SetColumnOffset(column_index + 1, GetColumnOffset(column_index) + width);
-}
-
-void ImGui::PushColumnClipRect(int column_index)
-{
- ImGuiWindow* window = GetCurrentWindowRead();
- ImGuiColumns* columns = window->DC.CurrentColumns;
- if (column_index < 0)
- column_index = columns->Current;
-
- ImGuiColumnData* column = &columns->Columns[column_index];
- PushClipRect(column->ClipRect.Min, column->ClipRect.Max, false);
-}
-
-// Get into the columns background draw command (which is generally the same draw command as before we called BeginColumns)
-void ImGui::PushColumnsBackground()
-{
- ImGuiWindow* window = GetCurrentWindowRead();
- ImGuiColumns* columns = window->DC.CurrentColumns;
- if (columns->Count == 1)
- return;
- window->DrawList->ChannelsSetCurrent(0);
- int cmd_size = window->DrawList->CmdBuffer.Size;
- PushClipRect(columns->HostClipRect.Min, columns->HostClipRect.Max, false);
- IM_UNUSED(cmd_size);
- IM_ASSERT(cmd_size == window->DrawList->CmdBuffer.Size); // Being in channel 0 this should not have created an ImDrawCmd
-}
-
-void ImGui::PopColumnsBackground()
-{
- ImGuiWindow* window = GetCurrentWindowRead();
- ImGuiColumns* columns = window->DC.CurrentColumns;
- if (columns->Count == 1)
- return;
- window->DrawList->ChannelsSetCurrent(columns->Current + 1);
- PopClipRect();
-}
-
-ImGuiColumns* ImGui::FindOrCreateColumns(ImGuiWindow* window, ImGuiID id)
-{
- // We have few columns per window so for now we don't need bother much with turning this into a faster lookup.
- for (int n = 0; n < window->ColumnsStorage.Size; n++)
- if (window->ColumnsStorage[n].ID == id)
- return &window->ColumnsStorage[n];
-
- window->ColumnsStorage.push_back(ImGuiColumns());
- ImGuiColumns* columns = &window->ColumnsStorage.back();
- columns->ID = id;
- return columns;
-}
-
-ImGuiID ImGui::GetColumnsID(const char* str_id, int columns_count)
-{
- ImGuiWindow* window = GetCurrentWindow();
-
- // Differentiate column ID with an arbitrary prefix for cases where users name their columns set the same as another widget.
- // In addition, when an identifier isn't explicitly provided we include the number of columns in the hash to make it uniquer.
- PushID(0x11223347 + (str_id ? 0 : columns_count));
- ImGuiID id = window->GetID(str_id ? str_id : "columns");
- PopID();
-
- return id;
-}
-
-void ImGui::BeginColumns(const char* str_id, int columns_count, ImGuiColumnsFlags flags)
-{
- ImGuiContext& g = *GImGui;
- ImGuiWindow* window = GetCurrentWindow();
-
- IM_ASSERT(columns_count >= 1);
- IM_ASSERT(window->DC.CurrentColumns == NULL); // Nested columns are currently not supported
-
- // Acquire storage for the columns set
- ImGuiID id = GetColumnsID(str_id, columns_count);
- ImGuiColumns* columns = FindOrCreateColumns(window, id);
- IM_ASSERT(columns->ID == id);
- columns->Current = 0;
- columns->Count = columns_count;
- columns->Flags = flags;
- window->DC.CurrentColumns = columns;
-
- columns->HostCursorPosY = window->DC.CursorPos.y;
- columns->HostCursorMaxPosX = window->DC.CursorMaxPos.x;
- columns->HostClipRect = window->ClipRect;
- columns->HostWorkRect = window->WorkRect;
-
- // Set state for first column
- // We aim so that the right-most column will have the same clipping width as other after being clipped by parent ClipRect
- const float column_padding = g.Style.ItemSpacing.x;
- const float half_clip_extend_x = ImFloor(ImMax(window->WindowPadding.x * 0.5f, window->WindowBorderSize));
- const float max_1 = window->WorkRect.Max.x + column_padding - ImMax(column_padding - window->WindowPadding.x, 0.0f);
- const float max_2 = window->WorkRect.Max.x + half_clip_extend_x;
- columns->OffMinX = window->DC.Indent.x - column_padding + ImMax(column_padding - window->WindowPadding.x, 0.0f);
- columns->OffMaxX = ImMax(ImMin(max_1, max_2) - window->Pos.x, columns->OffMinX + 1.0f);
- columns->LineMinY = columns->LineMaxY = window->DC.CursorPos.y;
-
- // Clear data if columns count changed
- if (columns->Columns.Size != 0 && columns->Columns.Size != columns_count + 1)
- columns->Columns.resize(0);
-
- // Initialize default widths
- columns->IsFirstFrame = (columns->Columns.Size == 0);
- if (columns->Columns.Size == 0)
- {
- columns->Columns.reserve(columns_count + 1);
- for (int n = 0; n < columns_count + 1; n++)
- {
- ImGuiColumnData column;
- column.OffsetNorm = n / (float)columns_count;
- columns->Columns.push_back(column);
- }
}
-
- for (int n = 0; n < columns_count; n++)
- {
- // Compute clipping rectangle
- ImGuiColumnData* column = &columns->Columns[n];
- float clip_x1 = IM_ROUND(window->Pos.x + GetColumnOffset(n));
- float clip_x2 = IM_ROUND(window->Pos.x + GetColumnOffset(n + 1) - 1.0f);
- column->ClipRect = ImRect(clip_x1, -FLT_MAX, clip_x2, +FLT_MAX);
- column->ClipRect.ClipWith(window->ClipRect);
- }
-
- if (columns->Count > 1)
+ else if (unsaved_marker_visible)
{
- window->DrawList->ChannelsSplit(1 + columns->Count);
- window->DrawList->ChannelsSetCurrent(1);
- PushColumnClipRect(0);
+ const ImRect bullet_bb(button_pos, button_pos + ImVec2(button_sz, button_sz) + g.Style.FramePadding * 2.0f);
+ RenderBullet(draw_list, bullet_bb.GetCenter(), GetColorU32(ImGuiCol_Text));
}
- // We don't generally store Indent.x inside ColumnsOffset because it may be manipulated by the user.
- float offset_0 = GetColumnOffset(columns->Current);
- float offset_1 = GetColumnOffset(columns->Current + 1);
- float width = offset_1 - offset_0;
- PushItemWidth(width * 0.65f);
- window->DC.ColumnsOffset.x = ImMax(column_padding - window->WindowPadding.x, 0.0f);
- window->DC.CursorPos.x = IM_FLOOR(window->Pos.x + window->DC.Indent.x + window->DC.ColumnsOffset.x);
- window->WorkRect.Max.x = window->Pos.x + offset_1 - column_padding;
-}
-
-void ImGui::NextColumn()
-{
- ImGuiWindow* window = GetCurrentWindow();
- if (window->SkipItems || window->DC.CurrentColumns == NULL)
- return;
-
- ImGuiContext& g = *GImGui;
- ImGuiColumns* columns = window->DC.CurrentColumns;
-
- if (columns->Count == 1)
- {
- window->DC.CursorPos.x = IM_FLOOR(window->Pos.x + window->DC.Indent.x + window->DC.ColumnsOffset.x);
- IM_ASSERT(columns->Current == 0);
- return;
- }
- PopItemWidth();
- PopClipRect();
-
- const float column_padding = g.Style.ItemSpacing.x;
- columns->LineMaxY = ImMax(columns->LineMaxY, window->DC.CursorPos.y);
- if (++columns->Current < columns->Count)
- {
- // Columns 1+ ignore IndentX (by canceling it out)
- // FIXME-COLUMNS: Unnecessary, could be locked?
- window->DC.ColumnsOffset.x = GetColumnOffset(columns->Current) - window->DC.Indent.x + column_padding;
- window->DrawList->ChannelsSetCurrent(columns->Current + 1);
- }
- else
- {
- // New row/line
- // Column 0 honor IndentX
- window->DC.ColumnsOffset.x = ImMax(column_padding - window->WindowPadding.x, 0.0f);
- window->DrawList->ChannelsSetCurrent(1);
- columns->Current = 0;
- columns->LineMinY = columns->LineMaxY;
- }
- window->DC.CursorPos.x = IM_FLOOR(window->Pos.x + window->DC.Indent.x + window->DC.ColumnsOffset.x);
- window->DC.CursorPos.y = columns->LineMinY;
- window->DC.CurrLineSize = ImVec2(0.0f, 0.0f);
- window->DC.CurrLineTextBaseOffset = 0.0f;
-
- PushColumnClipRect(columns->Current); // FIXME-COLUMNS: Could it be an overwrite?
-
- // FIXME-COLUMNS: Share code with BeginColumns() - move code on columns setup.
- float offset_0 = GetColumnOffset(columns->Current);
- float offset_1 = GetColumnOffset(columns->Current + 1);
- float width = offset_1 - offset_0;
- PushItemWidth(width * 0.65f);
- window->WorkRect.Max.x = window->Pos.x + offset_1 - column_padding;
-}
-
-void ImGui::EndColumns()
-{
- ImGuiContext& g = *GImGui;
- ImGuiWindow* window = GetCurrentWindow();
- ImGuiColumns* columns = window->DC.CurrentColumns;
- IM_ASSERT(columns != NULL);
-
- PopItemWidth();
- if (columns->Count > 1)
+ // This is all rather complicated
+ // (the main idea is that because the close button only appears on hover, we don't want it to alter the ellipsis position)
+ // FIXME: if FramePadding is noticeably large, ellipsis_max_x will be wrong here (e.g. #3497), maybe for consistency that parameter of RenderTextEllipsis() shouldn't exist..
+ float ellipsis_max_x = close_button_visible ? text_pixel_clip_bb.Max.x : bb.Max.x - 1.0f;
+ if (close_button_visible || unsaved_marker_visible)
{
- PopClipRect();
- window->DrawList->ChannelsMerge();
+ text_pixel_clip_bb.Max.x -= close_button_visible ? (button_sz) : (button_sz * 0.80f);
+ text_ellipsis_clip_bb.Max.x -= unsaved_marker_visible ? (button_sz * 0.80f) : 0.0f;
+ ellipsis_max_x = text_pixel_clip_bb.Max.x;
}
+ RenderTextEllipsis(draw_list, text_ellipsis_clip_bb.Min, text_ellipsis_clip_bb.Max, text_pixel_clip_bb.Max.x, ellipsis_max_x, label, NULL, &label_size);
- const ImGuiColumnsFlags flags = columns->Flags;
- columns->LineMaxY = ImMax(columns->LineMaxY, window->DC.CursorPos.y);
- window->DC.CursorPos.y = columns->LineMaxY;
- if (!(flags & ImGuiColumnsFlags_GrowParentContentsSize))
- window->DC.CursorMaxPos.x = columns->HostCursorMaxPosX; // Restore cursor max pos, as columns don't grow parent
-
- // Draw columns borders and handle resize
- // The IsBeingResized flag ensure we preserve pre-resize columns width so back-and-forth are not lossy
- bool is_being_resized = false;
- if (!(flags & ImGuiColumnsFlags_NoBorder) && !window->SkipItems)
- {
- // We clip Y boundaries CPU side because very long triangles are mishandled by some GPU drivers.
- const float y1 = ImMax(columns->HostCursorPosY, window->ClipRect.Min.y);
- const float y2 = ImMin(window->DC.CursorPos.y, window->ClipRect.Max.y);
- int dragging_column = -1;
- for (int n = 1; n < columns->Count; n++)
- {
- ImGuiColumnData* column = &columns->Columns[n];
- float x = window->Pos.x + GetColumnOffset(n);
- const ImGuiID column_id = columns->ID + ImGuiID(n);
- const float column_hit_hw = COLUMNS_HIT_RECT_HALF_WIDTH;
- const ImRect column_hit_rect(ImVec2(x - column_hit_hw, y1), ImVec2(x + column_hit_hw, y2));
- KeepAliveID(column_id);
- if (IsClippedEx(column_hit_rect, column_id, false))
- continue;
-
- bool hovered = false, held = false;
- if (!(flags & ImGuiColumnsFlags_NoResize))
- {
- ButtonBehavior(column_hit_rect, column_id, &hovered, &held);
- if (hovered || held)
- g.MouseCursor = ImGuiMouseCursor_ResizeEW;
- if (held && !(column->Flags & ImGuiColumnsFlags_NoResize))
- dragging_column = n;
- }
-
- // Draw column
- const ImU32 col = GetColorU32(held ? ImGuiCol_SeparatorActive : hovered ? ImGuiCol_SeparatorHovered : ImGuiCol_Separator);
- const float xi = IM_FLOOR(x);
- window->DrawList->AddLine(ImVec2(xi, y1 + 1.0f), ImVec2(xi, y2), col);
- }
-
- // Apply dragging after drawing the column lines, so our rendered lines are in sync with how items were displayed during the frame.
- if (dragging_column != -1)
- {
- if (!columns->IsBeingResized)
- for (int n = 0; n < columns->Count + 1; n++)
- columns->Columns[n].OffsetNormBeforeResize = columns->Columns[n].OffsetNorm;
- columns->IsBeingResized = is_being_resized = true;
- float x = GetDraggedColumnOffset(columns, dragging_column);
- SetColumnOffset(dragging_column, x);
- }
- }
- columns->IsBeingResized = is_being_resized;
+#if 0
+ if (!is_contents_visible)
+ g.Style.Alpha = backup_alpha;
+#endif
- window->WorkRect = columns->HostWorkRect;
- window->DC.CurrentColumns = NULL;
- window->DC.ColumnsOffset.x = 0.0f;
- window->DC.CursorPos.x = IM_FLOOR(window->Pos.x + window->DC.Indent.x + window->DC.ColumnsOffset.x);
+ if (out_just_closed)
+ *out_just_closed = close_button_pressed;
}
-// [2018-03: This is currently the only public API, while we are working on making BeginColumns/EndColumns user-facing]
-void ImGui::Columns(int columns_count, const char* id, bool border)
-{
- ImGuiWindow* window = GetCurrentWindow();
- IM_ASSERT(columns_count >= 1);
-
- ImGuiColumnsFlags flags = (border ? 0 : ImGuiColumnsFlags_NoBorder);
- //flags |= ImGuiColumnsFlags_NoPreserveWidths; // NB: Legacy behavior
- ImGuiColumns* columns = window->DC.CurrentColumns;
- if (columns != NULL && columns->Count == columns_count && columns->Flags == flags)
- return;
- if (columns != NULL)
- EndColumns();
-
- if (columns_count != 1)
- BeginColumns(id, columns_count, flags);
-}
-
-//-------------------------------------------------------------------------
+#endif // #ifndef IMGUI_DISABLE