diff --git a/NAPS2.Lib.Gtk/EtoForms/Gtk/GtkEtoPlatform.cs b/NAPS2.Lib.Gtk/EtoForms/Gtk/GtkEtoPlatform.cs index 133c4b4ea1..924a2103f5 100644 --- a/NAPS2.Lib.Gtk/EtoForms/Gtk/GtkEtoPlatform.cs +++ b/NAPS2.Lib.Gtk/EtoForms/Gtk/GtkEtoPlatform.cs @@ -38,6 +38,7 @@ public override IListView CreateListView(ListViewBehavior behavior) => public override void ConfigureImageButton(Button button, ButtonFlags flags) { + AttachDpiDependency(button, _ => button.ScaleImage()); } public override Bitmap ToBitmap(IMemoryImage image) @@ -269,10 +270,11 @@ public override Control AccessibleImageButton(Image image, string text, Action o public override void ConfigureZoomButton(Button button, string icon) { + var gtkButton = button.ToNative(); button.Text = ""; - button.Image = IconProvider.GetIcon(icon); + button.Image = IconProvider.GetIcon(icon, gtkButton.ScaleFactor); + button.ScaleImage(); button.Size = Size.Empty; - var gtkButton = button.ToNative(); gtkButton.StyleContext.AddClass("zoom-button"); gtkButton.SetSizeRequest(0, 0); } diff --git a/NAPS2.Lib.Gtk/Util/GtkEtoExtensions.cs b/NAPS2.Lib.Gtk/Util/GtkEtoExtensions.cs index 50d3e0a80a..774586b977 100644 --- a/NAPS2.Lib.Gtk/Util/GtkEtoExtensions.cs +++ b/NAPS2.Lib.Gtk/Util/GtkEtoExtensions.cs @@ -1,3 +1,7 @@ +using System.Reflection; +using Eto.Forms; +using Eto.GtkSharp; +using Eto.GtkSharp.Forms.Controls; using Gdk; using Gtk; @@ -5,9 +9,42 @@ namespace NAPS2.Util; public static class GtkEtoExtensions { + private static readonly FieldInfo GtkImageField = + typeof(ButtonHandler).GetField("gtkimage", + BindingFlags.Instance | BindingFlags.NonPublic)!; + + private static readonly MethodInfo SetImagePositionMethod = + typeof(ButtonHandler).GetMethod( + "SetImagePosition", BindingFlags.Instance | BindingFlags.NonPublic)!; + public static Image ToScaledImage(this Pixbuf pixbuf, int scaleFactor) { + // Creating a Gtk.Image directly from a Pixbuf doesn't work if we want to render at high dpi. + // For example, if we have a 32x32 logical image size that actually gets rendered at 64x64 pixels due to a 2x + // display scaling, naively using the 64x64 image will get displayed with 64x64 logical size and 128x128 + // physical size, which will look too big and blurry. To get an actual crisp image rendered at 32x32 logical + // pixels and 64x64 physical pixels, we first create a surface with a 2x scale factor and then create the + // Gtk.Image from that. var surface = Gdk.CairoHelper.SurfaceCreateFromPixbuf(pixbuf, scaleFactor, null); return new Image(surface); } + + public static void SetImage(this Eto.Forms.Button button, Image image) + { + // Hack to inject a Gtk.Image directly into an Eto.Forms.Button. Normally Eto only takes an Eto.Drawing.Image + // (which wraps a Pixbuf) but to correctly scale images at high dpi we need a Gtk.Image constructed from a + // surface. + image.Show(); + GtkImageField.SetValue(button.Handler, image); + SetImagePositionMethod.Invoke(button.Handler, []); + } + + public static void ScaleImage(this Eto.Forms.Button button) + { + // Helper to read the Eto.Drawing.Image from the Eto.Forms.Button, scale it, then inject it back into the + // button. + int scaleFactor = button.ToNative().ScaleFactor; + var image = button.Image.ToGdk().ToScaledImage(scaleFactor); + button.SetImage(image); + } } \ No newline at end of file