From e595eb72306e1984926dab28c9aec6d7ae3e2209 Mon Sep 17 00:00:00 2001 From: Daniele Pighin Date: Mon, 27 Jul 2026 12:52:48 +0300 Subject: [PATCH] Fixes #21594 and #21606 by gating `off_image` geometry --- src/views/view.c | 81 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 54 insertions(+), 27 deletions(-) diff --git a/src/views/view.c b/src/views/view.c index 0372e5c6d43a..965e5754ee4c 100644 --- a/src/views/view.c +++ b/src/views/view.c @@ -1946,36 +1946,58 @@ void dt_view_paint_surface(cairo_t *cr, const int maxw = MIN(port->width, backbuf_scale * processed_width * (1<height, backbuf_scale * processed_height * (1<width, (-0.5 - zoom_x) * img_w); + const double vis_x1 = fmin(0.5 * port->width, (0.5 - zoom_x) * img_w); + const double vis_y0 = fmax(-0.5 * port->height, (-0.5 - zoom_y) * img_h); + const double vis_y1 = fmin(0.5 * port->height, (0.5 - zoom_y) * img_h); + + // Whether the viewport is panned beyond the image, i.e. off-image content is + // deliberately in view. In normal use the pan clamp keeps the viewport inside + // the picture, so this is FALSE; it only becomes TRUE while editing a mask, + // where the clamp is relaxed to reach handles outside the image (see + // _clamp_zoom_to_mask). We keep darktable's original clip and coverage + // behaviour in the normal case so a stale backbuf lagging a drag can't leave a + // grey seam at the border (#21606) or a growing frame over the grey + // background (#21594); the viewport-centre-driven geometry is used only for + // the off-image mask-editing case it was introduced for. + const float halfw_img = img_w > 0 ? 0.5f * port->width / (float)img_w : 0.5f; + const float halfh_img = img_h > 0 ? 0.5f * port->height / (float)img_h : 0.5f; + const gboolean off_image = + fabsf(zoom_x) > fmaxf(0.0f, 0.5f - halfw_img) + 1e-4f + || fabsf(zoom_y) > fmaxf(0.0f, 0.5f - halfh_img) + 1e-4f; + if(port->color_assessment && window != DT_WINDOW_SLIDESHOW) { - // draw the white frame around picture + // draw the white frame hugging the visible image edge. Sized from the + // visible rectangle (image intersected with viewport) so it can't grow out + // across the grey background as you zoom in (#21594). const double ratio = dt_conf_get_float("darkroom/ui/color_assessment_border_white_ratio"); - const double borw = img_w + 2.0 * tb * ratio; - const double borh = img_h + 2.0 * tb * ratio; - cairo_rectangle(cr, -0.5 * borw - zoom_x * img_w, -0.5 * borh - zoom_y * img_h, borw, borh); + const double borw = fmax(0.0, vis_x1 - vis_x0) + 2.0 * tb * ratio; + const double borh = fmax(0.0, vis_y1 - vis_y0) + 2.0 * tb * ratio; + cairo_rectangle(cr, vis_x0 - tb * ratio, vis_y0 - tb * ratio, borw, borh); dt_gui_gtk_set_source_rgb(cr, DT_GUI_COLOR_COLOR_ASSESSMENT_FG); cairo_fill(cr); } - // clip to the image rectangle intersected with the viewport - const double clip_x0 = fmax(-0.5 * port->width, (-0.5 - zoom_x) * img_w); - const double clip_x1 = fmin(0.5 * port->width, (0.5 - zoom_x) * img_w); - const double clip_y0 = fmax(-0.5 * port->height, (-0.5 - zoom_y) * img_h); - const double clip_y1 = fmin(0.5 * port->height, (0.5 - zoom_y) * img_h); - cairo_rectangle(cr, clip_x0, clip_y0, fmax(0.0, clip_x1 - clip_x0), fmax(0.0, clip_y1 - clip_y0)); + if(off_image) + // Editing a mask with the viewport panned past the picture: clip to the + // image's true border (intersected with the viewport) so the off-image + // background shows where the handles are being placed. + cairo_rectangle(cr, vis_x0, vis_y0, fmax(0.0, vis_x1 - vis_x0), fmax(0.0, vis_y1 - vis_y0)); + else + // Original darktable clip: MIN(viewport, image) centred on the viewport. It + // never sits exactly on the image border while zoomed in, so a backbuf that + // lags a drag can't leave a grey seam at the border (#21606). + cairo_rectangle(cr, -0.5 * maxw, -0.5 * maxh, maxw, maxh); cairo_clip(cr); const double back_scale = (buf_scale == 0 ? 1.0 : backbuf_scale / buf_scale) * (1<= 0.5f ? 0.0f : CLAMP(zoom_x, -(0.5f - rhx), 0.5f - rhx); - const float reach_y = rhy >= 0.5f ? 0.0f : CLAMP(zoom_y, -(0.5f - rhy), 0.5f - rhy); - const double cov_trans_x = (offset_x - reach_x) * processed_width * buf_scale - 0.5 * buf_width; - const double cov_trans_y = (offset_y - reach_y) * processed_height * buf_scale - 0.5 * buf_height; + // the pipe every frame. So in that case measure against the closest centre the + // renderer can actually reach. In normal use we measure against the real zoom + // centre, so a drag that brings the border into view still re-renders to fill + // it instead of leaving it grey. + float cov_zoom_x = zoom_x, cov_zoom_y = zoom_y; + if(off_image) + { + const float rhx = 0.5f * buf_width / fmaxf(1.0f, (float)processed_width * buf_scale); + const float rhy = 0.5f * buf_height / fmaxf(1.0f, (float)processed_height * buf_scale); + cov_zoom_x = rhx >= 0.5f ? 0.0f : CLAMP(zoom_x, -(0.5f - rhx), 0.5f - rhx); + cov_zoom_y = rhy >= 0.5f ? 0.0f : CLAMP(zoom_y, -(0.5f - rhy), 0.5f - rhy); + } + const double cov_trans_x = (offset_x - cov_zoom_x) * processed_width * buf_scale - 0.5 * buf_width; + const double cov_trans_y = (offset_y - cov_zoom_y) * processed_height * buf_scale - 0.5 * buf_height; // Check if we should use the preview pipe for fallback rendering // This is only valid for the main develop (not for pinned images which have dev != darktable.develop)