aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--layout/draw.go7
-rw-r--r--layout/math.go33
-rw-r--r--layout/math_test.go19
-rw-r--r--layout/mux.go29
-rw-r--r--layout/region.go18
-rw-r--r--layout/rows.go121
-rw-r--r--layout/waitgroup.go32
-rw-r--r--test/region/main.go (renamed from test/region.go)4
-rw-r--r--test/rows/main.go61
9 files changed, 311 insertions, 13 deletions
diff --git a/layout/draw.go b/layout/draw.go
index 760a149..1d341f4 100644
--- a/layout/draw.go
+++ b/layout/draw.go
@@ -18,6 +18,13 @@ func subimage(m draw.Image, r image.Rectangle) draw.Image {
return m.(subimager).SubImage(r).(draw.Image)
}
+// drawSubimage translates a draw call onto the given subimage area.
+func drawSubImage(f func(draw.Image) image.Rectangle, r image.Rectangle) func(draw.Image) image.Rectangle {
+ return func(img draw.Image) image.Rectangle {
+ return f(subimage(img, r))
+ }
+}
+
// drawBackground returns a draw call that fills the entire image with a color.
func drawBackground(c color.Color) func(draw.Image) image.Rectangle {
return func(img draw.Image) image.Rectangle {
diff --git a/layout/math.go b/layout/math.go
new file mode 100644
index 0000000..f09cf00
--- /dev/null
+++ b/layout/math.go
@@ -0,0 +1,33 @@
+package layout
+
+type number interface {
+ complex | float | integer
+}
+
+type complex interface {
+ ~complex64 | ~complex128
+}
+
+type float interface {
+ ~float32 | ~float64
+}
+
+type integer interface {
+ signed | unsigned
+}
+
+type signed interface {
+ ~int | ~int8 | ~int16 | ~int32 | ~int64
+}
+
+type unsigned interface {
+ ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
+}
+
+func sum[N number](ns []N) N {
+ var n N
+ for i := range ns {
+ n += ns[i]
+ }
+ return n
+}
diff --git a/layout/math_test.go b/layout/math_test.go
new file mode 100644
index 0000000..7ff6b78
--- /dev/null
+++ b/layout/math_test.go
@@ -0,0 +1,19 @@
+package layout
+
+import "testing"
+
+func TestSum(t *testing.T) {
+ t.Parallel()
+ testSum(t, []int{}, 0)
+ testSum(t, []int{0}, 0)
+ testSum(t, []int{1}, 1)
+ testSum(t, []int{12, 34}, 46)
+ testSum(t, []int{12, 34, 56}, 102)
+}
+
+func testSum[N number](t *testing.T, s []N, want N) {
+ n := sum(s)
+ if n != want {
+ t.Errorf("sum(%v) = %v; want %v", s, n, want)
+ }
+}
diff --git a/layout/mux.go b/layout/mux.go
new file mode 100644
index 0000000..680e984
--- /dev/null
+++ b/layout/mux.go
@@ -0,0 +1,29 @@
+package layout
+
+import (
+ "image"
+ "image/draw"
+)
+
+// TaggedDrawCall is a draw function tagged with the index of a child Env within a layout.
+type taggedDrawCall struct {
+ f func(draw.Image) image.Rectangle
+ idx uint
+}
+
+// MuxDrawCalls tags draw functions with the index of a child Env, and forwards them to an output channel.
+// When the input channel is closed, it decrements the waitgroup counter.
+// It does NOT close the output channel.
+func muxDrawCalls(in <-chan func(draw.Image) image.Rectangle, idx uint, out chan<- taggedDrawCall, wg waitgroup) {
+ for f := range in {
+ out <- taggedDrawCall{f, idx}
+ }
+ wg.Done()
+}
+
+// Multicast sends a message to multiple recipients.
+func multicast[T any](msg T, recipients []chan T) {
+ for _, c := range recipients {
+ c <- msg
+ }
+}
diff --git a/layout/region.go b/layout/region.go
index 59e1e0c..93d930a 100644
--- a/layout/region.go
+++ b/layout/region.go
@@ -15,6 +15,7 @@ type Region struct {
// NewRegion creates a region layout that occupies part of the parent env's area, as determined by the resize function.
// Resize takes the area of the parent and returns the area of the region.
+// It returns the child Env.
func NewRegion(env gui.Env, resize func(image.Rectangle) image.Rectangle, o ...Option) gui.Env {
opts := evalOptions(o...)
@@ -27,14 +28,18 @@ func NewRegion(env gui.Env, resize func(image.Rectangle) image.Rectangle, o ...O
area := resize(event.(gui.Resize).Rectangle) // first event guaranteed to be Resize
events <- gui.Resize{area}
- env.Draw() <- drawBackground(opts.bg)
+ // Draw background
+ redrawBg := func(area image.Rectangle) func(draw.Image) image.Rectangle {
+ return drawSubImage(drawBackground(opts.bg), area)
+ }
+ env.Draw() <- redrawBg(area)
for {
select {
case event := <-env.Events(): // event from parent
switch event := event.(type) {
case gui.Resize:
- env.Draw() <- drawRegion(drawBackground(opts.bg), area)
+ env.Draw() <- redrawBg(area)
area = resize(event.Rectangle)
events <- gui.Resize{area} // forward to child
default:
@@ -46,7 +51,7 @@ func NewRegion(env gui.Env, resize func(image.Rectangle) image.Rectangle, o ...O
close(env.Draw())
return
}
- env.Draw() <- drawRegion(f, area)
+ env.Draw() <- drawSubImage(f, area)
}
}
}(events, drw)
@@ -54,13 +59,6 @@ func NewRegion(env gui.Env, resize func(image.Rectangle) image.Rectangle, o ...O
return Region{events, drw}
}
-// Translate a draw call to the given area.
-func drawRegion(f func(draw.Image) image.Rectangle, area image.Rectangle) func(draw.Image) image.Rectangle {
- return func(img draw.Image) image.Rectangle {
- return f(subimage(img, area))
- }
-}
-
// Events implements the Env interface.
func (r Region) Events() <-chan gui.Event { return r.events }
diff --git a/layout/rows.go b/layout/rows.go
new file mode 100644
index 0000000..3912878
--- /dev/null
+++ b/layout/rows.go
@@ -0,0 +1,121 @@
+package layout
+
+import (
+ "image"
+ "image/draw"
+
+ "github.com/faiface/gui"
+)
+
+// NewRows creates layout with nrows children arranged in rows.
+// It returns a slice containing an Env for each row.
+// The height of each row is determined by the draw calls received from that row.
+func NewRows(env gui.Env, nrows uint, o ...Option) []gui.Env {
+ opts := evalOptions(o...)
+
+ // Create event and draw channels for each row
+ eventss := make([]chan gui.Event, nrows) // to children
+ drawss := make([]chan func(draw.Image) image.Rectangle, nrows) // from children
+ var i uint
+ for i = 0; i < nrows; i++ {
+ eventss[i] = make(chan gui.Event)
+ drawss[i] = make(chan func(draw.Image) image.Rectangle)
+ }
+
+ go func() {
+ defer close(env.Draw())
+ defer closeAll(eventss)
+
+ resize := func(area image.Rectangle, rowHeights []uint) {
+ // Redraw background
+ env.Draw() <- drawSubImage(drawBackground(opts.bg), area)
+
+ // Send resize events to rows
+ off := area.Min // vertical offset from parent area origin
+ var i uint
+ for i = 0; i < nrows; i++ {
+ eventss[i] <- gui.Resize{image.Rectangle{
+ off,
+ off.Add(image.Pt(area.Dx(), int(rowHeights[i])))}}
+ off.Y += int(rowHeights[i])
+ }
+ }
+
+ // Receive and send first Resize event
+ event := (<-env.Events()).(gui.Resize) // first event guaranteed to be Resize
+ area := event.Rectangle
+ rowHeights := make([]uint, nrows) // initially zero until draw call received
+ resize(area, rowHeights) // send first Resize to children
+
+ // Multiplex rows' draw channels. Tag draw functions with row index.
+ draws := make(chan taggedDrawCall)
+ wg := newWaitGroup(nrows) // done when all rows close their Draw channel
+ var i uint
+ for i = 0; i < nrows; i++ {
+ go muxDrawCalls(drawss[i], i, draws, wg)
+ }
+ defer close(draws)
+
+ for {
+ select {
+ case event := <-env.Events(): // event from parent
+ switch event := event.(type) {
+ case gui.Resize:
+ area = event.Rectangle
+ resize(area, rowHeights)
+ default:
+ multicast(event, eventss) // forward event to all rows
+ }
+ case drw := <-draws: // draw call from a row
+ rh := rowHeight(area, drw.f)
+ oldrh := rowHeights[drw.idx]
+ rowHeights[drw.idx] = rh
+ if rh != oldrh { // size changed; redraw all rows
+ go resize(area, rowHeights)
+ } else { // Same size; just redraw the one row
+ env.Draw() <- drawSubImage(drw.f, rowArea(area, rowHeights, drw.idx))
+ }
+ case <-wg.Wait(): // all rows' draw channels closed
+ return
+ }
+ }
+ }()
+
+ // Create and return row Envs
+ rows := make([]gui.Env, nrows)
+ for i := range rows {
+ rows[i] = rowEnv{eventss[i], drawss[i]}
+ }
+ return rows
+}
+
+// RowHeight calculates the height of a row within the area of the layout
+// using a draw function received from the row.
+func rowHeight(area image.Rectangle, drw func(draw.Image) image.Rectangle) uint {
+ img := image.NewAlpha(area)
+ return uint(drw(img).Canon().Dy())
+}
+
+// RowArea returns the drawing area of row i within the area of the layout.
+func rowArea(area image.Rectangle, rowHeights []uint, i uint) image.Rectangle {
+ min := area.Min.Add(image.Pt(0, int(sum(rowHeights[:i]))))
+ max := min.Add(image.Pt(area.Dx(), int(rowHeights[i])))
+ return image.Rectangle{min, max}
+}
+
+type rowEnv struct {
+ events <-chan gui.Event
+ draw chan<- func(draw.Image) image.Rectangle
+}
+
+// Events implements the Env interface.
+func (r rowEnv) Events() <-chan gui.Event { return r.events }
+
+// Draw implements the Env interface.
+func (r rowEnv) Draw() chan<- func(draw.Image) image.Rectangle { return r.draw }
+
+func closeAll[T any](cs []chan T) {
+ for _, c := range cs {
+ close(c)
+ }
+}
diff --git a/layout/waitgroup.go b/layout/waitgroup.go
new file mode 100644
index 0000000..e32b350
--- /dev/null
+++ b/layout/waitgroup.go
@@ -0,0 +1,32 @@
+package layout
+
+// WaitGroup is a counting semaphore used to wait for a group of goroutines to finish.
+// It differs from sync/WaitGroup in that Wait() is a channel rather than a blocking function.
+type waitgroup struct {
+ done chan<- struct{}
+ alldone <-chan struct{}
+}
+
+// NewWaitGroup creates a group of n goroutines: a semaphore with a capacity of n.
+func newWaitGroup(n uint) waitgroup {
+ done, alldone := make(chan struct{}), make(chan struct{})
+ go func() {
+ for ; n > 0; n-- {
+ <-done
+ }
+ alldone <- *new(struct{})
+ close(done)
+ close(alldone)
+ }()
+ return waitgroup{done, alldone}
+}
+
+// Done decrements the task counter by one.
+func (wg waitgroup) Done() {
+ wg.done <- *new(struct{})
+}
+
+// Wait returns a channel that blocks until the task counter is zero.
+func (wg waitgroup) Wait() <-chan struct{} {
+ return wg.alldone
+}
diff --git a/test/region.go b/test/region/main.go
index 682e23f..f335b4f 100644
--- a/test/region.go
+++ b/test/region/main.go
@@ -11,9 +11,7 @@ import (
"github.com/faiface/mainthread"
)
-var (
- bg = gui.HexToColor("#999999") // background color
-)
+var bg = gui.HexToColor("#999999") // background color
func main() {
mainthread.Run(run)
diff --git a/test/rows/main.go b/test/rows/main.go
new file mode 100644
index 0000000..9848ddf
--- /dev/null
+++ b/test/rows/main.go
@@ -0,0 +1,61 @@
+package main
+
+import (
+ "image"
+ "image/color"
+ "image/draw"
+
+ "github.com/faiface/gui"
+ "github.com/faiface/gui/layout"
+ "github.com/faiface/gui/win"
+ "github.com/faiface/mainthread"
+)
+
+const (
+ nrows = 16
+ rowHeight = 12
+ rowWidth = 128
+)
+
+var bg = gui.HexToColor("#ffffea") // background color
+
+func main() {
+ mainthread.Run(run)
+}
+
+func run() {
+ w, err := win.New(win.Title("Grid Layout Test"), win.Resizable())
+ if err != nil {
+ panic(err)
+ }
+
+ mux, env := gui.NewMux(w)
+
+ rows := layout.NewRows(mux.MakeEnv(), nrows, layout.Background(bg))
+ for i, row := range rows {
+ go colorBlock(row, image.Pt(rowWidth, rowHeight), color.RGBA{uint8(i * 256 / 4), 0x20, 0x20, 0xFF})
+ }
+
+ for event := range env.Events() {
+ switch event.(type) {
+ case win.WiClose:
+ close(env.Draw())
+ return
+ }
+ }
+}
+
+func colorBlock(env gui.Env, size image.Point, c color.Color) {
+ redraw := func(img draw.Image) image.Rectangle {
+ r := image.Rectangle{img.Bounds().Min, img.Bounds().Min.Add(size)}
+ draw.Draw(img, r, &image.Uniform{c}, image.ZP, draw.Src)
+ return r
+ }
+ for event := range env.Events() {
+ switch event.(type) {
+ case gui.Resize:
+ env.Draw() <- redraw
+ }
+ }
+ close(env.Draw())
+}