diff options
| -rw-r--r-- | .gitignore | 2 | ||||
| -rw-r--r-- | constraint.go | 34 | ||||
| -rw-r--r-- | env.go | 50 | ||||
| -rw-r--r-- | examples/imageviewer/browser.go | 2 | ||||
| -rw-r--r-- | examples/imageviewer/button.go | 2 | ||||
| -rw-r--r-- | examples/imageviewer/main.go | 2 | ||||
| -rw-r--r-- | examples/imageviewer/splits.go | 14 | ||||
| -rw-r--r-- | examples/imageviewer/viewer.go | 2 | ||||
| -rw-r--r-- | examples/paint/main.go | 6 | ||||
| -rw-r--r-- | examples/pexeso/main.go | 4 | ||||
| -rw-r--r-- | len.go | 25 | ||||
| -rw-r--r-- | len_test.go | 14 | ||||
| -rw-r--r-- | mux.go | 169 | ||||
| -rw-r--r-- | win/options.go | 48 | ||||
| -rw-r--r-- | win/win.go | 130 |
15 files changed, 311 insertions, 193 deletions
@@ -1,5 +1,5 @@ .vscode -test +bin examples/imageviewer/imageviewer examples/paint/paint examples/pexeso/pexeso diff --git a/constraint.go b/constraint.go new file mode 100644 index 0000000..c08e222 --- /dev/null +++ b/constraint.go @@ -0,0 +1,34 @@ +package gui + +// Constraint imposes a restriction on the size of a widget or layout. +type Constraint struct { + // Dim is the dimension to constrain: width/height. + Dim + + // Relation declares whether the constraint is an upper, lower, or exact bound. + Relation + + // Length is the target or threshold value. + Length +} + +// Dim is a dimension of a widget or layout that can be constrained. +type Dim int + +const ( + _ Dim = iota + Width + Height +) + +// Relation is an (in)equality. +type Relation int + +const ( + _ Relation = iota + Eq // == + Gteq // >= + Gt // > + Lteq // <= + Lt // < +) @@ -7,27 +7,37 @@ import ( // Env is the most important thing in this package. It is an interactive graphical // environment, such as a window. -// -// It has two channels: Events() and Draw(). -// -// The Events() channel produces events, like mouse and keyboard presses, while the -// Draw() channel receives drawing functions. A drawing function draws onto the -// supplied draw.Image, which is the drawing area of the Env and returns a rectangle -// covering the whole part of the image that got changed. -// -// An Env guarantees to produce a "resize/<x0>/<y0>/<x1>/<y1>" event as its first event. -// -// The Events() channel must be unlimited in capacity. Use MakeEventsChan() to create -// a channel of events with an unlimited capacity. -// -// The Draw() channel may be synchronous. -// -// Drawing functions sent to the Draw() channel are not guaranteed to be executed. -// -// Closing the Draw() channel results in closing the Env. The Env will subsequently -// close the Events() channel. On the other hand, when the Events() channel gets closed -// the user of the Env should subsequently close the Draw() channel. type Env interface { + // The Events() channel produces events, like mouse and keyboard presses. + // + // An Env guarantees to produce a "resize/<x0>/<y0>/<x1>/<y1>" event as its first event. + // + // The Events() channel must be unlimited in capacity. Use MakeEventsChan() to create + // a channel of events with an unlimited capacity. Events() <-chan Event + + // The Draw() channel receives drawing functions. + // + // A drawing function draws onto the supplied draw.Image, which is the drawing area + // of the Env, and returns a rectangle covering the whole part of the image that + // got changed. + // + // Drawing functions are not guaranteed to be executed. + // + // The Draw() channel may be synchronous. Draw() chan<- func(draw.Image) image.Rectangle + + // The Impose() channel receives constraints that are imposed on the size/layout of + // the Env by the widget occupying it. + // + // The Env may respond to constraints by sending a Resize event on the Events() channel. + // However, constraints are not guaranteed to be satisfied, e.g. if there is not + // enough space. + // + // The Impose() channel may be synchronous. + Impose() chan<- Constraint + + // Close destroys the Env. The Env will subsequently close the Events(), Draw(), + // and Impose() channels. + Close() } diff --git a/examples/imageviewer/browser.go b/examples/imageviewer/browser.go index cdac56f..6716918 100644 --- a/examples/imageviewer/browser.go +++ b/examples/imageviewer/browser.go @@ -108,7 +108,7 @@ func Browser(env gui.Env, theme *Theme, dir string, cd <-chan string, view chan< case e, ok := <-env.Events(): if !ok { - close(env.Draw()) + env.Close() return } diff --git a/examples/imageviewer/button.go b/examples/imageviewer/button.go index 0693e06..4e00287 100644 --- a/examples/imageviewer/button.go +++ b/examples/imageviewer/button.go @@ -58,5 +58,5 @@ func Button(env gui.Env, theme *Theme, text string, action func()) { } } - close(env.Draw()) + env.Close() } diff --git a/examples/imageviewer/main.go b/examples/imageviewer/main.go index 67f97fb..10335a1 100644 --- a/examples/imageviewer/main.go +++ b/examples/imageviewer/main.go @@ -56,7 +56,7 @@ func run() { for e := range env.Events() { switch e.(type) { case win.WiClose: - close(env.Draw()) + env.Close() } } } diff --git a/examples/imageviewer/splits.go b/examples/imageviewer/splits.go index f9daf2e..93a91be 100644 --- a/examples/imageviewer/splits.go +++ b/examples/imageviewer/splits.go @@ -8,12 +8,16 @@ import ( ) type envPair struct { + env gui.Env events <-chan gui.Event draw chan<- func(draw.Image) image.Rectangle + impose chan<- gui.Constraint } func (ep *envPair) Events() <-chan gui.Event { return ep.events } func (ep *envPair) Draw() chan<- func(draw.Image) image.Rectangle { return ep.draw } +func (ep *envPair) Impose() chan<- gui.Constraint { return ep.impose } +func (ep *envPair) Close() { ep.env.Close() } func FixedLeft(env gui.Env, maxX int) gui.Env { out, in := gui.MakeEventsChan() @@ -30,7 +34,7 @@ func FixedLeft(env gui.Env, maxX int) gui.Env { close(in) }() - return &envPair{out, env.Draw()} + return &envPair{env, out, env.Draw(), env.Impose()} } func FixedRight(env gui.Env, minX int) gui.Env { @@ -48,7 +52,7 @@ func FixedRight(env gui.Env, minX int) gui.Env { close(in) }() - return &envPair{out, env.Draw()} + return &envPair{env, out, env.Draw(), env.Impose()} } func FixedTop(env gui.Env, maxY int) gui.Env { @@ -66,7 +70,7 @@ func FixedTop(env gui.Env, maxY int) gui.Env { close(in) }() - return &envPair{out, env.Draw()} + return &envPair{env, out, env.Draw(), env.Impose()} } func FixedBottom(env gui.Env, minY int) gui.Env { @@ -84,7 +88,7 @@ func FixedBottom(env gui.Env, minY int) gui.Env { close(in) }() - return &envPair{out, env.Draw()} + return &envPair{env, out, env.Draw(), env.Impose()} } func EvenHorizontal(env gui.Env, minI, maxI, n int) gui.Env { @@ -103,5 +107,5 @@ func EvenHorizontal(env gui.Env, minI, maxI, n int) gui.Env { close(in) }() - return &envPair{out, env.Draw()} + return &envPair{env, out, env.Draw(), env.Impose()} } diff --git a/examples/imageviewer/viewer.go b/examples/imageviewer/viewer.go index 6f68e15..35e4081 100644 --- a/examples/imageviewer/viewer.go +++ b/examples/imageviewer/viewer.go @@ -50,7 +50,7 @@ func Viewer(env gui.Env, theme *Theme, view <-chan string) { case e, ok := <-env.Events(): if !ok { - close(env.Draw()) + env.Close() return } if resize, ok := e.(gui.Resize); ok { diff --git a/examples/paint/main.go b/examples/paint/main.go index fec06c3..7aa0179 100644 --- a/examples/paint/main.go +++ b/examples/paint/main.go @@ -26,7 +26,7 @@ func ColorPicker(env gui.Env, pick chan<- color.Color, r image.Rectangle, clr co } } - close(env.Draw()) + env.Close() } func Canvas(env gui.Env, pick <-chan color.Color, r image.Rectangle) { @@ -51,7 +51,7 @@ func Canvas(env gui.Env, pick <-chan color.Color, r image.Rectangle) { case event, ok := <-env.Events(): if !ok { - close(env.Draw()) + env.Close() return } @@ -118,7 +118,7 @@ func run() { for event := range env.Events() { switch event.(type) { case win.WiClose: - close(env.Draw()) + env.Close() } } } diff --git a/examples/pexeso/main.go b/examples/pexeso/main.go index 56bd6e6..f990951 100644 --- a/examples/pexeso/main.go +++ b/examples/pexeso/main.go @@ -90,7 +90,7 @@ func Tile(env gui.Env, pair chan PairMsg, r image.Rectangle, clr color.Color) { } if correct { - close(env.Draw()) + env.Close() return } @@ -144,7 +144,7 @@ func run() { for event := range env.Events() { switch event.(type) { case win.WiClose: - close(env.Draw()) + env.Close() } } } @@ -0,0 +1,25 @@ +package gui + +// TODO: add font-size-relative units once a text rendering package is added. + +// Length allows distance or size to be expressed in absolute or relative units. +type Length interface { + // Px resolves the Length to pixels for the given parent Env's size in pixels. + Px(parent int) int +} + +// Px is a Length expressed in pixels. +type Px int + +// Px implements the Length interface. +func (p Px) Px(parent int) int { return int(p) } + +// Relative is a Length relative to the width or height of the parent Env. +// +// Relative(0.10) <=> 10%. +type Relative float64 + +// Px implements the Length interface. +func (r Relative) Px(parent int) int { + return int(float64(r) * float64(parent)) +} diff --git a/len_test.go b/len_test.go new file mode 100644 index 0000000..269f90c --- /dev/null +++ b/len_test.go @@ -0,0 +1,14 @@ +package gui_test + +import ( + "fmt" + + "github.com/faiface/gui" +) + +func ExampleRelative() { + var l gui.Length = gui.Relative(0.10) // 10% + fmt.Println(l.Px(100)) + // Output: + // 10 +} @@ -3,53 +3,66 @@ package gui import ( "image" "image/draw" - "sync" ) // 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 and their draw functions get redirected to the root Env. +// events, and their draw functions and constraints get redirected to the root Env. type Mux struct { - mu sync.Mutex - lastResize Event - eventsIns []chan<- Event - draw chan<- func(draw.Image) image.Rectangle + eventsIns chan chan<- Event + draw chan<- func(draw.Image) image.Rectangle + impose chan<- Constraint + finish chan<- struct{} } -// NewMux creates a new Mux that multiplexes the given Env. It returns the Mux along with -// a master Env. The master Env is just like any other Env created by the Mux, except that -// closing the Draw() channel on the master Env closes the whole Mux and all other Envs -// created by the Mux. +// NewMux creates a new Mux that multiplexes the given Env. It returns the Mux along +// with a master Env. The master Env is just like any other Env created by the Mux, +// except that closing the master Env closes the whole Mux and all other Envs created +// by the Mux. func NewMux(env Env) (mux *Mux, master Env) { - drawChan := make(chan func(draw.Image) image.Rectangle) - mux = &Mux{draw: drawChan} - master = mux.makeEnv(true) - - go func() { - for d := range drawChan { - env.Draw() <- d - } - close(env.Draw()) - }() + finish := make(chan struct{}) + mux = &Mux{ + make(chan chan<- Event), + env.Draw(), + env.Impose(), + finish, + } go func() { - for e := range env.Events() { - mux.mu.Lock() - if resize, ok := e.(Resize); ok { - mux.lastResize = resize + var ( + eventsIns []chan<- Event // one per child Env + lastResize Event // TODO: should we block until we receive the first one from the root Env? + ) + defer func() { + for _, evIn := range eventsIns { + close(evIn) } - for _, eventsIn := range mux.eventsIns { - eventsIn <- e + close(mux.eventsIns) + close(mux.finish) + env.Close() + }() + for { + select { + case e := <-env.Events(): + if resize, ok := e.(Resize); ok { + lastResize = resize + } + for _, eventsIn := range eventsIns { + eventsIn <- e + } + case evIn := <-mux.eventsIns: // new env created by makeEnv() + eventsIns = append(eventsIns, evIn) + // Make sure to always send a resize event to a new Env if we got the size already. + if lastResize != nil { + evIn <- lastResize + } + case <-finish: + return } - mux.mu.Unlock() - } - mux.mu.Lock() - for _, eventsIn := range mux.eventsIns { - close(eventsIn) } - mux.mu.Unlock() }() + master = mux.makeEnv(true) return mux, master } @@ -63,70 +76,62 @@ func (mux *Mux) MakeEnv() Env { type muxEnv struct { events <-chan Event draw chan<- func(draw.Image) image.Rectangle + impose chan<- Constraint + finish chan<- struct{} } func (m *muxEnv) Events() <-chan Event { return m.events } func (m *muxEnv) Draw() chan<- func(draw.Image) image.Rectangle { return m.draw } +func (m *muxEnv) Impose() chan<- Constraint { return m.impose } + +func (m *muxEnv) Close() { + m.finish <- *new(struct{}) + close(m.finish) + close(m.draw) + close(m.impose) +} func (mux *Mux) makeEnv(master bool) Env { eventsOut, eventsIn := MakeEventsChan() - drawChan := make(chan func(draw.Image) image.Rectangle) - env := &muxEnv{eventsOut, drawChan} + draws := make(chan func(draw.Image) image.Rectangle) + imposes := make(chan Constraint) + finish := make(chan struct{}) + env := &muxEnv{eventsOut, draws, imposes, finish} - mux.mu.Lock() - mux.eventsIns = append(mux.eventsIns, eventsIn) - // make sure to always send a resize event to a new Env if we got the size already - // that means it missed the resize event by the root Env - if mux.lastResize != nil { - eventsIn <- mux.lastResize - } - mux.mu.Unlock() + mux.eventsIns <- eventsIn 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 drawChan { - } - } - }() - for d := range drawChan { - mux.draw <- d // ! - } - }() if master { - mux.mu.Lock() - for _, eventsIn := range mux.eventsIns { - close(eventsIn) - } - mux.eventsIns = nil - close(mux.draw) - mux.mu.Unlock() - } else { - mux.mu.Lock() - i := -1 - for i = range mux.eventsIns { - if mux.eventsIns[i] == eventsIn { - break + // Close Mux and all other child Envs when master Env is closed + defer func() { mux.finish <- *new(struct{}) }() + } + + // When the master Env gets closed, the Mux closes all the Events() + // channels of all the children Envs, and it also closes the Mux's draw channel. 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 Mux's draw channel gets closed, the line marked with ! will + // cause panic. We recover this panic, then we receive, but ignore all further draw + // commands, correctly draining the Env until it closes itself. + defer func() { + if recover() != nil { + for range draws { } } - if i != -1 { - mux.eventsIns = append(mux.eventsIns[:i], mux.eventsIns[i+1:]...) + }() + for { + select { + case d := <-draws: + mux.draw <- d // ! + case c := <-imposes: + mux.impose <- c + case <-finish: + return } - mux.mu.Unlock() } }() diff --git a/win/options.go b/win/options.go new file mode 100644 index 0000000..38f94e3 --- /dev/null +++ b/win/options.go @@ -0,0 +1,48 @@ +package win + +// Option is a functional option to the window constructor New. +type Option func(*options) + +type options struct { + title string + width, height int + resizable bool + borderless bool + maximized bool +} + +// Title option sets the title (caption) of the window. +func Title(title string) Option { + return func(o *options) { + o.title = title + } +} + +// Size option sets the width and height of the window. +func Size(width, height int) Option { + return func(o *options) { + o.width = width + o.height = height + } +} + +// Resizable option makes the window resizable by the user. +func Resizable() Option { + return func(o *options) { + o.resizable = true + } +} + +// Borderless option makes the window borderless. +func Borderless() Option { + return func(o *options) { + o.borderless = true + } +} + +// Maximized option makes the window start maximized. +func Maximized() Option { + return func(o *options) { + o.maximized = true + } +} @@ -1,6 +1,7 @@ package win import ( + "context" "image" "image/draw" "runtime" @@ -13,51 +14,24 @@ import ( "github.com/go-gl/glfw/v3.2/glfw" ) -// Option is a functional option to the window constructor New. -type Option func(*options) - -type options struct { - title string - width, height int - resizable bool - borderless bool - maximized bool -} - -// Title option sets the title (caption) of the window. -func Title(title string) Option { - return func(o *options) { - o.title = title - } -} - -// Size option sets the width and height of the window. -func Size(width, height int) Option { - return func(o *options) { - o.width = width - o.height = height - } -} - -// Resizable option makes the window resizable by the user. -func Resizable() Option { - return func(o *options) { - o.resizable = true - } -} +// Win is an Env that handles an actual graphical window. +// +// It receives its events from the OS and it draws to the surface of the window. +// +// Warning: only one window can be open at a time. This will be fixed. +type Win struct { + eventsOut <-chan gui.Event + eventsIn chan<- gui.Event + draw chan func(draw.Image) image.Rectangle + impose chan gui.Constraint -// Borderless option makes the window borderless. -func Borderless() Option { - return func(o *options) { - o.borderless = true - } -} + newSize chan image.Rectangle + ctx context.Context + cancel func() -// Maximized option makes the window start maximized. -func Maximized() Option { - return func(o *options) { - o.maximized = true - } + w *glfw.Window + img *image.RGBA + ratio int } // New creates a new window with all the supplied options. @@ -77,13 +51,15 @@ func New(opts ...Option) (*Win, error) { } eventsOut, eventsIn := gui.MakeEventsChan() - + ctx, cancel := context.WithCancel(context.Background()) w := &Win{ eventsOut: eventsOut, eventsIn: eventsIn, draw: make(chan func(draw.Image) image.Rectangle), + impose: make(chan gui.Constraint), newSize: make(chan image.Rectangle), - finish: make(chan struct{}), + ctx: ctx, + cancel: cancel, } var err error @@ -152,30 +128,29 @@ func makeGLFWWin(o *options) (*glfw.Window, error) { return w, nil } -// Win is an Env that handles an actual graphical window. -// -// It receives its events from the OS and it draws to the surface of the window. -// -// Warning: only one window can be open at a time. This will be fixed. -type Win struct { - eventsOut <-chan gui.Event - eventsIn chan<- gui.Event - draw chan func(draw.Image) image.Rectangle - - newSize chan image.Rectangle - finish chan struct{} - - w *glfw.Window - img *image.RGBA - ratio int -} - // Events returns the events channel of the window. func (w *Win) Events() <-chan gui.Event { return w.eventsOut } // Draw returns the draw channel of the window. func (w *Win) Draw() chan<- func(draw.Image) image.Rectangle { return w.draw } +// Impose returns the impose channel of the window. +// The window ignores constraints sent to the impose channel. +func (w *Win) Impose() chan<- gui.Constraint { return w.impose } + +// Close destroys the window. +func (w *Win) Close() { + w.cancel() +} + +func (w *Win) close() { + close(w.eventsIn) + close(w.draw) + close(w.impose) + close(w.newSize) + w.w.Destroy() +} + var buttons = map[glfw.MouseButton]Button{ glfw.MouseButtonLeft: ButtonLeft, glfw.MouseButtonRight: ButtonRight, @@ -264,9 +239,8 @@ func (w *Win) eventThread() { for { select { - case <-w.finish: - close(w.eventsIn) - w.w.Destroy() + case <-w.ctx.Done(): + w.close() return default: glfw.WaitEventsTimeout(1.0 / 30) @@ -291,13 +265,15 @@ loop: w.img = img totalR = totalR.Union(r) - case d, ok := <-w.draw: - if !ok { - close(w.finish) - return - } + case d := <-w.draw: r := d(w.img) totalR = totalR.Union(r) + + case <-w.impose: + // ignore + + case <-w.ctx.Done(): + return } for { @@ -313,13 +289,15 @@ loop: w.img = img totalR = totalR.Union(r) - case d, ok := <-w.draw: - if !ok { - close(w.finish) - return - } + case d := <-w.draw: r := d(w.img) totalR = totalR.Union(r) + + case <-w.impose: + // ignore + + case <-w.ctx.Done(): + return } } } |