Skip to content

Commit

Permalink
Drags, Sliders: Logarithmic: WIP experiments with trying to make loga…
Browse files Browse the repository at this point in the history
…rithmic sliders sensible (#3361, #1823, #1316, #642)
  • Loading branch information
ShironekoBen authored and ocornut committed Aug 17, 2020
1 parent 46d7520 commit a252a28
Show file tree
Hide file tree
Showing 2 changed files with 107 additions and 8 deletions.
8 changes: 7 additions & 1 deletion imgui_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
107 changes: 100 additions & 7 deletions imgui_widgets.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand All @@ -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;
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit a252a28

Please sign in to comment.