aboutsummaryrefslogtreecommitdiffstats
path: root/layout
diff options
context:
space:
mode:
authorClement Benard <contact@clementbenard.com>2019-08-07 16:02:33 +0200
committerClement Benard <contact@clementbenard.com>2019-08-07 16:02:33 +0200
commit8d183ef96a57e3a2f42c0cb4ec0ab4c256e0d47e (patch)
tree4fa644f93ceeb5c102c5c1dcf85105890c0cf25b /layout
parent8b70878ccc7fe324f3647e56503a37f3780f9d41 (diff)
downloadgui-8d183ef96a57e3a2f42c0cb4ec0ab4c256e0d47e.zip
Made the layout package actually usable
Diffstat (limited to 'layout')
-rw-r--r--layout/box.go57
-rw-r--r--layout/grid.go95
-rw-r--r--layout/intercepter.go43
-rw-r--r--layout/layout.go9
-rw-r--r--layout/mux.go16
-rw-r--r--layout/scroller.go133
-rw-r--r--layout/split.go6
7 files changed, 265 insertions, 94 deletions
diff --git a/layout/box.go b/layout/box.go
deleted file mode 100644
index 24f0c0d..0000000
--- a/layout/box.go
+++ /dev/null
@@ -1,57 +0,0 @@
-package layout
-
-import (
- "image"
- "image/color"
- "image/draw"
-)
-
-type Box struct {
- // Number of child elements
- Length int
- // Background changes the background of the Box to a uniform color.
- Background color.Color
- // Split changes the way the space is divided among the elements.
- Split SplitFunc
- // Gap changes the Box gap.
- // The gap is identical everywhere (top, left, bottom, right).
- Gap int
-
- // Vertical changes the otherwise horizontal Box to be vertical.
- Vertical bool
-}
-
-func (b Box) Redraw(drw draw.Image, bounds image.Rectangle) {
- col := b.Background
- if col == nil {
- col = image.Black
- }
-
- draw.Draw(drw, bounds, image.NewUniform(col), image.ZP, draw.Src)
-}
-
-func (b Box) Lay(bounds image.Rectangle) []image.Rectangle {
- items := b.Length
- gap := b.Gap
- split := b.Split
- if split == nil {
- split = EvenSplit
- }
- ret := make([]image.Rectangle, 0, items)
- if b.Vertical {
- spl := split(items, bounds.Dy()-(gap*(items+1)))
- Y := bounds.Min.Y + gap
- for _, item := range spl {
- ret = append(ret, image.Rect(bounds.Min.X+gap, Y, bounds.Max.X-gap, Y+item))
- Y += item + gap
- }
- } else {
- spl := split(items, bounds.Dx()-(gap*(items+1)))
- X := bounds.Min.X + gap
- for _, item := range spl {
- ret = append(ret, image.Rect(X, bounds.Min.Y+gap, X+item, bounds.Max.Y-gap))
- X += item + gap
- }
- }
- return ret
-}
diff --git a/layout/grid.go b/layout/grid.go
index f3cced6..3ff8112 100644
--- a/layout/grid.go
+++ b/layout/grid.go
@@ -4,8 +4,15 @@ import (
"image"
"image/color"
"image/draw"
+ "log"
+
+ "github.com/faiface/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
@@ -13,42 +20,92 @@ type Grid struct {
Background color.Color
// Gap represents the grid gap, equal on all sides.
Gap int
- // SplitX represents the way the space is divided among the columns in each row.
- SplitX SplitFunc
- // SplitY represents the way the space is divided among the rows.
- SplitY SplitFunc
+ // 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) {
+func (g Grid) redraw(drw draw.Image, bounds image.Rectangle) {
col := g.Background
if col == nil {
- col = image.Black
+ 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, image.NewUniform(col), 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
- splitX := g.SplitX
- if splitX == nil {
- splitX = EvenSplit
+ splitMain := g.Split
+ if splitMain == nil {
+ splitMain = EvenSplit
+ }
+ splitSec := g.SplitRows
+ if splitSec == nil {
+ splitSec = EvenSplit
}
- splitY := g.SplitY
- if splitY == nil {
- splitY = 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)
- rowsH := splitY(len(rows), bounds.Dy()-(gap*(len(rows)+1)))
- X := gap + bounds.Min.X
- Y := gap + bounds.Min.Y
+
+ // 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 := splitX(cols, bounds.Dx()-(gap*(cols+1)))
- X = gap + bounds.Min.X
+ colsW := splitMain(cols, W-(gap*(cols+1))-margin*2)
+ X = gap + mX + margin
for _, w := range colsW {
- ret = append(ret, image.Rect(X, Y, X+w, Y+h))
+ 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
diff --git a/layout/intercepter.go b/layout/intercepter.go
new file mode 100644
index 0000000..eee7aff
--- /dev/null
+++ b/layout/intercepter.go
@@ -0,0 +1,43 @@
+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
index 8b8e039..4d8e616 100644
--- a/layout/layout.go
+++ b/layout/layout.go
@@ -2,7 +2,6 @@ package layout
import (
"image"
- "image/draw"
)
// Layout represents any graphical layout
@@ -10,10 +9,12 @@ import (
// 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.
-// The order must be the same as Items.
//
-// Redraw only draws the background or frame of the Layout, not the 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
- Redraw(draw.Image, image.Rectangle)
+ Intercepter
}
diff --git a/layout/mux.go b/layout/mux.go
index d56d266..8eb19d6 100644
--- a/layout/mux.go
+++ b/layout/mux.go
@@ -21,7 +21,6 @@ type Mux struct {
eventsIns []chan<- gui.Event
draw chan<- func(draw.Image) image.Rectangle
- evIn chan<- gui.Event
layout Layout
}
@@ -33,7 +32,8 @@ func (mux *Mux) Layout() 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(env gui.Env, envs []*gui.Env, l Layout) (mux *Mux, master gui.Env) {
+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,
@@ -41,7 +41,6 @@ func NewMux(env gui.Env, envs []*gui.Env, l Layout) (mux *Mux, master gui.Env) {
}
master, masterIn := mux.makeEnv(true)
events := make(chan gui.Event)
- mux.evIn = events
go func() {
for d := range drawChan {
env.Draw() <- d
@@ -64,18 +63,12 @@ func NewMux(env gui.Env, envs []*gui.Env, l Layout) (mux *Mux, master gui.Env) {
mux.lastResize = resize
rect := resize.Rectangle
lay := mux.layout.Lay(rect)
- if len(lay) != len(mux.eventsIns) {
- log.Printf("Lay of %T has %d elements while mux has %d, skipping\n", l, len(lay), len(envs))
+ 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
}
- // Redraw self
- mux.draw <- func(drw draw.Image) image.Rectangle {
- mux.layout.Redraw(drw, rect)
- return rect
- }
-
// Send appropriate resize Events to childs
for i, eventsIn := range mux.eventsIns {
resize.Rectangle = lay[i]
@@ -172,7 +165,6 @@ func (mux *Mux) makeEnv(master bool) (env gui.Env, eventsIn chan<- gui.Event) {
mux.eventsIns = append(mux.eventsIns[:i], mux.eventsIns[i+1:]...)
}
mux.mu.Unlock()
- mux.evIn <- mux.lastResize
}
}()
diff --git a/layout/scroller.go b/layout/scroller.go
new file mode 100644
index 0000000..151a4f2
--- /dev/null
+++ b/layout/scroller.go
@@ -0,0 +1,133 @@
+package layout
+
+import (
+ "image"
+ "image/color"
+ "image/draw"
+ // "log"
+ "sync"
+
+ "github.com/faiface/gui"
+ "github.com/faiface/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/layout/split.go b/layout/split.go
index fb6d36e..db04225 100644
--- a/layout/split.go
+++ b/layout/split.go
@@ -7,18 +7,20 @@ import "fmt"
// 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, 0, elements)
+ ret := make([]int, elements)
for elements > 0 {
v := width / elements
width -= v
elements -= 1
- ret = append(ret, v)
+ ret[elements] = v
}
return ret
}