Image view

The widget.Image type draws an image to the buffer at the same resolution as it was loaded (a pixel in the source image matches a pixel on screen). What we need to do for the image viewer is scale it to fit the available space. To do this, we create a new custom widget named scaledImage. The code is very similar to the Shiny image widget but with a more complicated PaintBase() function.

This function calculates imgWidth and imgHeight to fit within the current bounds of the widget and maintain the aspect ratio of the source image. It then scales the image using the scaleImage() helper function defined earlier, ready to paint at the correct resolution. Lastly, offset is calculated so that the image is centered within the available space:

func (w *scaledImage) PaintBase(ctx *node.PaintBaseContext, origin image.Point) error {
w.Marks.UnmarkNeedsPaintBase()
if w.Src == nil {
return nil
}

wRect := w.Rect.Add(origin)
ratio := float32(w.Src.Bounds().Max.X)/float32(w.Src.Bounds().Max.Y)
width := wRect.Max.X - wRect.Min.X
height := wRect.Max.Y - wRect.Min.Y

imgWidth := int(math.Min(float64(width), float64(w.Src.Bounds().Max.X)))
imgHeight := int(float32(imgWidth)/ratio)

if imgHeight > height {
imgHeight = int(math.Min(float64(height), float64(w.Src.Bounds().Max.Y)))
imgWidth = int(float32(imgHeight)*ratio)
}

scaled := scaleImage(w.Src, imgWidth, imgHeight)
offset := image.Point{(imgWidth-width)/2, (imgHeight-height)/2}

draw.Draw(ctx.Dst, wRect, scaled, offset, draw.Over)
return nil
}

To avoid a blank space being left by the preceding calculations, let's add a checkered pattern typical in many other image applications. To make this possible, we create a custom image type named checkerImage that simply returns pixels from the At() function based on a regular checker pattern. As images are bounded, we need to add a resize() function so the image can expand to fill the space:

var checkers = &checkerImage{}

type checkerImage struct {
bounds image.Rectangle
}

func (c *checkerImage) resize(width, height int) {
c.bounds = image.Rectangle{image.Pt(0, 0), image.Pt(width, height)}
}

func (c *checkerImage) ColorModel() color.Model {
return color.RGBAModel
}

func (c *checkerImage) Bounds() image.Rectangle {
return c.bounds
}

func (c *checkerImage) At(x, y int) color.Color {
xr := x/10
yr := y/10

if xr%2 == yr%2 {
return color.RGBA{0xc0, 0xc0, 0xc0, 0xff}
} else {
return color.RGBA{0x99, 0x99, 0x99, 0xff}
}
}

To include the checker pattern, we simply need to update the end of the PaintBase() function of scaledImage. Before the image itself is drawn, we set the checker pattern to expand to the correct size and paint it onto the background. The checkers are drawn with the draw.Src mode and the image is then drawn over the top using the draw.Over mode:

func (w *scaledImage) PaintBase(ctx *node.PaintBaseContext, origin image.Point) error {

...

checkers.resize(width, height)
draw.Draw(ctx.Dst, wRect, checkers, checkers.Bounds().Min, draw.Src)
draw.Draw(ctx.Dst, wRect, scaled, offset, draw.Over)
return nil
}

With all of this code in place, we have an updated application that correctly fills the layout we designed and scales and positions the placeholder image we have to fit within the available space:

The interface updated to show images centered at the correct aspect ratio

That's the majority of our graphical code complete. Next, we will make the necessary additions to load real content from the local filesystem.