Drags, Sliders: Logarithmic: WIP experiments with trying to make logarithmic sliders sensible (#3361, #1823, #1316, #642)

This commit is contained in:
Ben Carter 2020-05-24 18:05:09 +09:00 committed by ocornut
parent 46d75202b8
commit a252a287bf
2 changed files with 107 additions and 8 deletions

View File

@ -348,6 +348,12 @@ IMGUI_API void* ImFileLoadToMemory(const char* filename, const char*
#define ImCeil(X) ceilf(X)
static inline float ImPow(float x, float y) { return powf(x, y); } // DragBehaviorT/SliderBehaviorT uses ImPow with either float/double and need the precision
static inline double ImPow(double x, double y) { return pow(x, y); }
static inline float ImLog(float x) { return logf(x); } // DragBehaviorT/SliderBehaviorT uses ImLog with either float/double and need the precision
static inline double ImLog(double x) { return log(x); }
static inline float ImAbs(float x) { return fabsf(x); }
static inline double ImAbs(double x) { return fabs(x); }
static inline float ImSign(float x) { return (x < 0.0f) ? -1.0f : ((x > 0.0f) ? 1.0f : 0.0f); } // Sign operator - returns -1, 0 or 1 based on sign of argument
static inline double ImSign(double x) { return (x < 0.0) ? -1.0 : ((x > 0.0) ? 1.0 : 0.0); }
#endif
// - ImMin/ImMax/ImClamp/ImLerp/ImSwap are used by widgets which support variety of types: signed/unsigned int/long long float/double
// (Exceptionally using templates here but we could also redefine them for those types)
@ -1987,7 +1993,7 @@ namespace ImGui
// e.g. " extern template IMGUI_API float RoundScalarWithFormatT<float, float>(const char* format, ImGuiDataType data_type, float v); "
template<typename T, typename SIGNED_T, typename FLOAT_T> IMGUI_API bool DragBehaviorT(ImGuiDataType data_type, T* v, float v_speed, T v_min, T v_max, const char* format, float power, ImGuiDragFlags flags);
template<typename T, typename SIGNED_T, typename FLOAT_T> IMGUI_API bool SliderBehaviorT(const ImRect& bb, ImGuiID id, ImGuiDataType data_type, T* v, T v_min, T v_max, const char* format, float power, ImGuiSliderFlags flags, ImRect* out_grab_bb);
template<typename T, typename FLOAT_T> IMGUI_API float SliderCalcRatioFromValueT(ImGuiDataType data_type, T v, T v_min, T v_max, float power, float linear_zero_pos);
template<typename T, typename FLOAT_T> IMGUI_API float SliderCalcRatioFromValueT(ImGuiDataType data_type, T v, T v_min, T v_max, float power, float linear_zero_pos, float logarithmic_zero_epsilon);
template<typename T, typename SIGNED_T> IMGUI_API T RoundScalarWithFormatT(const char* format, ImGuiDataType data_type, T v);
// Data type helpers

View File

@ -2374,15 +2374,57 @@ bool ImGui::DragIntRange2(const char* label, int* v_current_min, int* v_current_
// - VSliderInt()
//-------------------------------------------------------------------------
// Convert a value v in the output space of a slider into a parametric position on the slider itself
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)
float ImGui::SliderCalcRatioFromValueT(ImGuiDataType data_type, TYPE v, TYPE v_min, TYPE v_max, float power, float linear_zero_pos, float logarithmic_zero_epsilon)
{
if (v_min == v_max)
return 0.0f;
const bool is_power = (power != 1.0f) && (data_type == ImGuiDataType_Float || data_type == ImGuiDataType_Double);
const bool is_logarithmic = (power == 0.0f) && (data_type == ImGuiDataType_Float || data_type == ImGuiDataType_Double);
const bool is_power = (power != 1.0f) && (data_type == ImGuiDataType_Float || data_type == ImGuiDataType_Double) && (!is_logarithmic);
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)
{
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
{
float zero_point = (-(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)
if (v == 0.0f)
result = zero_point; // 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;
else
result = zero_point + ((float)(ImLog((FLOATTYPE)v_clamped / logarithmic_zero_epsilon) / ImLog(v_max_fudged / logarithmic_zero_epsilon)) * (1.0f - zero_point));
}
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 if (is_power)
{
if (v_clamped < 0.0f)
{
@ -2409,7 +2451,8 @@ bool ImGui::SliderBehaviorT(const ImRect& bb, ImGuiID id, ImGuiDataType data_typ
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 = (power == 0.0f) && is_decimal;
const bool is_power = (power != 1.0f) && is_decimal && (!is_logarithmic);
const float grab_padding = 2.0f;
const float slider_sz = (bb.Max[axis] - bb.Min[axis]) - grab_padding * 2.0f;
@ -2437,6 +2480,14 @@ bool ImGui::SliderBehaviorT(const ImRect& bb, ImGuiID id, ImGuiDataType data_typ
linear_zero_pos = v_min < 0.0f ? 1.0f : 0.0f;
}
float logarithmic_zero_epsilon = 0.0f; // Only valid when is_logarithmic is true
if (is_logarithmic)
{
// 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_decimal ? ImParseFormatPrecision(format, 3) : 1;
logarithmic_zero_epsilon = ImPow(0.1f, (float)decimal_precision);
}
// Process interacting with the slider
bool value_changed = false;
if (g.ActiveId == id)
@ -2468,7 +2519,7 @@ bool ImGui::SliderBehaviorT(const ImRect& bb, ImGuiID id, ImGuiDataType data_typ
}
else if (delta != 0.0f)
{
clicked_t = SliderCalcRatioFromValueT<TYPE, FLOATTYPE>(data_type, *v, v_min, v_max, power, linear_zero_pos);
clicked_t = SliderCalcRatioFromValueT<TYPE, FLOATTYPE>(data_type, *v, v_min, v_max, power, linear_zero_pos, logarithmic_zero_epsilon);
const int decimal_precision = is_decimal ? ImParseFormatPrecision(format, 3) : 0;
if ((decimal_precision > 0) || is_power)
{
@ -2496,7 +2547,49 @@ bool ImGui::SliderBehaviorT(const ImRect& bb, ImGuiID id, ImGuiDataType data_typ
if (set_new_value)
{
TYPE v_new;
if (is_power)
if (is_logarithmic)
{
// We special-case the extents because otherwise our fudging can lead to "mathematically correct" but non-intuitive behaviors like a fully-left slider not actually reaching the minimum value
if (clicked_t <= 0.0f)
v_new = v_min;
else if (clicked_t >= 1.0f)
v_new = v_max;
else
{
bool flipped = v_max < v_min;
// 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;
// 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;
if (flipped)
ImSwap(v_min_fudged, v_max_fudged);
float clicked_t_with_flip = flipped ? (1.0f - clicked_t) : clicked_t;
if ((v_min * v_max) < 0.0f) // Range crosses zero, so we have to do this in two parts
{
float zero_point = (-(float)ImMin(v_min, v_max)) / ImAbs((float)v_max - (float)v_min); // The zero point in parametric space
if (clicked_t_with_flip == zero_point)
v_new = (TYPE)0.0f; // Special case to make getting exactly zero possible (the epsilon prevents it otherwise)
else if (clicked_t_with_flip < zero_point)
v_new = (TYPE)-(logarithmic_zero_epsilon * ImPow(-v_min_fudged / logarithmic_zero_epsilon, (FLOATTYPE)(1.0f - (clicked_t_with_flip / zero_point))));
else
v_new = (TYPE)(logarithmic_zero_epsilon * ImPow(v_max_fudged / logarithmic_zero_epsilon, (FLOATTYPE)((clicked_t_with_flip - zero_point) / (1.0f - zero_point))));
}
else if ((v_min < 0.0f) || (v_max < 0.0f)) // Entirely negative slider
v_new = (TYPE)-(-v_max_fudged * ImPow(-v_min_fudged / -v_max_fudged, (FLOATTYPE)(1.0f - clicked_t_with_flip)));
else
v_new = (TYPE)(v_min_fudged * ImPow(v_max_fudged / v_min_fudged, (FLOATTYPE)clicked_t_with_flip));
}
}
else if (is_power)
{
// Account for power curve scale on both sides of the zero
if (clicked_t < linear_zero_pos)
@ -2558,7 +2651,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 = SliderCalcRatioFromValueT<TYPE, FLOATTYPE>(data_type, *v, v_min, v_max, power, linear_zero_pos, logarithmic_zero_epsilon);
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);