Background processing

Image processing, even just loading the images to be viewed, is a CPU-intensive task, so if we open a directory with a lot of pictures, the application will be very slow to load. We can fix this delay by moving our image loading to work in the background while we load the user interface. Thankfully, creating new threads for asynchronous processing is very simple with Go (as we explored in Chapter 3, Go to the Rescue!), but we also need to ensure that the user interface is updated accordingly.

To delay the loading of images until there is processing power available, we can replace uses of loadImage() with a replacement asyncImage type that will handle the heavy lifting. The main image loading code will be moved into a private load() function that's called from newAsyncImage() using go img.load(), therefore starting it in the background:

type asyncImage struct {
path string
img image.Image
callback func(image.Image)
}

func (a *asyncImage) load() {
reader, err := os.Open(a.path)
if err != nil {
log.Fatal(err)
}
defer reader.Close()

a.img, _, err = image.Decode(reader)
if err != nil {
log.Fatal(err)
}

a.callback(a.img)
}

func newAsyncImage(path string, loaded func(image.Image)) *asyncImage {
img := &asyncImage{path: path, callback:loaded}
go img.load()

return img
}

With the definition of an asynchronous image loader, we can replace the use of image.Image with asyncImage. The important thing to remember is that the image in the img field will be nil until the load() function has completed. Be sure that any code using images checks for nil data before processing. The first function we update is makeCell() so that it no longer accepts an image parameter. Instead, we pass a loaded callback function to set the image once it is loaded. We update makeList() to replace the cell creation code with the following:

   cell := makeCell(idx, name)
i := idx
img := newAsyncImage(path.Join(dir, name), func(img image.Image) {
cell.icon.SetImage(img)
if i == index {
view.SetImage(img)
}
})

This code will ensure that thumbnails are shown once the image has loaded but also that, if the image is the current selection, it updates the main image view as well.

If you run the application at this point, you will notice that some images are loaded while others may not be. This is due to us not having signaled to Shiny that a re-paint is necessary. The marks that were applied to the widgets to force them to be repainted do not actually trigger the painting of the interface; it simply marks them as needing to be painted the next time a re-paint is triggered:

A partial rendering when loading images in the background

There is no easy way to signal Shiny to refresh the user interface, so we will make a refresh() function for convenience. This should be called when the text for a filename is updated and when a different (or lazy-loaded) image is set on the scaledImage widget:

func chooseImage(idx int, img image.Image) {
...

name.Mark(node.MarkNeedsPaintBase)
refresh(name)
}

func (w *scaledImage) SetImage(img image.Image) {
w.Src = img
w.Mark(node.MarkNeedsPaintBase)

refresh(w)
}

func refresh(_ node.Node) {
// Ideally we should refresh but this requires a reference to the window
// win.Send(paint.Event{})
}

Unfortunately, at this point, we can't proceed further without a significant amount of extra code. This is a limitation of the recommended widget.RunWindow() function that we used to load our interface. The window reference that we would need to send the paint event to is not available outside the Shiny package. To resolve this issue, it would be necessary to use the NewWindow() function on the screen.Screen instance, passed into the driver.Main() function—but to do so would mean completely re-implementing the event loop as well, which is a lot of work.

The reason we didn't notice the issue when setting the main image earlier is because, when the application is receiving user events (mouse moves and so on), its event loop runs. Each time an iteration of the loop completes, the user interface is repainted. Sending the paint.Event previously illustrated would also cause this to happen. Therefore, it follows that the interface will update after background image loading if the user is currently interacting with the GUI (even just moving the mouse over it). It's left as an exercise for the reader to implement the replacement lifecycle to resolve this issue, if desired.