diff --git a/android/tests_backend/widgets/imageview.py b/android/tests_backend/widgets/imageview.py index 8f1e4208ee..4b4091f8ee 100644 --- a/android/tests_backend/widgets/imageview.py +++ b/android/tests_backend/widgets/imageview.py @@ -9,3 +9,8 @@ class ImageViewProbe(SimpleProbe): @property def preserve_aspect_ratio(self): return self.native.getScaleType() == ImageView.ScaleType.FIT_CENTER + + def assert_image_size(self, width, height): + # Android internally scales the image to the container, + # so there's no image size check required. + pass diff --git a/changes/2119.misc.rst b/changes/2119.misc.rst new file mode 100644 index 0000000000..941f7e555a --- /dev/null +++ b/changes/2119.misc.rst @@ -0,0 +1 @@ +An issue with imageview scaling on GTK was resolved. diff --git a/cocoa/tests_backend/widgets/imageview.py b/cocoa/tests_backend/widgets/imageview.py index 6594021022..8c1cd133ba 100644 --- a/cocoa/tests_backend/widgets/imageview.py +++ b/cocoa/tests_backend/widgets/imageview.py @@ -9,3 +9,8 @@ class ImageViewProbe(SimpleProbe): @property def preserve_aspect_ratio(self): return self.native.imageScaling == NSImageScaleProportionallyUpOrDown + + def assert_image_size(self, width, height): + # Cocoa internally scales the image to the container, + # so there's no image size check required. + pass diff --git a/gtk/src/toga_gtk/widgets/imageview.py b/gtk/src/toga_gtk/widgets/imageview.py index b6b79b29b4..6ca7047e2b 100644 --- a/gtk/src/toga_gtk/widgets/imageview.py +++ b/gtk/src/toga_gtk/widgets/imageview.py @@ -7,49 +7,51 @@ class ImageView(Widget): def create(self): self.native = Gtk.Image() + self.native.connect("size-allocate", self.gtk_size_allocate) self._aspect_ratio = None def set_image(self, image): if image: - self.native.set_from_pixbuf(image._impl.native) + self.set_scaled_pixbuf(image._impl.native, self.native.get_allocation()) else: self.native.set_from_pixbuf(None) - def set_bounds(self, x, y, width, height): - super().set_bounds(x, y, width, height) - - # GTK doesn't have any native image resizing; we need to manually - # scale the native pixbuf to the preferred size as a result of - # resizing the image. + def gtk_size_allocate(self, widget, allocation): + # GTK doesn't have any native image resizing; so, when the Gtk.Image + # has a new size allocated, we need to manually scale the native pixbuf + # to the preferred size as a result of resizing the image. if self.interface.image: - if self._aspect_ratio is None: - # Don't preserve aspect ratio; image fits the available space. - image_width = width - image_height = height + self.set_scaled_pixbuf(self.interface.image._impl.native, allocation) + + def set_scaled_pixbuf(self, image, allocation): + if self._aspect_ratio is None: + # Don't preserve aspect ratio; image fits the available space. + image_width = allocation.width + image_height = allocation.height + else: + # Determine what the width/height of the image would be + # preserving the aspect ratio. If the scaled size exceeds + # the allocated size, then that isn't the dimension + # being preserved. + candidate_width = int(allocation.height * self._aspect_ratio) + candidate_height = int(allocation.width / self._aspect_ratio) + if candidate_width > allocation.width: + image_width = allocation.width + image_height = candidate_height else: - # Determine what the width/height of the image would be - # preserving the aspect ratio. If the scaled size exceeds - # the allocated size, then that isn't the dimension - # being preserved. - candidate_width = int(height * self._aspect_ratio) - candidate_height = int(width / self._aspect_ratio) - if candidate_width > width: - image_width = width - image_height = candidate_height - else: - image_width = candidate_width - image_height = height - - # Minimum image size is 1x1 - image_width = max(1, image_width) - image_height = max(1, image_height) - - # Scale the pixbuf to fit the provided space. - scaled = self.interface.image._impl.native.scale_simple( - image_width, image_height, GdkPixbuf.InterpType.BILINEAR - ) - - self.native.set_from_pixbuf(scaled) + image_width = candidate_width + image_height = allocation.height + + # Minimum image size is 1x1 + image_width = max(1, image_width) + image_height = max(1, image_height) + + # Scale the pixbuf to fit the provided space. + scaled = self.interface.image._impl.native.scale_simple( + image_width, image_height, GdkPixbuf.InterpType.BILINEAR + ) + + self.native.set_from_pixbuf(scaled) def rehint(self): width, height, self._aspect_ratio = rehint_imageview( diff --git a/gtk/tests_backend/widgets/imageview.py b/gtk/tests_backend/widgets/imageview.py index 7e2b3c0069..cc0e3e1ad1 100644 --- a/gtk/tests_backend/widgets/imageview.py +++ b/gtk/tests_backend/widgets/imageview.py @@ -9,3 +9,8 @@ class ImageViewProbe(SimpleProbe): @property def preserve_aspect_ratio(self): return self.impl._aspect_ratio is not None + + def assert_image_size(self, width, height): + # Confirm the underlying pixelbuf has been scaled to the appropriate size. + pixbuf = self.native.get_pixbuf() + assert (pixbuf.get_width(), pixbuf.get_height()) == (width, height) diff --git a/iOS/tests_backend/widgets/imageview.py b/iOS/tests_backend/widgets/imageview.py index 43379a6836..699200bc58 100644 --- a/iOS/tests_backend/widgets/imageview.py +++ b/iOS/tests_backend/widgets/imageview.py @@ -9,3 +9,8 @@ class ImageViewProbe(SimpleProbe): @property def preserve_aspect_ratio(self): return self.native.contentMode == UIViewContentMode.ScaleAspectFit.value + + def assert_image_size(self, width, height): + # UIKit internally scales the image to the container, + # so there's no image size check required. + pass diff --git a/testbed/tests/widgets/test_imageview.py b/testbed/tests/widgets/test_imageview.py index 4a2881ba14..8610095e88 100644 --- a/testbed/tests/widgets/test_imageview.py +++ b/testbed/tests/widgets/test_imageview.py @@ -24,6 +24,7 @@ async def test_implicit_size(widget, probe, container_probe): assert probe.width == pytest.approx(144, abs=2) assert probe.height == pytest.approx(72, abs=2) assert probe.preserve_aspect_ratio + probe.assert_image_size(144, 72) # Clear the image; it's now an explicit sized empty image. widget.image = None @@ -34,6 +35,7 @@ async def test_implicit_size(widget, probe, container_probe): assert not probe.preserve_aspect_ratio # Restore the image; Make the parent a flex row + # Image will become as wide as the container. widget.image = "resources/sample.png" widget.style.flex = 1 widget.parent.style.direction = ROW @@ -42,25 +44,36 @@ async def test_implicit_size(widget, probe, container_probe): assert probe.width == pytest.approx(container_probe.width, abs=2) assert probe.height == pytest.approx(container_probe.height, abs=2) assert probe.preserve_aspect_ratio + probe.assert_image_size( + pytest.approx(probe.width, abs=2), + pytest.approx(probe.width // 2, abs=2), + ) # Make the parent a flex column + # Image will try to be as tall as the container, but will be + # constrained by preserving the aspect ratio widget.parent.style.direction = COLUMN await probe.redraw("Image is in a column box") assert probe.width == pytest.approx(container_probe.width, abs=2) assert probe.height == pytest.approx(container_probe.height, abs=2) assert probe.preserve_aspect_ratio + probe.assert_image_size( + pytest.approx(probe.width, abs=2), + pytest.approx(probe.width // 2, abs=2), + ) async def test_explicit_width(widget, probe, container_probe): """If the image width is explicit, the image view will resize preserving aspect ratio.""" - # Explicitly set width + # Explicitly set width; height follows aspect raio widget.style.width = 200 await probe.redraw("Image has explicit width") assert probe.width == pytest.approx(200, abs=2) assert probe.height == pytest.approx(100, abs=2) assert probe.preserve_aspect_ratio + probe.assert_image_size(200, 100) # Clear the image; it's now an explicit sized empty image. widget.image = None @@ -79,6 +92,8 @@ async def test_explicit_width(widget, probe, container_probe): assert probe.width == pytest.approx(200, abs=2) assert probe.height == pytest.approx(container_probe.height, abs=2) assert probe.preserve_aspect_ratio + # Container has fixed width; aspect ratio is preserved, so image isn't tall + probe.assert_image_size(200, 100) # Make the parent a flex column widget.parent.style.direction = COLUMN @@ -87,17 +102,20 @@ async def test_explicit_width(widget, probe, container_probe): assert probe.width == pytest.approx(200, abs=2) assert probe.height == pytest.approx(container_probe.height, abs=2) assert probe.preserve_aspect_ratio + # Container has fixed width; aspect ratio is preserved, image is implicit height + probe.assert_image_size(200, 100) async def test_explicit_height(widget, probe, container_probe): """If the image height is explicit, the image view will resize preserving aspect ratio.""" - # Explicitly set height + # Explicitly set height; width follows aspect raio widget.style.height = 150 await probe.redraw("Image has explicit height") assert probe.width == pytest.approx(300, abs=2) assert probe.height == pytest.approx(150, abs=2) assert probe.preserve_aspect_ratio + probe.assert_image_size(300, 150) # Clear the image; it's now an explicit sized empty image. widget.image = None @@ -116,6 +134,8 @@ async def test_explicit_height(widget, probe, container_probe): assert probe.width == pytest.approx(container_probe.width, abs=2) assert probe.height == pytest.approx(150, abs=2) assert probe.preserve_aspect_ratio + # Container has fixed height; aspect ratio is preserved, so image isn't wide + probe.assert_image_size(300, 150) # Make the parent a flex column widget.parent.style.direction = COLUMN @@ -124,6 +144,8 @@ async def test_explicit_height(widget, probe, container_probe): assert probe.width == pytest.approx(container_probe.width, abs=2) assert probe.height == pytest.approx(150, abs=2) assert probe.preserve_aspect_ratio + # Container has fixed height; aspect ratio is preserved, image is implicit height + probe.assert_image_size(300, 150) async def test_explicit_size(widget, probe): @@ -136,6 +158,8 @@ async def test_explicit_size(widget, probe): assert probe.width == pytest.approx(200, abs=2) assert probe.height == pytest.approx(300, abs=2) assert not probe.preserve_aspect_ratio + # Image is the size specified. + probe.assert_image_size(200, 300) # Clear the image; it's now an explicit sized empty image. widget.image = None @@ -154,6 +178,8 @@ async def test_explicit_size(widget, probe): assert probe.width == pytest.approx(200, abs=2) assert probe.height == pytest.approx(300, abs=2) assert not probe.preserve_aspect_ratio + # Image is the size specified. + probe.assert_image_size(200, 300) # Make the parent a flex column widget.parent.style.direction = COLUMN @@ -162,3 +188,5 @@ async def test_explicit_size(widget, probe): assert probe.width == pytest.approx(200, abs=2) assert probe.height == pytest.approx(300, abs=2) assert not probe.preserve_aspect_ratio + # Image is the size specified. + probe.assert_image_size(200, 300) diff --git a/winforms/tests_backend/widgets/imageview.py b/winforms/tests_backend/widgets/imageview.py index 3473f6ef4a..d988752677 100644 --- a/winforms/tests_backend/widgets/imageview.py +++ b/winforms/tests_backend/widgets/imageview.py @@ -9,3 +9,8 @@ class ImageViewProbe(SimpleProbe): @property def preserve_aspect_ratio(self): return self.native.SizeMode == WinForms.PictureBoxSizeMode.Zoom + + def assert_image_size(self, width, height): + # Winforms internally scales the image to the container, + # so there's no image size check required. + pass