aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--constraint.go34
-rw-r--r--env.go50
-rw-r--r--examples/imageviewer/browser.go2
-rw-r--r--examples/imageviewer/button.go2
-rw-r--r--examples/imageviewer/main.go2
-rw-r--r--examples/imageviewer/splits.go14
-rw-r--r--examples/imageviewer/viewer.go2
-rw-r--r--examples/paint/main.go6
-rw-r--r--examples/pexeso/main.go4
-rw-r--r--len.go25
-rw-r--r--len_test.go14
-rw-r--r--mux.go169
-rw-r--r--win/options.go48
-rw-r--r--win/win.go130
15 files changed, 311 insertions, 193 deletions
diff --git a/.gitignore b/.gitignore
index a0222db..a016080 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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 // <
+)
diff --git a/env.go b/env.go
index 2515417..005ef80 100644
--- a/env.go
+++ b/env.go
@@ -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()
}
}
}
diff --git a/len.go b/len.go
new file mode 100644
index 0000000..895688d
--- /dev/null
+++ b/len.go
@@ -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
+}
diff --git a/mux.go b/mux.go
index 4198d61..c3a8149 100644
--- a/mux.go
+++ b/mux.go
@@ -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
+ }
+}
diff --git a/win/win.go b/win/win.go
index 1741549..06f3f68 100644
--- a/win/win.go
+++ b/win/win.go
@@ -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
}
}
}