aboutsummaryrefslogtreecommitdiffstats
path: root/gui
diff options
context:
space:
mode:
Diffstat (limited to 'gui')
-rw-r--r--gui/layout/doc.go10
-rw-r--r--gui/layout/grid.go115
-rw-r--r--gui/layout/intercepter.go43
-rw-r--r--gui/layout/layout.go20
-rw-r--r--gui/layout/mux.go172
-rw-r--r--gui/layout/scroller.go132
-rw-r--r--gui/layout/split.go26
-rw-r--r--gui/widget/text.go19
-rw-r--r--gui/widget/widget.go28
9 files changed, 547 insertions, 18 deletions
diff --git a/gui/layout/doc.go b/gui/layout/doc.go
new file mode 100644
index 0000000..8305d43
--- /dev/null
+++ b/gui/layout/doc.go
@@ -0,0 +1,10 @@
+/*
+Package layout provides a Layout system for faiface/gui.
+
+The core of the package is the Layout interface, everything else is just
+implementation.
+
+The Layouts represent a Layout, and the Mux makes them usable.
+The Mux basically acts as a sort of driver.
+*/
+package layout
diff --git a/gui/layout/grid.go b/gui/layout/grid.go
new file mode 100644
index 0000000..324353c
--- /dev/null
+++ b/gui/layout/grid.go
@@ -0,0 +1,115 @@
+package layout
+
+import (
+ "image"
+ "image/color"
+ "image/draw"
+ "log"
+
+ "volute/gui"
+)
+
+var _ Layout = Grid{}
+
+// Grid represents a grid with rows and columns in each row.
+// Each row can be a different length.
+type Grid struct {
+ // Rows represents the number of childs of each row.
+ Rows []int
+ // Background represents the background of the grid as a uniform color.
+ Background color.Color
+ // Gap represents the grid gap, equal on all sides.
+ Gap int
+ // Split represents the way the space is divided among the columns in each row.
+ Split SplitFunc
+ // SplitRows represents the way the space is divided among the rows.
+ SplitRows SplitFunc
+
+ Margin int
+ Border int
+ BorderColor color.Color
+
+ // Flip represents the orientation of the grid.
+ // When false, rows are spread in the Y axis and columns in the X axis.
+ // When true, rows are spread in the X axis and columns in the Y axis.
+ Flip bool
+}
+
+func (g Grid) redraw(drw draw.Image, bounds image.Rectangle) {
+ col := g.Background
+ if col == nil {
+ col = color.Black
+ }
+ if g.Border > 0 {
+ bcol := g.BorderColor
+ if bcol == nil {
+ bcol = color.Black
+ }
+ draw.Draw(drw, bounds, image.NewUniform(bcol), image.ZP, draw.Src)
+ }
+ draw.Draw(drw, bounds.Inset(g.Border), image.NewUniform(col), image.ZP, draw.Src)
+}
+
+func (g Grid) Intercept(env gui.Env) gui.Env {
+ return RedrawIntercepter{g.redraw}.Intercept(env)
+}
+
+func (g Grid) Lay(bounds image.Rectangle) []image.Rectangle {
+ gap := g.Gap
+ rows := g.Rows
+ splitMain := g.Split
+ if splitMain == nil {
+ splitMain = EvenSplit
+ }
+ splitSec := g.SplitRows
+ if splitSec == nil {
+ splitSec = EvenSplit
+ }
+ margin := g.Margin
+ flip := g.Flip
+ if margin+gap < 0 {
+ log.Println("Grid goes out of bounds")
+ }
+ if margin+gap < g.Border {
+ log.Println("Grid border will not be shown properly")
+ }
+
+ ret := make([]image.Rectangle, 0)
+
+ // Sorry it's not very understandable
+ var H, W int
+ var mX, mY int
+ if flip {
+ H = bounds.Dx()
+ W = bounds.Dy()
+ mX = bounds.Min.Y
+ mY = bounds.Min.X
+ } else {
+ H = bounds.Dy()
+ W = bounds.Dx()
+ mX = bounds.Min.X
+ mY = bounds.Min.Y
+ }
+ rowsH := splitSec(len(rows), H-(gap*(len(rows)+1))-margin*2)
+ var X int
+ var Y int
+ Y = gap + mY + margin
+ for y, cols := range rows {
+ h := rowsH[y]
+ colsW := splitMain(cols, W-(gap*(cols+1))-margin*2)
+ X = gap + mX + margin
+ for _, w := range colsW {
+ var r image.Rectangle
+ if flip {
+ r = image.Rect(Y, X, Y+h, X+w)
+ } else {
+ r = image.Rect(X, Y, X+w, Y+h)
+ }
+ ret = append(ret, r)
+ X += gap + w
+ }
+ Y += gap + h
+ }
+
+ return ret
+}
diff --git a/gui/layout/intercepter.go b/gui/layout/intercepter.go
new file mode 100644
index 0000000..d25d578
--- /dev/null
+++ b/gui/layout/intercepter.go
@@ -0,0 +1,43 @@
+package layout
+
+import (
+ "image"
+ "image/draw"
+
+ "volute/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/gui/layout/layout.go b/gui/layout/layout.go
new file mode 100644
index 0000000..4d8e616
--- /dev/null
+++ b/gui/layout/layout.go
@@ -0,0 +1,20 @@
+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/gui/layout/mux.go b/gui/layout/mux.go
new file mode 100644
index 0000000..7b193e7
--- /dev/null
+++ b/gui/layout/mux.go
@@ -0,0 +1,172 @@
+package layout
+
+import (
+ "image"
+ "image/draw"
+ "log"
+ "sync"
+
+ "volute/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.
+// The master Env gets the original gui.Resize events.
+type Mux struct {
+ mu sync.Mutex
+ lastResize gui.Event
+ eventsIns []chan<- gui.Event
+ draw chan<- func(draw.Image) image.Rectangle
+
+ layout Layout
+}
+
+// Layout returns the underlying Layout of the Mux.
+func (mux *Mux) Layout() Layout {
+ return mux.layout
+}
+
+// NewMux should only be used internally by Layouts.
+// It has mostly the same behaviour as gui.Mux, except for its use of an underlying Layout
+// for modifying the gui.Resize events sent to the childs.
+func NewMux(ev gui.Env, envs []*gui.Env, l Layout) (mux *Mux, master gui.Env) {
+ env := l.Intercept(ev)
+ drawChan := make(chan func(draw.Image) image.Rectangle)
+ mux = &Mux{
+ layout: l,
+ draw: drawChan,
+ }
+ master, masterIn := mux.makeEnv(true)
+ events := make(chan gui.Event)
+ go func() {
+ for d := range drawChan {
+ env.Draw() <- d
+ }
+ close(env.Draw())
+ }()
+
+ go func() {
+ for e := range env.Events() {
+ events <- e
+ }
+ }()
+
+ go func() {
+ for e := range events {
+ // master gets a copy of all events to the Mux
+ masterIn <- e
+ mux.mu.Lock()
+ if resize, ok := e.(gui.Resize); ok {
+ mux.lastResize = resize
+ rect := resize.Rectangle
+ lay := mux.layout.Lay(rect)
+ if len(lay) < len(envs) {
+ log.Printf("Lay of %T is not large enough (%d) for %d childs, skipping\n", l, len(lay), len(envs))
+ mux.mu.Unlock()
+ continue
+ }
+
+ // Send appropriate resize Events to childs
+ for i, eventsIn := range mux.eventsIns {
+ resize.Rectangle = lay[i]
+ eventsIn <- resize
+ }
+
+ } else {
+ for _, eventsIn := range mux.eventsIns {
+ eventsIn <- e
+ }
+ }
+ mux.mu.Unlock()
+ }
+ mux.mu.Lock()
+ for _, eventsIn := range mux.eventsIns {
+ close(eventsIn)
+ }
+ mux.mu.Unlock()
+ }()
+
+ for _, en := range envs {
+ *en, _ = mux.makeEnv(false)
+ }
+ return
+}
+
+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 }
+
+// We do not store master env
+func (mux *Mux) makeEnv(master bool) (env gui.Env, eventsIn chan<- gui.Event) {
+ eventsOut, eventsIn := gui.MakeEventsChan()
+ drawChan := make(chan func(draw.Image) image.Rectangle)
+ env = &muxEnv{eventsOut, drawChan}
+
+ if !master {
+ 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()
+ }
+
+ 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
+ }
+ }
+ if i != -1 {
+ mux.eventsIns = append(mux.eventsIns[:i], mux.eventsIns[i+1:]...)
+ }
+ mux.mu.Unlock()
+ }
+ }()
+
+ return env, eventsIn
+}
diff --git a/gui/layout/scroller.go b/gui/layout/scroller.go
new file mode 100644
index 0000000..b7fc3f5
--- /dev/null
+++ b/gui/layout/scroller.go
@@ -0,0 +1,132 @@
+package layout
+
+import (
+ "image"
+ "image/color"
+ "image/draw"
+ "sync"
+
+ "volute/gui"
+ "volute/gui/win"
+)
+
+var _ Layout = &Scroller{}
+
+type Scroller struct {
+ Background color.Color
+ Length int
+ ChildHeight int
+ Offset int
+ Gap int
+ Vertical bool
+}
+
+func (s Scroller) redraw(drw draw.Image, bounds image.Rectangle) {
+ col := s.Background
+ if col == nil {
+ col = image.Black
+ }
+ draw.Draw(drw, bounds, image.NewUniform(col), image.ZP, draw.Src)
+}
+
+func clamp(val, a, b int) int {
+ if a > b {
+ if val < b {
+ return b
+ }
+ if val > a {
+ return a
+ }
+ } else {
+ if val > b {
+ return b
+ }
+ if val < a {
+ return a
+ }
+ }
+ return val
+}
+
+func (s *Scroller) Intercept(env gui.Env) gui.Env {
+ evs := env.Events()
+ out, in := gui.MakeEventsChan()
+ drawChan := make(chan func(draw.Image) image.Rectangle)
+ ret := &muxEnv{out, drawChan}
+ var lastResize gui.Resize
+ var img draw.Image
+ img = image.NewRGBA(image.ZR)
+ var mu sync.Mutex
+ var over bool
+
+ go func() {
+ for dc := range drawChan {
+ mu.Lock()
+ // draw.Draw will not draw out of bounds, call should be inexpensive if element not visible
+ res := dc(img)
+ // Only send a draw call up if visibly changed
+ if res.Intersect(img.Bounds()) != image.ZR {
+ env.Draw() <- func(drw draw.Image) image.Rectangle {
+ draw.Draw(drw, lastResize.Rectangle, img, lastResize.Rectangle.Min, draw.Over)
+ return img.Bounds()
+ }
+ }
+ mu.Unlock()
+ }
+ }()
+
+ go func() {
+ for ev := range evs {
+ switch ev := ev.(type) {
+ case win.MoMove:
+ mu.Lock()
+ over = ev.Point.In(lastResize.Rectangle)
+ mu.Unlock()
+ case win.MoScroll:
+ if !over {
+ continue
+ }
+ mu.Lock()
+ oldoff := s.Offset
+ v := s.Length*s.ChildHeight + ((s.Length + 1) * s.Gap)
+ if s.Vertical {
+ h := lastResize.Dx()
+ s.Offset = clamp(s.Offset+ev.Point.X*16, h-v, 0)
+ } else {
+ h := lastResize.Dy()
+ s.Offset = clamp(s.Offset+ev.Point.Y*16, h-v, 0)
+ }
+ if oldoff != s.Offset {
+ s.redraw(img, img.Bounds())
+ in <- lastResize
+ }
+ mu.Unlock()
+ case gui.Resize:
+ mu.Lock()
+ lastResize = ev
+ img = image.NewRGBA(lastResize.Rectangle)
+ s.redraw(img, img.Bounds())
+ mu.Unlock()
+ in <- ev
+ default:
+ in <- ev
+ }
+ }
+ }()
+ return ret
+}
+
+func (s Scroller) Lay(bounds image.Rectangle) []image.Rectangle {
+ items := s.Length
+ ch := s.ChildHeight
+ gap := s.Gap
+
+ ret := make([]image.Rectangle, items)
+ Y := bounds.Min.Y + s.Offset + gap
+ for i := 0; i < items; i++ {
+ r := image.Rect(bounds.Min.X+gap, Y, bounds.Max.X-gap, Y+ch)
+ ret[i] = r
+ Y += ch + gap
+ }
+ return ret
+}
diff --git a/gui/layout/split.go b/gui/layout/split.go
new file mode 100644
index 0000000..db04225
--- /dev/null
+++ b/gui/layout/split.go
@@ -0,0 +1,26 @@
+package layout
+
+import "fmt"
+
+// SplitFunc represents a way to split a space among a number of elements.
+// The length of the returned slice must be equal to the number of elements.
+// The sum of all elements of the returned slice must be eqal to the space.
+type SplitFunc func(elements int, space int) []int
+
+var _ SplitFunc = EvenSplit
+
+// EvenSplit is a SplitFunc used to split a space (almost) evenly among the elements.
+// It is almost evenly because width may not be divisible by elements.
+func EvenSplit(elements int, width int) []int {
+ if elements <= 0 {
+ panic(fmt.Errorf("EvenSplit: elements must be greater than 0"))
+ }
+ ret := make([]int, elements)
+ for elements > 0 {
+ v := width / elements
+ width -= v
+ elements -= 1
+ ret[elements] = v
+ }
+ return ret
+}
diff --git a/gui/widget/text.go b/gui/widget/text.go
index 5e37d70..e336874 100644
--- a/gui/widget/text.go
+++ b/gui/widget/text.go
@@ -5,6 +5,7 @@ import (
"sync"
"image"
+ "image/color"
"image/draw"
"golang.org/x/image/font"
@@ -14,12 +15,10 @@ import (
)
var (
- FONT = goregular.TTF
- FONT_SIZE = 15
- DPI = 72
- PADDING = 3
- BG_COLOR = image.White
- TEXT_COLOR = image.Black
+ FONT = goregular.TTF
+ FONT_SIZE = 15
+ DPI = 72
+ PADDING = 3
)
var face *concurrentFace
@@ -47,20 +46,20 @@ func TextHeight() int {
return FONT_SIZE + 2*PADDING
}
-func drawText(text []byte, dst draw.Image, r image.Rectangle) {
+func drawText(text []byte, dst draw.Image, r image.Rectangle, fg, bg color.Color) {
drawer := font.Drawer{
- Src: TEXT_COLOR,
+ Src: &image.Uniform{fg},
Face: face,
Dot: fixed.P(0, 0),
}
// background
- draw.Draw(dst, r, BG_COLOR, image.ZP, draw.Src)
+ draw.Draw(dst, r, &image.Uniform{bg}, image.ZP, draw.Src)
// text image
bounds := textBounds(text, drawer)
textImg := image.NewRGBA(bounds)
- draw.Draw(textImg, bounds, BG_COLOR, image.ZP, draw.Src)
+ draw.Draw(textImg, bounds, &image.Uniform{bg}, image.ZP, draw.Src)
drawer.Dst = textImg
drawer.DrawBytes(text)
diff --git a/gui/widget/widget.go b/gui/widget/widget.go
index 94d1915..6da7aaf 100644
--- a/gui/widget/widget.go
+++ b/gui/widget/widget.go
@@ -5,15 +5,22 @@ import (
"fmt"
"image"
+ "image/color"
"image/draw"
"volute/gui"
"volute/gui/win"
)
+var (
+ FOCUS_COLOR = color.RGBA{179, 217, 255, 255}
+ BLACK = color.Gray{0}
+ WHITE = color.Gray{255}
+)
+
func Label(text string, r image.Rectangle, env gui.Env) {
redraw := func(drw draw.Image) image.Rectangle {
- drawText([]byte(text), drw, r)
+ drawText([]byte(text), drw, r, BLACK, WHITE)
return r
}
env.Draw() <- redraw
@@ -29,36 +36,41 @@ func Label(text string, r image.Rectangle, env gui.Env) {
}
func Input(val chan<- uint, r image.Rectangle, focusChan <-chan bool, env gui.Env) {
- redraw := func(text []byte) func(draw.Image) image.Rectangle {
+ redraw := func(text []byte, focus bool) func(draw.Image) image.Rectangle {
return func(drw draw.Image) image.Rectangle {
- drawText(text, drw, r)
+ if focus {
+ drawText(text, drw, r, BLACK, FOCUS_COLOR)
+ } else {
+ drawText(text, drw, r, BLACK, WHITE)
+ }
return r
}
}
text := []byte{'0'}
focus := false
- env.Draw() <- redraw(text)
+ env.Draw() <- redraw(text, focus)
for {
select {
case focus = <-focusChan:
+ env.Draw() <- redraw(text, focus)
case event := <-env.Events():
switch event := event.(type) {
case win.WiFocus:
if event.Focused {
- env.Draw() <- redraw(text)
+ env.Draw() <- redraw(text, focus)
}
case win.KbType:
if focus && isDigit(event.Rune) {
text = fmt.Appendf(text, "%c", event.Rune)
- env.Draw() <- redraw(text)
+ env.Draw() <- redraw(text, focus)
val <- atoi(text)
}
case win.KbDown:
if focus && event.Key == win.KeyBackspace && len(text) > 0 {
text = text[:len(text)-1]
- env.Draw() <- redraw(text)
+ env.Draw() <- redraw(text, focus)
val <- atoi(text)
}
}
@@ -70,7 +82,7 @@ func Input(val chan<- uint, r image.Rectangle, focusChan <-chan bool, env gui.En
func Output(val <-chan uint, r image.Rectangle, env gui.Env) {
redraw := func(n uint) func(draw.Image) image.Rectangle {
return func(drw draw.Image) image.Rectangle {
- drawText([]byte(fmt.Sprint(n)), drw, r)
+ drawText([]byte(fmt.Sprint(n)), drw, r, BLACK, WHITE)
return r
}
}