UI: add non-linear slider support

This patch introduces non linear sliders. That means, that the movement
of the mouse doesn't map linearly to the value of the slider.

The following changes have been made.

- Free logarithmic sliders with maximum range of (`0 <= x < inf`)
- Logarithmic sliders with correct value indication bar.
- Free cubic sliders with maximum range of (`-inf < x < inf`)
- Cubic sliders with correct value indication bar.

Cubic mapping has been added as well, because it's used for brush sizes
in other applications (Krita for e.g.).

To make a slider have a different scale type use following line in RNA:
 `RNA_def_property_ui_scale_type(prop, PROP_SCALE_LOGARITHMIC);`
or:
 `RNA_def_property_ui_scale_type(prop, PROP_SCALE_CUBIC);`

Test the precision, step size and soft-min if you change the scale type
of a property as it will feel very different and may need tweaking.

Ref D9074
This commit is contained in:
Henrik Dick 2021-05-17 17:28:12 +10:00 committed by Campbell Barton
parent 45863059f7
commit 0447aedb96
Notes: blender-bot 2023-04-29 15:20:34 +02:00
Referenced by pull request #107466, Fix #107011: Boolean Modifier Overlap Threshold property issue in UI when value is 0
11 changed files with 351 additions and 49 deletions

View File

@ -2327,6 +2327,14 @@ bool ui_but_is_float(const uiBut *but)
return false;
}
PropertyScaleType ui_but_scale_type(const uiBut *but)
{
if (but->rnaprop) {
return RNA_property_ui_scale(but->rnaprop);
}
return PROP_SCALE_LINEAR;
}
bool ui_but_is_bool(const uiBut *but)
{
if (ELEM(but->type,

View File

@ -125,6 +125,24 @@
*/
#define UI_MAX_PASSWORD_STR 128
/**
* This is a lower limit on the soft minimum of the range.
* Usually the derived lower limit from the visible precision is higher,
* so this number is the backup minimum.
*
* Logarithmic scale does not work with a minimum value of zero,
* but we want to support it anyway. It is set to 0.5e... for
* correct rounding since when the tweaked value is lower than
* the log minimum (lower limit), it will snap to 0.
*/
#define UI_PROP_SCALE_LOG_MIN 0.5e-8f
/**
* This constant defines an offset for the precision change in
* snap rounding, when going to higher values. It is set to
* `0.5 - log10(3) = 0.03` to make the switch at `0.3` values.
*/
#define UI_PROP_SCALE_LOG_SNAP_OFFSET 0.03f
/**
* When #USER_CONTINUOUS_MOUSE is disabled or tablet input is used,
* Use this as a maximum soft range for mapping cursor motion to the value.
@ -3938,9 +3956,16 @@ static void ui_numedit_begin(uiBut *but, uiHandleButtonData *data)
float softmin = but->softmin;
float softmax = but->softmax;
float softrange = softmax - softmin;
const PropertyScaleType scale_type = ui_but_scale_type(but);
float log_min = (scale_type == PROP_SCALE_LOG) ? max_ff(softmin, UI_PROP_SCALE_LOG_MIN) : 0.0f;
if ((but->type == UI_BTYPE_NUM) && (ui_but_is_cursor_warp(but) == false)) {
uiButNumber *number_but = (uiButNumber *)but;
if (scale_type == PROP_SCALE_LOG) {
log_min = max_ff(log_min, powf(10, -number_but->precision) * 0.5f);
}
/* Use a minimum so we have a predictable range,
* otherwise some float buttons get a large range. */
const float value_step_float_min = 0.1f;
@ -3989,7 +4014,31 @@ static void ui_numedit_begin(uiBut *but, uiHandleButtonData *data)
}
}
data->dragfstart = (softrange == 0.0f) ? 0.0f : ((float)data->value - softmin) / softrange;
if (softrange == 0.0f) {
data->dragfstart = 0.0f;
}
else {
switch (scale_type) {
case PROP_SCALE_LINEAR: {
data->dragfstart = ((float)data->value - softmin) / softrange;
break;
}
case PROP_SCALE_LOG: {
BLI_assert(log_min != 0.0f);
const float base = softmax / log_min;
data->dragfstart = logf((float)data->value / log_min) / logf(base);
break;
}
case PROP_SCALE_CUBIC: {
const float cubic_min = cube_f(softmin);
const float cubic_max = cube_f(softmax);
const float cubic_range = cubic_max - cubic_min;
const float f = ((float)data->value - softmin) * cubic_range / softrange + cubic_min;
data->dragfstart = (cbrtf(f) - softmin) / softrange;
break;
}
}
}
data->dragf = data->dragfstart;
data->drag_map_soft_min = softmin;
@ -4707,6 +4756,7 @@ static float ui_numedit_apply_snapf(
/* pass */
}
else {
const PropertyScaleType scale_type = ui_but_scale_type(but);
float softrange = softmax - softmin;
float fac = 1.0f;
@ -4744,30 +4794,29 @@ static float ui_numedit_apply_snapf(
}
}
if (snap == SNAP_ON) {
if (softrange < 2.10f) {
tempf = roundf(tempf * 10.0f) * 0.1f;
BLI_assert(ELEM(snap, SNAP_ON, SNAP_ON_SMALL));
switch (scale_type) {
case PROP_SCALE_LINEAR:
case PROP_SCALE_CUBIC: {
const float snap_fac = (snap == SNAP_ON_SMALL ? 0.1f : 1.0f);
if (softrange < 2.10f) {
tempf = roundf(tempf * 10.0f / snap_fac) * 0.1f * snap_fac;
}
else if (softrange < 21.0f) {
tempf = roundf(tempf / snap_fac) * snap_fac;
}
else {
tempf = roundf(tempf * 0.1f / snap_fac) * 10.0f * snap_fac;
}
break;
}
else if (softrange < 21.0f) {
tempf = roundf(tempf);
case PROP_SCALE_LOG: {
const float snap_fac = powf(10.0f,
roundf(log10f(tempf) + UI_PROP_SCALE_LOG_SNAP_OFFSET) -
(snap == SNAP_ON_SMALL ? 2.0f : 1.0f));
tempf = roundf(tempf / snap_fac) * snap_fac;
break;
}
else {
tempf = roundf(tempf * 0.1f) * 10.0f;
}
}
else if (snap == SNAP_ON_SMALL) {
if (softrange < 2.10f) {
tempf = roundf(tempf * 100.0f) * 0.01f;
}
else if (softrange < 21.0f) {
tempf = roundf(tempf * 10.0f) * 0.1f;
}
else {
tempf = roundf(tempf);
}
}
else {
BLI_assert(0);
}
if (fac != 1.0f) {
@ -4813,6 +4862,7 @@ static bool ui_numedit_but_NUM(uiButNumber *number_but,
int lvalue, temp;
bool changed = false;
const bool is_float = ui_but_is_float(but);
const PropertyScaleType scale_type = ui_but_scale_type(but);
/* prevent unwanted drag adjustments, test motion so modifier keys refresh. */
if ((is_motion || data->draglock) && (ui_but_dragedit_update_mval(data, mx) == false)) {
@ -4824,21 +4874,74 @@ static bool ui_numedit_but_NUM(uiButNumber *number_but,
const float softmax = but->softmax;
const float softrange = softmax - softmin;
const float log_min = (scale_type == PROP_SCALE_LOG) ?
max_ff(max_ff(softmin, UI_PROP_SCALE_LOG_MIN),
powf(10, -number_but->precision) * 0.5f) :
0;
/* Mouse location isn't screen clamped to the screen so use a linear mapping
* 2px == 1-int, or 1px == 1-ClickStep */
if (is_float) {
fac *= 0.01f * number_but->step_size;
tempf = (float)data->startvalue + ((float)(mx - data->dragstartx) * fac);
switch (scale_type) {
case PROP_SCALE_LINEAR: {
tempf = (float)data->startvalue + (float)(mx - data->dragstartx) * fac;
break;
}
case PROP_SCALE_LOG: {
const float startvalue = max_ff((float)data->startvalue, log_min);
tempf = expf((float)(mx - data->dragstartx) * fac) * startvalue;
if (tempf <= log_min) {
tempf = 0.0f;
}
break;
}
case PROP_SCALE_CUBIC: {
tempf = cbrtf((float)data->startvalue) + (float)(mx - data->dragstartx) * fac;
tempf *= tempf * tempf;
break;
}
}
tempf = ui_numedit_apply_snapf(but, tempf, softmin, softmax, snap);
#if 1 /* fake moving the click start, nicer for dragging back after passing the limit */
if (tempf < softmin) {
data->dragstartx -= (softmin - tempf) / fac;
tempf = softmin;
}
else if (tempf > softmax) {
data->dragstartx += (tempf - softmax) / fac;
tempf = softmax;
switch (scale_type) {
case PROP_SCALE_LINEAR: {
if (tempf < softmin) {
data->dragstartx -= (softmin - tempf) / fac;
tempf = softmin;
}
else if (tempf > softmax) {
data->dragstartx -= (softmax - tempf) / fac;
tempf = softmax;
}
break;
}
case PROP_SCALE_LOG: {
if (tempf < log_min) {
data->dragstartx -= logf(log_min / (float)data->startvalue) / fac -
(float)(mx - data->dragstartx);
tempf = softmin;
}
else if (tempf > softmax) {
data->dragstartx -= logf(softmax / (float)data->startvalue) / fac -
(float)(mx - data->dragstartx);
tempf = softmax;
}
break;
}
case PROP_SCALE_CUBIC: {
if (tempf < softmin) {
data->dragstartx = mx - (int)((cbrtf(softmin) - cbrtf((float)data->startvalue)) / fac);
tempf = softmin;
}
else if (tempf > softmax) {
data->dragstartx = mx - (int)((cbrtf(softmax) - cbrtf((float)data->startvalue)) / fac);
tempf = softmax;
}
break;
}
}
#else
CLAMP(tempf, softmin, softmax);
@ -4945,7 +5048,31 @@ static bool ui_numedit_but_NUM(uiButNumber *number_but,
}
data->draglastx = mx;
tempf = (softmin + data->dragf * softrange);
switch (scale_type) {
case PROP_SCALE_LINEAR: {
tempf = (softmin + data->dragf * softrange);
break;
}
case PROP_SCALE_LOG: {
const float log_min = max_ff(max_ff(softmin, UI_PROP_SCALE_LOG_MIN),
powf(10.0f, -number_but->precision) * 0.5f);
const float base = softmax / log_min;
tempf = powf(base, data->dragf) * log_min;
if (tempf <= log_min) {
tempf = 0.0f;
}
break;
}
case PROP_SCALE_CUBIC: {
tempf = (softmin + data->dragf * softrange);
tempf *= tempf * tempf;
float cubic_min = softmin * softmin * softmin;
float cubic_max = softmax * softmax * softmax;
tempf = (tempf - cubic_min) / (cubic_max - cubic_min) * softrange + softmin;
break;
}
}
if (!is_float) {
temp = round_fl_to_int(tempf);
@ -5192,9 +5319,19 @@ static int ui_do_but_NUM(
else {
/* Float Value. */
if (but->drawflag & (UI_BUT_ACTIVE_LEFT | UI_BUT_ACTIVE_RIGHT)) {
const PropertyScaleType scale_type = ui_but_scale_type(but);
button_activate_state(C, but, BUTTON_STATE_NUM_EDITING);
const double value_step = (double)number_but->step_size * UI_PRECISION_FLOAT_SCALE;
double value_step;
if (scale_type == PROP_SCALE_LOG) {
value_step = powf(10.0f,
(roundf(log10f(data->value) + UI_PROP_SCALE_LOG_SNAP_OFFSET) - 1.0f) +
log10f(number_but->step_size));
}
else {
value_step = (double)number_but->step_size * UI_PRECISION_FLOAT_SCALE;
}
BLI_assert(value_step > 0.0f);
const double value_test = (but->drawflag & UI_BUT_ACTIVE_LEFT) ?
(double)max_ff(but->softmin,
@ -5242,6 +5379,8 @@ static bool ui_numedit_but_SLI(uiBut *but,
return changed;
}
const PropertyScaleType scale_type = ui_but_scale_type(but);
softmin = but->softmin;
softmax = but->softmax;
softrange = softmax - softmin;
@ -5283,7 +5422,24 @@ static bool ui_numedit_but_SLI(uiBut *but,
#endif
/* done correcting mouse */
tempf = softmin + f * softrange;
switch (scale_type) {
case PROP_SCALE_LINEAR: {
tempf = softmin + f * softrange;
break;
}
case PROP_SCALE_LOG: {
tempf = powf(softmax / softmin, f) * softmin;
break;
}
case PROP_SCALE_CUBIC: {
const float cubicmin = cube_f(softmin);
const float cubicmax = cube_f(softmax);
const float cubicrange = cubicmax - cubicmin;
tempf = cube_f(softmin + f * softrange);
tempf = (tempf - cubicmin) / cubicrange * softrange + softmin;
break;
}
}
temp = round_fl_to_int(tempf);
if (snap) {
@ -5477,6 +5633,8 @@ static int ui_do_but_SLI(
if (click) {
if (click == 2) {
const PropertyScaleType scale_type = ui_but_scale_type(but);
/* nudge slider to the left or right */
float f, tempf, softmin, softmax, softrange;
int temp;
@ -5501,14 +5659,20 @@ static int ui_do_but_SLI(
f = (float)(mx - but->rect.xmin) / (BLI_rctf_size_x(&but->rect));
}
f = softmin + f * softrange;
if (scale_type == PROP_SCALE_LOG) {
f = powf(softmax / softmin, f) * softmin;
}
else {
f = softmin + f * softrange;
}
if (!ui_but_is_float(but)) {
int value_step = 1;
if (f < temp) {
temp--;
temp -= value_step;
}
else {
temp++;
temp += value_step;
}
if (temp >= softmin && temp <= softmax) {
@ -5519,14 +5683,23 @@ static int ui_do_but_SLI(
}
}
else {
if (f < tempf) {
tempf -= 0.01f;
}
else {
tempf += 0.01f;
}
if (tempf >= softmin && tempf <= softmax) {
float value_step;
if (scale_type == PROP_SCALE_LOG) {
value_step = powf(10.0f, roundf(log10f(tempf) + UI_PROP_SCALE_LOG_SNAP_OFFSET) - 1.0f);
}
else {
value_step = 0.01f;
}
if (f < tempf) {
tempf -= value_step;
}
else {
tempf += value_step;
}
CLAMP(tempf, softmin, softmax);
data->value = tempf;
}
else {

View File

@ -660,6 +660,7 @@ bool ui_but_context_poll_operator(struct bContext *C, struct wmOperatorType *ot,
extern void ui_but_update(uiBut *but);
extern void ui_but_update_edited(uiBut *but);
extern PropertyScaleType ui_but_scale_type(const uiBut *but) ATTR_WARN_UNUSED_RESULT;
extern bool ui_but_is_float(const uiBut *but) ATTR_WARN_UNUSED_RESULT;
extern bool ui_but_is_bool(const uiBut *but) ATTR_WARN_UNUSED_RESULT;
extern bool ui_but_is_unit(const uiBut *but) ATTR_WARN_UNUSED_RESULT;

View File

@ -3752,12 +3752,35 @@ static void widget_numslider(
float factor, factor_ui;
float factor_discard = 1.0f; /* No discard. */
const float value = (float)ui_but_value_get(but);
const float softmin = but->softmin;
const float softmax = but->softmax;
const float softrange = softmax - softmin;
const PropertyScaleType scale_type = ui_but_scale_type(but);
if (but->rnaprop && (RNA_property_subtype(but->rnaprop) == PROP_PERCENTAGE)) {
factor = value / but->softmax;
}
else {
factor = (value - but->softmin) / (but->softmax - but->softmin);
switch (scale_type) {
case PROP_SCALE_LINEAR: {
if (but->rnaprop && (RNA_property_subtype(but->rnaprop) == PROP_PERCENTAGE)) {
factor = value / softmax;
}
else {
factor = (value - softmin) / softrange;
}
break;
}
case PROP_SCALE_LOG: {
const float logmin = fmaxf(softmin, 0.5e-8f);
const float base = softmax / logmin;
factor = logf(value / logmin) / logf(base);
break;
}
case PROP_SCALE_CUBIC: {
const float cubicmin = cube_f(softmin);
const float cubicmax = cube_f(softmax);
const float cubicrange = cubicmax - cubicmin;
const float f = (value - softmin) * cubicrange / softrange + cubicmin;
factor = (cbrtf(f) - softmin) / softrange;
break;
}
}
const float width = (float)BLI_rcti_size_x(rect);

View File

@ -845,6 +845,7 @@ const char *RNA_property_description(PropertyRNA *prop);
PropertyType RNA_property_type(PropertyRNA *prop);
PropertySubType RNA_property_subtype(PropertyRNA *prop);
PropertyUnit RNA_property_unit(PropertyRNA *prop);
PropertyScaleType RNA_property_ui_scale(PropertyRNA *prop);
int RNA_property_flag(PropertyRNA *prop);
int RNA_property_override_flag(PropertyRNA *prop);
int RNA_property_tags(PropertyRNA *prop);

View File

@ -388,6 +388,7 @@ void RNA_def_property_string_default(PropertyRNA *prop, const char *value);
void RNA_def_property_ui_text(PropertyRNA *prop, const char *name, const char *description);
void RNA_def_property_ui_range(
PropertyRNA *prop, double min, double max, double step, int precision);
void RNA_def_property_ui_scale_type(PropertyRNA *prop, PropertyScaleType scale_type);
void RNA_def_property_ui_icon(PropertyRNA *prop, int icon, int consecutive);
void RNA_def_property_update(PropertyRNA *prop, int noteflag, const char *updatefunc);

View File

@ -95,6 +95,32 @@ typedef enum PropertyUnit {
PROP_UNIT_TEMPERATURE = (11 << 16), /* C */
} PropertyUnit;
/**
* Use values besides #PROP_SCALE_LINEAR
* so the movement of the mouse doesn't map linearly to the value of the slider.
*
* For some settings it's useful to space motion in a non-linear way, see T77868.
*
* NOTE: The scale types are available for all float sliders.
* For integer sliders they are only available if they use the visible value bar.
* Sliders with logarithmic scale and value bar must have a range > 0
* while logarithmic sliders without the value bar can have a range of >= 0.
*/
typedef enum PropertyScaleType {
/** Linear scale (default). */
PROP_SCALE_LINEAR = 0,
/**
* Logarithmic scale
* - Maximum range: `0 <= x < inf`
*/
PROP_SCALE_LOG = 1,
/**
* Cubic scale.
* - Maximum range: `-inf < x < inf`
*/
PROP_SCALE_CUBIC = 2,
} PropertyScaleType;
#define RNA_SUBTYPE_UNIT(subtype) ((subtype)&0x00FF0000)
#define RNA_SUBTYPE_VALUE(subtype) ((subtype) & ~0x00FF0000)
#define RNA_SUBTYPE_UNIT_VALUE(subtype) ((subtype) >> 16)

View File

@ -684,6 +684,29 @@ static char *rna_def_property_get_func(
}
}
}
/* Check log scale sliders for negative range. */
if (prop->type == PROP_FLOAT) {
FloatPropertyRNA *fprop = (FloatPropertyRNA *)prop;
/* NOTE: UI_BTYPE_NUM_SLIDER can't have a softmin of zero. */
if ((fprop->ui_scale_type == PROP_SCALE_LOG) && (fprop->hardmin < 0 || fprop->softmin < 0)) {
CLOG_ERROR(
&LOG, "\"%s.%s\", range for log scale < 0.", srna->identifier, prop->identifier);
DefRNA.error = true;
return NULL;
}
}
if (prop->type == PROP_INT) {
IntPropertyRNA *iprop = (IntPropertyRNA *)prop;
/* Only UI_BTYPE_NUM_SLIDER is implemented and that one can't have a softmin of zero. */
if ((iprop->ui_scale_type == PROP_SCALE_LOG) &&
(iprop->hardmin <= 0 || iprop->softmin <= 0)) {
CLOG_ERROR(
&LOG, "\"%s.%s\", range for log scale <= 0.", srna->identifier, prop->identifier);
DefRNA.error = true;
return NULL;
}
}
}
func = rna_alloc_function_name(srna->identifier, rna_safe_id(prop->identifier), "get");
@ -3935,6 +3958,8 @@ static void rna_generate_property(FILE *f, StructRNA *srna, const char *nest, Pr
rna_function_string(iprop->getarray_ex),
rna_function_string(iprop->setarray_ex),
rna_function_string(iprop->range_ex));
rna_int_print(f, iprop->ui_scale_type);
fprintf(f, ", ");
rna_int_print(f, iprop->softmin);
fprintf(f, ", ");
rna_int_print(f, iprop->softmax);
@ -3969,6 +3994,8 @@ static void rna_generate_property(FILE *f, StructRNA *srna, const char *nest, Pr
rna_function_string(fprop->getarray_ex),
rna_function_string(fprop->setarray_ex),
rna_function_string(fprop->range_ex));
rna_float_print(f, fprop->ui_scale_type);
fprintf(f, ", ");
rna_float_print(f, fprop->softmin);
fprintf(f, ", ");
rna_float_print(f, fprop->softmax);

View File

@ -1189,6 +1189,24 @@ PropertyUnit RNA_property_unit(PropertyRNA *prop)
return RNA_SUBTYPE_UNIT(RNA_property_subtype(prop));
}
PropertyScaleType RNA_property_ui_scale(PropertyRNA *prop)
{
PropertyRNA *rna_prop = rna_ensure_property(prop);
switch (rna_prop->type) {
case PROP_INT: {
IntPropertyRNA *iprop = (IntPropertyRNA *)rna_prop;
return iprop->ui_scale_type;
}
case PROP_FLOAT: {
FloatPropertyRNA *fprop = (FloatPropertyRNA *)rna_prop;
return fprop->ui_scale_type;
}
default:
return PROP_SCALE_LINEAR;
}
}
int RNA_property_flag(PropertyRNA *prop)
{
return rna_ensure_property(prop)->flag;

View File

@ -1754,6 +1754,28 @@ void RNA_def_property_ui_range(
}
}
void RNA_def_property_ui_scale_type(PropertyRNA *prop, PropertyScaleType ui_scale_type)
{
StructRNA *srna = DefRNA.laststruct;
switch (prop->type) {
case PROP_INT: {
IntPropertyRNA *iprop = (IntPropertyRNA *)prop;
iprop->ui_scale_type = ui_scale_type;
break;
}
case PROP_FLOAT: {
FloatPropertyRNA *fprop = (FloatPropertyRNA *)prop;
fprop->ui_scale_type = ui_scale_type;
break;
}
default:
CLOG_ERROR(&LOG, "\"%s.%s\", invalid type for scale.", srna->identifier, prop->identifier);
DefRNA.error = true;
break;
}
}
void RNA_def_property_range(PropertyRNA *prop, double min, double max)
{
StructRNA *srna = DefRNA.laststruct;

View File

@ -400,6 +400,7 @@ typedef struct IntPropertyRNA {
PropIntArraySetFuncEx setarray_ex;
PropIntRangeFuncEx range_ex;
PropertyScaleType ui_scale_type;
int softmin, softmax;
int hardmin, hardmax;
int step;
@ -423,6 +424,7 @@ typedef struct FloatPropertyRNA {
PropFloatArraySetFuncEx setarray_ex;
PropFloatRangeFuncEx range_ex;
PropertyScaleType ui_scale_type;
float softmin, softmax;
float hardmin, hardmax;
float step;