From 27de7304daf6674252e4c4878a9a7c7320b99e85 Mon Sep 17 00:00:00 2001 From: John Cupitt Date: Thu, 31 Jul 2025 12:01:26 +0100 Subject: [PATCH 1/7] enable highdpi display though mag display and compute needs doing too --- src/imagedisplay.c | 62 +++++++++++++++++++++++++++++++++++----------- src/tilecache.c | 22 ++++++---------- src/tilecache.h | 1 - 3 files changed, 56 insertions(+), 29 deletions(-) diff --git a/src/imagedisplay.c b/src/imagedisplay.c index bb83501..9c5a04f 100644 --- a/src/imagedisplay.c +++ b/src/imagedisplay.c @@ -79,6 +79,11 @@ struct _Imagedisplay { double scale; double x, y; + /* The size of physical display pixels in gtk coordinates, eg. for a 200% + * desktop this would be 0.5. + */ + double pixel_size; + /* Draw the screen in debug mode. */ gboolean debug; @@ -126,6 +131,9 @@ enum { */ PROP_DEBUG, + /* Read out display density with this. + */ + PROP_PIXEL_SIZE, }; enum { @@ -476,6 +484,8 @@ imagedisplay_set_property(GObject *object, { Imagedisplay *imagedisplay = (Imagedisplay *) object; + double d; + #ifdef DEBUG { g_autofree char *str = g_strdup_value_contents(value); @@ -573,6 +583,15 @@ imagedisplay_set_property(GObject *object, gtk_widget_queue_draw(GTK_WIDGET(imagedisplay)); break; + case PROP_PIXEL_SIZE: + d = g_value_get_double(value); + if (imagedisplay->pixel_size != d) { + imagedisplay->pixel_size = d; + imagedisplay_layout(imagedisplay); + gtk_widget_queue_draw(GTK_WIDGET(imagedisplay)); + } + break; + default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; @@ -632,6 +651,10 @@ imagedisplay_get_property(GObject *object, g_value_set_boolean(value, imagedisplay->debug); break; + case PROP_PIXEL_SIZE: + g_value_set_double(value, imagedisplay->pixel_size); + break; + default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; @@ -649,6 +672,15 @@ imagedisplay_snapshot(GtkWidget *widget, GtkSnapshot *snapshot) GTK_WIDGET_CLASS(imagedisplay_parent_class)->snapshot(widget, snapshot); + /* This can change on each repaint as windows are dragged. + */ + GtkNative *native = gtk_widget_get_native(widget); + GdkSurface *surface = gtk_native_get_surface(native); + double pixel_size = 1.0 / gdk_surface_get_scale_factor(surface); + g_object_set(imagedisplay, "pixel-size", pixel_size, NULL); + + gtk_snapshot_scale(snapshot, pixel_size, pixel_size); + #ifdef HAVE_GTK_SNAPSHOT_SET_SNAP gtk_snapshot_set_snap(snapshot, GSK_RECT_SNAP_ROUND); #endif /*HAVE_GTK_SNAPSHOT_SET_SNAP*/ @@ -658,26 +690,21 @@ imagedisplay_snapshot(GtkWidget *widget, GtkSnapshot *snapshot) */ gtk_snapshot_push_clip(snapshot, &GRAPHENE_RECT_INIT(0, 0, - gtk_widget_get_width(widget), gtk_widget_get_height(widget))); + gtk_widget_get_width(widget) / pixel_size, + gtk_widget_get_height(widget) / pixel_size)); graphene_rect_t paint; - paint.origin.x = imagedisplay->paint_rect.left; - paint.origin.y = imagedisplay->paint_rect.top; - paint.size.width = imagedisplay->paint_rect.width; - paint.size.height = imagedisplay->paint_rect.height; - - /* If there's no gtk snapping, we do our own based on the hardware pixel - * size for the surface this snapshot will be rendered to. - */ - GtkNative *native = gtk_widget_get_native(widget); - GdkSurface *surface = gtk_native_get_surface(native); - double pixel_size = 1.0 / gdk_surface_get_scale_factor(surface); + paint.origin.x = imagedisplay->paint_rect.left / pixel_size; + paint.origin.y = imagedisplay->paint_rect.top / pixel_size; + paint.size.width = imagedisplay->paint_rect.width / pixel_size; + paint.size.height = imagedisplay->paint_rect.height / pixel_size; if (imagedisplay->tilecache && imagedisplay->tilecache->n_levels > 0) tilecache_snapshot(imagedisplay->tilecache, snapshot, - pixel_size, - imagedisplay->scale, imagedisplay->x, imagedisplay->y, + imagedisplay->scale / pixel_size, + imagedisplay->x / pixel_size, + imagedisplay->y / pixel_size, &paint, imagedisplay->debug); // draw any overlays @@ -828,6 +855,13 @@ imagedisplay_class_init(ImagedisplayClass *class) FALSE, G_PARAM_READWRITE)); + g_object_class_install_property(gobject_class, PROP_PIXEL_SIZE, + g_param_spec_double("pixel-size", + _("Pixel size"), + _("Size of hardware display pixels in gtk coordinates"), + 0.0, 10.0, 0.0, + G_PARAM_READWRITE)); + g_object_class_override_property(gobject_class, PROP_HADJUSTMENT, "hadjustment"); g_object_class_override_property(gobject_class, diff --git a/src/tilecache.c b/src/tilecache.c index b9111bd..04a9b16 100644 --- a/src/tilecache.c +++ b/src/tilecache.c @@ -841,14 +841,12 @@ tilecache_draw_bounds(GtkSnapshot *snapshot, * white lines on tile edges. */ static void -tilecache_snap_rect_to_boundary(graphene_rect_t *bounds, double pixel_size) +tilecache_snap_rect_to_boundary(graphene_rect_t *bounds) { - double left = rint(bounds->origin.x * pixel_size) / pixel_size; - double top = rint(bounds->origin.y * pixel_size) / pixel_size; - double right = - rint((bounds->origin.x + bounds->size.width) * pixel_size) / pixel_size; - double bottom = - rint((bounds->origin.y + bounds->size.height) * pixel_size) / pixel_size; + double left = rint(bounds->origin.x); + double top = rint(bounds->origin.y); + double right = rint(bounds->origin.x + bounds->size.width); + double bottom = rint(bounds->origin.y + bounds->size.height); bounds->origin.x = left; bounds->origin.y = top; @@ -860,16 +858,12 @@ tilecache_snap_rect_to_boundary(graphene_rect_t *bounds, double pixel_size) /* Scale is how much the level0 image has been scaled, x/y is the position of * the top-left corner of @paint in the scaled image. * - * @pixel_scale is gdk_surface_get_scale() for the surface this snapshot will - * be rendered to. - * * @paint is the pixel area in gtk coordinates that we paint in the widget. * * Set debug to draw tile boundaries for debugging. */ void tilecache_snapshot(Tilecache *tilecache, GtkSnapshot *snapshot, - double pixel_size, double scale, double x, double y, graphene_rect_t *paint, gboolean debug) { /* In debug mode, scale and offset so we can see tile clipping. @@ -941,7 +935,7 @@ tilecache_snapshot(Tilecache *tilecache, GtkSnapshot *snapshot, */ graphene_rect_t backdrop = *paint; #ifndef HAVE_GTK_SNAPSHOT_SET_SNAP - tilecache_snap_rect_to_boundary(&backdrop, pixel_size); + tilecache_snap_rect_to_boundary(&backdrop); #endif /*!HAVE_GTK_SNAPSHOT_SET_SNAP*/ gtk_snapshot_push_repeat(snapshot, &backdrop, NULL); @@ -965,7 +959,7 @@ tilecache_snapshot(Tilecache *tilecache, GtkSnapshot *snapshot, * blur the image. For zooming out, we want trilinear to get * mipmaps and antialiasing. */ - GskScalingFilter filter = scale >= pixel_size ? + GskScalingFilter filter = scale >= 1.0 ? GSK_SCALING_FILTER_NEAREST : GSK_SCALING_FILTER_TRILINEAR; graphene_rect_t bounds; @@ -976,7 +970,7 @@ tilecache_snapshot(Tilecache *tilecache, GtkSnapshot *snapshot, bounds.size.height = tile->bounds0.height * scale; #ifndef HAVE_GTK_SNAPSHOT_SET_SNAP - tilecache_snap_rect_to_boundary(&bounds, pixel_size); + tilecache_snap_rect_to_boundary(&bounds); #endif /*!HAVE_GTK_SNAPSHOT_SET_SNAP*/ gtk_snapshot_append_scaled_texture(snapshot, diff --git a/src/tilecache.h b/src/tilecache.h index 4dcef1d..3e54755 100644 --- a/src/tilecache.h +++ b/src/tilecache.h @@ -110,7 +110,6 @@ Tilecache *tilecache_new(); /* Render the tiles to a snapshot. */ void tilecache_snapshot(Tilecache *tilecache, GtkSnapshot *snapshot, - double pixel_size, double scale, double x, double y, graphene_rect_t *paint, gboolean debug); #endif /*__TILECACHE_H*/ From dfa102437e8882a33159d9c4acb8e988bd24daec Mon Sep 17 00:00:00 2001 From: John Cupitt Date: Thu, 31 Jul 2025 12:24:24 +0100 Subject: [PATCH 2/7] magnification display accounts for display scale --- src/imageui.c | 22 ++++++++++++++++++++++ src/imagewindow.c | 14 ++++++++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/imageui.c b/src/imageui.c index 0cc9c85..67eb3b6 100644 --- a/src/imageui.c +++ b/src/imageui.c @@ -125,6 +125,7 @@ enum { PROP_ZOOM, PROP_X, PROP_Y, + PROP_PIXEL_SIZE, PROP_LAST }; @@ -194,6 +195,10 @@ imageui_property_name(guint prop_id) return "Y"; break; + case PROP_PIXEL_SIZE: + return "PIXEL_SIZE"; + break; + default: return ""; } @@ -253,6 +258,11 @@ imageui_set_property(GObject *object, "y", value); break; + case PROP_PIXEL_SIZE: + g_object_set_property(G_OBJECT(imageui->imagedisplay), + "pixel-size", value); + break; + default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; @@ -296,6 +306,11 @@ imageui_get_property(GObject *object, g_object_get_property(G_OBJECT(imageui->imagedisplay), "y", value); break; + case PROP_PIXEL_SIZE: + g_object_get_property(G_OBJECT(imageui->imagedisplay), + "pixel-size", value); + break; + default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; @@ -1011,6 +1026,13 @@ imageui_class_init(ImageuiClass *class) -VIPS_MAX_COORD, VIPS_MAX_COORD, 0, G_PARAM_READWRITE)); + g_object_class_install_property(gobject_class, PROP_PIXEL_SIZE, + g_param_spec_double("pixel_size", + _("Pixel size"), + _("Size of hardware display pixels in gtk coordinates"), + 0.0, 10.0, 0.0, + G_PARAM_READWRITE)); + imageui_signals[SIG_CHANGED] = g_signal_new("changed", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_LAST, diff --git a/src/imagewindow.c b/src/imagewindow.c index 1ed3a08..01a682b 100644 --- a/src/imagewindow.c +++ b/src/imagewindow.c @@ -377,11 +377,13 @@ imagewindow_files_set(Imagewindow *win, char **files, int n_files) } } +/* Get the effective zoom, ie. the zoom considering the hardware pixel display + * density. + */ double imagewindow_get_zoom(Imagewindow *win) { double zoom; - if (win->imageui) g_object_get(win->imageui, "zoom", &zoom, @@ -389,7 +391,15 @@ imagewindow_get_zoom(Imagewindow *win) else zoom = 1.0; - return zoom; + double pixel_size; + if (win->imageui) + g_object_get(win->imageui, + "pixel_size", &pixel_size, + NULL); + else + pixel_size = 1.0; + + return zoom / pixel_size; } void From a1458362ad0306b239fb991c0ad92a4bc3ecc574 Mon Sep 17 00:00:00 2001 From: John Cupitt Date: Thu, 31 Jul 2025 12:34:26 +0100 Subject: [PATCH 3/7] scale image zoom setting by display density --- CHANGELOG.md | 1 + src/imageui.c | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7be7b28..7aace20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ master - do our own tile snapping if gtk has no snap mechanism +- high-dpi support ## 4.1.0 26/07/25 diff --git a/src/imageui.c b/src/imageui.c index 67eb3b6..9078ec4 100644 --- a/src/imageui.c +++ b/src/imageui.c @@ -444,6 +444,18 @@ imageui_get_zoom(Imageui *imageui) return zoom; } +double +imageui_get_pixel_size(Imageui *imageui) +{ + double pixel_size; + + g_object_get(imageui, + "pixel-size", &pixel_size, + NULL); + + return pixel_size; +} + static gboolean imageui_tick(GtkWidget *widget, GdkFrameClock *frame_clock, gpointer user_data) { @@ -566,7 +578,7 @@ imageui_magout(Imageui *imageui) void imageui_oneone(Imageui *imageui) { - imageui_zoom_to_eased(imageui, 1.0); + imageui_zoom_to_eased(imageui, imageui_get_pixel_size(imageui)); } static void @@ -800,7 +812,8 @@ imageui_key_pressed(GtkEventControllerKey *self, if (state & GDK_CONTROL_MASK) zoom = 1.0 / zoom; - imageui_zoom_to_eased(imageui, zoom); + imageui_zoom_to_eased(imageui, + zoom * imageui_get_pixel_size(imageui)); handled = TRUE; break; From 9944156c3c258e4fa60a17dcff9b56f537000e59 Mon Sep 17 00:00:00 2001 From: John Cupitt Date: Thu, 31 Jul 2025 12:43:03 +0100 Subject: [PATCH 4/7] enclose drawing ion save/restore --- src/imagedisplay.c | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/imagedisplay.c b/src/imagedisplay.c index 9c5a04f..762b414 100644 --- a/src/imagedisplay.c +++ b/src/imagedisplay.c @@ -679,12 +679,14 @@ imagedisplay_snapshot(GtkWidget *widget, GtkSnapshot *snapshot) double pixel_size = 1.0 / gdk_surface_get_scale_factor(surface); g_object_set(imagedisplay, "pixel-size", pixel_size, NULL); - gtk_snapshot_scale(snapshot, pixel_size, pixel_size); + gtk_snapshot_save(snapshot); #ifdef HAVE_GTK_SNAPSHOT_SET_SNAP gtk_snapshot_set_snap(snapshot, GSK_RECT_SNAP_ROUND); #endif /*HAVE_GTK_SNAPSHOT_SET_SNAP*/ + gtk_snapshot_scale(snapshot, pixel_size, pixel_size); + /* Clip to the widget area, or we may paint over the display control * bar. */ @@ -707,10 +709,13 @@ imagedisplay_snapshot(GtkWidget *widget, GtkSnapshot *snapshot) imagedisplay->y / pixel_size, &paint, imagedisplay->debug); + // end of clip + gtk_snapshot_pop(snapshot); + + gtk_snapshot_restore(snapshot); + // draw any overlays imagedisplay_overlay_snapshot(imagedisplay, snapshot); - - gtk_snapshot_pop(snapshot); } static void From 454a9ed1028453dc6152bea094a81c3cce8d701f Mon Sep 17 00:00:00 2001 From: John Cupitt Date: Thu, 31 Jul 2025 13:02:25 +0100 Subject: [PATCH 5/7] small rename --- src/tilecache.c | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/tilecache.c b/src/tilecache.c index 04a9b16..99b577f 100644 --- a/src/tilecache.c +++ b/src/tilecache.c @@ -836,12 +836,8 @@ tilecache_draw_bounds(GtkSnapshot *snapshot, } #ifndef HAVE_GTK_SNAPSHOT_SET_SNAP -/* Snap a graphene rect to a hardware pixel boundary on the output surface. We - * need to do this if the gtk snap mechachanism is missing or we'll get thin - * white lines on tile edges. - */ static void -tilecache_snap_rect_to_boundary(graphene_rect_t *bounds) +tilecache_snap_rect(graphene_rect_t *bounds) { double left = rint(bounds->origin.x); double top = rint(bounds->origin.y); @@ -935,7 +931,7 @@ tilecache_snapshot(Tilecache *tilecache, GtkSnapshot *snapshot, */ graphene_rect_t backdrop = *paint; #ifndef HAVE_GTK_SNAPSHOT_SET_SNAP - tilecache_snap_rect_to_boundary(&backdrop); + tilecache_snap_rect(&backdrop); #endif /*!HAVE_GTK_SNAPSHOT_SET_SNAP*/ gtk_snapshot_push_repeat(snapshot, &backdrop, NULL); @@ -970,9 +966,8 @@ tilecache_snapshot(Tilecache *tilecache, GtkSnapshot *snapshot, bounds.size.height = tile->bounds0.height * scale; #ifndef HAVE_GTK_SNAPSHOT_SET_SNAP - tilecache_snap_rect_to_boundary(&bounds); + tilecache_snap_rect(&bounds); #endif /*!HAVE_GTK_SNAPSHOT_SET_SNAP*/ - gtk_snapshot_append_scaled_texture(snapshot, tile_get_texture(tile), filter, &bounds); From 45deb2970841ef8f6351fbba2b1ec5f34da82c1e Mon Sep 17 00:00:00 2001 From: John Cupitt Date: Thu, 31 Jul 2025 13:03:29 +0100 Subject: [PATCH 6/7] Update src/imagedisplay.c Co-authored-by: Kleis Auke Wolthuizen --- src/imagedisplay.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/imagedisplay.c b/src/imagedisplay.c index 762b414..9ff6b66 100644 --- a/src/imagedisplay.c +++ b/src/imagedisplay.c @@ -676,7 +676,7 @@ imagedisplay_snapshot(GtkWidget *widget, GtkSnapshot *snapshot) */ GtkNative *native = gtk_widget_get_native(widget); GdkSurface *surface = gtk_native_get_surface(native); - double pixel_size = 1.0 / gdk_surface_get_scale_factor(surface); + double pixel_size = 1.0 / gdk_surface_get_scale(surface); g_object_set(imagedisplay, "pixel-size", pixel_size, NULL); gtk_snapshot_save(snapshot); From 68594f02f909ab6d15ed58e91b07c55a0e50cf38 Mon Sep 17 00:00:00 2001 From: John Cupitt Date: Thu, 31 Jul 2025 15:47:14 +0100 Subject: [PATCH 7/7] better clipping for client views clip clients, client should use regular coordinate space --- src/imagedisplay.c | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/imagedisplay.c b/src/imagedisplay.c index 9ff6b66..01c319b 100644 --- a/src/imagedisplay.c +++ b/src/imagedisplay.c @@ -672,6 +672,15 @@ imagedisplay_snapshot(GtkWidget *widget, GtkSnapshot *snapshot) GTK_WIDGET_CLASS(imagedisplay_parent_class)->snapshot(widget, snapshot); + /* Clip to the widget area, or we may paint over the display control + * bar. + */ + gtk_snapshot_push_clip(snapshot, + &GRAPHENE_RECT_INIT(0, 0, + gtk_widget_get_width(widget), gtk_widget_get_height(widget))); + + gtk_snapshot_save(snapshot); + /* This can change on each repaint as windows are dragged. */ GtkNative *native = gtk_widget_get_native(widget); @@ -679,22 +688,12 @@ imagedisplay_snapshot(GtkWidget *widget, GtkSnapshot *snapshot) double pixel_size = 1.0 / gdk_surface_get_scale(surface); g_object_set(imagedisplay, "pixel-size", pixel_size, NULL); - gtk_snapshot_save(snapshot); - #ifdef HAVE_GTK_SNAPSHOT_SET_SNAP gtk_snapshot_set_snap(snapshot, GSK_RECT_SNAP_ROUND); #endif /*HAVE_GTK_SNAPSHOT_SET_SNAP*/ gtk_snapshot_scale(snapshot, pixel_size, pixel_size); - /* Clip to the widget area, or we may paint over the display control - * bar. - */ - gtk_snapshot_push_clip(snapshot, - &GRAPHENE_RECT_INIT(0, 0, - gtk_widget_get_width(widget) / pixel_size, - gtk_widget_get_height(widget) / pixel_size)); - graphene_rect_t paint; paint.origin.x = imagedisplay->paint_rect.left / pixel_size; paint.origin.y = imagedisplay->paint_rect.top / pixel_size; @@ -709,13 +708,14 @@ imagedisplay_snapshot(GtkWidget *widget, GtkSnapshot *snapshot) imagedisplay->y / pixel_size, &paint, imagedisplay->debug); - // end of clip - gtk_snapshot_pop(snapshot); - + // undo snap and scale gtk_snapshot_restore(snapshot); - // draw any overlays + // draw any overlays back in the regular coordinate space imagedisplay_overlay_snapshot(imagedisplay, snapshot); + + // end of clip + gtk_snapshot_pop(snapshot); } static void