aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--env.go102
-rw-r--r--gui_test.go86
-rw-r--r--intercepter.go35
-rw-r--r--layout.go106
-rw-r--r--layout/intercepter.go43
-rw-r--r--layout/layout.go20
-rw-r--r--layout/mux.go146
-rw-r--r--layout_test.go96
-rw-r--r--mux_test.go82
9 files changed, 424 insertions, 292 deletions
diff --git a/env.go b/env.go
index 05276b5..f5f31cf 100644
--- a/env.go
+++ b/env.go
@@ -28,3 +28,105 @@ type Env interface {
killer
}
+
+type env struct {
+ events <-chan Event
+ draw chan<- func(draw.Image) image.Rectangle
+ attachChan chan<- attachable
+ kill chan<- bool
+ dead <-chan bool
+ detachChan <-chan bool
+}
+
+// newEnv makes an Env that receives Events from, and sends draw functions to, the parent.
+//
+// Each Event received from parent is passed to filterEvents() along with the Events() channel
+// of the Env. Each draw function received from the Env's Draw() channel is passed to
+// filterDraws() along with the Draw() channel of the parent Env.
+//
+// filterEvents() and filterDraws() can be used to, e.g., simply forward the Event or draw function
+// to the channel, modify it before sending, not send it at all, or produce some other side-effects.
+//
+// shutdown() is called before the Env dies.
+func newEnv(parent Env,
+ filterEvents func(Event, chan<- Event),
+ filterDraws func(func(draw.Image) image.Rectangle, chan<- func(draw.Image) image.Rectangle),
+ shutdown func(),
+) Env {
+ eventsOut, eventsIn := makeEventsChan()
+ drawChan := make(chan func(draw.Image) image.Rectangle)
+ child := newAttachHandler()
+ kill := make(chan bool)
+ dead := make(chan bool)
+ detachFromParent := make(chan bool)
+
+ go func() {
+ defer func() {
+ dead <- true
+ close(dead)
+ }()
+ defer func() {
+ detachFromParent <- true
+ close(detachFromParent)
+ }()
+ defer shutdown()
+ defer close(eventsIn)
+ defer close(drawChan)
+ defer close(kill)
+ defer func() {
+ go drain(drawChan)
+ child.kill <- true
+ <-child.dead
+ }()
+
+ for {
+ select {
+ case e := <-parent.Events():
+ filterEvents(e, eventsIn)
+ case d := <-drawChan:
+ filterDraws(d, parent.Draw())
+ case <-kill:
+ return
+ }
+ }
+ }()
+
+ e := env{
+ events: eventsOut,
+ draw: drawChan,
+ attachChan: child.attach(),
+ kill: kill,
+ dead: dead,
+ detachChan: detachFromParent,
+ }
+ parent.attach() <- e
+ return e
+}
+
+func (e env) Events() <-chan Event {
+ return e.events
+}
+
+func (e env) Draw() chan<- func(draw.Image) image.Rectangle {
+ return e.draw
+}
+
+func (e env) Kill() chan<- bool {
+ return e.kill
+}
+
+func (e env) Dead() <-chan bool {
+ return e.dead
+}
+
+func (e env) attach() chan<- attachable {
+ return e.attachChan
+}
+
+func (e env) detach() <-chan bool {
+ return e.detachChan
+}
+
+func send[T any](v T, c chan<- T) {
+ c <- v
+}
diff --git a/gui_test.go b/gui_test.go
index 584544a..0d18c92 100644
--- a/gui_test.go
+++ b/gui_test.go
@@ -1,6 +1,10 @@
package gui
-import "time"
+import (
+ "image"
+ "image/draw"
+ "time"
+)
const timeout = 1 * time.Second
@@ -25,3 +29,83 @@ func tryRecv[T any](c <-chan T, timeout time.Duration) (*T, bool) {
return nil, false
}
}
+
+type dummyEnv struct {
+ eventsIn chan<- Event
+ eventsOut <-chan Event
+
+ drawIn chan<- func(draw.Image) image.Rectangle
+ drawOut <-chan func(draw.Image) image.Rectangle
+
+ kill chan<- bool
+ dead <-chan bool
+
+ attachChan chan<- attachable
+}
+
+func newDummyEnv(size image.Rectangle) dummyEnv {
+ eventsOut, eventsIn := makeEventsChan()
+ drawIn := make(chan func(draw.Image) image.Rectangle)
+ drawOut := make(chan func(draw.Image) image.Rectangle)
+ kill := make(chan bool)
+ dead := make(chan bool)
+
+ attached := newAttachHandler()
+
+ go func() {
+ defer func() {
+ dead <- true
+ close(dead)
+ }()
+ defer close(kill)
+ defer close(drawOut)
+ defer close(drawIn)
+ defer close(eventsIn)
+ defer func() {
+ go drain(drawIn)
+ attached.kill <- true
+ <-attached.dead
+ }()
+
+ for {
+ select {
+ case d := <-drawIn:
+ drawOut <- d
+ case <-kill:
+ return
+ }
+ }
+ }()
+
+ eventsIn <- Resize{size}
+
+ return dummyEnv{eventsIn, eventsOut, drawIn, drawOut, kill, dead, attached.attach()}
+}
+
+func (de dummyEnv) Events() <-chan Event {
+ return de.eventsOut
+}
+
+func (de dummyEnv) Draw() chan<- func(draw.Image) image.Rectangle {
+ return de.drawIn
+}
+
+func (de dummyEnv) Kill() chan<- bool {
+ return de.kill
+}
+
+func (de dummyEnv) Dead() <-chan bool {
+ return de.dead
+}
+
+func (de dummyEnv) attach() chan<- attachable {
+ return de.attachChan
+}
+
+type dummyEvent struct {
+ s string
+}
+
+func (e dummyEvent) String() string {
+ return e.s
+}
diff --git a/intercepter.go b/intercepter.go
new file mode 100644
index 0000000..95df3c6
--- /dev/null
+++ b/intercepter.go
@@ -0,0 +1,35 @@
+package gui
+
+import (
+ "image"
+ "image/draw"
+)
+
+// Intercepter represents an element that can interact with Envs.
+// An Intercepter can modify Events, stop them or emit arbitrary ones.
+// It can also put itself in the draw pipeline, for throttling very
+// expensive draw calls for example.
+type Intercepter interface {
+ Intercept(Env) Env
+}
+
+// RedrawIntercepter is a basic Intercepter, it is meant for use in simple Layouts
+// that only need to redraw themselves.
+type RedrawIntercepter struct {
+ Redraw func(draw.Image, image.Rectangle)
+}
+
+func (ri RedrawIntercepter) Intercept(parent Env) Env {
+ return newEnv(parent,
+ func(e Event, c chan<- Event) {
+ c <- e
+ if resize, ok := e.(Resize); ok {
+ parent.Draw() <- func(drw draw.Image) image.Rectangle {
+ ri.Redraw(drw, resize.Rectangle)
+ return resize.Rectangle
+ }
+ }
+ },
+ send, // forward draw functions un-modified
+ func() {})
+}
diff --git a/layout.go b/layout.go
new file mode 100644
index 0000000..f0cce7f
--- /dev/null
+++ b/layout.go
@@ -0,0 +1,106 @@
+package gui
+
+import "image"
+
+// Scheme represents the appearance and behavior of a layout.
+type Scheme interface {
+ // The Partitioner represents the way to divide space among the children.
+ // It takes a parameter of how much space is available, and returns a space for each child.
+ Partitioner
+
+ // The Intercepter transforms an Env channel to another.
+ // This way the Layout can emit its own Events, re-emit previous ones,
+ // or even stop an event from propagating, think win.MoScroll.
+ // It can be a no-op.
+ Intercepter
+}
+
+// Partitioner divides a large Rectangle into several smaller sub-Rectangles.
+type Partitioner interface {
+ Partition(image.Rectangle) []image.Rectangle
+}
+
+// NewLayout takes an array of uninitialized `child' Envs and multiplexes the `parent' Env
+// according to the provided Scheme. The children receive the same events from the parent
+// aside from Resize, and their draw functions get redirected to the parent Env.
+//
+// The Scheme determines the look and behavior of the Layout. Resize events for each child
+// are modified according to the Partitioner. Other Events and draw functions can be modified
+// by the Intercepter.
+//
+// Killing the returned layout kills all of the children.
+func NewLayout(parent Env, children []*Env, scheme Scheme) Killable {
+ env := newEnv(parent, send, send, func() {})
+
+ // Capture Resize Events to be sent to the Partitioner.
+ resizeSniffer, resizes := newSniffer(env, func(e Event) (r image.Rectangle, ok bool) {
+ if resize, ok := e.(Resize); ok {
+ return resize.Rectangle, true
+ }
+ return image.Rectangle{}, false
+ })
+
+ intercepter := scheme.Intercept(resizeSniffer)
+
+ mux := NewMux(intercepter)
+ muxEnvs := make([]Env, len(children))
+ resizers := make([]Env, len(children))
+ resizerChans := make([]chan image.Rectangle, len(children))
+ for i, child := range children {
+ muxEnvs[i] = mux.MakeEnv()
+ resizerChans[i] = make(chan image.Rectangle)
+ resizers[i] = newResizer(muxEnvs[i], resizerChans[i])
+ *child = resizers[i]
+ }
+
+ go func() {
+ for rect := range resizes {
+ for i, r := range scheme.Partition(rect) {
+ resizerChans[i] <- r
+ }
+ }
+ for _, c := range resizerChans {
+ close(c)
+ }
+ }()
+
+ return env
+}
+
+// newSniffer makes an Env that forwards all Events and Draws unchanged, but emits a signal
+// whenever a certain event is encountered. It returns the new Env and the signal channel.
+//
+// Each Event from parent is passed to sniff(). If sniff() accepts the Event, the return
+// value of sniff() is sent to the signal channel.
+//
+// signal is closed automatically when the sniffer dies.
+func newSniffer[T any](parent Env, sniff func(Event) (v T, ok bool)) (Env, <-chan T) {
+ signal := make(chan T)
+ env := newEnv(parent,
+ func(e Event, c chan<- Event) {
+ c <- e
+ if sig, ok := sniff(e); ok {
+ signal <- sig
+ }
+ },
+ send, // forward draw functions un-modified
+ func() {
+ close(signal)
+ })
+ return env, signal
+}
+
+// newResizer makes an Env that replaces the values all Resize events with
+// Rectangles received from the resize channel.
+// It waits for a Rectangle from resize each time a Resize Event is received from parent.
+func newResizer(parent Env, resize <-chan image.Rectangle) Env {
+ return newEnv(parent,
+ func(e Event, c chan<- Event) {
+ if _, ok := e.(Resize); ok {
+ e = Resize{<-resize}
+ }
+ c <- e
+ },
+ send, // forward draw functions un-modified
+ func() {})
+}
diff --git a/layout/intercepter.go b/layout/intercepter.go
deleted file mode 100644
index eee7aff..0000000
--- a/layout/intercepter.go
+++ /dev/null
@@ -1,43 +0,0 @@
-package layout
-
-import (
- "image"
- "image/draw"
-
- "github.com/faiface/gui"
-)
-
-// Intercepter represents an element that can interact with Envs.
-// An Intercepter can modify Events, stop them or emit arbitrary ones.
-// It can also put itself in the draw pipeline, for throttling very
-// expensive draw calls for example.
-type Intercepter interface {
- Intercept(gui.Env) gui.Env
-}
-
-var _ Intercepter = RedrawIntercepter{}
-
-// RedrawIntercepter is a basic Intercepter, it is meant for use in simple Layouts
-// that only need to redraw themselves.
-type RedrawIntercepter struct {
- Redraw func(draw.Image, image.Rectangle)
-}
-
-// Intercept implements Intercepter
-func (ri RedrawIntercepter) Intercept(env gui.Env) gui.Env {
- out, in := gui.MakeEventsChan()
- go func() {
- for e := range env.Events() {
- in <- e
- if resize, ok := e.(gui.Resize); ok {
- env.Draw() <- func(drw draw.Image) image.Rectangle {
- bounds := resize.Rectangle
- ri.Redraw(drw, bounds)
- return bounds
- }
- }
- }
- }()
- ret := &muxEnv{out, env.Draw()}
- return ret
-}
diff --git a/layout/layout.go b/layout/layout.go
deleted file mode 100644
index 4d8e616..0000000
--- a/layout/layout.go
+++ /dev/null
@@ -1,20 +0,0 @@
-package layout
-
-import (
- "image"
-)
-
-// Layout represents any graphical layout
-//
-// Lay represents the way to divide space among your childs.
-// It takes a parameter of how much space is available,
-// and returns where exactly to put its childs.
-//
-// Intercept transforms an Env channel to another.
-// This way the Layout can emit its own Events, re-emit previous ones,
-// or even stop an event from propagating, think win.MoScroll.
-// It can be a no-op.
-type Layout interface {
- Lay(image.Rectangle) []image.Rectangle
- Intercepter
-}
diff --git a/layout/mux.go b/layout/mux.go
deleted file mode 100644
index cab7cfa..0000000
--- a/layout/mux.go
+++ /dev/null
@@ -1,146 +0,0 @@
-package layout
-
-import (
- "image"
- "image/draw"
- "log"
-
- "git.samanthony.xyz/share"
- "github.com/faiface/gui"
-)
-
-// Mux can be used to multiplex an Env, let's call it a root Env. Mux implements a way to
-// create multiple virtual Envs that all interact with the root Env. They receive the same
-// events apart from gui.Resize, and their draw functions get redirected to the root Env.
-//
-// All gui.Resize events are instead modified according to the underlying Layout.
-type Mux struct {
- // Sending any value to Kill will terminate the Mux.
- Kill chan<- any
-
- bounds share.Val[image.Rectangle]
- draw chan<- func(draw.Image) image.Rectangle
- eventsIns share.ConstSlice[chan<- gui.Event]
- layout Layout
-}
-
-// Layout returns the underlying Layout of the Mux.
-func (mux *Mux) Layout() Layout {
- return mux.layout
-}
-
-func NewMux(parent gui.Env, children []*gui.Env, layout Layout) Mux {
- parent = layout.Intercept(parent)
-
- kill := make(chan any)
- bounds := share.NewVal[image.Rectangle]()
- drawChan := make(chan func(draw.Image) image.Rectangle)
- eventsIns := func() share.ConstSlice[chan<- gui.Event] { // create child Env's
- evIns := make([]chan<- gui.Event, len(children))
- for i, child := range children {
- *child, evIns[i] = makeEnv(drawChan)
- }
- return share.NewConstSlice(evIns)
- }()
- mux := Mux{
- kill,
- bounds,
- drawChan,
- eventsIns,
- layout,
- }
-
- go func() {
- defer close(parent.Draw())
- defer close(kill)
- defer bounds.Close()
- defer close(drawChan)
- defer func() {
- for eventsIn := range eventsIns.Elems() {
- close(eventsIn)
- }
- eventsIns.Close()
- }()
-
- for {
- select {
- case <-kill:
- return
- case d := <-drawChan:
- parent.Draw() <- d
- case e := <-parent.Events():
- if resize, ok := e.(gui.Resize); ok {
- bounds.Set <- resize.Rectangle
- mux.resizeChildren()
- } else {
- for eventsIn := range eventsIns.Elems() {
- eventsIn <- e
- }
- }
- }
- }
- }()
-
- // First event of a new Env must be Resize.
- mux.resizeChildren()
-
- return mux
-}
-
-func (mux *Mux) resizeChildren() {
- rect := mux.bounds.Get()
- lay := mux.layout.Lay(rect)
- i := 0
- for eventsIn := range mux.eventsIns.Elems() {
- if i > len(lay) {
- log.Printf("Lay of %T is not large enough (%d) for the number of children, skipping\n",
- mux.layout, len(lay))
- break
- }
- eventsIn <- gui.Resize{lay[i]}
- i++
- }
-}
-
-type muxEnv struct {
- events <-chan gui.Event
- draw chan<- func(draw.Image) image.Rectangle
-}
-
-func (m *muxEnv) Events() <-chan gui.Event { return m.events }
-func (m *muxEnv) Draw() chan<- func(draw.Image) image.Rectangle { return m.draw }
-
-func makeEnv(muxDraw chan<- func(draw.Image) image.Rectangle) (env gui.Env, eventsIn chan<- gui.Event) {
- eventsOut, eventsIn := gui.MakeEventsChan()
- envDraw := make(chan func(draw.Image) image.Rectangle)
- env = &muxEnv{eventsOut, envDraw}
-
- go func() {
- func() {
- // When the master Env gets its Draw() channel closed, it closes all the Events()
- // channels of all the children Envs, and it also closes the internal draw channel
- // of the Mux. Otherwise, closing the Draw() channel of the master Env wouldn't
- // close the Env the Mux is muxing. However, some child Envs of the Mux may still
- // send some drawing commmands before they realize that their Events() channel got
- // closed.
- //
- // That is perfectly fine if their drawing commands simply get ignored. This down here
- // is a little hacky, but (I hope) perfectly fine solution to the problem.
- //
- // When the internal draw channel of the Mux gets closed, the line marked with ! will
- // cause panic. We recover this panic, then we receive, but ignore all furhter draw
- // commands, correctly draining the Env until it closes itself.
- defer func() {
- if recover() != nil {
- for range envDraw {
- }
- }
- }()
- for d := range envDraw {
- muxDraw <- d // !
- }
- }()
- }()
-
- return env, eventsIn
-}
diff --git a/layout_test.go b/layout_test.go
new file mode 100644
index 0000000..1999c63
--- /dev/null
+++ b/layout_test.go
@@ -0,0 +1,96 @@
+package gui
+
+import (
+ "image"
+ "testing"
+)
+
+func TestSniffer(t *testing.T) {
+ root := newDummyEnv(image.Rect(12, 34, 56, 78))
+ defer func() {
+ root.kill <- true
+ <-root.dead
+ }()
+
+ expectSig := "got fooEvent"
+ sniffer, signal := newSniffer(root, func(e Event) (string, bool) {
+ if e.String() == "fooEvent" {
+ return expectSig, true
+ }
+ return "", false
+ })
+
+ // First event should be Resize.
+ eventp, ok := tryRecv(sniffer.Events(), timeout)
+ if !ok {
+ t.Fatalf("no Resize event received after %v", timeout)
+ }
+ if _, ok := (*eventp).(Resize); !ok {
+ t.Fatalf("got %v Event; wanted Resize", *eventp)
+ }
+
+ for i := 0; i < 3; i++ { // arbitrary number of iterations
+ // Send events to sniffer.
+ events := []Event{dummyEvent{"barEvent"}, dummyEvent{"fooEvent"}}
+ for _, event := range events {
+ root.eventsIn <- event
+
+ eventp, ok := tryRecv(sniffer.Events(), timeout)
+ if !ok {
+ t.Fatalf("no Event received after %v", timeout)
+ }
+ if *eventp != event {
+ t.Errorf("received Event %v; wanted %v", *eventp, event)
+ }
+ }
+
+ // One of the events should trigger a signal.
+ sigp, ok := tryRecv(signal, timeout)
+ if !ok {
+ t.Fatalf("no signal received after %v", timeout)
+ }
+ if *sigp != expectSig {
+ t.Errorf("received signal %v; wanted %v", *sigp, expectSig)
+ }
+ }
+}
+
+func TestResizer(t *testing.T) {
+ root := newDummyEnv(image.Rectangle{})
+ defer func() {
+ root.kill <- true
+ <-root.dead
+ }()
+
+ resizeChan := make(chan image.Rectangle)
+ defer close(resizeChan)
+ resizer := newResizer(root, resizeChan)
+
+ sizes := []image.Rectangle{
+ image.Rect(11, 22, 33, 44),
+ image.Rect(55, 66, 77, 88),
+ image.Rect(99, 111, 222, 333),
+ }
+ for _, size := range sizes {
+ // First Resize event is sent automatically by root.
+
+ if !trySend(resizeChan, size, timeout) {
+ t.Errorf("resizer did not accept Rectangle after %v", timeout)
+ }
+
+ eventp, ok := tryRecv(resizer.Events(), timeout)
+ if !ok {
+ t.Fatalf("no Event received from resizer after %v", timeout)
+ }
+ resize, ok := (*eventp).(Resize)
+ if !ok {
+ t.Fatalf("received %v Event from resizer; wanted Resize", *eventp)
+ }
+ if resize.Rectangle != size {
+ t.Errorf("got %v from resizer; wanted %v", resize.Rectangle, size)
+ }
+
+ // this event should be replaced by the resizer
+ root.eventsIn <- Resize{image.Rectangle{}}
+ }
+}
diff --git a/mux_test.go b/mux_test.go
index b159e99..75f6cac 100644
--- a/mux_test.go
+++ b/mux_test.go
@@ -139,85 +139,3 @@ func writeImg(img image.Image, fname string) error {
}
return jpeg.Encode(f, img, nil)
}
-
-type dummyEnv struct {
- eventsIn chan<- Event
- eventsOut <-chan Event
-
- drawIn chan<- func(draw.Image) image.Rectangle
- drawOut <-chan func(draw.Image) image.Rectangle
-
- kill chan<- bool
- dead <-chan bool
-
- attachChan chan<- attachable
-}
-
-func newDummyEnv(size image.Rectangle) dummyEnv {
- eventsOut, eventsIn := MakeEventsChan()
- drawIn := make(chan func(draw.Image) image.Rectangle)
- drawOut := make(chan func(draw.Image) image.Rectangle)
- kill := make(chan bool)
- dead := make(chan bool)
-
- attached := newAttachHandler()
-
- go func() {
- defer func() {
- dead <- true
- close(dead)
- }()
- defer close(kill)
- defer close(drawOut)
- defer close(drawIn)
- defer close(eventsIn)
- defer func() {
- attached.kill <- true
- <-attached.dead
- }()
- defer func() {
- go drain(drawIn)
- }()
-
- for {
- select {
- case d := <-drawIn:
- drawOut <- d
- case <-kill:
- return
- }
- }
- }()
-
- eventsIn <- Resize{size}
-
- return dummyEnv{eventsIn, eventsOut, drawIn, drawOut, kill, dead, attached.attach()}
-}
-
-func (de dummyEnv) Events() <-chan Event {
- return de.eventsOut
-}
-
-func (de dummyEnv) Draw() chan<- func(draw.Image) image.Rectangle {
- return de.drawIn
-}
-
-func (de dummyEnv) Kill() chan<- bool {
- return de.kill
-}
-
-func (de dummyEnv) Dead() <-chan bool {
- return de.dead
-}
-
-func (de dummyEnv) attach() chan<- attachable {
- return de.attachChan
-}
-
-type dummyEvent struct {
- s string
-}
-
-func (e dummyEvent) String() string {
- return e.s
-}