Skip to content

Commit 920d020

Browse files
authored
Merge pull request #2119 from freakboy3742/gtk-imageview-sizing
Correct image scaling on GTK
2 parents 82fad50 + cf7abe2 commit 920d020

File tree

8 files changed

+92
-36
lines changed

8 files changed

+92
-36
lines changed

android/tests_backend/widgets/imageview.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,8 @@ class ImageViewProbe(SimpleProbe):
99
@property
1010
def preserve_aspect_ratio(self):
1111
return self.native.getScaleType() == ImageView.ScaleType.FIT_CENTER
12+
13+
def assert_image_size(self, width, height):
14+
# Android internally scales the image to the container,
15+
# so there's no image size check required.
16+
pass

changes/2119.misc.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
An issue with imageview scaling on GTK was resolved.

cocoa/tests_backend/widgets/imageview.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,8 @@ class ImageViewProbe(SimpleProbe):
99
@property
1010
def preserve_aspect_ratio(self):
1111
return self.native.imageScaling == NSImageScaleProportionallyUpOrDown
12+
13+
def assert_image_size(self, width, height):
14+
# Cocoa internally scales the image to the container,
15+
# so there's no image size check required.
16+
pass

gtk/src/toga_gtk/widgets/imageview.py

Lines changed: 36 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -7,49 +7,51 @@
77
class ImageView(Widget):
88
def create(self):
99
self.native = Gtk.Image()
10+
self.native.connect("size-allocate", self.gtk_size_allocate)
1011
self._aspect_ratio = None
1112

1213
def set_image(self, image):
1314
if image:
14-
self.native.set_from_pixbuf(image._impl.native)
15+
self.set_scaled_pixbuf(image._impl.native, self.native.get_allocation())
1516
else:
1617
self.native.set_from_pixbuf(None)
1718

18-
def set_bounds(self, x, y, width, height):
19-
super().set_bounds(x, y, width, height)
20-
21-
# GTK doesn't have any native image resizing; we need to manually
22-
# scale the native pixbuf to the preferred size as a result of
23-
# resizing the image.
19+
def gtk_size_allocate(self, widget, allocation):
20+
# GTK doesn't have any native image resizing; so, when the Gtk.Image
21+
# has a new size allocated, we need to manually scale the native pixbuf
22+
# to the preferred size as a result of resizing the image.
2423
if self.interface.image:
25-
if self._aspect_ratio is None:
26-
# Don't preserve aspect ratio; image fits the available space.
27-
image_width = width
28-
image_height = height
24+
self.set_scaled_pixbuf(self.interface.image._impl.native, allocation)
25+
26+
def set_scaled_pixbuf(self, image, allocation):
27+
if self._aspect_ratio is None:
28+
# Don't preserve aspect ratio; image fits the available space.
29+
image_width = allocation.width
30+
image_height = allocation.height
31+
else:
32+
# Determine what the width/height of the image would be
33+
# preserving the aspect ratio. If the scaled size exceeds
34+
# the allocated size, then that isn't the dimension
35+
# being preserved.
36+
candidate_width = int(allocation.height * self._aspect_ratio)
37+
candidate_height = int(allocation.width / self._aspect_ratio)
38+
if candidate_width > allocation.width:
39+
image_width = allocation.width
40+
image_height = candidate_height
2941
else:
30-
# Determine what the width/height of the image would be
31-
# preserving the aspect ratio. If the scaled size exceeds
32-
# the allocated size, then that isn't the dimension
33-
# being preserved.
34-
candidate_width = int(height * self._aspect_ratio)
35-
candidate_height = int(width / self._aspect_ratio)
36-
if candidate_width > width:
37-
image_width = width
38-
image_height = candidate_height
39-
else:
40-
image_width = candidate_width
41-
image_height = height
42-
43-
# Minimum image size is 1x1
44-
image_width = max(1, image_width)
45-
image_height = max(1, image_height)
46-
47-
# Scale the pixbuf to fit the provided space.
48-
scaled = self.interface.image._impl.native.scale_simple(
49-
image_width, image_height, GdkPixbuf.InterpType.BILINEAR
50-
)
51-
52-
self.native.set_from_pixbuf(scaled)
42+
image_width = candidate_width
43+
image_height = allocation.height
44+
45+
# Minimum image size is 1x1
46+
image_width = max(1, image_width)
47+
image_height = max(1, image_height)
48+
49+
# Scale the pixbuf to fit the provided space.
50+
scaled = self.interface.image._impl.native.scale_simple(
51+
image_width, image_height, GdkPixbuf.InterpType.BILINEAR
52+
)
53+
54+
self.native.set_from_pixbuf(scaled)
5355

5456
def rehint(self):
5557
width, height, self._aspect_ratio = rehint_imageview(

gtk/tests_backend/widgets/imageview.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,8 @@ class ImageViewProbe(SimpleProbe):
99
@property
1010
def preserve_aspect_ratio(self):
1111
return self.impl._aspect_ratio is not None
12+
13+
def assert_image_size(self, width, height):
14+
# Confirm the underlying pixelbuf has been scaled to the appropriate size.
15+
pixbuf = self.native.get_pixbuf()
16+
assert (pixbuf.get_width(), pixbuf.get_height()) == (width, height)

iOS/tests_backend/widgets/imageview.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,8 @@ class ImageViewProbe(SimpleProbe):
99
@property
1010
def preserve_aspect_ratio(self):
1111
return self.native.contentMode == UIViewContentMode.ScaleAspectFit.value
12+
13+
def assert_image_size(self, width, height):
14+
# UIKit internally scales the image to the container,
15+
# so there's no image size check required.
16+
pass

testbed/tests/widgets/test_imageview.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ async def test_implicit_size(widget, probe, container_probe):
2424
assert probe.width == pytest.approx(144, abs=2)
2525
assert probe.height == pytest.approx(72, abs=2)
2626
assert probe.preserve_aspect_ratio
27+
probe.assert_image_size(144, 72)
2728

2829
# Clear the image; it's now an explicit sized empty image.
2930
widget.image = None
@@ -34,6 +35,7 @@ async def test_implicit_size(widget, probe, container_probe):
3435
assert not probe.preserve_aspect_ratio
3536

3637
# Restore the image; Make the parent a flex row
38+
# Image will become as wide as the container.
3739
widget.image = "resources/sample.png"
3840
widget.style.flex = 1
3941
widget.parent.style.direction = ROW
@@ -42,25 +44,36 @@ async def test_implicit_size(widget, probe, container_probe):
4244
assert probe.width == pytest.approx(container_probe.width, abs=2)
4345
assert probe.height == pytest.approx(container_probe.height, abs=2)
4446
assert probe.preserve_aspect_ratio
47+
probe.assert_image_size(
48+
pytest.approx(probe.width, abs=2),
49+
pytest.approx(probe.width // 2, abs=2),
50+
)
4551

4652
# Make the parent a flex column
53+
# Image will try to be as tall as the container, but will be
54+
# constrained by preserving the aspect ratio
4755
widget.parent.style.direction = COLUMN
4856

4957
await probe.redraw("Image is in a column box")
5058
assert probe.width == pytest.approx(container_probe.width, abs=2)
5159
assert probe.height == pytest.approx(container_probe.height, abs=2)
5260
assert probe.preserve_aspect_ratio
61+
probe.assert_image_size(
62+
pytest.approx(probe.width, abs=2),
63+
pytest.approx(probe.width // 2, abs=2),
64+
)
5365

5466

5567
async def test_explicit_width(widget, probe, container_probe):
5668
"""If the image width is explicit, the image view will resize preserving aspect ratio."""
57-
# Explicitly set width
69+
# Explicitly set width; height follows aspect raio
5870
widget.style.width = 200
5971

6072
await probe.redraw("Image has explicit width")
6173
assert probe.width == pytest.approx(200, abs=2)
6274
assert probe.height == pytest.approx(100, abs=2)
6375
assert probe.preserve_aspect_ratio
76+
probe.assert_image_size(200, 100)
6477

6578
# Clear the image; it's now an explicit sized empty image.
6679
widget.image = None
@@ -79,6 +92,8 @@ async def test_explicit_width(widget, probe, container_probe):
7992
assert probe.width == pytest.approx(200, abs=2)
8093
assert probe.height == pytest.approx(container_probe.height, abs=2)
8194
assert probe.preserve_aspect_ratio
95+
# Container has fixed width; aspect ratio is preserved, so image isn't tall
96+
probe.assert_image_size(200, 100)
8297

8398
# Make the parent a flex column
8499
widget.parent.style.direction = COLUMN
@@ -87,17 +102,20 @@ async def test_explicit_width(widget, probe, container_probe):
87102
assert probe.width == pytest.approx(200, abs=2)
88103
assert probe.height == pytest.approx(container_probe.height, abs=2)
89104
assert probe.preserve_aspect_ratio
105+
# Container has fixed width; aspect ratio is preserved, image is implicit height
106+
probe.assert_image_size(200, 100)
90107

91108

92109
async def test_explicit_height(widget, probe, container_probe):
93110
"""If the image height is explicit, the image view will resize preserving aspect ratio."""
94-
# Explicitly set height
111+
# Explicitly set height; width follows aspect raio
95112
widget.style.height = 150
96113

97114
await probe.redraw("Image has explicit height")
98115
assert probe.width == pytest.approx(300, abs=2)
99116
assert probe.height == pytest.approx(150, abs=2)
100117
assert probe.preserve_aspect_ratio
118+
probe.assert_image_size(300, 150)
101119

102120
# Clear the image; it's now an explicit sized empty image.
103121
widget.image = None
@@ -116,6 +134,8 @@ async def test_explicit_height(widget, probe, container_probe):
116134
assert probe.width == pytest.approx(container_probe.width, abs=2)
117135
assert probe.height == pytest.approx(150, abs=2)
118136
assert probe.preserve_aspect_ratio
137+
# Container has fixed height; aspect ratio is preserved, so image isn't wide
138+
probe.assert_image_size(300, 150)
119139

120140
# Make the parent a flex column
121141
widget.parent.style.direction = COLUMN
@@ -124,6 +144,8 @@ async def test_explicit_height(widget, probe, container_probe):
124144
assert probe.width == pytest.approx(container_probe.width, abs=2)
125145
assert probe.height == pytest.approx(150, abs=2)
126146
assert probe.preserve_aspect_ratio
147+
# Container has fixed height; aspect ratio is preserved, image is implicit height
148+
probe.assert_image_size(300, 150)
127149

128150

129151
async def test_explicit_size(widget, probe):
@@ -136,6 +158,8 @@ async def test_explicit_size(widget, probe):
136158
assert probe.width == pytest.approx(200, abs=2)
137159
assert probe.height == pytest.approx(300, abs=2)
138160
assert not probe.preserve_aspect_ratio
161+
# Image is the size specified.
162+
probe.assert_image_size(200, 300)
139163

140164
# Clear the image; it's now an explicit sized empty image.
141165
widget.image = None
@@ -154,6 +178,8 @@ async def test_explicit_size(widget, probe):
154178
assert probe.width == pytest.approx(200, abs=2)
155179
assert probe.height == pytest.approx(300, abs=2)
156180
assert not probe.preserve_aspect_ratio
181+
# Image is the size specified.
182+
probe.assert_image_size(200, 300)
157183

158184
# Make the parent a flex column
159185
widget.parent.style.direction = COLUMN
@@ -162,3 +188,5 @@ async def test_explicit_size(widget, probe):
162188
assert probe.width == pytest.approx(200, abs=2)
163189
assert probe.height == pytest.approx(300, abs=2)
164190
assert not probe.preserve_aspect_ratio
191+
# Image is the size specified.
192+
probe.assert_image_size(200, 300)

winforms/tests_backend/widgets/imageview.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,8 @@ class ImageViewProbe(SimpleProbe):
99
@property
1010
def preserve_aspect_ratio(self):
1111
return self.native.SizeMode == WinForms.PictureBoxSizeMode.Zoom
12+
13+
def assert_image_size(self, width, height):
14+
# Winforms internally scales the image to the container,
15+
# so there's no image size check required.
16+
pass

0 commit comments

Comments
 (0)