~eliasnaur/gio-example

ecd2a626cd1977c7f6ea4722d985cde140a94021 — Egon Elbre 7 months ago 8cec6ce
life: add game of life

Signed-off-by: Egon Elbre <egonelbre@gmail.com>
3 files changed, 301 insertions(+), 0 deletions(-)

A life/board.go
A life/main.go
A life/style.go
A life/board.go => life/board.go +116 -0
@@ 0,0 1,116 @@
// SPDX-License-Identifier: Unlicense OR MIT

package main

import (
	"image"
	"math/rand"
)

// Board implements game of life logic.
type Board struct {
	// Size is the count of cells in a particular dimension.
	Size image.Point
	// Cells contains the alive or dead cells.
	Cells []byte

	// buffer is used to avoid reallocating a new cells
	// slice for every update.
	buffer []byte
}

// NewBoard returns a new game of life with the defined size.
func NewBoard(size image.Point) *Board {
	return &Board{
		Size:   size,
		Cells:  make([]byte, size.X*size.Y),
		buffer: make([]byte, size.X*size.Y),
	}
}

// Randomize randomizes each cell state.
func (b *Board) Randomize() {
	rand.Read(b.Cells)
	for i, v := range b.Cells {
		if v < 0x30 {
			b.Cells[i] = 1
		} else {
			b.Cells[i] = 0
		}
	}
}

// Pt returns the coordinate given a index in b.Cells.
func (b *Board) Pt(i int) image.Point {
	x, y := i%b.Size.X, i/b.Size.Y
	return image.Point{X: x, Y: y}
}

// At returns the b.Cells index, given a wrapped coordinate.
func (b *Board) At(c image.Point) int {
	if c.X < 0 {
		c.X += b.Size.X
	}
	if c.X >= b.Size.X {
		c.X -= b.Size.X
	}
	if c.Y < 0 {
		c.Y += b.Size.Y
	}
	if c.Y >= b.Size.Y {
		c.Y -= b.Size.Y
	}
	return b.Size.Y*c.Y + c.X
}

// SetWithoutWrap sets a cell to alive.
func (b *Board) SetWithoutWrap(c image.Point) {
	if !c.In(image.Rectangle{Max: b.Size}) {
		return
	}

	b.Cells[b.At(c)] = 1
}

// Advance advances the board state by 1.
func (b *Board) Advance() {
	next, cur := b.buffer, b.Cells
	defer func() { b.Cells, b.buffer = next, cur }()

	for i := range next {
		next[i] = 0
	}

	for y := 0; y < b.Size.Y; y++ {
		for x := 0; x < b.Size.X; x++ {
			var t byte
			t += cur[b.At(image.Pt(x-1, y-1))]
			t += cur[b.At(image.Pt(x+0, y-1))]
			t += cur[b.At(image.Pt(x+1, y-1))]
			t += cur[b.At(image.Pt(x-1, y+0))]
			t += cur[b.At(image.Pt(x+1, y+0))]
			t += cur[b.At(image.Pt(x-1, y+1))]
			t += cur[b.At(image.Pt(x+0, y+1))]
			t += cur[b.At(image.Pt(x+1, y+1))]

			// Any live cell with fewer than two live neighbours dies, as if by underpopulation.
			// Any live cell with two or three live neighbours lives on to the next generation.
			// Any live cell with more than three live neighbours dies, as if by overpopulation.
			// Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.

			p := b.At(image.Pt(x, y))
			switch {
			case t < 2:
				t = 0
			case t == 2:
				t = cur[p]
			case t == 3:
				t = 1
			case t > 3:
				t = 0
			}

			next[p] = t
		}
	}
}

A life/main.go => life/main.go +117 -0
@@ 0,0 1,117 @@
// SPDX-License-Identifier: Unlicense OR MIT

package main

import (
	"image"
	"log"
	"os"
	"time"

	"gioui.org/app"       // app contains Window handling.
	"gioui.org/io/key"    // key is used for keyboard events.
	"gioui.org/io/system" // system is used for system events (e.g. closing the window).
	"gioui.org/layout"    // layout is used for layouting widgets.
	"gioui.org/op"        // op is used for recording different operations.
	"gioui.org/unit"      // unit is used to define pixel-independent sizes
)

var (
	// cellSizePx is the cell size in pixels.
	cellSizePx = 5
	// boardSize is the count of cells in a particular dimension.
	boardSize = image.Pt(50, 50)
)

func main() {
	// The ui loop is separated from the application window creation
	// such that it can be used for testing.
	ui := NewUI()

	// This creates a new application window and starts the UI.
	go func() {
		windowSize := layout.FPt(boardSize.Add(image.Pt(2, 2)).Mul(cellSizePx))
		w := app.NewWindow(
			app.Title("Game of Life"),
			app.Size(unit.Px(windowSize.X), unit.Px(windowSize.Y)),
		)
		if err := ui.Run(w); err != nil {
			log.Println(err)
			os.Exit(1)
		}
		os.Exit(0)
	}()

	// This starts Gio main.
	app.Main()
}

// UI holds all of the application state.
type UI struct {
	// Board handles all game-of-life logic.
	Board *Board
}

// NewUI creates a new UI using the Go Fonts.
func NewUI() *UI {
	// We start with a new random board.
	board := NewBoard(boardSize)
	board.Randomize()

	return &UI{
		Board: board,
	}
}

// Run handles window events and renders the application.
func (ui *UI) Run(w *app.Window) error {
	var ops op.Ops

	// Update the board 3 times per second.
	advanceBoard := time.NewTicker(time.Second / 3)
	defer advanceBoard.Stop()

	// listen for events happening on the window.
	for {
		select {
		case e := <-w.Events():
			// detect the type of the event.
			switch e := e.(type) {
			// this is sent when the application should re-render.
			case system.FrameEvent:
				// gtx is used to pass around rendering and event information.
				gtx := layout.NewContext(&ops, e)
				// render and handle UI.
				ui.Layout(gtx)
				// render and handle the operations from the UI.
				e.Frame(gtx.Ops)

			// handle a global key press.
			case key.Event:
				switch e.Name {
				// when we click escape, let's close the window.
				case key.NameEscape:
					return nil
				}

			// this is sent when the application is closed.
			case system.DestroyEvent:
				return e.Err
			}

		case <-advanceBoard.C:
			ui.Board.Advance()
			w.Invalidate()
		}
	}
}

// Layout displays the main program layout.
func (ui *UI) Layout(gtx layout.Context) layout.Dimensions {
	return layout.Center.Layout(gtx,
		BoardStyle{
			CellSizePx: cellSizePx,
			Board:      ui.Board,
		}.Layout,
	)
}

A life/style.go => life/style.go +68 -0
@@ 0,0 1,68 @@
// SPDX-License-Identifier: Unlicense OR MIT

package main

import (
	"image"
	"image/color"

	"gioui.org/f32"        // f32 is used for shape calculations.
	"gioui.org/io/pointer" // system is used for system events (e.g. closing the window).
	"gioui.org/layout"     // layout is used for layouting widgets.
	"gioui.org/op"         // op is used for recording different operations.
	"gioui.org/op/clip"    // clip is used to draw the cell shape.
	"gioui.org/op/paint"   // paint is used to paint the cells.
)

// BoardStyle draws Board with rectangles.
type BoardStyle struct {
	CellSizePx int
	*Board
}

// Layout draws the Board and accepts input for adding alive cells.
func (board BoardStyle) Layout(gtx layout.Context) layout.Dimensions {
	defer op.Push(gtx.Ops).Pop()

	// Calculate the board size based on the cell size in pixels.
	size := board.Size.Mul(board.CellSizePx)
	gtx.Constraints = layout.Exact(size)

	// Handle any input from a pointer.
	for _, ev := range gtx.Events(board.Board) {
		if ev, ok := ev.(pointer.Event); ok {
			p := image.Pt(int(ev.Position.X), int(ev.Position.Y))
			// Calculate the board coordinate given a cursor position.
			p = p.Div(board.CellSizePx)
			board.SetWithoutWrap(p)
		}
	}
	// Register to listen for pointer Drag events.
	pointer.Rect(image.Rectangle{Max: size}).Add(gtx.Ops)
	pointer.InputOp{Tag: board.Board, Types: pointer.Drag}.Add(gtx.Ops)

	cellSize := float32(board.CellSizePx)

	// Draw a shape for each alive cell.
	var p clip.Path
	p.Begin(gtx.Ops)
	for i, v := range board.Cells {
		if v == 0 {
			continue
		}

		c := layout.FPt(board.Pt(i).Mul(board.CellSizePx))
		p.MoveTo(f32.Pt(c.X, c.Y))
		p.LineTo(f32.Pt(c.X+cellSize, c.Y))
		p.LineTo(f32.Pt(c.X+cellSize, c.Y+cellSize))
		p.LineTo(f32.Pt(c.X, c.Y+cellSize))
		p.Close()
	}
	clip.Outline{Path: p.End()}.Op().Add(gtx.Ops)

	// Paint the shape with a black color.
	paint.ColorOp{Color: color.NRGBA{A: 0xFF}}.Add(gtx.Ops)
	paint.PaintOp{}.Add(gtx.Ops)

	return layout.Dimensions{Size: size}
}