diff options
| -rw-r--r-- | layout/draw.go | 7 | ||||
| -rw-r--r-- | layout/math.go | 33 | ||||
| -rw-r--r-- | layout/math_test.go | 19 | ||||
| -rw-r--r-- | layout/mux.go | 29 | ||||
| -rw-r--r-- | layout/region.go | 18 | ||||
| -rw-r--r-- | layout/rows.go | 121 | ||||
| -rw-r--r-- | layout/waitgroup.go | 32 | ||||
| -rw-r--r-- | test/region/main.go (renamed from test/region.go) | 4 | ||||
| -rw-r--r-- | test/rows/main.go | 61 |
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()) +} |