Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions data/darktableconfig.xml.in
Original file line number Diff line number Diff line change
Expand Up @@ -2440,6 +2440,13 @@
<shortdescription>enable touchpad gestures in darkroom</shortdescription>
<longdescription><![CDATA[use two-finger touchpad gestures in darkroom for panning and pinch-to-zoom. <b>enabled:</b> touchpad pinch gestures zoom the image and two-finger touchpad scrolling pans it; <i>Ctrl+scroll</i> still uses the legacy zoom behavior. <b>disabled:</b> touchpad gestures are ignored and darkroom falls back to the legacy scroll behavior, including <i>Ctrl+scroll</i> for zooming in and out.]]></longdescription>
</dtconfig>
<dtconfig prefs="misc" section="interface">
<name>darkroom/ui/constrain_zoom</name>
<type>bool</type>
<default>true</default>
<shortdescription>constrain darkroom zoom between screen fit and 100%</shortdescription>
<longdescription><![CDATA[limits scroll-wheel and pinch-to-zoom in the darkroom to the range between screen fit (zoom out) and 100% (zoom in). <b>enabled:</b> zoom is constrained; hold <i>Ctrl</i> while zooming to temporarily lift the limit and zoom beyond 100%. <b>disabled:</b> zoom is never constrained and the additional <i>Ctrl</i> key press is not needed to zoom beyond 100%.]]></longdescription>
</dtconfig>
<dtconfig prefs="darkroom" section="general">
<name>plugins/darkroom/ui/border_size</name>
<type>int</type>
Expand Down
116 changes: 72 additions & 44 deletions src/develop/develop.c
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,8 @@ void dt_dev_init(dt_develop_t *dev,
dev->full.color_assessment = dt_conf_get_bool("full_window/color_assessment");
dev->preview2.color_assessment = dt_conf_get_bool("second_window/color_assessment");

dev->constrain_zoom = dt_conf_get_bool("darkroom/ui/constrain_zoom");

dev->full.zoom = dev->preview2.zoom = DT_ZOOM_FIT;
dev->full.closeup = dev->preview2.closeup = 0;
dev->full.zoom_x = dev->full.zoom_y = dev->preview2.zoom_x = dev->preview2.zoom_y = 0.0f;
Expand Down Expand Up @@ -2847,11 +2849,20 @@ gboolean dt_dev_get_processed_size(dt_dev_viewport_t *port,
return FALSE;
}

static float _calculate_new_scroll_zoom_tscale(const int up,
const gboolean constrained,
const float tscaleold,
const float tscalefit)
// Compute the zoom soft limits for a given old tscale.
// Shared between scroll-step zoom and continuous (pinch) zoom so both honor
// the same constrain semantics: cap at 100%/200%/top depending on where the
// previous scale sits, with CTRL clearing `constrained` upstream as the
// escape hatch.
static void _zoom_constraint_bounds(const gboolean constrained,
const float tscaleold,
const float tscalefit,
float *tscalemin,
float *tscalemax)
{
const float tscaletop = 16.0f;
const float tscalefloor = MIN(0.5f * tscalefit, 1.0f);

enum {
SIZE_SMALL,
SIZE_MEDIUM,
Expand All @@ -2865,69 +2876,72 @@ static float _calculate_new_scroll_zoom_tscale(const int up,
else
image_size = SIZE_SMALL;

// at 200% zoom level or more, we use a step of 2x, while at lower level we use 1.1x
const float step =
up
? (tscaleold >= 2.0f ? 2.0f : 1.1f)
: (tscaleold > 2.0f ? 2.0f : 1.1f);

// we calculate the new scale
float tscalenew = up ? tscaleold * step : tscaleold / step;

// when zooming, secure we include 2:1, 1:1 and FIT levels anyway in the zoom stops
if((tscalenew - tscalefit) * (tscaleold - tscalefit) < 0 && image_size != SIZE_SMALL)
tscalenew = tscalefit;
else if((tscalenew - 1.0f) * (tscaleold - 1.0f) < 0)
tscalenew = 1.0f;
else if((tscalenew - 2.0f) * (tscaleold - 2.0f) < 0)
tscalenew = 2.0f;

float tscalemax, tscalemin; // the zoom soft limits
const float tscaletop = 16.0f; // the zoom hard limits
const float tscalefloor = MIN(0.5f * tscalefit, 1.0f);

switch (image_size) // here we set the logic of zoom limits
{
switch(image_size)
{
case SIZE_LARGE:
tscalemax = constrained
? (tscaleold >= 2.0f
*tscalemax = constrained
? (tscaleold > 2.0f
? tscaletop
: (tscaleold >= 1.0f ? 2.0f : 1.0f))
: (tscaleold > 1.0f ? 2.0f : 1.0f))
: tscaletop;
tscalemin = constrained
*tscalemin = constrained
? (tscaleold < tscalefit
? tscalefloor
: tscalefit)
: tscalefloor;
break;
case SIZE_MEDIUM:
tscalemax = constrained
? (tscaleold >= 2.0f
*tscalemax = constrained
? (tscaleold > 2.0f
? tscaletop
: 2.0f)
: tscaletop;
tscalemin = constrained
*tscalemin = constrained
? (tscaleold < tscalefit
? tscalefloor
: tscalefit)
: tscalefloor;
break;
case SIZE_SMALL:
tscalemax = constrained
? (tscaleold >= 2.0f
*tscalemax = constrained
? (tscaleold > 2.0f
? tscaletop
: tscalefit)
: tscaletop;
tscalemin = tscalefloor;
*tscalemin = tscalefloor;
break;
}
}
}

static float _calculate_new_scroll_zoom_tscale(const int up,
const gboolean constrained,
const float tscaleold,
const float tscalefit)
{
// at 200% zoom level or more, we use a step of 2x, while at lower level we use 1.1x
const float step =
up
? (tscaleold >= 2.0f ? 2.0f : 1.1f)
: (tscaleold > 2.0f ? 2.0f : 1.1f);

// we calculate the new scale
float tscalenew = up ? tscaleold * step : tscaleold / step;

// when zooming, secure we include 2:1, 1:1 and FIT levels anyway in the zoom stops
const gboolean is_small = tscalefit > 2.0f;
if((tscalenew - tscalefit) * (tscaleold - tscalefit) < 0 && !is_small)
tscalenew = tscalefit;
else if((tscalenew - 1.0f) * (tscaleold - 1.0f) < 0)
tscalenew = 1.0f;
else if((tscalenew - 2.0f) * (tscaleold - 2.0f) < 0)
tscalenew = 2.0f;

// we enforce the zoom limits
tscalenew = up
float tscalemin, tscalemax;
_zoom_constraint_bounds(constrained, tscaleold, tscalefit, &tscalemin, &tscalemax);

return up
? MIN(tscalenew, tscalemax)
: MAX(tscalenew, tscalemin);

return tscalenew;
}

static char *_transform_type(const dt_dev_transform_direction_t transf_direction)
Expand Down Expand Up @@ -3105,13 +3119,13 @@ void dt_dev_zoom_move(dt_dev_viewport_t *port,
else if(zoom == DT_ZOOM_SCROLL)
{
zoom = DT_ZOOM_FREE;
const float fitscale = dt_dev_get_zoom_scale(port, DT_ZOOM_FIT, 1.0, FALSE);
const float fitscale = dt_dev_get_zoom_scale(port, DT_ZOOM_FIT, 1, FALSE);
const float tscaleold = cur_scale * ppd;
const float tscale = _calculate_new_scroll_zoom_tscale (closeup, constrain, tscaleold, fitscale * ppd);
scale = tscale / ppd;

closeup = 0;
if(tscale < 1.9999)
if(tscale < 1.9999f)
scale = tscale / ppd;
else
{
Expand Down Expand Up @@ -3144,6 +3158,20 @@ void dt_dev_zoom_move(dt_dev_viewport_t *port,
zoom_y = dev->full_preview_last_zoom_y;
scale = port->zoom_scale;
}
else if(zoom == DT_ZOOM_FREE && constrain)
{
// Continuous zoom (pinch): apply the same soft caps as scroll. Using
// the current scale as tscaleold means once we clamp at 100%, the next
// frame still sees tscaleold == 1.0 and stays clamped; if a prior CTRL
// frame lifted us past 1.0, the released-CTRL frame sees tscaleold > 1.0
// and progresses up to the 200% cap — matching the scroll escape hatch.
const float fitscale = dt_dev_get_zoom_scale(port, DT_ZOOM_FIT, 1, FALSE);
const float tscaleold = cur_scale * ppd;
float tscalemin, tscalemax;
_zoom_constraint_bounds(TRUE, tscaleold, fitscale * ppd,
&tscalemin, &tscalemax);
scale = CLAMP(scale * ppd, tscalemin, tscalemax) / ppd;
}

port->closeup = closeup;
port->zoom_scale = scale;
Expand Down
2 changes: 2 additions & 0 deletions src/develop/develop.h
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,7 @@ typedef struct dt_develop_t
struct
{
GtkWidget *floating_window, *softproof_button, *gamut_button; // TODO (#18559): remove gtk stuff from here
GtkWidget *display_intent_widget, *display2_intent_widget; // TODO (#18559): remove gtk stuff from here
} profile;

GtkWidget *second_wnd, *second_wnd_button; // TODO (#18559): remove gtk stuff from here
Expand All @@ -357,6 +358,7 @@ typedef struct dt_develop_t
int mask_form_selected_id; // select a mask inside an iop
gboolean darkroom_skip_mouse_events; // skip mouse events for masks
gboolean darkroom_mouse_in_center_area; // TRUE if the mouse cursor is in center area
gboolean constrain_zoom; // cached value of "darkroom/ui/constrain_zoom" pref

GList *module_filter_out;

Expand Down
27 changes: 12 additions & 15 deletions src/gui/gtk.c
Original file line number Diff line number Diff line change
Expand Up @@ -727,20 +727,13 @@ static gboolean _draw(GtkWidget *da,
}

static GdkDevice *_touchpad = NULL;
static gboolean _touchpad_gestures_enabled(void)

static void _touchpad_gestures_pref_changed(gpointer instance,
gpointer user_data)
{
// If conf_gen.h was built before darktableconfig.xml.in gained this key
// (incremental build without cmake reconfigure), dt_confgen_value_exists
// returns FALSE and dt_conf_get_bool gets an empty string → FALSE.
// Default to enabled in that case so a stale build doesn't silently break gestures.
if(!dt_confgen_value_exists("darkroom/ui/touchpad_gestures", DT_DEFAULT))
{
dt_print(DT_DEBUG_INPUT,
"[touchpad] 'darkroom/ui/touchpad_gestures' missing from confgen"
" (stale conf_gen.h — run cmake reconfigure), defaulting to enabled");
return TRUE;
}
return dt_conf_get_bool("darkroom/ui/touchpad_gestures");
(void)instance;
dt_gui_gtk_t *gui = user_data;
gui->touchpad_gestures_enabled = dt_conf_get_bool("darkroom/ui/touchpad_gestures");
}

static gboolean _input_event(GtkWidget *widget,
Expand Down Expand Up @@ -773,7 +766,7 @@ static gboolean _input_event(GtkWidget *widget,
break;
}

if(event->type == GDK_TOUCHPAD_PINCH && _touchpad_gestures_enabled())
if(event->type == GDK_TOUCHPAD_PINCH && darktable.gui->touchpad_gestures_enabled)
{
const GdkEventTouchpadPinch *pinch = &event->touchpad_pinch;
dt_print(DT_DEBUG_INPUT,
Expand Down Expand Up @@ -805,7 +798,7 @@ static gboolean _scrolled(GtkWidget *widget,
{
(void)user_data;
GdkDevice *device = gdk_event_get_source_device((GdkEvent *)event);
const gboolean touchpad_enabled = _touchpad_gestures_enabled();
const gboolean touchpad_enabled = darktable.gui->touchpad_gestures_enabled;
const gboolean ctrl_pressed = dt_modifier_is(event->state, GDK_CONTROL_MASK);

dt_print(DT_DEBUG_INPUT,
Expand Down Expand Up @@ -1515,6 +1508,10 @@ int dt_gui_gtk_init(dt_gui_gtk_t *gui)
// Init focus peaking
gui->show_focus_peaking = dt_conf_get_bool("ui/show_focus_peaking");

gui->touchpad_gestures_enabled = TRUE;
DT_CONTROL_SIGNAL_CONNECT(DT_SIGNAL_PREFERENCES_CHANGE,
_touchpad_gestures_pref_changed, gui);

/* Have the delete event (window close) end the program */
snprintf(path, sizeof(path), "%s/icons", datadir);
gtk_icon_theme_append_search_path(gtk_icon_theme_get_default(), path);
Expand Down
1 change: 1 addition & 0 deletions src/gui/gtk.h
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ typedef struct dt_gui_gtk_t

gboolean show_overlays;
gboolean show_focus_peaking;
gboolean touchpad_gestures_enabled;
double overlay_red, overlay_blue, overlay_green, overlay_contrast;
GtkWidget *focus_peaking_button;

Expand Down
5 changes: 4 additions & 1 deletion src/libs/navigation.c
Original file line number Diff line number Diff line change
Expand Up @@ -466,7 +466,10 @@ static void _zoom_changed(GtkWidget *widget, gpointer user_data)
else
scale = val / 100.0f * ppd;

dt_dev_zoom_move(port, zoom, scale, closeup, -1.0f, -1.0f, TRUE);
// Preset zoom picks (small / 50% / custom %) take DT_ZOOM_FREE with an
// explicit scale — don't run them through the constrain soft caps.
dt_dev_zoom_move(port, zoom, scale, closeup, -1.0f, -1.0f,
zoom != DT_ZOOM_FREE);
}

static gboolean _lib_navigation_widget_to_center(GtkEventController *controller,
Expand Down
Loading
Loading