Repository: beeware/toga

Issue:
Crash of ImageView example in Gtk
**Describe the bug**
The `imageview` example crashes on Gtk because `toga_gtk.ImageView.rehint()` is called before `toga_gtk.ImageView._pixbuf` has been set by the interface layer. The following traceback is produced:

```
Traceback (most recent call last):
  File "/home/samschott/.local/lib/python3.8/site-packages/toga_gtk/app.py", line 93, in gtk_startup
    self.interface.startup()
  File "/media/psf/Home/Python/toga/examples/imageview/imageview/app.py", line 18, in startup
    imageview_from_path = toga.ImageView(image_from_path)
  File "/home/samschott/.local/lib/python3.8/site-packages/toga/widgets/imageview.py", line 25, in __init__
    self._impl = self.factory.ImageView(interface=self)
  File "/home/samschott/.local/lib/python3.8/site-packages/toga_gtk/widgets/base.py", line 12, in __init__
    self.interface.style.reapply()
  File "/home/samschott/.local/lib/python3.8/site-packages/travertino/declaration.py", line 88, in reapply
    self.apply(style, getattr(self, style))
  File "/home/samschott/.local/lib/python3.8/site-packages/toga/style/pack.py", line 104, in apply
    self._applicator.set_font(
  File "/home/samschott/.local/lib/python3.8/site-packages/toga/style/applicator.py", line 25, in set_font
    self.widget._impl.rehint()
  File "/home/samschott/.local/lib/python3.8/site-packages/toga_gtk/widgets/imageview.py", line 20, in rehint
    original_height=self._pixbuf.get_height(),
AttributeError: 'NoneType' object has no attribute 'get_height'
```

**To Reproduce**
Run the imageview example:
```shell
python3 -m imageview
```

**Environment:**
 - Operating System: Ubuntu 20.04
 - Python version: Python 3.8
 - Software versions:
   - Toga: 0.3.0.dev23

**Additional context**
This is a tricky issue and I suspect it was introduced by a change to when the style is applied. Essentially, the interface does set image (pixbuf) during init. Nevertheless, the style is already applied during the init of `toga_gtk.base.Widget`, before setting the image (line 12):

https://github.com/beeware/toga/blob/f8bea583c87642ad102776e1b58fd8bb9265b135/src/gtk/toga_gtk/widgets/base.py#L5-L12

The quickest solution may be to guard against `pixbuf` not being set in the `rehint` implementation.


Discussion:
On the note of testing: I quite like your concept of using the examples as a visual test for the user interface. Would it make sense to also use them as a type of integration test and on the very basic level just assure that they start without crashes? This could be easily implemented in Github actions.

The second step, probably more useful, would be to find a way of defining and testing UI functionality and appearance of the examples. This of course is a difficult task...

Resolving patch:
diff --git a/src/gtk/toga_gtk/widgets/imageview.py b/src/gtk/toga_gtk/widgets/imageview.py
--- a/src/gtk/toga_gtk/widgets/imageview.py
+++ b/src/gtk/toga_gtk/widgets/imageview.py
@@ -1,10 +1,8 @@
-
-from ..libs import GdkPixbuf, Gtk
+from ..libs import GdkPixbuf, Gtk, Gdk
 from .base import Widget
 
 
 class ImageView(Widget):
-
     def create(self):
         self.native = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
         self._image = Gtk.Image()
@@ -15,16 +13,30 @@ def create(self):
     def set_image(self, image):
         self._pixbuf = image._impl.native
 
+    def set_bounds(self, x, y, width, height):
+        super().set_bounds(x, y, width, height)
+        # rehint to update scaling of pixbuf
+        self.rehint()
+
     def rehint(self):
-        height, width = self._resize_max(
-            original_height=self._pixbuf.get_height(),
-            original_width=self._pixbuf.get_width(),
-            max_height=self.native.get_allocated_height(),
-            max_width=self.native.get_allocated_width()
-        )
+        if self._pixbuf:
+            height, width = self._resize_max(
+                original_height=self._pixbuf.get_height(),
+                original_width=self._pixbuf.get_width(),
+                max_height=self.native.get_allocated_height(),
+                max_width=self.native.get_allocated_width(),
+            )
+
+            dpr = self.native.get_scale_factor()
+
+            scaled_pixbuf = self._pixbuf.scale_simple(
+                width * dpr, height * dpr, GdkPixbuf.InterpType.BILINEAR
+            )
 
-        scaled_pixbuf = self._pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.BILINEAR)
-        self._image.set_from_pixbuf(scaled_pixbuf)
+            surface = Gdk.cairo_surface_create_from_pixbuf(
+                scaled_pixbuf, 0, self.native.get_window()  # scale: 0 = same as window
+            )
+            self._image.set_from_surface(surface)
 
     @staticmethod
     def _resize_max(original_height, original_width, max_height, max_width):
@@ -33,8 +45,8 @@ def _resize_max(original_height, original_width, max_height, max_width):
         if min(original_height, original_width, max_height, max_width) <= 0:
             return 1, 1
 
-        width_ratio = max_width/original_width
-        height_ratio = max_height/original_height
+        width_ratio = max_width / original_width
+        height_ratio = max_height / original_height
 
         height = original_height * width_ratio
         if height <= max_height:


Test patch: