diff --git a/src/dtgtk/culling.c b/src/dtgtk/culling.c index ace2129344b5..e5bcc122e6ab 100644 --- a/src/dtgtk/culling.c +++ b/src/dtgtk/culling.c @@ -324,21 +324,11 @@ static void _set_table_zoom_ratio(dt_culling_t *table, dt_thumbnail_t *th) table->zoom_ratio = dt_thumbnail_get_zoom_ratio(th); } -static void _get_root_offset(GtkWidget *w_image_box, - const float x_root, - const float y_root, - int *x_offset, - int *y_offset) -{ - gdk_window_get_origin(gtk_widget_get_window(w_image_box), x_offset, y_offset); - *x_offset = x_root - *x_offset; - *y_offset = y_root - *y_offset; -} - static gboolean _zoom_and_shift(dt_thumbnail_t *th, const int x_offset, const int y_offset, - const float zoom_delta) + const float zoom_delta, + const gboolean deferred) { const float zd = CLAMP(th->zoom + zoom_delta, 1.0f, th->zoom_100); if(zd == th->zoom) @@ -352,39 +342,62 @@ static gboolean _zoom_and_shift(dt_thumbnail_t *th, const int iw = gtk_widget_get_allocated_width(th->w_image); const int ih = gtk_widget_get_allocated_height(th->w_image); + const int box_w = gtk_widget_get_allocated_width(th->w_image_box); + const int box_h = gtk_widget_get_allocated_height(th->w_image_box); + + dt_print(DT_DEBUG_INPUT, + "[culling _zoom_and_shift] offset=(%d,%d) iw=%d ih=%d box=(%d,%d)" + " zoom=%.3f->%.3f z_ratio=%.4f", + posx, posy, iw, ih, box_w, box_h, th->zoom / z_ratio, zd, z_ratio); // we center the zoom around cursor position if(posx >= 0 && posy >= 0) { // we take in account that the image may be smaller that the imagebox - posx -= (gtk_widget_get_allocated_width(th->w_image_box) - iw) / 2; - posy -= (gtk_widget_get_allocated_height(th->w_image_box) - ih) / 2; + posx -= (box_w - iw) / 2; + posy -= (box_h - ih) / 2; } + dt_print(DT_DEBUG_INPUT, + "[culling _zoom_and_shift] posx_after_center=%d posy_after_center=%d" + " old_zoomx=%.1f old_zoomy=%.1f", + posx, posy, th->zoomx, th->zoomy); + // we change the value. Values will be sanitized in the drawing event th->zoomx = posx - (posx - th->zoomx) * z_ratio; th->zoomy = posy - (posy - th->zoomy) * z_ratio; - dt_thumbnail_image_refresh(th); + dt_print(DT_DEBUG_INPUT, + "[culling _zoom_and_shift] new_zoomx=%.1f new_zoomy=%.1f", + th->zoomx, th->zoomy); + + if(deferred) + // Fast path: reuse existing surface via cairo scale transform in _thumb_draw_image. + // dt_culling_zoom_end() will trigger the proper reload when the gesture finishes. + dt_thumbnail_image_preview_zoom(th); + else + // Full reload: preserves the native mipmap surface cache so the expensive color + // conversion is skipped when zoom stays in the same mipmap bucket. + dt_thumbnail_image_refresh_zoom(th); return TRUE; } static gboolean _zoom_to_x_root(dt_thumbnail_t *th, - const float x_root, - const float y_root, - const float zoom_delta) + const float x_culling, + const float y_culling, + const float zoom_delta, + const gboolean deferred) { - int x_offset = 0; - int y_offset = 0; - - _get_root_offset(th->w_image_box, x_root, y_root, &x_offset, &y_offset); + const int posx = x_culling - th->x; + const int posy = y_culling - th->y; - return _zoom_and_shift(th, x_offset, y_offset, zoom_delta); + return _zoom_and_shift(th, posx, posy, zoom_delta, deferred); } static gboolean _zoom_to_center(dt_thumbnail_t *th, - const float zoom_delta) + const float zoom_delta, + const gboolean deferred) { const float zd = CLAMP(th->zoom + zoom_delta, 1.0f, th->zoom_100); if(zd == th->zoom) @@ -396,21 +409,34 @@ static gboolean _zoom_to_center(dt_thumbnail_t *th, int iw = 0; int ih = 0; gtk_widget_get_size_request(th->w_image_box, &iw, &ih); - th->zoomx = fmaxf(iw - th->img_width * z_ratio, + // Compute the bound from the new zoom directly. img_width * z_ratio assumes + // the surface is at the previous zoom, but during deferred pinch zoom the + // surface stays at the gesture-start zoom and z_ratio is only the per-step + // ratio — so img_width * z_ratio under-estimates the effective width and the + // image progressively drifts toward the top-left corner. + int img_w = 0, img_h = 0; + gtk_widget_get_size_request(th->w_image, &img_w, &img_h); + const float effective_w = (float)img_w * darktable.gui->ppd_thb * zd; + const float effective_h = (float)img_h * darktable.gui->ppd_thb * zd; + th->zoomx = fmaxf((float)iw - effective_w, fminf(0.0f, iw / 2.0 - (iw / 2.0 - th->zoomx) * z_ratio)); - th->zoomy = fmaxf(ih - th->img_height * z_ratio, + th->zoomy = fmaxf((float)ih - effective_h, fminf(0.0f, ih / 2.0 - (ih / 2.0 - th->zoomy) * z_ratio)); - dt_thumbnail_image_refresh(th); + if(deferred) + dt_thumbnail_image_preview_zoom(th); + else + dt_thumbnail_image_refresh_zoom(th); return TRUE; } static gboolean _thumbs_zoom_add(dt_culling_t *table, const float zoom_delta, - const float x_root, - const float y_root, - const int state) + const float x_culling, + const float y_culling, + const int state, + const gboolean deferred) { const int max_in_memory_images = _get_max_in_memory_images(); if(table->mode == DT_CULLING_MODE_CULLING && table->thumbs_count > max_in_memory_images) @@ -438,7 +464,7 @@ static gboolean _thumbs_zoom_add(dt_culling_t *table, dt_thumbnail_t *th = l->data; if(th->imgid == mouseid) { - if(_zoom_to_x_root(th, x_root, y_root, zoom_delta)) + if(_zoom_to_x_root(th, x_culling, y_culling, zoom_delta, deferred)) _set_table_zoom_ratio(table, th); break; } @@ -446,30 +472,42 @@ static gboolean _thumbs_zoom_add(dt_culling_t *table, } else { + // Synchronized zoom: compute the focal point in the cursor thumb's + // image-local coordinates, then apply that same image-local focal point + // to every thumbnail so all views zoom around the equivalent location. const dt_imgid_t mouseid = dt_control_get_mouse_over_id(); - int x_offset = 0; - int y_offset = 0; - gboolean to_pointer = FALSE; - - // get the offset for the image under the cursor + dt_thumbnail_t *cursor_th = NULL; for(GList *l = table->list; l; l = g_list_next(l)) { - const dt_thumbnail_t *th = l->data; + dt_thumbnail_t *th = l->data; if(th->imgid == mouseid) { - _get_root_offset(th->w_image_box, x_root, y_root, &x_offset, &y_offset); - to_pointer = TRUE; + cursor_th = th; break; } } - // apply the offset to all images - for(GList *l = table->list; l; l = g_list_next(l)) + if(cursor_th) { - dt_thumbnail_t *th = l->data; - if(to_pointer == TRUE ? _zoom_and_shift(th, x_offset, y_offset, zoom_delta) - : _zoom_to_center(th, zoom_delta)) - _set_table_zoom_ratio(table, th); + const int posx = x_culling - cursor_th->x; + const int posy = y_culling - cursor_th->y; + for(GList *l = table->list; l; l = g_list_next(l)) + { + dt_thumbnail_t *th = l->data; + if(_zoom_and_shift(th, posx, posy, zoom_delta, deferred)) + _set_table_zoom_ratio(table, th); + } + } + else + { + // No image under cursor (e.g. gesture outside any thumbnail) — + // fall back to zooming all thumbnails toward their centers. + for(GList *l = table->list; l; l = g_list_next(l)) + { + dt_thumbnail_t *th = l->data; + if(_zoom_to_center(th, zoom_delta, deferred)) + _set_table_zoom_ratio(table, th); + } } } } @@ -477,7 +515,7 @@ static gboolean _thumbs_zoom_add(dt_culling_t *table, { // FULL PREVIEW or CULLING with 1 image dt_thumbnail_t *th = table->list->data; - if(_zoom_to_x_root(th, x_root, y_root, zoom_delta)) + if(_zoom_to_x_root(th, x_culling, y_culling, zoom_delta, deferred)) _set_table_zoom_ratio(table, th); } @@ -489,21 +527,21 @@ static void _zoom_thumb_fit(dt_thumbnail_t *th) th->zoom = 1.0; th->zoomx = 0; th->zoomy = 0; - dt_thumbnail_image_refresh(th); + dt_thumbnail_image_refresh_zoom(th); } static gboolean _zoom_thumb_max(dt_thumbnail_t *th, - const float x_root, - const float y_root) + const float x_culling, + const float y_culling) { dt_thumbnail_get_zoom100(th); - return _zoom_to_x_root(th, x_root, y_root, ZOOM_MAX); + return _zoom_to_x_root(th, x_culling, y_culling, ZOOM_MAX, FALSE); } // toggle zoom max / zoom fit of image currently having mouse over id static void _toggle_zoom_current(dt_culling_t *table, - const float x_root, - const float y_root) + const float x_culling, + const float y_culling) { const dt_imgid_t id = dt_control_get_mouse_over_id(); for(GList *l = table->list; l; l = g_list_next(l)) @@ -512,7 +550,7 @@ static void _toggle_zoom_current(dt_culling_t *table, if(th->imgid == id) { if(th->zoom_100 < 1.0 || th->zoom < th->zoom_100) - _zoom_thumb_max(th, x_root, y_root); + _zoom_thumb_max(th, x_culling, y_culling); else _zoom_thumb_fit(th); break; @@ -522,8 +560,8 @@ static void _toggle_zoom_current(dt_culling_t *table, // toggle zoom max / zoom fit of all images in culling table static void _toggle_zoom_all(dt_culling_t *table, - const float x_root, - const float y_root) + const float x_culling, + const float y_culling) { gboolean zmax = TRUE; for(GList *l = table->list; l; l = g_list_next(l)) @@ -539,7 +577,7 @@ static void _toggle_zoom_all(dt_culling_t *table, if(zmax) dt_culling_zoom_fit(table); else - _thumbs_zoom_add(table, ZOOM_MAX, x_root, y_root, 0); + _thumbs_zoom_add(table, ZOOM_MAX, x_culling, y_culling, 0, FALSE); } static void _update_selected_thumbnail(dt_culling_t *table, @@ -561,25 +599,147 @@ static void _update_selected_thumbnail(dt_culling_t *table, } } +static gboolean _event_gesture(GtkWidget *widget, + GdkEvent *event, + gpointer user_data) +{ + if(event->type != GDK_TOUCHPAD_PINCH) return FALSE; + const GdkEventTouchpadPinch *pinch = &event->touchpad_pinch; + dt_print(DT_DEBUG_INPUT, + "[culling gesture] pinch phase=%d x=%.1f y=%.1f dx=%.3f dy=%.3f scale=%.6f state=0x%x", + pinch->phase, pinch->x, pinch->y, pinch->dx, pinch->dy, pinch->scale, pinch->state); + // Forward root (screen-absolute) coordinates — same convention as _event_scroll + // passing e->x_root, e->y_root. Using pinch->x_root avoids a manual conversion + // via gdk_window_get_origin that was producing the wrong focal point. + dt_view_manager_gesture_pinch(darktable.view_manager, + pinch->x_root, pinch->y_root, + pinch->dx, pinch->dy, + pinch->phase, + pinch->scale, + pinch->state & 0xf); + gtk_widget_queue_draw(widget); + return TRUE; +} + static gboolean _event_scroll(GtkWidget *widget, GdkEvent *event, gpointer user_data) { GdkEventScroll *e = (GdkEventScroll *)event; dt_culling_t *table = (dt_culling_t *)user_data; - int delta; + GdkDevice *device = gdk_event_get_source_device(event); + dt_print(DT_DEBUG_INPUT, + "[culling scroll] direction=%d smooth=%s stop=%s ctrl=%s" + " device='%s' source-type=%d x_root=%.1f y_root=%.1f" + " delta_x=%.3f delta_y=%.3f state=0x%x", + e->direction, + e->direction == GDK_SCROLL_SMOOTH ? "yes" : "no", + e->is_stop ? "yes" : "no", + dt_modifier_is(e->state, GDK_CONTROL_MASK) ? "yes" : "no", + device ? gdk_device_get_name(device) : "", + device ? (int)gdk_device_get_source(device) : -1, + e->x_root, e->y_root, + e->delta_x, e->delta_y, e->state); + + // ctrl + smooth scroll: apply fractional zoom on every event so the zoom + // feels seamless rather than stepped. This must come before the integer- + // accumulator path below, which would otherwise batch several events into + // a single coarse 0.5-unit step. + // + // Scroll stop event: trigger a proper surface reload now that the gesture is done. + if(e->direction == GDK_SCROLL_SMOOTH && e->is_stop + && dt_modifier_is(e->state, GDK_CONTROL_MASK)) + { + dt_culling_zoom_end(table); + return TRUE; + } + + if(e->direction == GDK_SCROLL_SMOOTH && !e->is_stop + && dt_modifier_is(e->state, GDK_CONTROL_MASK)) + { + gdouble dx = 0.0, dy = 0.0; + if(dt_gui_get_scroll_deltas(e, &dx, &dy) && (dx != 0.0 || dy != 0.0)) + { + // dt_gui_get_scroll_deltas gives the raw fractional platform delta. + // Scale so that one full unit of scroll (delta_y == 1.0) matches the + // 0.5 zoom_delta of a discrete mouse-wheel click. + const float zoom_delta = (float)(-(dx + dy) * 0.5); + // convert screen to culling coordinates + int ox = 0, oy = 0; + GdkWindow *win = gtk_widget_get_window(table->widget); + if(win) + gdk_window_get_origin(win, &ox, &oy); + const float x_culling = e->x_root - ox; + const float y_culling = e->y_root - oy; + dt_print(DT_DEBUG_INPUT, + "[culling scroll] ctrl+smooth zoom_delta=%.4f x_culling=%.1f y_culling=%.1f", + zoom_delta, x_culling, y_culling); + if(fabsf(zoom_delta) > 0.001f) + _thumbs_zoom_add(table, zoom_delta, x_culling, y_culling, e->state, TRUE); + } + return TRUE; + } + + // Smooth scroll (touchpad two-finger swipe): pan zoomed images or navigate images. + // We check before the unit-delta path so fractional smooth scroll is used for panning + // with full fidelity rather than being accumulated into integer steps. + if(e->direction == GDK_SCROLL_SMOOTH && !e->is_stop + && !dt_modifier_is(e->state, GDK_CONTROL_MASK)) + { + // Check if any thumbnail is zoomed in; if so, pan instead of navigate. + float fz = 1.0f; + for(GList *l = table->list; l; l = g_list_next(l)) + { + const dt_thumbnail_t *th = l->data; + fz = fmaxf(fz, th->zoom); + } + dt_print(DT_DEBUG_INPUT, + "[culling scroll] smooth: max_zoom=%.3f -> %s", + fz, fz > 1.0f ? "pan path" : "navigate path"); + if(fz > 1.0f) + { + gdouble dx = 0.0, dy = 0.0; + if(dt_gui_get_scroll_deltas(e, &dx, &dy) && (dx != 0.0 || dy != 0.0)) + { + // dt_gui_get_scroll_deltas returns platform-normalised fractional units; + // scale to pixel-scale (matches the factor used by the center-widget pan path). + dt_print(DT_DEBUG_INPUT, + "[culling scroll] panning dx=%.3f dy=%.3f (scaled: dx=%.1f dy=%.1f)", + dx, dy, dx * 50.0, dy * 50.0); + dt_culling_pan_move(table, (float)(-dx * 50.0), (float)(-dy * 50.0), e->state); + } + else + { + dt_print(DT_DEBUG_INPUT, "[culling scroll] smooth pan: no delta from dt_gui_get_scroll_deltas"); + } + return TRUE; + } + } + + int delta; if(dt_gui_get_scroll_unit_delta(e, &delta)) { if(dt_modifiers_include(e->state, GDK_CONTROL_MASK)) { // zooming const float zoom_delta = delta < 0 ? 0.5f : -0.5f; - _thumbs_zoom_add(table, zoom_delta, e->x_root, e->y_root, e->state); + // convert screen to culling coordinates + int ox = 0, oy = 0; + GdkWindow *win = gtk_widget_get_window(table->widget); + if(win) + gdk_window_get_origin(win, &ox, &oy); + const float x_culling = e->x_root - ox; + const float y_culling = e->y_root - oy; + dt_print(DT_DEBUG_INPUT, + "[culling scroll] ctrl+scroll zoom_delta=%.2f x_culling=%.1f y_culling=%.1f", + zoom_delta, x_culling, y_culling); + _thumbs_zoom_add(table, zoom_delta, x_culling, y_culling, e->state, TRUE); } else { const int move = delta < 0 ? -1 : 1; + dt_print(DT_DEBUG_INPUT, "[culling scroll] navigate move=%d", move); _thumbs_move(table, move); } } @@ -659,11 +819,19 @@ static gboolean _event_button_press(GtkWidget *widget, if(event->button == GDK_BUTTON_MIDDLE) { + // convert screen coordinates to culling coordinates + int ox = 0, oy = 0; + GdkWindow *win = gtk_widget_get_window(table->widget); + if(win) + gdk_window_get_origin(win, &ox, &oy); + const float x_culling = event->x_root - ox; + const float y_culling = event->y_root - oy; + // if shift is pressed, we work only with image hovered if(dt_modifier_is(event->state, GDK_SHIFT_MASK)) - _toggle_zoom_current(table, event->x_root, event->y_root); + _toggle_zoom_current(table, x_culling, y_culling); else - _toggle_zoom_all(table, event->x_root, event->y_root); + _toggle_zoom_all(table, x_culling, y_culling); return TRUE; } @@ -757,8 +925,8 @@ static gboolean _event_motion_notify(GtkWidget *widget, int iw = 0; int ih = 0; gtk_widget_get_size_request(th->w_image, &iw, &ih); - const int mindx = iw * darktable.gui->ppd_thb - th->img_width; - const int mindy = ih * darktable.gui->ppd_thb - th->img_height; + const int mindx = (int)(iw * darktable.gui->ppd_thb * (1.0f - th->zoom)); + const int mindy = (int)(ih * darktable.gui->ppd_thb * (1.0f - th->zoom)); if(th->zoomx > 0) th->zoomx = 0; if(th->zoomx < mindx) @@ -1003,11 +1171,14 @@ dt_culling_t *dt_culling_new(const dt_culling_mode_t mode) | GDK_BUTTON_RELEASE_MASK | GDK_STRUCTURE_MASK | GDK_ENTER_NOTIFY_MASK - | GDK_LEAVE_NOTIFY_MASK); + | GDK_LEAVE_NOTIFY_MASK + | GDK_TOUCHPAD_GESTURE_MASK); gtk_widget_set_app_paintable(table->widget, TRUE); gtk_widget_set_can_focus(table->widget, TRUE); + g_signal_connect(G_OBJECT(table->widget), "event", + G_CALLBACK(_event_gesture), table); g_signal_connect(G_OBJECT(table->widget), "scroll-event", G_CALLBACK(_event_scroll), table); g_signal_connect(G_OBJECT(table->widget), "draw", @@ -2037,7 +2208,7 @@ void dt_culling_zoom_max(dt_culling_t *table) x = gtk_widget_get_allocated_width(th->w_image_box) / 2.0; y = gtk_widget_get_allocated_height(th->w_image_box) / 2.0; } - _thumbs_zoom_add(table, ZOOM_MAX, x, y, 0); + _thumbs_zoom_add(table, ZOOM_MAX, x, y, 0, FALSE); } void dt_culling_zoom_fit(dt_culling_t *table) @@ -2049,6 +2220,116 @@ void dt_culling_zoom_fit(dt_culling_t *table) } } +gboolean dt_culling_zoom_add(dt_culling_t *table, + const float zoom_delta, + const float x_root, + const float y_root, + const int state) +{ + if(!table) return FALSE; + // Convert root (screen-absolute) coords to culling-widget-local, matching + // the same conversion done in the scroll handler for e->x_root / e->y_root. + int ox = 0, oy = 0; + GdkWindow *win = gtk_widget_get_window(table->widget); + if(win) gdk_window_get_origin(win, &ox, &oy); + const float x_culling = x_root - ox; + const float y_culling = y_root - oy; + return _thumbs_zoom_add(table, zoom_delta, x_culling, y_culling, state, TRUE); +} + +// Called when a zoom gesture ends (pinch END/CANCEL or scroll stop). +// For each thumbnail that has a pending deferred reload, trigger the proper +// surface reload at the current zoom level now that the gesture is done. +void dt_culling_zoom_end(dt_culling_t *table) +{ + if(!table) return; + for(GList *l = table->list; l; l = g_list_next(l)) + { + dt_thumbnail_t *th = l->data; + if(th->zoom_preview_pending) + dt_thumbnail_image_refresh_zoom(th); + // dt_thumbnail_image_refresh_zoom clears zoom_preview_pending + } +} + +gboolean dt_culling_pan_move(dt_culling_t *table, + const float dx, + const float dy, + const int state) +{ + if(!table) return FALSE; + + const int max_in_memory_images = _get_max_in_memory_images(); + if(table->mode == DT_CULLING_MODE_CULLING + && table->thumbs_count > max_in_memory_images) + { + return FALSE; + } + + // check that at least one thumbnail is zoomed in + float fz = 1.0f; + for(GList *l = table->list; l; l = g_list_next(l)) + { + const dt_thumbnail_t *th = l->data; + fz = fmaxf(fz, th->zoom); + } + + if(fz <= 1.0f) + { + return FALSE; + } + + const float scale = darktable.gui->ppd_thb / darktable.gui->ppd; + const float valx = dx * scale; + const float valy = dy * scale; + + if(dt_modifier_is(state, GDK_SHIFT_MASK)) + { + const dt_imgid_t mouseid = dt_control_get_mouse_over_id(); + for(GList *l = table->list; l; l = g_list_next(l)) + { + dt_thumbnail_t *th = l->data; + if(th->imgid == mouseid) + { + th->zoomx += valx; + th->zoomy += valy; + break; + } + } + } + else + { + for(GList *l = table->list; l; l = g_list_next(l)) + { + dt_thumbnail_t *th = l->data; + th->zoomx += valx; + th->zoomy += valy; + } + } + + // sanitize per-thumbnail pan bounds + for(GList *l = table->list; l; l = g_list_next(l)) + { + dt_thumbnail_t *th = l->data; + int iw = 0, ih = 0; + gtk_widget_get_size_request(th->w_image, &iw, &ih); + const int mindx = (int)(iw * darktable.gui->ppd_thb * (1.0f - th->zoom)); + const int mindy = (int)(ih * darktable.gui->ppd_thb * (1.0f - th->zoom)); + if(th->zoomx > 0) th->zoomx = 0; + if(th->zoomx < mindx) th->zoomx = mindx; + if(th->zoomy > 0) th->zoomy = 0; + if(th->zoomy < mindy) th->zoomy = mindy; + } + + for(GList *l = table->list; l; l = g_list_next(l)) + { + dt_thumbnail_t *th = l->data; + dt_thumbnail_image_refresh_position(th); + } + + return TRUE; +} + // change the type of overlays that should be shown void dt_culling_set_overlays_mode(dt_culling_t *table, const dt_thumbnail_overlay_t over) diff --git a/src/dtgtk/culling.h b/src/dtgtk/culling.h index 2ca57afd670d..cd3e2e8486c3 100644 --- a/src/dtgtk/culling.h +++ b/src/dtgtk/culling.h @@ -105,6 +105,17 @@ void dt_culling_change_offset_image(dt_culling_t *table, void dt_culling_zoom_max(dt_culling_t *table); void dt_culling_zoom_fit(dt_culling_t *table); +// zoom by zoom_delta (in th->zoom units) centered on culling-local (x_culling, y_culling). +// equivalent to _thumbs_zoom_add. +gboolean dt_culling_zoom_add(dt_culling_t *table, float zoom_delta, float x_culling, float y_culling, int state); +// Finalise a zoom gesture: reload surfaces at the correct resolution for thumbnails +// that were in deferred-preview mode during the gesture. +void dt_culling_zoom_end(dt_culling_t *table); + +// translate all zoomed thumbnails by (dx, dy) screen pixels. +// state is used to optionally restrict panning to the hovered image (GDK_SHIFT_MASK). +gboolean dt_culling_pan_move(dt_culling_t *table, float dx, float dy, int state); + // set the overlays type void dt_culling_set_overlays_mode(dt_culling_t *table, const dt_thumbnail_overlay_t over); diff --git a/src/dtgtk/thumbnail.c b/src/dtgtk/thumbnail.c index e73f926baf10..3cb8fcb73490 100644 --- a/src/dtgtk/thumbnail.c +++ b/src/dtgtk/thumbnail.c @@ -353,10 +353,30 @@ static void _thumb_draw_image(dt_thumbnail_t *thumb, { cairo_save(cr); const float scaler = 1.0f / darktable.gui->ppd_thb; + + // During an active zoom gesture (zoom_preview_pending) the surface may have been + // rendered at a different zoom level (zoom_rendered) than the current th->zoom. + // Compute an extra scale factor so the stale surface is displayed at the correct + // visual size without needing to reload/recreate it. When zoom_rendered == th->zoom + // (normal case) extra_scale collapses to 1.0 and the code path is identical to before. + float extra_scale = 1.0f; + if(w > 0 && thumb->img_width > 0 && thumb->zoom > 0.0f) + { + const float zoom_rendered = (float)thumb->img_width + / ((float)w * darktable.gui->ppd_thb); + if(zoom_rendered > 0.0f) + extra_scale = CLAMP(thumb->zoom / zoom_rendered, 0.1f, 20.0f); + } + + // Apply outer scaler (handles HiDPI) for frame; apply extra_scale inner for image. cairo_scale(cr, scaler, scaler); - cairo_set_source_surface(cr, thumb->img_surf, thumb->zoomx * darktable.gui->ppd, - thumb->zoomy * darktable.gui->ppd); + // Draw the image with the zoom-corrected scale. + cairo_save(cr); + cairo_scale(cr, extra_scale, extra_scale); + cairo_set_source_surface(cr, thumb->img_surf, + thumb->zoomx * darktable.gui->ppd / extra_scale, + thumb->zoomy * darktable.gui->ppd / extra_scale); // get the transparency value GdkRGBA im_color; @@ -364,8 +384,9 @@ static void _thumb_draw_image(dt_thumbnail_t *thumb, gtk_widget_get_state_flags(thumb->w_image), &im_color); cairo_paint_with_alpha(cr, im_color.alpha); + cairo_restore(cr); - // and eventually the image border + // and eventually the image border (at scaler-only, not affected by extra_scale) gtk_render_frame(context, cr, 0, 0, w * darktable.gui->ppd_thb, h * darktable.gui->ppd_thb); @@ -744,10 +765,16 @@ static gboolean _event_image_draw(GtkWidget *widget, if(zoom100 > 1.0f) thumb->zoom = MIN(thumb->zoom, zoom100); } - res = dt_view_image_get_surface(thumb->imgid, - image_w * thumb->zoom, - image_h * thumb->zoom, - &img_surf, FALSE); + // Use the cached variant: on zoom events where image content + // hasn't changed, this reuses the native-resolution + // color-converted surface and only re-scales it, skipping the + // expensive mipmap fetch + calloc + color transform. + res = dt_view_image_get_surface_cached(thumb->imgid, + image_w * thumb->zoom, + image_h * thumb->zoom, + &img_surf, FALSE, + &thumb->img_surf_mip_native, + &thumb->img_surf_mip_level); } else { @@ -1223,8 +1250,12 @@ static void _dt_preview_updated_callback(gpointer instance, || darktable.develop->preview_pipe->output_imgid == thumb->imgid) && darktable.develop->preview_pipe->backbuf) { - // reset surface + // reset surface and invalidate native mipmap cache (content changed) thumb->img_surf_dirty = TRUE; + if(thumb->img_surf_mip_native + && cairo_surface_get_reference_count(thumb->img_surf_mip_native) > 0) + cairo_surface_destroy(thumb->img_surf_mip_native); + thumb->img_surf_mip_native = NULL; gtk_widget_queue_draw(thumb->w_main); } } @@ -1240,8 +1271,12 @@ static void _dt_mipmaps_updated_callback(gpointer instance, // we recompte the history tooltip if needed _thumb_update_altered_tooltip(thumb); - // reset surface + // reset surface and invalidate native mipmap cache (content changed) thumb->img_surf_dirty = TRUE; + if(thumb->img_surf_mip_native + && cairo_surface_get_reference_count(thumb->img_surf_mip_native) > 0) + cairo_surface_destroy(thumb->img_surf_mip_native); + thumb->img_surf_mip_native = NULL; gtk_widget_queue_draw(thumb->w_main); } @@ -2189,6 +2224,54 @@ void dt_thumbnail_set_drop(dt_thumbnail_t *thumb, void dt_thumbnail_image_refresh(dt_thumbnail_t *thumb) { thumb->img_surf_dirty = TRUE; + // Invalidate the native mipmap surface cache: the image content may + // have changed (edit, processing, profile change) so any cached + // color-converted surface is now stale. + if(thumb->img_surf_mip_native + && cairo_surface_get_reference_count(thumb->img_surf_mip_native) > 0) + cairo_surface_destroy(thumb->img_surf_mip_native); + thumb->img_surf_mip_native = NULL; + + // we ensure that the image is not completely outside the thumbnail, + // otherwise the image_draw is not triggered + if(gtk_widget_get_margin_start(thumb->w_image_box) >= thumb->width + || gtk_widget_get_margin_top(thumb->w_image_box) >= thumb->height) + { + gtk_widget_set_margin_start(thumb->w_image_box, 0); + gtk_widget_set_margin_top(thumb->w_image_box, 0); + } + gtk_widget_queue_draw(thumb->w_main); +} + +// force redraw for zoom-only changes: marks the surface dirty but keeps +// the native mipmap surface cache so the expensive calloc + color-transform +// is skipped on the next draw if the zoom stays in the same mipmap bucket. +void dt_thumbnail_image_refresh_zoom(dt_thumbnail_t *thumb) +{ + thumb->img_surf_dirty = TRUE; + thumb->zoom_preview_pending = FALSE; + // img_surf_mip_native is intentionally NOT cleared here — the mipmap + // content hasn't changed, only the zoom level. + + // we ensure that the image is not completely outside the thumbnail, + // otherwise the image_draw is not triggered + if(gtk_widget_get_margin_start(thumb->w_image_box) >= thumb->width + || gtk_widget_get_margin_top(thumb->w_image_box) >= thumb->height) + { + gtk_widget_set_margin_start(thumb->w_image_box, 0); + gtk_widget_set_margin_top(thumb->w_image_box, 0); + } + gtk_widget_queue_draw(thumb->w_main); +} + +// Instant zoom preview: does NOT set img_surf_dirty, so the expensive +// mipmap reload is skipped. _thumb_draw_image will scale the existing +// surface via an extra cairo transform to show the new zoom immediately. +// Call dt_culling_zoom_end() when the gesture finishes to trigger the +// proper surface reload at the final zoom level. +void dt_thumbnail_image_preview_zoom(dt_thumbnail_t *thumb) +{ + thumb->zoom_preview_pending = TRUE; // we ensure that the image is not completely outside the thumbnail, // otherwise the image_draw is not triggered @@ -2275,18 +2358,16 @@ void dt_thumbnail_set_overlay(dt_thumbnail_t *thumb, void dt_thumbnail_image_refresh_position(dt_thumbnail_t *thumb) { // let's sanitize and apply panning values - // here we have to make sure to properly align according to ppd + // Use thumb->zoom (always current) rather than img_width/img_height: during a + // deferred pinch-zoom gesture the surface may still be at zoom=1.0 while + // thumb->zoom has been advanced, so img_width-based bounds would clamp to 0 + // and discard the in-flight pan. After a surface reload these formulas are + // algebraically identical since img_width = iw * ppd_thb * zoom. int iw = 0; int ih = 0; gtk_widget_get_size_request(thumb->w_image, &iw, &ih); - thumb->zoomx = - CLAMP(thumb->zoomx, - (iw * darktable.gui->ppd_thb - thumb->img_width) / darktable.gui->ppd_thb, - 0); - thumb->zoomy = - CLAMP(thumb->zoomy, - (ih * darktable.gui->ppd_thb - thumb->img_height) / darktable.gui->ppd_thb, - 0); + thumb->zoomx = CLAMP(thumb->zoomx, iw * (1.0f - thumb->zoom), 0); + thumb->zoomy = CLAMP(thumb->zoomy, ih * (1.0f - thumb->zoom), 0); gtk_widget_queue_draw(thumb->w_main); } @@ -2376,6 +2457,11 @@ void dt_thumbnail_surface_destroy(dt_thumbnail_t *thumb) cairo_surface_destroy(thumb->img_surf); thumb->img_surf = NULL; thumb->img_surf_dirty = TRUE; + // Also free the cached native mipmap surface. + if(thumb->img_surf_mip_native + && cairo_surface_get_reference_count(thumb->img_surf_mip_native) > 0) + cairo_surface_destroy(thumb->img_surf_mip_native); + thumb->img_surf_mip_native = NULL; } void dt_thumbnail_set_selection(dt_thumbnail_t *thumb, diff --git a/src/dtgtk/thumbnail.h b/src/dtgtk/thumbnail.h index f55529164024..610f07de18b1 100644 --- a/src/dtgtk/thumbnail.h +++ b/src/dtgtk/thumbnail.h @@ -24,6 +24,7 @@ #include #include "common/darktable.h" +#include "common/mipmap_cache.h" #define MAX_STARS 5 #define IMG_TO_FIT 0.0f @@ -107,6 +108,12 @@ typedef struct cairo_surface_t *img_surf; // cached surface at exact dimensions to speed up redraw gboolean img_surf_preview; // if TRUE, the image is originated from preview pipe gboolean img_surf_dirty; // if TRUE, we need to recreate the surface on next drawing code + gboolean zoom_preview_pending; // TRUE during active gesture: draw stale surface scaled, defer surface reload + // Cache of the native-resolution (pre-scaling) color-converted mipmap surface. + // Reused across zoom events at the same mipmap level so the expensive + // calloc + color-transform is only done once per mip-level transition. + cairo_surface_t *img_surf_mip_native; + dt_mipmap_size_t img_surf_mip_level; // which mipmap level img_surf_mip_native covers GtkWidget *w_cursor; // GtkDrawingArea -- triangle to show current image(s) in filmstrip GtkWidget *w_bottom_eb; // GtkEventBox -- background of the bottom infos area (contains w_bottom) @@ -192,8 +199,17 @@ void dt_thumbnail_update_selection(dt_thumbnail_t *thumb); void dt_thumbnail_set_selection(dt_thumbnail_t *thumb, const gboolean selected); -// force image recomputing +// force image recomputing (use this when image content changes) void dt_thumbnail_image_refresh(dt_thumbnail_t *thumb); +// force image recomputing for zoom changes only: marks the surface +// dirty but preserves the native mipmap surface cache so the expensive +// color conversion step can be skipped on the next redraw when the +// zoom stays within the same mipmap bucket. +void dt_thumbnail_image_refresh_zoom(dt_thumbnail_t *thumb); +// instant zoom preview: skip surface reload, just queue a redraw and let +// _thumb_draw_image rescale the stale surface via cairo transform. +// Call dt_culling_zoom_end() to trigger the deferred surface reload. +void dt_thumbnail_image_preview_zoom(dt_thumbnail_t *thumb); // do we need to display simple overlays or extended ? void dt_thumbnail_set_overlay(dt_thumbnail_t *thumb, diff --git a/src/gui/gtk.c b/src/gui/gtk.c index e498c0c9488a..9b6bf4be98b7 100644 --- a/src/gui/gtk.c +++ b/src/gui/gtk.c @@ -746,7 +746,8 @@ static gboolean _input_event(GtkWidget *widget, if(event->type == GDK_TOUCHPAD_PINCH) { const GdkEventTouchpadPinch *pinch = &event->touchpad_pinch; - if(dt_view_manager_gesture_pinch(darktable.view_manager, pinch->x, pinch->y, + if(dt_view_manager_gesture_pinch(darktable.view_manager, pinch->x_root, pinch->y_root, + pinch->dx, pinch->dy, pinch->phase, pinch->scale, pinch->state & 0xf)) { gtk_widget_queue_draw(widget); diff --git a/src/views/darkroom.c b/src/views/darkroom.c index f06f70331db1..f175aab83716 100644 --- a/src/views/darkroom.c +++ b/src/views/darkroom.c @@ -4238,6 +4238,8 @@ gboolean gesture_pan(dt_view_t *self, gboolean gesture_pinch(dt_view_t *self, const double x, const double y, + const double dx, + const double dy, const int phase, const double scale, const int state) diff --git a/src/views/lighttable.c b/src/views/lighttable.c index 55af4bfed0af..662d3ca46539 100644 --- a/src/views/lighttable.c +++ b/src/views/lighttable.c @@ -710,6 +710,148 @@ void reset(dt_view_t *self) dt_control_set_mouse_over_id(NO_IMGID); } +// Return the active dt_culling_t for gesture dispatch: the preview widget when in +// preview mode, the culling widget when in a culling layout, NULL otherwise. +static dt_culling_t *_active_culling(const dt_library_t *lib) +{ + if(lib->preview_state) + { + return lib->preview; + } + + if(lib->current_layout == DT_LIGHTTABLE_LAYOUT_CULLING + || lib->current_layout == DT_LIGHTTABLE_LAYOUT_CULLING_DYNAMIC) + { + return lib->culling; + } + + return NULL; +} + +gboolean gesture_pan(dt_view_t *self, + const double x, + const double y, + const double dx, + const double dy, + const int state) +{ + const dt_library_t *lib = self->data; + dt_culling_t *table = _active_culling(lib); + dt_print(DT_DEBUG_INPUT, + "[lighttable pan] x=%.1f y=%.1f dx=%.3f dy=%.3f state=0x%x" + " layout=%d preview=%d table=%s", + x, y, dx, dy, state, + lib->current_layout, lib->preview_state, + table ? "active" : "NULL (not in culling/preview)"); + if(!table) return FALSE; + + const gboolean moved = dt_culling_pan_move(table, (float)dx, (float)dy, state); + dt_print(DT_DEBUG_INPUT, "[lighttable pan] dt_culling_pan_move -> %s", moved ? "moved" : "no-op"); + if(moved) + { + gtk_widget_queue_draw(table->widget); + } + + return moved; +} + +gboolean gesture_pinch(dt_view_t *self, + const double x, + const double y, + const double dx, + const double dy, + const int phase, + const double scale, + const int state) +{ + const dt_library_t *lib = self->data; + dt_culling_t *table = _active_culling(lib); + if(!table) + { + return FALSE; + } + + // prev_scale tracks the cumulative scale from the last UPDATE so we can compute + // an incremental scale ratio each event rather than needing per-thumbnail begin state. + static double prev_scale = 1.0; + + dt_print(DT_DEBUG_INPUT, + "[lighttable pinch] phase=%d x=%.1f y=%.1f dx=%.3f dy=%.3f" + " scale=%.6f prev_scale=%.6f state=0x%x layout=%d preview=%d", + phase, x, y, dx, dy, scale, prev_scale, + state, lib->current_layout, lib->preview_state); + + if(phase == GDK_TOUCHPAD_GESTURE_PHASE_BEGIN) + { + prev_scale = 1.0; + dt_print(DT_DEBUG_INPUT, "[lighttable pinch] begin -> reset prev_scale"); + return TRUE; + } + if(phase == GDK_TOUCHPAD_GESTURE_PHASE_END + || phase == GDK_TOUCHPAD_GESTURE_PHASE_CANCEL) + { + prev_scale = 1.0; + dt_print(DT_DEBUG_INPUT, "[lighttable pinch] %s", + phase == GDK_TOUCHPAD_GESTURE_PHASE_END ? "end" : "cancel"); + // Gesture is done: reload surfaces at the correct zoom resolution now. + dt_culling_zoom_end(table); + return TRUE; + } + if(phase != GDK_TOUCHPAD_GESTURE_PHASE_UPDATE) + { + dt_print(DT_DEBUG_INPUT, "[lighttable pinch] unknown phase %d -> ignored", phase); + return FALSE; + } + + gboolean changed = FALSE; + + // pan component (combined pinch+translation, from GdkEventTouchpadPinch dx/dy) + // Negate dx/dy so the gesture feels like scrolling (moving fingers right shifts the + // viewport right, i.e. the image moves left) rather than touchscreen dragging. + if(dx != 0.0 || dy != 0.0) + { + dt_print(DT_DEBUG_INPUT, "[lighttable pinch] pan component dx=%.3f dy=%.3f", dx, dy); + changed |= dt_culling_pan_move(table, (float)-dx, (float)-dy, state); + } + + // zoom component — derive an incremental zoom_delta from the scale ratio. + // Tuning: a full 2× pinch spread (cumulative scale 1.0→2.0) should cover most of + // the fit-to-100% range. sum of (scale/prev_scale - 1) over a smooth 2× pinch + // ≈ ln(2) ≈ 0.69, and zoom_100 - 1 ≈ 3–5, giving SPEED ≈ 5–7. + // + // Always advance prev_scale so the dead zone is measured against the immediately + // preceding event rather than the last zoom-fire point. This keeps the worst-case + // accumulated noise to a single event step (~0.4%), well within the 1% threshold. + const float scale_increment = (float)(scale / prev_scale) - 1.0f; + prev_scale = scale; + if(fabsf(scale_increment) > 0.01f) + { + const float zoom_delta = scale_increment * 5.0f; + dt_print(DT_DEBUG_INPUT, + "[lighttable pinch] zoom scale_increment=%.6f zoom_delta=%.4f" + " x_root=%.1f y_root=%.1f", + scale_increment, zoom_delta, x, y); + + if(dt_culling_zoom_add(table, zoom_delta, x, y, state)) + { + changed = TRUE; + } + else + { + dt_print(DT_DEBUG_INPUT, "[lighttable pinch] dt_culling_zoom_add -> no-op"); + } + } + + prev_scale = scale; + dt_print(DT_DEBUG_INPUT, "[lighttable pinch] update done changed=%d", changed); + if(changed) + { + gtk_widget_queue_draw(table->widget); + } + + return TRUE; +} + void scrollbar_changed(dt_view_t *self, const double x, diff --git a/src/views/view.c b/src/views/view.c index 5be26a02990b..04e514eea8a6 100644 --- a/src/views/view.c +++ b/src/views/view.c @@ -715,6 +715,8 @@ gboolean dt_view_manager_gesture_pan(dt_view_manager_t *vm, gboolean dt_view_manager_gesture_pinch(dt_view_manager_t *vm, const double x, const double y, + const double dx, + const double dy, const int phase, const double scale, const int state) @@ -725,7 +727,7 @@ gboolean dt_view_manager_gesture_pinch(dt_view_manager_t *vm, } else if(vm->current_view->gesture_pinch) { - return vm->current_view->gesture_pinch(vm->current_view, x, y, phase, scale, state); + return vm->current_view->gesture_pinch(vm->current_view, x, y, dx, dy, phase, scale, state); } else { @@ -776,6 +778,17 @@ dt_view_surface_value_t dt_view_image_get_surface(const dt_imgid_t imgid, const int32_t height, cairo_surface_t **surface, const gboolean quality) +{ + return dt_view_image_get_surface_cached(imgid, width, height, surface, quality, NULL, NULL); +} + +dt_view_surface_value_t dt_view_image_get_surface_cached(const dt_imgid_t imgid, + const int32_t width, + const int32_t height, + cairo_surface_t **surface, + const gboolean quality, + cairo_surface_t **mip_cache, + dt_mipmap_size_t *mip_cache_level) { const double tt = dt_get_debug_wtime(); @@ -792,6 +805,39 @@ dt_view_surface_value_t dt_view_image_get_surface(const dt_imgid_t imgid, const int32_t mipheight = height * darktable.gui->ppd; dt_mipmap_size_t mip = dt_mipmap_cache_get_matching_size(mipwidth, mipheight); + // Fast path: if the caller provides a cached native-resolution surface for + // this exact mip level, skip the expensive mipmap fetch + color conversion + // and just rescale the existing cached surface. + if(mip_cache && *mip_cache && mip_cache_level && *mip_cache_level == mip + && cairo_surface_status(*mip_cache) == CAIRO_STATUS_SUCCESS) + { + const int32_t cached_wd = cairo_image_surface_get_width(*mip_cache); + const int32_t cached_ht = cairo_image_surface_get_height(*mip_cache); + if(cached_wd > 0 && cached_ht > 0) + { + float scale = fminf(width / (float)cached_wd, + height / (float)cached_ht) * darktable.gui->ppd_thb; + const int32_t img_width = roundf(cached_wd * scale); + const int32_t img_height = roundf(cached_ht * scale); + scale = fmaxf(img_width / (float)cached_wd, img_height / (float)cached_ht); + *surface = cairo_image_surface_create(CAIRO_FORMAT_RGB24, img_width, img_height); + if(*surface && cairo_surface_status(*surface) == CAIRO_STATUS_SUCCESS) + { + cairo_t *cr = cairo_create(*surface); + cairo_scale(cr, scale, scale); + cairo_set_source_surface(cr, *mip_cache, 0, 0); + cairo_pattern_set_filter(cairo_get_source(cr), darktable.gui->filter_image); + cairo_paint(cr); + cairo_destroy(cr); + return DT_VIEW_SURFACE_OK; + } + // Surface creation failed; fall through to the full path below. + if(*surface && cairo_surface_get_reference_count(*surface) > 0) + cairo_surface_destroy(*surface); + *surface = NULL; + } + } + // if needed, we load the mimap buffer dt_mipmap_buffer_t buf; dt_mipmap_cache_get(&buf, imgid, mip, DT_MIPMAP_BEST_EFFORT, 'r'); @@ -936,6 +982,42 @@ dt_view_surface_value_t dt_view_image_get_surface(const dt_imgid_t imgid, cairo_surface_destroy(tmp_surface); cairo_destroy(cr); + + // Cache the native-resolution surface so future zoom events at the + // same mip level can skip the expensive mipmap fetch + color conversion. + // Only cache when we have the exact mip (not a fallback standin) and + // the image is not a skull/error placeholder. + if(mip_cache && mip == buf.size && buf_wd > 30) + { + if(*mip_cache && cairo_surface_get_reference_count(*mip_cache) > 0) + cairo_surface_destroy(*mip_cache); + *mip_cache = cairo_image_surface_create(CAIRO_FORMAT_RGB24, buf_wd, buf_ht); + if(*mip_cache && cairo_surface_status(*mip_cache) == CAIRO_STATUS_SUCCESS) + { + // Reconstruct the tmp_surface content from rgbbuf into the cached surface. + // We use create_for_data here (cheap, no extra alloc) since rgbbuf is still + // live at this point. + const int32_t cache_stride = cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, buf_wd); + cairo_surface_t *src = cairo_image_surface_create_for_data( + rgbbuf, CAIRO_FORMAT_RGB24, buf_wd, buf_ht, cache_stride); + if(src && cairo_surface_status(src) == CAIRO_STATUS_SUCCESS) + { + cairo_t *cr_cache = cairo_create(*mip_cache); + cairo_set_source_surface(cr_cache, src, 0, 0); + cairo_pattern_set_filter(cairo_get_source(cr_cache), CAIRO_FILTER_NEAREST); + cairo_paint(cr_cache); + cairo_destroy(cr_cache); + cairo_surface_destroy(src); + } + else + { + if(src) cairo_surface_destroy(src); + cairo_surface_destroy(*mip_cache); + *mip_cache = NULL; + } + } + if(mip_cache_level) *mip_cache_level = mip; + } } // we consider skull/error as ok as the image hasn't to be reload diff --git a/src/views/view.h b/src/views/view.h index dfa572f6e46a..e2ba77dc9321 100644 --- a/src/views/view.h +++ b/src/views/view.h @@ -22,6 +22,7 @@ #include "common/action.h" #include "common/history.h" #include "common/image.h" +#include "common/mipmap_cache.h" #ifdef HAVE_PRINT #include "common/cups_print.h" #include "common/printing.h" @@ -181,6 +182,19 @@ dt_view_surface_value_t dt_view_image_get_surface(const dt_imgid_t imgid, cairo_surface_t **surface, const gboolean quality); +/** Like dt_view_image_get_surface but caches the native-resolution + * (pre-scaling) color-converted mipmap surface to skip the expensive + * calloc + color-transform on subsequent calls at the same mip level. + * Pass non-NULL mip_cache / mip_cache_level to enable caching; pass NULL + * to behave identically to dt_view_image_get_surface. */ +dt_view_surface_value_t dt_view_image_get_surface_cached(const dt_imgid_t imgid, + const int32_t width, + const int32_t height, + cairo_surface_t **surface, + const gboolean quality, + cairo_surface_t **mip_cache, + dt_mipmap_size_t *mip_cache_level); + /** Set the selection bit to a given value for the specified image */ void dt_view_set_selection(const dt_imgid_t imgid, @@ -459,9 +473,12 @@ gboolean dt_view_manager_gesture_pan(dt_view_manager_t *vm, const double dx, const double dy, const int state); +/** x, y are root (screen-absolute) coordinates, same convention as GdkEventScroll.x_root. */ gboolean dt_view_manager_gesture_pinch(dt_view_manager_t *vm, const double x, const double y, + const double dx, + const double dy, const int phase, const double scale, const int state); diff --git a/src/views/view_api.h b/src/views/view_api.h index 565bd858e34c..8dd86eff3a34 100644 --- a/src/views/view_api.h +++ b/src/views/view_api.h @@ -59,7 +59,7 @@ OPTIONAL(void, configure, struct dt_view_t *self, int width, int height); OPTIONAL(void, scrolled, struct dt_view_t *self, double x, double y, int up, int state); // mouse scrolled in view OPTIONAL(void, scrollbar_changed, struct dt_view_t *self, double x, double y); // scrollbars changed in view OPTIONAL(gboolean, gesture_pan, struct dt_view_t *self, double x, double y, double dx, double dy, int state); -OPTIONAL(gboolean, gesture_pinch, struct dt_view_t *self, double x, double y, int phase, double scale, int state); +OPTIONAL(gboolean, gesture_pinch, struct dt_view_t *self, double x, double y, double dx, double dy, int phase, double scale, int state); // x,y are root (screen-absolute) coords // list of mouse actions OPTIONAL(GSList *, mouse_actions, const struct dt_view_t *self);