diff options
| author | Sam Anthony <sam@samanthony.xyz> | 2024-08-24 15:04:33 -0400 |
|---|---|---|
| committer | Sam Anthony <sam@samanthony.xyz> | 2024-08-24 15:04:33 -0400 |
| commit | a8a38817d7bdd7505a7156e390460d48863a6bb3 (patch) | |
| tree | 77e0d50b01c0777789b72f4a59e2158352104a41 | |
| parent | eac0b4b31a1ae323222076dcb31dc7cd4d9402d5 (diff) | |
| download | gui-a8a38817d7bdd7505a7156e390460d48863a6bb3.zip | |
implement new layout design
| -rw-r--r-- | env.go | 102 | ||||
| -rw-r--r-- | gui_test.go | 86 | ||||
| -rw-r--r-- | intercepter.go | 35 | ||||
| -rw-r--r-- | layout.go | 106 | ||||
| -rw-r--r-- | layout/intercepter.go | 43 | ||||
| -rw-r--r-- | layout/layout.go | 20 | ||||
| -rw-r--r-- | layout/mux.go | 146 | ||||
| -rw-r--r-- | layout_test.go | 96 | ||||
| -rw-r--r-- | mux_test.go | 82 |
9 files changed, 424 insertions, 292 deletions
@@ -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 -} |