Merge 5487b0872ecee7bd4e4c3027552a3993332de46c into a1af3c1a59fe81f7a1cac10dcead0a383b99f245

This commit is contained in:
Entrpi 2026-11-05 11:45:44 +01:00 committed by GitHub
commit fc7b19c292
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 690 additions and 0 deletions

View File

@ -718,6 +718,9 @@ namespace ImGui
IMGUI_API bool SliderInt4(const char* label, int v[4], int v_min, int v_max, const char* format = "%d", ImGuiSliderFlags flags = 0);
IMGUI_API bool SliderScalar(const char* label, ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max, const char* format = NULL, ImGuiSliderFlags flags = 0);
IMGUI_API bool SliderScalarN(const char* label, ImGuiDataType data_type, void* p_data, int components, const void* p_min, const void* p_max, const char* format = NULL, ImGuiSliderFlags flags = 0);
IMGUI_API bool SliderScalarRange2(const char* label, ImGuiDataType data_type, void* p_v_min, void* p_v_max, const void* p_min, const void* p_max, const char* format = NULL, ImGuiSliderFlags flags = 0, const void* p_step = NULL);
IMGUI_API bool SliderFloatRange2(const char* label, float* v_current_min, float* v_current_max, float v_min = 0.0f, float v_max = 1.0f, const char* format = "%.3f", ImGuiSliderFlags flags = 0, float step = 0.0f);
IMGUI_API bool SliderIntRange2(const char* label, int* v_current_min, int* v_current_max, int v_min = 0, int v_max = 100, const char* format = "%d", ImGuiSliderFlags flags = 0, int step = 0);
IMGUI_API bool VSliderFloat(const char* label, const ImVec2& size, float* v, float v_min, float v_max, const char* format = "%.3f", ImGuiSliderFlags flags = 0);
IMGUI_API bool VSliderInt(const char* label, const ImVec2& size, int* v, int v_min, int v_max, const char* format = "%d", ImGuiSliderFlags flags = 0);
IMGUI_API bool VSliderScalar(const char* label, const ImVec2& size, ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max, const char* format = NULL, ImGuiSliderFlags flags = 0);
@ -1990,6 +1993,7 @@ enum ImGuiSliderFlags_
ImGuiSliderFlags_ClampZeroRange = 1 << 10, // Clamp even if min==max==0.0f. Otherwise due to legacy reason DragXXX functions don't clamp with those values. When your clamping limits are dynamic you almost always want to use it.
ImGuiSliderFlags_NoSpeedTweaks = 1 << 11, // Disable keyboard modifiers altering tweak speed. Useful if you want to alter tweak speed yourself based on your own logic.
ImGuiSliderFlags_ColorMarkers = 1 << 12, // DragScalarN(), SliderScalarN(): Draw R/G/B/A color markers on each component.
ImGuiSliderFlags_Range = 1 << 13, // SliderScalarN() with 2 components: Render as a range slider instead of two separate sliders. v[0] is min, v[1] is max.
ImGuiSliderFlags_AlwaysClamp = ImGuiSliderFlags_ClampOnInput | ImGuiSliderFlags_ClampZeroRange,
//ImGuiSliderFlags_LiveEditOnInput = 1 << 13, // Shortcut to enable LiveEdit for this field.
//ImGuiSliderFlags_NoLiveEditOnInput= 1 << 14, // Shortcut to disable LiveEdit for this field.

View File

@ -1085,6 +1085,17 @@ static void DemoWindowWidgetsBasic()
const char* elem_name = (elem >= 0 && elem < Element_COUNT) ? elems_names[elem] : "Unknown";
ImGui::SliderInt("slider enum", &elem, 0, Element_COUNT - 1, elem_name); // Use ImGuiSliderFlags_NoInput flag to disable Ctrl+Click here.
ImGui::SameLine(); HelpMarker("Using the format string parameter to display a name instead of the underlying integer.");
IMGUI_DEMO_MARKER("Widgets/Basic/SliderFloatRange2, SliderIntRange2");
static float fmin = 0.25f, fmax = 0.75f;
static int imin = 25, imax = 75;
ImGui::SliderFloatRange2("range float", &fmin, &fmax, 0.0f, 1.0f);
ImGui::SliderFloatRange2("range (size)", &fmin, &fmax, 0.0f, 1.0f, "%.2f...%.2f (%.2f)");
ImGui::SliderIntRange2("range int", &imin, &imax, 0, 100);
// Alternative: use SliderFloat2 with ImGuiSliderFlags_Range
static float range_arr[2] = { 0.25f, 0.75f };
ImGui::SliderFloat2("range (flag)", range_arr, 0.0f, 1.0f, "%.3f", ImGuiSliderFlags_Range);
ImGui::SameLine(); HelpMarker("Click and drag handles individually, or drag the bar between them to move both.\nCtrl+Click to input values. Use \"...\" or \" - \" as separator to set both.\n\nRange sliders can also be created with SliderFloat2(..., ImGuiSliderFlags_Range).");
}
ImGui::SeparatorText("Selectors/Pickers");

View File

@ -2618,6 +2618,7 @@ struct ImGuiContext
float SliderGrabClickOffset;
float SliderCurrentAccum; // Accumulated slider delta when using navigation controls.
bool SliderCurrentAccumDirty; // Has the accumulated slider delta changed since last time we tried to apply it?
ImS8 SliderRangeActiveHandle; // For SliderScalarRange2: which handle is active (0=min, 1=max, -1=bar)
bool DragCurrentAccumDirty;
float DragCurrentAccum; // Accumulator for dragging modification. Always high-precision, not rounded by end-user precision settings
float DragSpeedDefaultRatio; // If speed == 0.0f, uses (max-min) * DragSpeedDefaultRatio

View File

@ -3464,6 +3464,15 @@ bool ImGui::SliderScalarN(const char* label, ImGuiDataType data_type, void* v, i
if (window->SkipItems)
return false;
// Range slider mode: render 2 components as a single range slider
if (components == 2 && (flags & ImGuiSliderFlags_Range))
{
size_t type_size = GDataTypeInfo[data_type].Size;
void* v0 = v;
void* v1 = (void*)((char*)v + type_size);
return SliderScalarRange2(label, data_type, v0, v1, v_min, v_max, format, flags & ~ImGuiSliderFlags_Range);
}
ImGuiContext& g = *GImGui;
bool value_changed = false;
BeginGroup();
@ -3546,6 +3555,545 @@ bool ImGui::SliderInt4(const char* label, int v[4], int v_min, int v_max, const
return SliderScalarN(label, ImGuiDataType_S32, v, 4, &v_min, &v_max, format, flags);
}
// Visual range slider with two grab handles and colored fill between them
//-------------------------------------------------------------------------
// [SECTION] Widgets: SliderScalarRange2, SliderFloatRange2, SliderIntRange2
//-------------------------------------------------------------------------
// - SliderBehaviorRangeT<>() [Internal]
// - SliderBehaviorRange() [Internal]
// - SliderScalarRange2()
// - SliderFloatRange2()
// - SliderIntRange2()
//-------------------------------------------------------------------------
// Forward declaration for TempInput function used by SliderScalarRange2
static bool TempInputScalarRange2(const ImRect& bb, ImGuiID id, const char* label, ImGuiDataType data_type, void* p_data_min, void* p_data_max, const char* format, const char* format_max, const void* p_clamp_min, const void* p_clamp_max, int selected_handle);
// Helper to parse range format string "%.3f...%.3f" into separate min/max formats
// Returns pointer to start of max format (after dots), or NULL if no separator found
// Accepts any number of consecutive dots (2+) as separator
static const char* ParseRangeFormatSeparator(const char* format)
{
if (!format)
return NULL;
const char* p = format;
while (*p)
{
if (p[0] == '.' && p[1] == '.')
{
// Found at least "..", skip all consecutive dots
while (*p == '.')
p++;
return p; // Return start of max format
}
p++;
}
return NULL; // No separator found
}
template<typename TYPE, typename SIGNEDTYPE, typename FLOATTYPE>
static bool SliderBehaviorRangeT(const ImRect& bb, ImGuiID id, ImGuiDataType data_type, TYPE* v_min_val, TYPE* v_max_val, TYPE v_min, TYPE v_max, const char* format, const char* format_max, ImGuiSliderFlags flags, ImRect* out_grab_bb_min, ImRect* out_grab_bb_max, int* p_handle, TYPE step)
{
ImGuiContext& g = *GImGui;
const ImGuiStyle& style = g.Style;
const ImGuiAxis axis = (flags & ImGuiSliderFlags_Vertical) ? ImGuiAxis_Y : ImGuiAxis_X;
const bool is_logarithmic = (flags & ImGuiSliderFlags_Logarithmic) != 0;
const bool is_floating_point = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double);
const FLOATTYPE v_range_f = (FLOATTYPE)(v_min < v_max ? v_max - v_min : v_min - v_max);
// Calculate slider geometry
const float grab_padding = 2.0f;
const float slider_sz = (bb.Max[axis] - bb.Min[axis]) - grab_padding * 2.0f;
float grab_sz = style.GrabMinSize;
if (!is_floating_point && v_range_f >= 0)
grab_sz = ImMax(slider_sz / (float)((FLOATTYPE)(v_range_f) + 1), style.GrabMinSize);
grab_sz = ImMin(grab_sz, slider_sz * 0.4f); // Cap at 40% for range sliders
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;
float logarithmic_zero_epsilon = 0.0f;
float zero_deadzone_halfsize = 0.0f;
if (is_logarithmic)
{
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);
}
// Helper to get grab position from value
auto grab_pos_from_value = [&](TYPE v) {
float t = ImGui::ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, v, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
if (axis == ImGuiAxis_Y) t = 1.0f - t;
return ImLerp(slider_usable_pos_min, slider_usable_pos_max, t);
};
float grab_min_pos = grab_pos_from_value(*v_min_val);
float grab_max_pos = grab_pos_from_value(*v_max_val);
bool value_changed = false;
if (g.ActiveId == id)
{
bool set_new_value = false;
float clicked_t = 0.0f;
if (g.ActiveIdSource == ImGuiInputSource_Mouse)
{
if (!g.IO.MouseDown[0])
{
ImGui::ClearActiveID();
}
else
{
const float mouse_abs_pos = g.IO.MousePos[axis];
// On first click, determine which handle or bar based on proximity
if (g.ActiveIdIsJustActivated)
{
const float click_pos = g.IO.MouseClickedPos[0][axis];
float dist_min = ImAbs(click_pos - grab_min_pos);
float dist_max = ImAbs(click_pos - grab_max_pos);
float handle_threshold = grab_sz * 0.75f;
if (dist_min <= handle_threshold || dist_max <= handle_threshold)
{
// Click near a handle - if handles overlap, use undetermined state (-2)
if (ImAbs(grab_min_pos - grab_max_pos) < 1.0f)
*p_handle = -2; // Undetermined: will be resolved by drag direction
else
*p_handle = (dist_min <= dist_max) ? 0 : 1;
}
else if (click_pos > grab_min_pos && click_pos < grab_max_pos)
{
// Click on bar between handles - enable bar dragging
*p_handle = -1;
g.SliderGrabClickOffset = click_pos - (grab_min_pos + grab_max_pos) * 0.5f;
}
else
{
// Click outside range - pick nearest handle
*p_handle = (dist_min < dist_max) ? 0 : 1;
}
}
if (*p_handle == -1)
{
// Bar dragging: move both handles together
float bar_center = mouse_abs_pos - g.SliderGrabClickOffset;
float half_range = (grab_max_pos - grab_min_pos) * 0.5f;
float new_min_pos = bar_center - half_range;
float new_max_pos = bar_center + half_range;
// Clamp to slider bounds
if (new_min_pos < slider_usable_pos_min)
{
new_min_pos = slider_usable_pos_min;
new_max_pos = new_min_pos + (grab_max_pos - grab_min_pos);
}
if (new_max_pos > slider_usable_pos_max)
{
new_max_pos = slider_usable_pos_max;
new_min_pos = new_max_pos - (grab_max_pos - grab_min_pos);
}
// Convert positions to values
float t_min_new = (slider_usable_sz > 0.0f) ? ImSaturate((new_min_pos - slider_usable_pos_min) / slider_usable_sz) : 0.0f;
float t_max_new = (slider_usable_sz > 0.0f) ? ImSaturate((new_max_pos - slider_usable_pos_min) / slider_usable_sz) : 0.0f;
if (axis == ImGuiAxis_Y) { t_min_new = 1.0f - t_min_new; t_max_new = 1.0f - t_max_new; }
TYPE new_v_min = ImGui::ScaleValueFromRatioT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, t_min_new, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
TYPE new_v_max = ImGui::ScaleValueFromRatioT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, t_max_new, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
if (is_floating_point && !(flags & ImGuiSliderFlags_NoRoundToFormat))
{
new_v_min = ImGui::RoundScalarWithFormatT<TYPE>(format, data_type, new_v_min);
new_v_max = ImGui::RoundScalarWithFormatT<TYPE>(format_max, data_type, new_v_max);
}
if (*v_min_val != new_v_min || *v_max_val != new_v_max)
{
*v_min_val = new_v_min;
*v_max_val = new_v_max;
value_changed = true;
grab_min_pos = grab_pos_from_value(*v_min_val);
grab_max_pos = grab_pos_from_value(*v_max_val);
}
}
else if (*p_handle == -2)
{
// Undetermined state (handles overlapping): resolve based on drag direction
float delta = mouse_abs_pos - g.IO.MouseClickedPos[0][axis];
if (ImAbs(delta) > 1.0f)
{
// Resolved: positive delta = max handle, negative = min handle (for X axis)
*p_handle = (delta > 0.0f) ? 1 : 0;
if (axis == ImGuiAxis_Y)
*p_handle = 1 - *p_handle; // Invert for Y axis
}
// Don't set value until resolved
}
else
{
// Single handle dragging
if (slider_usable_sz > 0.0f)
clicked_t = ImSaturate((mouse_abs_pos - slider_usable_pos_min) / slider_usable_sz);
if (axis == ImGuiAxis_Y)
clicked_t = 1.0f - clicked_t;
set_new_value = true;
}
}
}
else if (g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad)
{
if (g.ActiveIdIsJustActivated)
{
g.SliderCurrentAccum = 0.0f;
g.SliderCurrentAccumDirty = false;
}
// Up/Down switches handles, Left/Right adjusts value
const ImGuiAxis switch_axis = (axis == ImGuiAxis_X) ? ImGuiAxis_Y : ImGuiAxis_X;
if (float switch_delta = ImGui::GetNavTweakPressedAmount(switch_axis))
*p_handle = (switch_delta > 0.0f) ? 1 : 0;
float input_delta = (axis == ImGuiAxis_X) ? ImGui::GetNavTweakPressedAmount(axis) : -ImGui::GetNavTweakPressedAmount(axis);
if (input_delta != 0.0f)
{
const bool tweak_slow = ImGui::IsKeyDown((g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakSlow : ImGuiKey_NavKeyboardTweakSlow);
const bool tweak_fast = ImGui::IsKeyDown((g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakFast : ImGuiKey_NavKeyboardTweakFast);
const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, 3) : 0;
if (decimal_precision > 0)
input_delta /= tweak_slow ? 1000.0f : 100.0f;
else if ((v_range_f >= -100.0f && v_range_f <= 100.0f && v_range_f != 0.0f) || tweak_slow)
input_delta = ((input_delta < 0.0f) ? -1.0f : +1.0f) / (float)v_range_f;
else
input_delta /= 100.0f;
if (tweak_fast)
input_delta *= 10.0f;
g.SliderCurrentAccum += input_delta;
g.SliderCurrentAccumDirty = true;
}
if (g.NavActivatePressedId == id && !g.ActiveIdIsJustActivated)
ImGui::ClearActiveID();
else if (g.SliderCurrentAccumDirty)
{
TYPE* active_val = (*p_handle == 0) ? v_min_val : v_max_val;
clicked_t = ImGui::ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, *active_val, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
float delta = g.SliderCurrentAccum;
if ((clicked_t >= 1.0f && delta > 0.0f) || (clicked_t <= 0.0f && delta < 0.0f))
{
g.SliderCurrentAccum = 0.0f;
}
else
{
set_new_value = true;
float old_clicked_t = clicked_t;
clicked_t = ImSaturate(clicked_t + delta);
TYPE v_new = ImGui::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 = ImGui::RoundScalarWithFormatT<TYPE>((*p_handle == 0) ? format : format_max, data_type, v_new);
float new_clicked_t = ImGui::ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, v_new, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
g.SliderCurrentAccum -= (delta > 0) ? ImMin(new_clicked_t - old_clicked_t, delta) : ImMax(new_clicked_t - old_clicked_t, delta);
}
g.SliderCurrentAccumDirty = false;
}
}
if (set_new_value && ((g.LastItemData.ItemFlags & ImGuiItemFlags_ReadOnly) || (flags & ImGuiSliderFlags_ReadOnly)))
set_new_value = false;
if (set_new_value)
{
TYPE v_new = ImGui::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 = ImGui::RoundScalarWithFormatT<TYPE>((*p_handle == 0) ? format : format_max, data_type, v_new);
// Apply step snapping if step > 0
if (step > (TYPE)0)
{
if (is_floating_point)
v_new = (TYPE)(ImFloor(((FLOATTYPE)(v_new - v_min) / (FLOATTYPE)step) + (FLOATTYPE)0.5) * (FLOATTYPE)step) + v_min;
else
v_new = (TYPE)(((v_new - v_min + step / (TYPE)2) / step) * step) + v_min;
}
// Clamp each handle against the other
if (*p_handle == 0)
v_new = ImMin(v_new, *v_max_val);
else
v_new = ImMax(v_new, *v_min_val);
TYPE* target = (*p_handle == 0) ? v_min_val : v_max_val;
if (*target != v_new)
{
*target = v_new;
value_changed = true;
}
}
}
// Output grab bounding boxes
grab_min_pos = grab_pos_from_value(*v_min_val);
grab_max_pos = grab_pos_from_value(*v_max_val);
if (slider_sz < 1.0f)
{
*out_grab_bb_min = *out_grab_bb_max = ImRect(bb.Min, bb.Min);
}
else if (axis == ImGuiAxis_X)
{
*out_grab_bb_min = ImRect(grab_min_pos - grab_sz * 0.5f, bb.Min.y + grab_padding, grab_min_pos + grab_sz * 0.5f, bb.Max.y - grab_padding);
*out_grab_bb_max = ImRect(grab_max_pos - grab_sz * 0.5f, bb.Min.y + grab_padding, grab_max_pos + grab_sz * 0.5f, bb.Max.y - grab_padding);
}
else
{
*out_grab_bb_min = ImRect(bb.Min.x + grab_padding, grab_min_pos - grab_sz * 0.5f, bb.Max.x - grab_padding, grab_min_pos + grab_sz * 0.5f);
*out_grab_bb_max = ImRect(bb.Min.x + grab_padding, grab_max_pos - grab_sz * 0.5f, bb.Max.x - grab_padding, grab_max_pos + grab_sz * 0.5f);
}
return value_changed;
}
static bool SliderBehaviorRange(const ImRect& bb, ImGuiID id, ImGuiDataType data_type, void* p_v_min, void* p_v_max, const void* p_min, const void* p_max, const char* format, const char* format_max, ImGuiSliderFlags flags, ImRect* out_grab_bb_min, ImRect* out_grab_bb_max, int* p_handle, const void* p_step)
{
switch (data_type)
{
case ImGuiDataType_S32:
return SliderBehaviorRangeT<ImS32, ImS32, float>(bb, id, data_type, (ImS32*)p_v_min, (ImS32*)p_v_max, *(const ImS32*)p_min, *(const ImS32*)p_max, format, format_max, flags, out_grab_bb_min, out_grab_bb_max, p_handle, p_step ? *(const ImS32*)p_step : 0);
case ImGuiDataType_Float:
return SliderBehaviorRangeT<float, float, float>(bb, id, data_type, (float*)p_v_min, (float*)p_v_max, *(const float*)p_min, *(const float*)p_max, format, format_max, flags, out_grab_bb_min, out_grab_bb_max, p_handle, p_step ? *(const float*)p_step : 0.0f);
default:
IM_ASSERT(0 && "Unsupported data type");
return false;
}
}
bool ImGui::SliderScalarRange2(const char* label, ImGuiDataType data_type, void* p_v_min, void* p_v_max, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags, const void* p_step)
{
ImGuiWindow* window = GetCurrentWindow();
if (window->SkipItems)
return false;
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 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, temp_input_allowed ? ImGuiItemFlags_Inputable : 0))
return false;
// Default format and parse "min...max" format string
if (format == NULL)
format = DataTypeGetInfo(data_type)->PrintFmt;
// Parse format string: "%.3f...%.3f" or "%.3f...%.3f (%.3f)" for range size display
char format_min_buf[64];
char format_max_buf[64];
const char* format_min = format;
const char* format_max = format; // Default: same format for both
const char* format_range = NULL; // Optional range size format
const char* separator = ParseRangeFormatSeparator(format);
if (separator)
{
// Copy min format (everything before the dots)
const char* dots = format;
while (dots < separator && !(dots[0] == '.' && dots[1] == '.'))
dots++;
size_t min_len = ImMin((size_t)(dots - format), sizeof(format_min_buf) - 1);
memcpy(format_min_buf, format, min_len);
format_min_buf[min_len] = '\0';
format_min = format_min_buf;
// Check for range size format: look for " (" or " [" followed by "%"
const char* range_start = separator;
while (*range_start && *range_start != '(' && *range_start != '[')
range_start++;
if (*range_start && strchr(range_start, '%'))
{
// Found range size format - copy max format (between separator and range_start)
size_t max_len = ImMin((size_t)(range_start - separator), sizeof(format_max_buf) - 1);
// Trim trailing spaces
while (max_len > 0 && separator[max_len - 1] == ' ')
max_len--;
memcpy(format_max_buf, separator, max_len);
format_max_buf[max_len] = '\0';
format_max = format_max_buf;
format_range = range_start;
}
else
{
format_max = separator;
}
}
const bool hovered = ItemHoverable(frame_bb, id, g.LastItemData.ItemFlags);
// Calculate handle positions for hover detection
const float grab_padding = 2.0f;
const float slider_sz = frame_bb.GetWidth() - grab_padding * 2.0f;
double v_min_d = (data_type == ImGuiDataType_Float) ? (double)*(float*)p_v_min : (double)*(int*)p_v_min;
double v_max_d = (data_type == ImGuiDataType_Float) ? (double)*(float*)p_v_max : (double)*(int*)p_v_max;
double range_min = (data_type == ImGuiDataType_Float) ? (double)*(float*)p_min : (double)*(int*)p_min;
double range_max = (data_type == ImGuiDataType_Float) ? (double)*(float*)p_max : (double)*(int*)p_max;
double range = (range_max > range_min) ? (range_max - range_min) : 1.0;
float t_min = (float)ImClamp((v_min_d - range_min) / range, 0.0, 1.0);
float t_max = (float)ImClamp((v_max_d - range_min) / range, 0.0, 1.0);
float grab_sz = ImMin(style.GrabMinSize, slider_sz * 0.4f);
float usable = slider_sz - grab_sz;
float pos_min = frame_bb.Min.x + grab_padding + grab_sz * 0.5f + t_min * usable;
float pos_max = frame_bb.Min.x + grab_padding + grab_sz * 0.5f + t_max * usable;
// Determine hovered handle (0=min, 1=max, -1=bar between handles)
int hovered_handle = -1;
if (hovered)
{
float mouse_x = g.IO.MousePos.x;
float dist_min = ImAbs(mouse_x - pos_min);
float dist_max = ImAbs(mouse_x - pos_max);
float handle_threshold = grab_sz * 0.75f;
if (dist_min <= handle_threshold || dist_max <= handle_threshold)
hovered_handle = (dist_min <= dist_max) ? 0 : 1;
else if (mouse_x > pos_min && mouse_x < pos_max)
hovered_handle = -1; // Bar region
else
hovered_handle = (dist_min < dist_max) ? 0 : 1; // Outside, pick nearest
}
// Use context struct for active handle tracking (only valid when this widget is active)
ImS8* p_handle = &g.SliderRangeActiveHandle;
// Ctrl+Click for text input
bool temp_input_is_active = temp_input_allowed && TempInputIsActive(id);
if (!temp_input_is_active)
{
const bool clicked = hovered && IsMouseClicked(0, ImGuiInputFlags_None, id);
const bool make_active = (clicked || g.NavActivateId == id);
if (make_active && clicked)
SetKeyOwner(ImGuiKey_MouseLeft, id);
if (make_active && temp_input_allowed)
{
// Determine which handle for Ctrl+Click (use hovered_handle calculated above)
if (clicked)
*p_handle = (hovered_handle >= 0) ? (ImS8)hovered_handle : 0;
if ((clicked && g.IO.KeyCtrl) || (g.NavActivateId == id && (g.NavActivateFlags & ImGuiActivateFlags_PreferInput)))
temp_input_is_active = true;
}
if (make_active && !temp_input_is_active)
{
// Reset handle when THIS widget becomes active (not for all non-active widgets)
if (g.ActiveId != id)
*p_handle = (hovered_handle >= 0) ? (ImS8)hovered_handle : 0;
SetActiveID(id, window);
SetFocusID(id, window);
FocusWindow(window);
g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right) | (1 << ImGuiDir_Up) | (1 << ImGuiDir_Down);
}
}
if (temp_input_is_active)
{
// Ctrl+Click text input: shows "min...max" format, user can type to set values
return TempInputScalarRange2(frame_bb, id, label, data_type, p_v_min, p_v_max, format_min, format_max,
(flags & ImGuiSliderFlags_AlwaysClamp) ? p_min : NULL,
(flags & ImGuiSliderFlags_AlwaysClamp) ? p_max : NULL,
*p_handle);
}
// Frame
RenderNavCursor(frame_bb, id);
RenderFrame(frame_bb.Min, frame_bb.Max, GetColorU32(g.ActiveId == id ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg), true, style.FrameRounding);
// Slider behavior
ImRect grab_bb_min, grab_bb_max;
int handle_int = *p_handle;
const bool value_changed = SliderBehaviorRange(frame_bb, id, data_type, p_v_min, p_v_max, p_min, p_max, format_min, format_max, flags, &grab_bb_min, &grab_bb_max, &handle_int, p_step);
*p_handle = (ImS8)handle_int;
if (value_changed)
MarkItemEdited(id);
// Render grabs with hover/active highlighting
const bool is_active = (g.ActiveId == id);
// Render range fill (bar between handles)
if (grab_bb_max.Min.x > grab_bb_min.Min.x)
{
bool bar_active = is_active && (*p_handle == -1);
bool bar_hovered = hovered && !is_active && hovered_handle == -1;
float bar_alpha = bar_active ? 0.7f : bar_hovered ? 0.6f : 0.4f;
ImU32 bar_col = GetColorU32(ImGuiCol_SliderGrab, bar_alpha);
window->DrawList->AddRectFilled(grab_bb_min.Min, grab_bb_max.Max, bar_col, style.GrabRounding);
}
// Min handle: active if dragging it, hovered if mouse near it
bool min_active = is_active && (*p_handle == 0);
bool min_hovered = hovered && !is_active && hovered_handle == 0;
ImU32 min_col = GetColorU32((min_active || min_hovered) ? ImGuiCol_SliderGrabActive : ImGuiCol_SliderGrab);
window->DrawList->AddRectFilled(grab_bb_min.Min, grab_bb_min.Max, min_col, style.GrabRounding);
// Max handle: active if dragging it, hovered if mouse near it
bool max_active = is_active && (*p_handle == 1);
bool max_hovered = hovered && !is_active && hovered_handle == 1;
ImU32 max_col = GetColorU32((max_active || max_hovered) ? ImGuiCol_SliderGrabActive : ImGuiCol_SliderGrab);
window->DrawList->AddRectFilled(grab_bb_max.Min, grab_bb_max.Max, max_col, style.GrabRounding);
// Display value
char value_buf[128];
const char* value_buf_end = value_buf + DataTypeFormatString(value_buf, IM_ARRAYSIZE(value_buf), data_type, p_v_min, format_min);
value_buf_end += ImFormatString((char*)value_buf_end, (size_t)(value_buf + IM_ARRAYSIZE(value_buf) - value_buf_end), " - ");
value_buf_end = value_buf_end + DataTypeFormatString((char*)value_buf_end, (size_t)(value_buf + IM_ARRAYSIZE(value_buf) - value_buf_end), data_type, p_v_max, format_max);
// Optionally display range size if format includes it
if (format_range)
{
// Calculate range size (max - min)
ImGuiDataTypeStorage range_size;
if (data_type == ImGuiDataType_Float)
{
float range = *(const float*)p_v_max - *(const float*)p_v_min;
memcpy(&range_size, &range, sizeof(float));
}
else if (data_type == ImGuiDataType_S32)
{
int range = *(const int*)p_v_max - *(const int*)p_v_min;
memcpy(&range_size, &range, sizeof(int));
}
value_buf_end += ImFormatString((char*)value_buf_end, (size_t)(value_buf + IM_ARRAYSIZE(value_buf) - value_buf_end), " ");
value_buf_end = value_buf_end + DataTypeFormatString((char*)value_buf_end, (size_t)(value_buf + IM_ARRAYSIZE(value_buf) - value_buf_end), data_type, &range_size, format_range);
}
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, g.LastItemData.StatusFlags | (temp_input_allowed ? ImGuiItemStatusFlags_Inputable : 0));
return value_changed;
}
bool ImGui::SliderFloatRange2(const char* label, float* v_current_min, float* v_current_max, float v_min, float v_max, const char* format, ImGuiSliderFlags flags, float step)
{
return SliderScalarRange2(label, ImGuiDataType_Float, v_current_min, v_current_max, &v_min, &v_max, format, flags, step > 0.0f ? &step : NULL);
}
bool ImGui::SliderIntRange2(const char* label, int* v_current_min, int* v_current_max, int v_min, int v_max, const char* format, ImGuiSliderFlags flags, int step)
{
return SliderScalarRange2(label, ImGuiDataType_S32, v_current_min, v_current_max, &v_min, &v_max, format, flags, step > 0 ? &step : NULL);
}
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();
@ -3853,6 +4401,132 @@ bool ImGui::TempInputScalar(const ImRect& bb, ImGuiID id, const char* label, ImG
return value_changed;
}
// TempInput for range widgets: click on handle edits that value, user can type "XXX...YYYY" to set both
static bool TempInputScalarRange2(const ImRect& bb, ImGuiID id, const char* label, ImGuiDataType data_type, void* p_data_min, void* p_data_max, const char* format, const char* format_max, const void* p_clamp_min, const void* p_clamp_max, int selected_handle)
{
ImGuiContext& g = *GImGui;
const ImGuiDataTypeInfo* type_info = ImGui::DataTypeGetInfo(data_type);
char fmt_buf[32];
char fmt_buf_max[32];
char data_buf[64];
// Parse formats
format = ImParseFormatTrimDecorations(format, fmt_buf, IM_COUNTOF(fmt_buf));
format_max = ImParseFormatTrimDecorations(format_max, fmt_buf_max, IM_COUNTOF(fmt_buf_max));
if (format[0] == 0)
format = type_info->PrintFmt;
if (format_max[0] == 0)
format_max = format;
// Format initial value: show "min...max" format by default
char* p = data_buf;
p += ImGui::DataTypeFormatString(p, IM_COUNTOF(data_buf), data_type, p_data_min, format);
p += ImFormatString(p, (size_t)(data_buf + IM_COUNTOF(data_buf) - p), "...");
p += ImGui::DataTypeFormatString(p, (size_t)(data_buf + IM_COUNTOF(data_buf) - p), data_type, p_data_max, format_max);
ImStrTrimBlanks(data_buf);
ImGuiInputTextFlags flags = ImGuiInputTextFlags_AutoSelectAll | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint;
g.LastItemData.ItemFlags |= ImGuiItemFlags_NoMarkEdited;
bool value_changed = false;
if (ImGui::TempInputText(bb, id, label, data_buf, IM_COUNTOF(data_buf), flags))
{
// Backup old values
size_t data_type_size = type_info->Size;
ImGuiDataTypeStorage data_backup_min, data_backup_max;
memcpy(&data_backup_min, p_data_min, data_type_size);
memcpy(&data_backup_max, p_data_max, data_type_size);
// Look for separator: ".." (any number of dots) or " - "
const char* separator = NULL;
const char* separator_end = NULL;
for (const char* s = data_buf; *s; s++)
{
if (s[0] == '.' && s[1] == '.')
{
separator = s;
separator_end = s;
while (*separator_end == '.')
separator_end++;
break;
}
if (s[0] == ' ' && s[1] == '-' && s[2] == ' ')
{
separator = s;
separator_end = s + 3;
break;
}
}
if (separator)
{
// Parse min value (before separator)
char min_buf[32];
size_t min_len = (size_t)(separator - data_buf);
if (min_len >= IM_COUNTOF(min_buf))
min_len = IM_COUNTOF(min_buf) - 1;
memcpy(min_buf, data_buf, min_len);
min_buf[min_len] = 0;
ImStrTrimBlanks(min_buf);
// Parse max value (after separator)
char max_buf[32];
ImStrncpy(max_buf, separator_end, IM_COUNTOF(max_buf));
ImStrTrimBlanks(max_buf);
// Apply values
if (min_buf[0])
ImGui::DataTypeApplyFromText(min_buf, data_type, p_data_min, format, NULL);
if (max_buf[0])
ImGui::DataTypeApplyFromText(max_buf, data_type, p_data_max, format_max, NULL);
// Ensure min <= max
if (ImGui::DataTypeCompare(data_type, p_data_min, p_data_max) > 0)
memcpy(p_data_max, p_data_min, data_type_size);
}
else
{
// No separator - update selected handle only, or both if bar was selected
if (selected_handle == -1)
{
// Bar selected: set both to same value
ImGui::DataTypeApplyFromText(data_buf, data_type, p_data_min, format, NULL);
memcpy(p_data_max, p_data_min, data_type_size);
}
else
{
// Handle selected: update only that handle
void* p_target = (selected_handle == 0) ? p_data_min : p_data_max;
const char* target_format = (selected_handle == 0) ? format : format_max;
ImGui::DataTypeApplyFromText(data_buf, data_type, p_target, target_format, NULL);
// Ensure min <= max after update
if (ImGui::DataTypeCompare(data_type, p_data_min, p_data_max) > 0)
{
if (selected_handle == 0)
memcpy(p_data_min, p_data_max, data_type_size); // Clamp min to max
else
memcpy(p_data_max, p_data_min, data_type_size); // Clamp max to min
}
}
}
// Clamp values to range bounds
if (p_clamp_min || p_clamp_max)
{
ImGui::DataTypeClamp(data_type, p_data_min, p_clamp_min, p_clamp_max);
ImGui::DataTypeClamp(data_type, p_data_max, p_clamp_min, p_clamp_max);
}
// Only mark as edited if values changed
g.LastItemData.ItemFlags &= ~ImGuiItemFlags_NoMarkEdited;
value_changed = memcmp(&data_backup_min, p_data_min, data_type_size) != 0 ||
memcmp(&data_backup_max, p_data_max, data_type_size) != 0;
if (value_changed)
ImGui::MarkItemEdited(id);
}
return value_changed;
}
void ImGui::SetNextItemRefVal(ImGuiDataType data_type, void* p_data)
{
ImGuiContext& g = *GImGui;