This guide provides a technical overview of the dt_bauhaus_slider widget in darktable for C/C++ developers. It details its usage, parameters, and the required order of operations for correct configuration.
The dt_bauhaus_slider separates its internal data value from its displayed value. This is fundamental to its flexibility.
- Internal Value: This is the slider's own
floatvalue, in the units of the parameter it represents (e.g., a field ofdt_iop_exposure_params_t), beforefactorandoffsetare applied for display. It is held by the widget, not by the parameter struct. All range limits (min,max) and get/set operations (dt_bauhaus_slider_get,dt_bauhaus_slider_set) work with this internal value. A slider created withdt_bauhaus_slider_from_params()copies it into its bound field, but not on every change; see section 3.1. - Displayed Value: This is the human-readable text shown in the GUI. It is derived from the internal value using the formula:
displayed_value = (internal_value * factor) + offsetThis value is then formatted into a string using thedigitsandformatproperties.
-
Introspection-based (Standard Method):
dt_bauhaus_slider_from_params(self, "param_name")This is the recommended method. It automatically configures the slider's range (hard_min,hard_max), default value, and data type by introspecting the module's parameter struct definition. -
Manual Creation:
dt_bauhaus_slider_new_with_range(self, min, max, step, defval, digits)This is used when a slider does not directly map to a parameter in the struct or requires a custom setup not supported by introspection.
The slider has two tiers of limits: "hard" and "soft".
- Hard Limits:
dt_bauhaus_slider_set_hard_min(),dt_bauhaus_slider_set_hard_max()These define the absolute boundaries of the slider's own value: neither the user nordt_bauhaus_slider_set()can move the slider beyond them.dt_bauhaus_slider_from_params()sets them from the field's$MINand$MAX. - Soft Limits:
dt_bauhaus_slider_set_soft_min(),dt_bauhaus_slider_set_soft_max(),dt_bauhaus_slider_set_soft_range()These define the default visible range for the user. They provide a sensible working area, preventing users from accidentally selecting extreme values. The user can temporarily exceed these soft limits (up to the hard limits) by holdingCtrl+Shiftwhile dragging the slider.
The hard range limits the widget, not the parameter it is bound to. dt_bauhaus_slider_set() brings an out-of-range argument into the hard range before storing it as the slider position: it clamps it, or wraps it around for a slider whose format is °. _slider_set_normalized() then rounds it to the slider's digits (see section 4.1). A _from_params slider copies that position back into its field only when it is set outside the GUI-update guard; under DT_ENTER_GUI_UPDATE(), which is how widgets are synced from params, nothing is written back (src/bauhaus/bauhaus.c).
So when a module assigns an out-of-range value to self->params itself and then syncs the slider, the slider shows a value the field does not hold, and the field keeps the original. dt_dev_add_history_item() copies that original into the history item (src/develop/develop.c), and the pixelpipe hands that copy to the module's commit_params() (src/develop/pixelpipe_hb.c, src/develop/imageop.c), which builds the data process() works from. Nothing clamps it on the way. The closest thing to a check is the -d params debug option: dt_iop_commit_params() then compares the fields of an enabled module with their declared ranges, and for a value outside them logs the field and shows an "introspection error" message (src/develop/imageop.c). It is a diagnostic only: it changes nothing, lets NaN and infinity through, and allows a float a small tolerance. Validate a computed value before assigning it.
These functions modify the displayed_value and have critical interdependencies.
dt_bauhaus_slider_set_factor(widget, factor): Multiplies the internal value before display.dt_bauhaus_slider_set_offset(widget, offset): Adds to the value after applying the factor.dt_bauhaus_slider_set_format(widget, format_string): Appends a literal string suffix (e.g.," EV"," %") to the formatted number.- Peculiarity - Percentage (
%): This function contains a special heuristic. If theformat_stringcontains a%character andfabsf(hard_max) <= 10, it assumes a percentage display is desired. It will automatically:- Set the
factorto100.0(if it was the default1.0). - Perform a relative modification:
digits -= 2;.
- Set the
- Peculiarity - Percentage (
dt_bauhaus_slider_set_digits(widget, digits): Sets the number of decimal places for the displayed number. This is the final step in formatting the numeric part of the display.
A slider can be associated with a color picker to allow its value to be set by sampling the image.
dt_color_picker_new(self, flags, slider_widget): This function acts as a wrapper. It takes a newly created slider as an argument and returns a newGtkWidgetthat combines the slider and a picker button. TheGtkWidgetpointer in your GUI data struct should store the widget returned bydt_color_picker_new.
The slider's internal update function, _slider_set_normalized(), rounds the internal value to ensure it matches the displayed precision. The formula used is:
rpos = roundf(base * rpos) / base;
where base = powf(10.0f, d->digits) * d->factor;
A common incorrect configuration sequence can lead to digits becoming a negative number, which causes this formula to fail catastrophically.
Example Failure Analysis:
- A slider is created. Its default state is
digits = 0,factor = 1.0. dt_bauhaus_slider_set_format(slider, "%");is called.- The special "%" logic triggers.
factorbecomes100.0.digitsis modified relative to its current value:digits = 0 - 2 = -2. - The rounding
baseis now calculated aspow(10, -2) * 100.0, which equals0.01 * 100.0 = 1.0. - The rounding formula becomes
rpos = roundf(1.0 * rpos) / 1.0;, which isrpos = roundf(rpos);.
When this is applied to a slider with an internal range of [0.0, 1.0], the internal value will be rounded to the nearest integer, meaning it can only ever be 0.0 or 1.0. This is why the slider snaps to its ends (0% or 100%) and rejects any intermediate values.
To prevent this rounding failure and ensure predictable behavior, it is imperative to set the slider properties in an explicit, override-style order.
- Set Transformation: Configure the
factorandoffsetfirst. - Set Format Suffix: Call
dt_bauhaus_slider_set_format(). This lets the convenience logic run, but its effects will be immediately corrected. - Set Final Precision: Call
dt_bauhaus_slider_set_digits()last. This sets the authoritative number of decimal places and overwrites any intermediate or incorrect values set by theset_formatside effect.
A plain number with no units, displayed with 3 decimal places.
GtkWidget *slider = dt_bauhaus_slider_from_params(self, "contrast");
dt_bauhaus_slider_set_soft_range(slider, 0.5, 2.0);
dt_bauhaus_slider_set_digits(slider, 3);
// No call to set_format is needed for plain numbers.An internal value of -1.0 to 1.0 displayed as -100.00 % to 100.00 %.
GtkWidget *slider = dt_bauhaus_slider_from_params(self, "saturation");
// 1. Set display factor.
dt_bauhaus_slider_set_factor(slider, 100.0f);
// 2. Set format suffix.
dt_bauhaus_slider_set_format(slider, " %");
// 3. Set final precision. This is the crucial last step.
dt_bauhaus_slider_set_digits(slider, 2);An angle stored in degrees, displayed with one decimal place.
GtkWidget *slider = dt_bauhaus_slider_from_params(self, "rotation");
dt_bauhaus_slider_set_format(slider, "°");
dt_bauhaus_slider_set_digits(slider, 1);An angle stored internally in radians but displayed in degrees.
// Internal value is in radians (e.g., M_PI for 180°)
GtkWidget *slider = dt_bauhaus_slider_from_params(self, "angle");
// RAD_2_DEG is defined in common/math.h (180/π ≈ 57.2957795)
dt_bauhaus_slider_set_factor(slider, RAD_2_DEG);
dt_bauhaus_slider_set_format(slider, "°");
dt_bauhaus_slider_set_digits(slider, 1);An exposure value with a unit suffix and two decimal places.
GtkWidget *slider = dt_bauhaus_slider_from_params(self, "exposure");
// Note the leading space for UI padding.
dt_bauhaus_slider_set_format(slider, " EV");
dt_bauhaus_slider_set_digits(slider, 2);An exposure slider linked to a color picker.
dt_iop_agx_gui_data_t *gui_data = self->gui_data;
// 1. Create the slider first, but do not add it to a container yet.
GtkWidget *slider = dt_bauhaus_slider_from_params(self, "range_white_relative_exposure");
// 2. Configure the slider using the recommended order.
dt_bauhaus_slider_set_soft_range(slider, 1.0f, 20.0f);
dt_bauhaus_slider_set_format(slider, " EV");
dt_bauhaus_slider_set_digits(slider, 2);
// 3. Create the color picker, passing the slider to it.
// Store the widget returned by dt_color_picker_new().
gui_data->white_exposure_picker = dt_color_picker_new(
self,
DT_COLOR_PICKER_AREA | DT_COLOR_PICKER_DENOISE,
slider
);
// 4. Set the tooltip on the final, combined widget.
gtk_widget_set_tooltip_text(gui_data->white_exposure_picker,
_("relative exposure above mid-grey (white point)"));
// 5. Pack the combined widget into your UI.
// (This is handled automatically if the slider was created with _from_params
// and it is the first widget, but manual packing is shown here for clarity).
dt_gui_box_add(self->widget, gui_data->white_exposure_picker);A hue slider displaying a color spectrum background using gradient stops.
g->hue = dt_bauhaus_slider_from_params(self, "hue");
// Disable the moving indicator bar (not useful over a rainbow)
dt_bauhaus_slider_set_feedback(g->hue, 0);
// Internal 0.0-1.0 displayed as 0°-360°
dt_bauhaus_slider_set_factor(g->hue, 360.0f);
dt_bauhaus_slider_set_format(g->hue, "°");
// Add color stops for rainbow gradient (position, R, G, B)
// Position is normalized 0.0-1.0, colors are 0.0-1.0
dt_bauhaus_slider_set_stop(g->hue, 0.000f, 1.0f, 0.0f, 0.0f); // Red
dt_bauhaus_slider_set_stop(g->hue, 0.166f, 1.0f, 1.0f, 0.0f); // Yellow
dt_bauhaus_slider_set_stop(g->hue, 0.333f, 0.0f, 1.0f, 0.0f); // Green
dt_bauhaus_slider_set_stop(g->hue, 0.500f, 0.0f, 1.0f, 1.0f); // Cyan
dt_bauhaus_slider_set_stop(g->hue, 0.666f, 0.0f, 0.0f, 1.0f); // Blue
dt_bauhaus_slider_set_stop(g->hue, 0.833f, 1.0f, 0.0f, 1.0f); // Magenta
dt_bauhaus_slider_set_stop(g->hue, 1.000f, 1.0f, 0.0f, 0.0f); // Red (wrap)For a saturation slider paired with a hue slider, the gradient should update dynamically to show grey at 0% and the current hue's saturated color at 100%.
// In gui_init(): Set initial placeholder stops
g->saturation = dt_bauhaus_slider_from_params(self, "saturation");
dt_bauhaus_slider_set_format(g->saturation, "%");
dt_bauhaus_slider_set_digits(g->saturation, 1);
// Initial stops (will be updated dynamically)
dt_bauhaus_slider_set_stop(g->saturation, 0.0f, 0.2f, 0.2f, 0.2f); // Gray
dt_bauhaus_slider_set_stop(g->saturation, 1.0f, 1.0f, 0.0f, 0.0f); // Red (placeholder)Then create a helper function to update the gradient based on the current hue:
// Helper to update saturation slider gradient based on hue
static inline void update_saturation_slider_end_color(GtkWidget *slider, float hue)
{
dt_aligned_pixel_t rgb;
hsl2rgb(rgb, hue, 1.0, 0.5); // Full saturation, mid lightness
dt_bauhaus_slider_set_stop(slider, 1.0, rgb[0], rgb[1], rgb[2]);
}
void gui_changed(dt_iop_module_t *self, GtkWidget *w, void *previous)
{
dt_iop_mymodule_params_t *p = self->params;
dt_iop_mymodule_gui_data_t *g = self->gui_data;
if(w == g->hue)
{
update_saturation_slider_end_color(g->saturation, p->hue);
gtk_widget_queue_draw(g->saturation); // Force redraw
}
}
void gui_update(dt_iop_module_t *self)
{
dt_iop_mymodule_params_t *p = self->params;
dt_iop_mymodule_gui_data_t *g = self->gui_data;
// Update gradient to match current hue
update_saturation_slider_end_color(g->saturation, p->hue);
gui_changed(self, NULL, NULL);
}For a fixed-hue saturation slider (no accompanying hue control), use static stops:
// Grey to red (fixed hue = 0)
dt_bauhaus_slider_set_stop(g->saturation, 0.0f, 0.5f, 0.5f, 0.5f); // Gray
dt_bauhaus_slider_set_stop(g->saturation, 1.0f, 1.0f, 0.0f, 0.0f); // Red
// Grey to cyan (fixed hue = 0.5)
dt_bauhaus_slider_set_stop(g->saturation, 0.0f, 0.5f, 0.5f, 0.5f); // Gray
dt_bauhaus_slider_set_stop(g->saturation, 1.0f, 0.0f, 1.0f, 1.0f); // CyanA slider showing desaturated-to-saturated gradient for a specific hue (e.g., red at 0°):
static GtkWidget *setup_purity_slider(dt_iop_module_t *self,
const char *param_name,
const float hue_deg,
const gboolean attenuate)
{
GtkWidget *slider = dt_bauhaus_slider_from_params(self, param_name);
dt_bauhaus_slider_set_feedback(slider, 0); // Hide bar, gradient shows value
dt_bauhaus_slider_set_format(slider, "%");
dt_bauhaus_slider_set_digits(slider, 2);
dt_bauhaus_slider_set_factor(slider, 100.f);
// Paint gradient using all stops
dt_aligned_pixel_t hsv, rgb;
for(int stop = 0; stop < DT_BAUHAUS_SLIDER_MAX_STOPS; stop++)
{
const float pos = (float)stop / (float)(DT_BAUHAUS_SLIDER_MAX_STOPS - 1);
hsv[0] = hue_deg / 360.0f;
// attenuate: full color at 0%, grey at 100% (desaturation slider)
// !attenuate: grey at 0%, full color at 100% (saturation slider)
hsv[1] = attenuate ? 1.0f - pos : pos;
hsv[2] = 1.0f;
dt_HSV_2_RGB(hsv, rgb);
dt_bauhaus_slider_set_stop(slider, pos, rgb[0], rgb[1], rgb[2]);
}
return slider;
}
// Usage:
g->red_inset = setup_purity_slider(self, "red_inset", 0.0f, TRUE); // Red desaturation
g->green_outset = setup_purity_slider(self, "green_outset", 120.0f, FALSE); // Green saturationA slider showing hue range centered on a primary color (e.g., red ±60°):
static GtkWidget *setup_hue_rotation_slider(dt_iop_module_t *self,
const char *param_name,
const float center_hue_deg,
const gboolean reverse)
{
GtkWidget *slider = dt_bauhaus_slider_from_params(self, param_name);
dt_bauhaus_slider_set_feedback(slider, 0);
dt_bauhaus_slider_set_format(slider, "°");
dt_bauhaus_slider_set_digits(slider, 1);
dt_bauhaus_slider_set_factor(slider, RAD_2_DEG); // Internal radians, display degrees
const float hue_range_deg = 60.0f;
dt_aligned_pixel_t hsv, rgb;
for(int stop = 0; stop < DT_BAUHAUS_SLIDER_MAX_STOPS; stop++)
{
const float pos = (float)stop / (float)(DT_BAUHAUS_SLIDER_MAX_STOPS - 1);
// Offset from -60° to +60° across slider
float hue_offset = -hue_range_deg + pos * (2.0f * hue_range_deg);
if(reverse) hue_offset = -hue_offset;
hsv[0] = fmodf(center_hue_deg + hue_offset + 360.0f, 360.0f) / 360.0f;
hsv[1] = 0.7f;
hsv[2] = 1.0f;
dt_HSV_2_RGB(hsv, rgb);
dt_bauhaus_slider_set_stop(slider, pos, rgb[0], rgb[1], rgb[2]);
}
return slider;
}
// Usage:
g->red_rotation = setup_hue_rotation_slider(self, "red_rotation", 0.0f, FALSE);
g->green_rotation = setup_hue_rotation_slider(self, "green_rotation", 120.0f, FALSE);
g->blue_rotation = setup_hue_rotation_slider(self, "blue_rotation", 240.0f, FALSE);The slider normally shows a filled bar indicating the current position. This can be disabled:
// 0 = no feedback bar, 1 = show feedback bar (default)
dt_bauhaus_slider_set_feedback(widget, 0);Use this for sliders where the gradient background conveys the value (like hue sliders) or where the bar would be visually distracting.
Add up to DT_BAUHAUS_SLIDER_MAX_STOPS (20) color stops to create a gradient background:
// dt_bauhaus_slider_set_stop(widget, position, red, green, blue)
// - position: 0.0 to 1.0 (normalized slider range)
// - red/green/blue: 0.0 to 1.0
dt_bauhaus_slider_set_stop(slider, 0.0f, 0.0f, 0.0f, 0.0f); // Black at start
dt_bauhaus_slider_set_stop(slider, 1.0f, 1.0f, 1.0f, 1.0f); // White at end
// To remove all stops and return to default appearance:
dt_bauhaus_slider_clear_stops(slider);Soft vs Hard Range Alignment: The gradient is painted across the hard range, but users
typically see the soft range. If your soft range differs from hard range, scale stop positions
accordingly (see agx.c _paint_slider_gradient() for a complete example):
const float soft_min = dt_bauhaus_slider_get_soft_min(slider);
const float soft_max = dt_bauhaus_slider_get_soft_max(slider);
const float hard_min = dt_bauhaus_slider_get_hard_min(slider);
const float hard_max = dt_bauhaus_slider_get_hard_max(slider);
// For each stop position (0.0-1.0 in soft range):
const float value_in_soft = soft_min + position * (soft_max - soft_min);
const float normalized_pos = (value_in_soft - hard_min) / (hard_max - hard_min);
dt_bauhaus_slider_set_stop(slider, normalized_pos, r, g, b);For sliders where linear movement doesn't match perception (e.g., exposure), you can apply a transformation curve:
// Use logarithmic curve (common for exposure-like values)
dt_bauhaus_slider_set_log_curve(widget);
// Or provide a custom curve function:
// The function maps normalized position (0-1) to/from value
static float my_curve(float value, dt_bauhaus_curve_t dir)
{
if(dir == DT_BAUHAUS_SET)
return /* transform value for display position */;
else // DT_BAUHAUS_GET
return /* transform display position back to value */;
}
dt_bauhaus_slider_set_curve(widget, my_curve);Control the increment when using keyboard arrows or mouse scroll:
// Default is (soft_max - soft_min) / 100
dt_bauhaus_slider_set_step(widget, 0.1f);