From 3a216b96b6a7c80275a2516e7de82d9b2ffc96df Mon Sep 17 00:00:00 2001 From: Clement Benard Date: Thu, 4 Jul 2019 16:28:51 +0200 Subject: added layout basics --- examples/layout/button.go | 64 ++++++++++++++++ examples/layout/main.go | 128 ++++++++++++++++++++++++++++++++ examples/layout/theme.go | 19 +++++ examples/layout/utils.go | 104 ++++++++++++++++++++++++++ layout/fixedgrid/fixedgrid.go | 88 ++++++++++++++++++++++ layout/layout.go | 165 ++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 568 insertions(+) create mode 100644 examples/layout/button.go create mode 100644 examples/layout/main.go create mode 100644 examples/layout/theme.go create mode 100644 examples/layout/utils.go create mode 100644 layout/fixedgrid/fixedgrid.go create mode 100644 layout/layout.go diff --git a/examples/layout/button.go b/examples/layout/button.go new file mode 100644 index 0000000..6c907e3 --- /dev/null +++ b/examples/layout/button.go @@ -0,0 +1,64 @@ +package main + +import ( + "image" + "image/color" + "image/draw" + "log" + + "github.com/faiface/gui" + "github.com/faiface/gui/win" +) + +func Button(env gui.Env, theme *Theme, text string, action func()) { + textImg := MakeTextImage(text, theme.Face, theme.Text) + + redraw := func(r image.Rectangle, over, pressed bool) func(draw.Image) image.Rectangle { + return func(drw draw.Image) image.Rectangle { + var clr color.Color + if pressed { + clr = theme.ButtonDown + } else if over { + clr = theme.ButtonOver + } else { + clr = theme.ButtonUp + } + draw.Draw(drw, r, &image.Uniform{clr}, image.ZP, draw.Src) + DrawCentered(drw, r, textImg, draw.Over) + return r + } + } + + var ( + r image.Rectangle + over bool + pressed bool + ) + + for e := range env.Events() { + switch e := e.(type) { + case gui.Resize: + r = e.Rectangle + log.Print("button ", e) + env.Draw() <- redraw(r, over, pressed) + + case win.MoDown: + newPressed := e.Point.In(r) + if newPressed != pressed { + pressed = newPressed + env.Draw() <- redraw(r, over, pressed) + } + + case win.MoUp: + if pressed { + if e.Point.In(r) { + action() + } + pressed = false + env.Draw() <- redraw(r, over, pressed) + } + } + } + + close(env.Draw()) +} diff --git a/examples/layout/main.go b/examples/layout/main.go new file mode 100644 index 0000000..47364a9 --- /dev/null +++ b/examples/layout/main.go @@ -0,0 +1,128 @@ +package main + +import ( + "image" + "image/draw" + "log" + "time" + + "github.com/faiface/gui" + "github.com/faiface/gui/fixedgrid" + "github.com/faiface/gui/win" + "github.com/faiface/mainthread" + "golang.org/x/image/colornames" + "golang.org/x/image/font/gofont/goregular" +) + +func Blinker(env gui.Env, closed bool) { + defer func() { + if recover() != nil { + log.Print("recovered blinker") + } + }() + + var r image.Rectangle + var visible bool = true + // redraw takes a bool and produces a draw command + redraw := func() func(draw.Image) image.Rectangle { + return func(drw draw.Image) image.Rectangle { + if visible { + draw.Draw(drw, r, image.White, image.ZP, draw.Src) + } else { + draw.Draw(drw, r, &image.Uniform{colornames.Firebrick}, image.ZP, draw.Src) + } + return r + } + } + + // first we draw a white rectangle + env.Draw() <- redraw() + go func() { + for event := range env.Events() { + switch event := event.(type) { + case win.MoDown: + if event.Point.In(r) { + go func() { + for i := 0; i < 3; i++ { + visible = false + env.Draw() <- redraw() + time.Sleep(time.Second / 3) + visible = true + env.Draw() <- redraw() + time.Sleep(time.Second / 3) + } + }() + } + case gui.Resize: + log.Print(event) + r = event.Rectangle + env.Draw() <- redraw() + } + } + }() + + if closed { + time.Sleep(time.Second * 1) + close(env.Draw()) + } +} + +func run() { + face, err := TTFToFace(goregular.TTF, 18) + if err != nil { + panic(err) + } + theme := &Theme{ + Face: face, + Background: colornames.White, + Empty: colornames.Darkgrey, + Text: colornames.Black, + Highlight: colornames.Blueviolet, + ButtonUp: colornames.Lightgrey, + ButtonDown: colornames.Grey, + } + w, err := win.New(win.Title("gui test"), + win.Resizable(), + ) + if err != nil { + panic(err) + } + mux, env := gui.NewMux(w) + gr := fixedgrid.New(mux.MakeEnv(), + fixedgrid.Rows(5), + fixedgrid.Columns(2), + fixedgrid.Gap(10), + ) + log.Print(gr) + go Blinker(gr.GetEnv("0;0"), false) + go Blinker(gr.GetEnv("0;1"), true) + go Blinker(gr.GetEnv("1;1"), false) + go Blinker(gr.GetEnv("0;2"), false) + go Blinker(gr.GetEnv("0;3"), false) + go Blinker(gr.GetEnv("0;4"), false) + sgr := fixedgrid.New(gr.GetEnv("1;0"), + fixedgrid.Columns(3), + fixedgrid.Gap(4), + fixedgrid.Background(colornames.Darkgrey), + ) + go Button(sgr.GetEnv("0;0"), theme, "Hey", func() { + log.Print("hey") + }) + go Button(sgr.GetEnv("1;0"), theme, "Ho", func() { + log.Print("ho") + }) + go Button(sgr.GetEnv("2;0"), theme, "Hu", func() { + log.Print("hu") + }) + // we use the master env now, w is used by the mux + for event := range env.Events() { + switch event.(type) { + case win.WiClose: + close(env.Draw()) + } + } +} + +func main() { + mainthread.Run(run) +} diff --git a/examples/layout/theme.go b/examples/layout/theme.go new file mode 100644 index 0000000..e37c0c0 --- /dev/null +++ b/examples/layout/theme.go @@ -0,0 +1,19 @@ +package main + +import ( + "image/color" + + "golang.org/x/image/font" +) + +type Theme struct { + Face font.Face + + Background color.Color + Empty color.Color + Text color.Color + Highlight color.Color + ButtonUp color.Color + ButtonOver color.Color + ButtonDown color.Color +} diff --git a/examples/layout/utils.go b/examples/layout/utils.go new file mode 100644 index 0000000..e799988 --- /dev/null +++ b/examples/layout/utils.go @@ -0,0 +1,104 @@ +package main + +import ( + "image" + "image/color" + "image/draw" + "sync" + + "github.com/golang/freetype/truetype" + + "golang.org/x/image/font" + "golang.org/x/image/math/fixed" +) + +type concurrentFace struct { + mu sync.Mutex + face font.Face +} + +func (cf *concurrentFace) Close() error { + cf.mu.Lock() + defer cf.mu.Unlock() + return cf.face.Close() +} + +func (cf *concurrentFace) Glyph(dot fixed.Point26_6, r rune) (dr image.Rectangle, mask image.Image, maskp image.Point, advance fixed.Int26_6, ok bool) { + cf.mu.Lock() + defer cf.mu.Unlock() + return cf.face.Glyph(dot, r) +} + +func (cf *concurrentFace) GlyphBounds(r rune) (bounds fixed.Rectangle26_6, advance fixed.Int26_6, ok bool) { + cf.mu.Lock() + defer cf.mu.Unlock() + return cf.face.GlyphBounds(r) +} + +func (cf *concurrentFace) GlyphAdvance(r rune) (advance fixed.Int26_6, ok bool) { + cf.mu.Lock() + defer cf.mu.Unlock() + return cf.face.GlyphAdvance(r) +} + +func (cf *concurrentFace) Kern(r0, r1 rune) fixed.Int26_6 { + cf.mu.Lock() + defer cf.mu.Unlock() + return cf.face.Kern(r0, r1) +} + +func (cf *concurrentFace) Metrics() font.Metrics { + cf.mu.Lock() + defer cf.mu.Unlock() + return cf.face.Metrics() +} + +func TTFToFace(ttf []byte, size float64) (font.Face, error) { + font, err := truetype.Parse(ttf) + if err != nil { + return nil, err + } + return &concurrentFace{face: truetype.NewFace(font, &truetype.Options{ + Size: size, + })}, nil +} + +func MakeTextImage(text string, face font.Face, clr color.Color) image.Image { + drawer := &font.Drawer{ + Src: &image.Uniform{clr}, + Face: face, + Dot: fixed.P(0, 0), + } + b26_6, _ := drawer.BoundString(text) + bounds := image.Rect( + b26_6.Min.X.Floor(), + b26_6.Min.Y.Floor(), + b26_6.Max.X.Ceil(), + b26_6.Max.Y.Ceil(), + ) + drawer.Dst = image.NewRGBA(bounds) + drawer.DrawString(text) + return drawer.Dst +} + +func DrawCentered(dst draw.Image, r image.Rectangle, src image.Image, op draw.Op) { + if src == nil { + return + } + bounds := src.Bounds() + center := bounds.Min.Add(bounds.Max).Div(2) + target := r.Min.Add(r.Max).Div(2) + delta := target.Sub(center) + draw.Draw(dst, bounds.Add(delta).Intersect(r), src, bounds.Min, op) +} + +func DrawLeftCentered(dst draw.Image, r image.Rectangle, src image.Image, op draw.Op) { + if src == nil { + return + } + bounds := src.Bounds() + leftCenter := image.Pt(bounds.Min.X, (bounds.Min.Y+bounds.Max.Y)/2) + target := image.Pt(r.Min.X, (r.Min.Y+r.Max.Y)/2) + delta := target.Sub(leftCenter) + draw.Draw(dst, bounds.Add(delta).Intersect(r), src, bounds.Min, op) +} diff --git a/layout/fixedgrid/fixedgrid.go b/layout/fixedgrid/fixedgrid.go new file mode 100644 index 0000000..a9fabf8 --- /dev/null +++ b/layout/fixedgrid/fixedgrid.go @@ -0,0 +1,88 @@ +package fixedgrid + +import ( + "fmt" + "image" + "image/color" + "image/draw" + + "github.com/faiface/gui" + "github.com/faiface/gui/layout" +) + +type FixedGrid struct { + Columns int + Rows int + Background color.Color + Gap int + + *layout.Layout +} + +func New(env gui.Env, options ...func(*FixedGrid)) *FixedGrid { + ret := &FixedGrid{ + // Bounds: image.ZR, + Background: image.Black, + Columns: 1, + Rows: 1, + Gap: 0, + } + + for _, f := range options { + f(ret) + } + + ret.Layout = layout.New(env, ret.layout, ret.redraw) + return ret +} + +func (g *FixedGrid) layout(bounds image.Rectangle) map[string]image.Rectangle { + gap := g.Gap + cols := g.Columns + rows := g.Rows + + w := (bounds.Dx() - (cols+1)*gap) / cols + h := (bounds.Dy() - (rows+1)*gap) / rows + + ret := make(map[string]image.Rectangle) + X := gap + bounds.Min.X + Y := gap + bounds.Min.Y + for x := 0; x < cols; x++ { + for y := 0; y < rows; y++ { + ret[fmt.Sprintf("%d;%d", x, y)] = image.Rect(X, Y, X+w, Y+h) + Y += gap + h + } + Y = gap + bounds.Min.Y + X += gap + w + } + + return ret +} + +func Background(c color.Color) func(*FixedGrid) { + return func(grid *FixedGrid) { + grid.Background = c + } +} + +func Gap(g int) func(*FixedGrid) { + return func(grid *FixedGrid) { + grid.Gap = g + } +} + +func Columns(cols int) func(*FixedGrid) { + return func(grid *FixedGrid) { + grid.Columns = cols + } +} + +func Rows(rows int) func(*FixedGrid) { + return func(grid *FixedGrid) { + grid.Rows = rows + } +} + +func (g *FixedGrid) redraw(drw draw.Image, bounds image.Rectangle) { + draw.Draw(drw, bounds, image.NewUniform(g.Background), image.ZP, draw.Src) +} diff --git a/layout/layout.go b/layout/layout.go new file mode 100644 index 0000000..3601629 --- /dev/null +++ b/layout/layout.go @@ -0,0 +1,165 @@ +package layout + +import ( + "image" + "image/draw" + "sync" + + "github.com/faiface/gui" +) + +type Layout struct { + masterEnv *MuxEnv + inEvent chan<- gui.Event + + mu sync.Mutex + lastResize gui.Event + eventsIns map[string]chan<- gui.Event + draw chan<- func(draw.Image) image.Rectangle + + Lay func(image.Rectangle) map[string]image.Rectangle + Redraw func(draw.Image, image.Rectangle) +} + +func New( + env gui.Env, + lay func(image.Rectangle) map[string]image.Rectangle, + redraw func(draw.Image, image.Rectangle), +) *Layout { + + mux := &Layout{ + Lay: lay, + Redraw: redraw, + } + drawChan := make(chan func(draw.Image) image.Rectangle) + mux.draw = drawChan + mux.masterEnv = mux.makeEnv("master", true) + mux.inEvent = mux.masterEnv.In + mux.eventsIns = make(map[string]chan<- gui.Event) + go func() { + for d := range drawChan { + env.Draw() <- d + } + close(env.Draw()) + }() + + go func() { + for e := range env.Events() { + mux.inEvent <- e + } + }() + + go func() { + for e := range mux.masterEnv.Events() { + mux.mu.Lock() + if resize, ok := e.(gui.Resize); ok { + mux.lastResize = resize + rect := resize.Rectangle + + mux.draw <- func(drw draw.Image) image.Rectangle { + mux.Redraw(drw, rect) + return rect + } + l := mux.Lay(rect) + + for key, eventsIn := range mux.eventsIns { + func(rz gui.Resize) { + rz.Rectangle = l[key] + eventsIn <- rz + }(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() + }() + + return mux +} + +func (mux *Layout) GetEnv(name string) gui.Env { + return mux.makeEnv(name, false) +} + +type MuxEnv struct { + In chan<- gui.Event + 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 *Layout) makeEnv(envName string, master bool) *MuxEnv { + eventsOut, eventsIn := gui.MakeEventsChan() + drawChan := make(chan func(draw.Image) image.Rectangle) + env := &MuxEnv{eventsIn, eventsOut, drawChan} + + mux.mu.Lock() + if !master { + mux.eventsIns[envName] = 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() + delete(mux.eventsIns, envName) + + close(eventsIn) + mux.mu.Unlock() + } + if mux.lastResize != nil { + mux.inEvent <- mux.lastResize + } + }() + + return env +} -- cgit v1.2.3 From 1223e277009005337243ca991cb54dd75bf723a7 Mon Sep 17 00:00:00 2001 From: Clement Benard Date: Tue, 9 Jul 2019 10:59:38 +0200 Subject: Layout system remaking --- examples/layout/layout | Bin 0 -> 5570192 bytes examples/layout/main.go | 122 +++++++++++++++++++++++------- layout/box.go | 99 +++++++++++++++++++++++++ layout/fixedgrid/fixedgrid.go | 88 ---------------------- layout/grid.go | 92 +++++++++++++++++++++++ layout/layout.go | 168 +++--------------------------------------- layout/mux.go | 150 +++++++++++++++++++++++++++++++++++++ 7 files changed, 446 insertions(+), 273 deletions(-) create mode 100755 examples/layout/layout create mode 100644 layout/box.go delete mode 100644 layout/fixedgrid/fixedgrid.go create mode 100644 layout/grid.go create mode 100644 layout/mux.go diff --git a/examples/layout/layout b/examples/layout/layout new file mode 100755 index 0000000..050fbdf Binary files /dev/null and b/examples/layout/layout differ diff --git a/examples/layout/main.go b/examples/layout/main.go index 47364a9..20d82c2 100644 --- a/examples/layout/main.go +++ b/examples/layout/main.go @@ -7,7 +7,7 @@ import ( "time" "github.com/faiface/gui" - "github.com/faiface/gui/fixedgrid" + "github.com/faiface/gui/layout" "github.com/faiface/gui/win" "github.com/faiface/mainthread" "golang.org/x/image/colornames" @@ -81,39 +81,105 @@ func run() { ButtonUp: colornames.Lightgrey, ButtonDown: colornames.Grey, } - w, err := win.New(win.Title("gui test"), - win.Resizable(), - ) + w, err := win.New(win.Title("gui test")) // win.Resizable(), + if err != nil { panic(err) } mux, env := gui.NewMux(w) - gr := fixedgrid.New(mux.MakeEnv(), - fixedgrid.Rows(5), - fixedgrid.Columns(2), - fixedgrid.Gap(10), + var ( + top gui.Env + left, right gui.Env + bottomLeft, bottom, bottomRight gui.Env + ) + layout.NewGrid( + mux.MakeEnv(), + [][]*gui.Env{ + {&top}, + {&left, &right}, + {&bottomLeft, &bottom, &bottomRight}, + }, + layout.GridGap(10), + layout.GridBackground(colornames.Sandybrown), + layout.GridSplitY(func(els int, width int) []int { + ret := make([]int, els) + total := 0 + for i := 0; i < els; i++ { + if i == els-1 { + ret[i] = width - total + } else { + v := (width - total) / 2 + ret[i] = v + total += v + } + } + return ret + }), + ) + go Blinker(right, false) + go Blinker(left, false) + go Blinker(bottomRight, false) + + var ( + b1, b2, b3, b4, b5, b6 gui.Env ) - log.Print(gr) - go Blinker(gr.GetEnv("0;0"), false) - go Blinker(gr.GetEnv("0;1"), true) - go Blinker(gr.GetEnv("1;1"), false) - go Blinker(gr.GetEnv("0;2"), false) - go Blinker(gr.GetEnv("0;3"), false) - go Blinker(gr.GetEnv("0;4"), false) - sgr := fixedgrid.New(gr.GetEnv("1;0"), - fixedgrid.Columns(3), - fixedgrid.Gap(4), - fixedgrid.Background(colornames.Darkgrey), + layout.NewBox( + top, + []*gui.Env{ + &b1, &b2, &b3, + }, + layout.BoxGap(10), + layout.BoxBackground(colornames.Lightblue), ) - go Button(sgr.GetEnv("0;0"), theme, "Hey", func() { - log.Print("hey") - }) - go Button(sgr.GetEnv("1;0"), theme, "Ho", func() { - log.Print("ho") - }) - go Button(sgr.GetEnv("2;0"), theme, "Hu", func() { - log.Print("hu") - }) + go Blinker(b1, false) + go Blinker(b2, false) + layout.NewBox( + b3, + []*gui.Env{ + &b4, &b5, &b6, + }, + layout.BoxVertical, + layout.BoxBackground(colornames.Pink), + layout.BoxGap(4), + layout.BoxSplit(func(els int, width int) []int { + ret := make([]int, els) + total := 0 + for i := 0; i < els; i++ { + if i == els-1 { + ret[i] = width - total + } else { + v := (width - total) / 2 + ret[i] = v + total += v + } + } + return ret + }), + ) + go Blinker(b4, false) + go Blinker(b5, false) + go Blinker(b6, false) + + var ( + btn1, btn2, btn3 gui.Env + ) + layout.NewGrid( + bottom, + [][]*gui.Env{ + {&btn1, &btn2, &btn3}, + }, + layout.GridGap(4), + layout.GridBackground(colornames.Darkgrey), + ) + btn := func(env gui.Env, name string) { + Button(env, theme, name, func() { + log.Print(name) + }) + } + go btn(btn1, "Hey") + go btn(btn2, "Ho") + go btn(btn3, "Hu") + // we use the master env now, w is used by the mux for event := range env.Events() { switch event.(type) { diff --git a/layout/box.go b/layout/box.go new file mode 100644 index 0000000..568ac58 --- /dev/null +++ b/layout/box.go @@ -0,0 +1,99 @@ +package layout + +import ( + "image" + "image/color" + "image/draw" + "log" + + "github.com/faiface/gui" +) + +func evenSplit(elements int, width int) []int { + ret := make([]int, 0, elements) + for elements > 0 { + v := width / elements + width -= v + elements -= 1 + ret = append(ret, v) + } + return ret +} + +type Box struct { + // Defaults to []*gui.Env{} + Contents []*gui.Env + // Defaults to image.Black + Background color.Color + // Defaults to an even split + Split func(int, int) []int + // Defaults to 0 + Gap int + + vertical bool +} + +func NewBox(env gui.Env, contents []*gui.Env, options ...func(*Box)) { + ret := &Box{ + Background: image.Black, + Contents: contents, + Split: evenSplit, + } + for _, f := range options { + f(ret) + } + + mux := NewMux(env, ret) + for _, item := range contents { + *item, _ = mux.makeEnv(false) + } +} + +func BoxVertical(b *Box) { + b.vertical = true +} + +func BoxBackground(c color.Color) func(*Box) { + return func(grid *Box) { + grid.Background = c + } +} + +func BoxSplit(split func(int, int) []int) func(*Box) { + return func(grid *Box) { + grid.Split = split + } +} + +func BoxGap(gap int) func(*Box) { + return func(grid *Box) { + grid.Gap = gap + } +} + +func (g *Box) Redraw(drw draw.Image, bounds image.Rectangle) { + draw.Draw(drw, bounds, image.NewUniform(g.Background), image.ZP, draw.Src) +} + +func (g *Box) Lay(bounds image.Rectangle) []image.Rectangle { + items := len(g.Contents) + ret := make([]image.Rectangle, 0, items) + if g.vertical { + spl := g.Split(items, bounds.Dy()-(g.Gap*(items+1))) + Y := bounds.Min.Y + g.Gap + for _, item := range spl { + ret = append(ret, image.Rect(bounds.Min.X+g.Gap, Y, bounds.Max.X-g.Gap, Y+item)) + Y += item + g.Gap + } + } else { + spl := g.Split(items, bounds.Dx()-(g.Gap*(items+1))) + X := bounds.Min.X + g.Gap + for _, item := range spl { + ret = append(ret, image.Rect(X, bounds.Min.Y+g.Gap, X+item, bounds.Max.Y-g.Gap)) + X += item + g.Gap + } + } + + log.Print(ret) + return ret +} diff --git a/layout/fixedgrid/fixedgrid.go b/layout/fixedgrid/fixedgrid.go deleted file mode 100644 index a9fabf8..0000000 --- a/layout/fixedgrid/fixedgrid.go +++ /dev/null @@ -1,88 +0,0 @@ -package fixedgrid - -import ( - "fmt" - "image" - "image/color" - "image/draw" - - "github.com/faiface/gui" - "github.com/faiface/gui/layout" -) - -type FixedGrid struct { - Columns int - Rows int - Background color.Color - Gap int - - *layout.Layout -} - -func New(env gui.Env, options ...func(*FixedGrid)) *FixedGrid { - ret := &FixedGrid{ - // Bounds: image.ZR, - Background: image.Black, - Columns: 1, - Rows: 1, - Gap: 0, - } - - for _, f := range options { - f(ret) - } - - ret.Layout = layout.New(env, ret.layout, ret.redraw) - return ret -} - -func (g *FixedGrid) layout(bounds image.Rectangle) map[string]image.Rectangle { - gap := g.Gap - cols := g.Columns - rows := g.Rows - - w := (bounds.Dx() - (cols+1)*gap) / cols - h := (bounds.Dy() - (rows+1)*gap) / rows - - ret := make(map[string]image.Rectangle) - X := gap + bounds.Min.X - Y := gap + bounds.Min.Y - for x := 0; x < cols; x++ { - for y := 0; y < rows; y++ { - ret[fmt.Sprintf("%d;%d", x, y)] = image.Rect(X, Y, X+w, Y+h) - Y += gap + h - } - Y = gap + bounds.Min.Y - X += gap + w - } - - return ret -} - -func Background(c color.Color) func(*FixedGrid) { - return func(grid *FixedGrid) { - grid.Background = c - } -} - -func Gap(g int) func(*FixedGrid) { - return func(grid *FixedGrid) { - grid.Gap = g - } -} - -func Columns(cols int) func(*FixedGrid) { - return func(grid *FixedGrid) { - grid.Columns = cols - } -} - -func Rows(rows int) func(*FixedGrid) { - return func(grid *FixedGrid) { - grid.Rows = rows - } -} - -func (g *FixedGrid) redraw(drw draw.Image, bounds image.Rectangle) { - draw.Draw(drw, bounds, image.NewUniform(g.Background), image.ZP, draw.Src) -} diff --git a/layout/grid.go b/layout/grid.go new file mode 100644 index 0000000..55111cb --- /dev/null +++ b/layout/grid.go @@ -0,0 +1,92 @@ +package layout + +import ( + "image" + "image/color" + "image/draw" + + "github.com/faiface/gui" +) + +// Grid represents a simple grid layout. +// Do not edit properties directly, use the constructor instead. +type Grid struct { + Contents [][]*gui.Env + Background color.Color + Gap int + SplitX func(int, int) []int + SplitY func(int, int) []int +} + +func NewGrid(env gui.Env, contents [][]*gui.Env, options ...func(*Grid)) { + ret := &Grid{ + Background: image.Black, + Gap: 0, + Contents: contents, + SplitX: evenSplit, + SplitY: evenSplit, + } + for _, f := range options { + f(ret) + } + + mux := NewMux(env, ret) + for _, row := range contents { + for _, item := range row { + *item, _ = mux.makeEnv(false) + } + } +} + +func GridBackground(c color.Color) func(*Grid) { + return func(grid *Grid) { + grid.Background = c + } +} + +func GridGap(g int) func(*Grid) { + return func(grid *Grid) { + grid.Gap = g + } +} + +func GridSplitX(split func(int, int) []int) func(*Grid) { + return func(grid *Grid) { + grid.SplitX = split + } +} + +func GridSplitY(split func(int, int) []int) func(*Grid) { + return func(grid *Grid) { + grid.SplitY = split + } +} + +func (g *Grid) Redraw(drw draw.Image, bounds image.Rectangle) { + draw.Draw(drw, bounds, image.NewUniform(g.Background), image.ZP, draw.Src) +} + +func (g *Grid) Lay(bounds image.Rectangle) []image.Rectangle { + gap := g.Gap + ret := make([]image.Rectangle, 0) + rows := len(g.Contents) + + rowsH := g.SplitY(rows, bounds.Dy()-(g.Gap*(rows+1))) + + X := gap + bounds.Min.X + Y := gap + bounds.Min.Y + for y, row := range g.Contents { + cols := len(row) + h := rowsH[y] + colsW := g.SplitX(cols, bounds.Dx()-(g.Gap*(cols+1))) + X = gap + bounds.Min.X + for x := range row { + w := colsW[x] + ret = append(ret, image.Rect(X, Y, X+w, Y+h)) + X += gap + w + } + Y += gap + h + } + + return ret +} diff --git a/layout/layout.go b/layout/layout.go index 3601629..987ee24 100644 --- a/layout/layout.go +++ b/layout/layout.go @@ -3,163 +3,17 @@ package layout import ( "image" "image/draw" - "sync" - - "github.com/faiface/gui" ) -type Layout struct { - masterEnv *MuxEnv - inEvent chan<- gui.Event - - mu sync.Mutex - lastResize gui.Event - eventsIns map[string]chan<- gui.Event - draw chan<- func(draw.Image) image.Rectangle - - Lay func(image.Rectangle) map[string]image.Rectangle - Redraw func(draw.Image, image.Rectangle) -} - -func New( - env gui.Env, - lay func(image.Rectangle) map[string]image.Rectangle, - redraw func(draw.Image, image.Rectangle), -) *Layout { - - mux := &Layout{ - Lay: lay, - Redraw: redraw, - } - drawChan := make(chan func(draw.Image) image.Rectangle) - mux.draw = drawChan - mux.masterEnv = mux.makeEnv("master", true) - mux.inEvent = mux.masterEnv.In - mux.eventsIns = make(map[string]chan<- gui.Event) - go func() { - for d := range drawChan { - env.Draw() <- d - } - close(env.Draw()) - }() - - go func() { - for e := range env.Events() { - mux.inEvent <- e - } - }() - - go func() { - for e := range mux.masterEnv.Events() { - mux.mu.Lock() - if resize, ok := e.(gui.Resize); ok { - mux.lastResize = resize - rect := resize.Rectangle - - mux.draw <- func(drw draw.Image) image.Rectangle { - mux.Redraw(drw, rect) - return rect - } - l := mux.Lay(rect) - - for key, eventsIn := range mux.eventsIns { - func(rz gui.Resize) { - rz.Rectangle = l[key] - eventsIn <- rz - }(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() - }() - - return mux -} - -func (mux *Layout) GetEnv(name string) gui.Env { - return mux.makeEnv(name, false) -} - -type MuxEnv struct { - In chan<- gui.Event - 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 *Layout) makeEnv(envName string, master bool) *MuxEnv { - eventsOut, eventsIn := gui.MakeEventsChan() - drawChan := make(chan func(draw.Image) image.Rectangle) - env := &MuxEnv{eventsIn, eventsOut, drawChan} - - mux.mu.Lock() - if !master { - mux.eventsIns[envName] = 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() - delete(mux.eventsIns, envName) - - close(eventsIn) - mux.mu.Unlock() - } - if mux.lastResize != nil { - mux.inEvent <- mux.lastResize - } - }() - - return env +// Layout represents any graphical layout +// +// A Layout needs to be able to redraw itself with the Redraw method. +// Redraw() only draws the background or frame of the Layout, not the childs. +// +// 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. +type Layout interface { + Lay(image.Rectangle) []image.Rectangle + Redraw(draw.Image, image.Rectangle) } diff --git a/layout/mux.go b/layout/mux.go new file mode 100644 index 0000000..abae087 --- /dev/null +++ b/layout/mux.go @@ -0,0 +1,150 @@ +package layout + +import ( + "image" + "image/draw" + "sync" + + "github.com/faiface/gui" +) + +type Mux struct { + masterEnv *muxEnv + inEvent chan<- gui.Event + + mu sync.Mutex + lastResize gui.Event + eventsIns []chan<- gui.Event + draw chan<- func(draw.Image) image.Rectangle + + Layout +} + +func NewMux(env gui.Env, l Layout) (mux *Mux) { + drawChan := make(chan func(draw.Image) image.Rectangle) + mux = &Mux{ + Layout: l, + draw: drawChan, + } + mux.masterEnv, mux.inEvent = mux.makeEnv(true) + mux.eventsIns = make([]chan<- gui.Event, 0) + + go func() { + for d := range drawChan { + env.Draw() <- d + } + close(env.Draw()) + }() + + go func() { + for e := range env.Events() { + mux.mu.Lock() + if resize, ok := e.(gui.Resize); ok { + mux.lastResize = resize + rect := resize.Rectangle + + // Redraw self + mux.draw <- func(drw draw.Image) image.Rectangle { + mux.Redraw(drw, rect) + return rect + } + + // Send appropriate resize Events to childs + lay := mux.Lay(rect) + 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() + }() + + return mux +} + +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) (*muxEnv, chan<- gui.Event) { + eventsOut, eventsIn := gui.MakeEventsChan() + drawChan := make(chan func(draw.Image) image.Rectangle) + env := &muxEnv{eventsOut, drawChan} + + 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() + } + if mux.lastResize != nil { + mux.inEvent <- mux.lastResize + } + }() + + return env, eventsIn +} -- cgit v1.2.3 From 1415586e633ee33194442f131a5a691f889c8ee5 Mon Sep 17 00:00:00 2001 From: Clement Benard Date: Tue, 9 Jul 2019 15:31:32 +0200 Subject: better Mux and Env handling in layout --- examples/layout/layout | Bin 5570192 -> 0 bytes examples/layout/main.go | 27 +++++++++++---------------- layout/box.go | 7 ++++--- layout/grid.go | 8 +++++--- layout/mux.go | 40 ++++++++++++++++++++++++---------------- 5 files changed, 44 insertions(+), 38 deletions(-) delete mode 100755 examples/layout/layout diff --git a/examples/layout/layout b/examples/layout/layout deleted file mode 100755 index 050fbdf..0000000 Binary files a/examples/layout/layout and /dev/null differ diff --git a/examples/layout/main.go b/examples/layout/main.go index 20d82c2..f7023a2 100644 --- a/examples/layout/main.go +++ b/examples/layout/main.go @@ -14,7 +14,7 @@ import ( "golang.org/x/image/font/gofont/goregular" ) -func Blinker(env gui.Env, closed bool) { +func Blinker(env gui.Env) { defer func() { if recover() != nil { log.Print("recovered blinker") @@ -23,7 +23,7 @@ func Blinker(env gui.Env, closed bool) { var r image.Rectangle var visible bool = true - // redraw takes a bool and produces a draw command + redraw := func() func(draw.Image) image.Rectangle { return func(drw draw.Image) image.Rectangle { if visible { @@ -37,7 +37,7 @@ func Blinker(env gui.Env, closed bool) { // first we draw a white rectangle env.Draw() <- redraw() - go func() { + func() { for event := range env.Events() { switch event := event.(type) { case win.MoDown: @@ -60,11 +60,6 @@ func Blinker(env gui.Env, closed bool) { } } }() - - if closed { - time.Sleep(time.Second * 1) - close(env.Draw()) - } } func run() { @@ -116,9 +111,9 @@ func run() { return ret }), ) - go Blinker(right, false) - go Blinker(left, false) - go Blinker(bottomRight, false) + go Blinker(right) + go Blinker(left) + go Blinker(bottomRight) var ( b1, b2, b3, b4, b5, b6 gui.Env @@ -131,8 +126,8 @@ func run() { layout.BoxGap(10), layout.BoxBackground(colornames.Lightblue), ) - go Blinker(b1, false) - go Blinker(b2, false) + go Blinker(b1) + go Blinker(b2) layout.NewBox( b3, []*gui.Env{ @@ -156,9 +151,9 @@ func run() { return ret }), ) - go Blinker(b4, false) - go Blinker(b5, false) - go Blinker(b6, false) + go Blinker(b4) + go Blinker(b5) + go Blinker(b6) var ( btn1, btn2, btn3 gui.Env diff --git a/layout/box.go b/layout/box.go index 568ac58..4e16a76 100644 --- a/layout/box.go +++ b/layout/box.go @@ -33,7 +33,7 @@ type Box struct { vertical bool } -func NewBox(env gui.Env, contents []*gui.Env, options ...func(*Box)) { +func NewBox(env gui.Env, contents []*gui.Env, options ...func(*Box)) gui.Env { ret := &Box{ Background: image.Black, Contents: contents, @@ -43,10 +43,11 @@ func NewBox(env gui.Env, contents []*gui.Env, options ...func(*Box)) { f(ret) } - mux := NewMux(env, ret) + mux, env := NewMux(env, ret) for _, item := range contents { - *item, _ = mux.makeEnv(false) + *item = mux.MakeEnv() } + return env } func BoxVertical(b *Box) { diff --git a/layout/grid.go b/layout/grid.go index 55111cb..1831794 100644 --- a/layout/grid.go +++ b/layout/grid.go @@ -18,7 +18,7 @@ type Grid struct { SplitY func(int, int) []int } -func NewGrid(env gui.Env, contents [][]*gui.Env, options ...func(*Grid)) { +func NewGrid(env gui.Env, contents [][]*gui.Env, options ...func(*Grid)) gui.Env { ret := &Grid{ Background: image.Black, Gap: 0, @@ -30,12 +30,14 @@ func NewGrid(env gui.Env, contents [][]*gui.Env, options ...func(*Grid)) { f(ret) } - mux := NewMux(env, ret) + mux, env := NewMux(env, ret) for _, row := range contents { for _, item := range row { - *item, _ = mux.makeEnv(false) + *item = mux.MakeEnv() } } + + return env } func GridBackground(c color.Color) func(*Grid) { diff --git a/layout/mux.go b/layout/mux.go index abae087..df499df 100644 --- a/layout/mux.go +++ b/layout/mux.go @@ -9,8 +9,7 @@ import ( ) type Mux struct { - masterEnv *muxEnv - inEvent chan<- gui.Event + inEvent chan<- gui.Event mu sync.Mutex lastResize gui.Event @@ -20,13 +19,15 @@ type Mux struct { Layout } -func NewMux(env gui.Env, l Layout) (mux *Mux) { +func (m *Mux) InEvent() chan<- gui.Event { return m.inEvent } + +func NewMux(env gui.Env, l Layout) (mux *Mux, master gui.Env) { drawChan := make(chan func(draw.Image) image.Rectangle) mux = &Mux{ Layout: l, draw: drawChan, } - mux.masterEnv, mux.inEvent = mux.makeEnv(true) + master, mux.inEvent = mux.makeEnv(true) mux.eventsIns = make([]chan<- gui.Event, 0) go func() { @@ -68,8 +69,7 @@ func NewMux(env gui.Env, l Layout) (mux *Mux) { } mux.mu.Unlock() }() - - return mux + return } type muxEnv struct { @@ -80,20 +80,27 @@ type muxEnv struct { func (m *muxEnv) Events() <-chan gui.Event { return m.events } func (m *muxEnv) Draw() chan<- func(draw.Image) image.Rectangle { return m.draw } +func (mux *Mux) MakeEnv() gui.Env { + env, _ := mux.makeEnv(false) + return env +} + // We do not store master env func (mux *Mux) makeEnv(master bool) (*muxEnv, chan<- gui.Event) { eventsOut, eventsIn := gui.MakeEventsChan() drawChan := make(chan func(draw.Image) image.Rectangle) env := &muxEnv{eventsOut, drawChan} - 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 + 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() } - mux.mu.Unlock() go func() { func() { @@ -126,6 +133,7 @@ func (mux *Mux) makeEnv(master bool) (*muxEnv, chan<- gui.Event) { close(eventsIn) } mux.eventsIns = nil + close(mux.inEvent) close(mux.draw) mux.mu.Unlock() } else { @@ -139,11 +147,11 @@ func (mux *Mux) makeEnv(master bool) (*muxEnv, chan<- gui.Event) { if i != -1 { mux.eventsIns = append(mux.eventsIns[:i], mux.eventsIns[i+1:]...) } + if mux.lastResize != nil { + mux.InEvent() <- mux.lastResize + } mux.mu.Unlock() } - if mux.lastResize != nil { - mux.inEvent <- mux.lastResize - } }() return env, eventsIn -- cgit v1.2.3 From 24286694a5ba80cc3f54a224b62ac11c773b4985 Mon Sep 17 00:00:00 2001 From: Clement Benard Date: Tue, 9 Jul 2019 17:30:30 +0200 Subject: Documenting, bug fixing and refactoring to be more in line with the project. --- examples/layout/main.go | 9 +++++++- layout/box.go | 3 --- layout/mux.go | 57 ++++++++++++++++++++++++++++++------------------- 3 files changed, 43 insertions(+), 26 deletions(-) diff --git a/examples/layout/main.go b/examples/layout/main.go index f7023a2..7e40ea4 100644 --- a/examples/layout/main.go +++ b/examples/layout/main.go @@ -128,7 +128,7 @@ func run() { ) go Blinker(b1) go Blinker(b2) - layout.NewBox( + box := layout.NewBox( b3, []*gui.Env{ &b4, &b5, &b6, @@ -151,6 +151,13 @@ func run() { return ret }), ) + log.Print(box) + // go func() { + // for v := range box.Events() { + // log.Print("box: ", v) + // } + // }() + go Blinker(b4) go Blinker(b5) go Blinker(b6) diff --git a/layout/box.go b/layout/box.go index 4e16a76..2fd29cf 100644 --- a/layout/box.go +++ b/layout/box.go @@ -4,7 +4,6 @@ import ( "image" "image/color" "image/draw" - "log" "github.com/faiface/gui" ) @@ -94,7 +93,5 @@ func (g *Box) Lay(bounds image.Rectangle) []image.Rectangle { X += item + g.Gap } } - - log.Print(ret) return ret } diff --git a/layout/mux.go b/layout/mux.go index df499df..208664c 100644 --- a/layout/mux.go +++ b/layout/mux.go @@ -8,28 +8,34 @@ import ( "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. +// The master Env gets the original gui.Resize events. type Mux struct { - inEvent chan<- gui.Event - mu sync.Mutex lastResize gui.Event eventsIns []chan<- gui.Event draw chan<- func(draw.Image) image.Rectangle - Layout + evIn chan<- gui.Event + layout Layout } -func (m *Mux) InEvent() chan<- gui.Event { return m.inEvent } - +// NewMux should only be used internally by Layouts. +// It has mostly the same behaviour as gui.Mux, except for its use of and underlying Layout +// for modifying the gui.Resize events. to the childs. func NewMux(env gui.Env, l Layout) (mux *Mux, master gui.Env) { drawChan := make(chan func(draw.Image) image.Rectangle) mux = &Mux{ - Layout: l, + layout: l, draw: drawChan, } - master, mux.inEvent = mux.makeEnv(true) - mux.eventsIns = make([]chan<- gui.Event, 0) - + master, masterIn := mux.makeEnv(true) + events := make(chan gui.Event, 0) + mux.evIn = events go func() { for d := range drawChan { env.Draw() <- d @@ -39,23 +45,33 @@ func NewMux(env gui.Env, l Layout) (mux *Mux, master gui.Env) { 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 // Redraw self mux.draw <- func(drw draw.Image) image.Rectangle { - mux.Redraw(drw, rect) + mux.layout.Redraw(drw, rect) return rect } // Send appropriate resize Events to childs - lay := mux.Lay(rect) + lay := mux.layout.Lay(rect) for i, eventsIn := range mux.eventsIns { resize.Rectangle = lay[i] eventsIn <- resize } + } else { for _, eventsIn := range mux.eventsIns { eventsIn <- e @@ -72,6 +88,11 @@ func NewMux(env gui.Env, l Layout) (mux *Mux, master gui.Env) { return } +func (mux *Mux) MakeEnv() gui.Env { + env, _ := mux.makeEnv(false) + return env +} + type muxEnv struct { events <-chan gui.Event draw chan<- func(draw.Image) image.Rectangle @@ -80,16 +101,11 @@ type muxEnv struct { func (m *muxEnv) Events() <-chan gui.Event { return m.events } func (m *muxEnv) Draw() chan<- func(draw.Image) image.Rectangle { return m.draw } -func (mux *Mux) MakeEnv() gui.Env { - env, _ := mux.makeEnv(false) - return env -} - // We do not store master env -func (mux *Mux) makeEnv(master bool) (*muxEnv, chan<- gui.Event) { +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} + env = &muxEnv{eventsOut, drawChan} if !master { mux.mu.Lock() @@ -133,7 +149,6 @@ func (mux *Mux) makeEnv(master bool) (*muxEnv, chan<- gui.Event) { close(eventsIn) } mux.eventsIns = nil - close(mux.inEvent) close(mux.draw) mux.mu.Unlock() } else { @@ -147,10 +162,8 @@ func (mux *Mux) makeEnv(master bool) (*muxEnv, chan<- gui.Event) { if i != -1 { mux.eventsIns = append(mux.eventsIns[:i], mux.eventsIns[i+1:]...) } - if mux.lastResize != nil { - mux.InEvent() <- mux.lastResize - } mux.mu.Unlock() + mux.evIn <- mux.lastResize } }() -- cgit v1.2.3 From 009f865bc5484e54f09d2173b2b3dbbf3838d391 Mon Sep 17 00:00:00 2001 From: Clement Benard Date: Thu, 11 Jul 2019 10:50:37 +0200 Subject: Added SplitFunc type, documented everything exported --- layout/box.go | 54 +++++++++++++++++++++++------------------------------- layout/grid.go | 43 ++++++++++++++++++++++++------------------- layout/split.go | 19 +++++++++++++++++++ 3 files changed, 66 insertions(+), 50 deletions(-) create mode 100644 layout/split.go diff --git a/layout/box.go b/layout/box.go index 2fd29cf..01a1143 100644 --- a/layout/box.go +++ b/layout/box.go @@ -8,35 +8,22 @@ import ( "github.com/faiface/gui" ) -func evenSplit(elements int, width int) []int { - ret := make([]int, 0, elements) - for elements > 0 { - v := width / elements - width -= v - elements -= 1 - ret = append(ret, v) - } - return ret -} - -type Box struct { - // Defaults to []*gui.Env{} - Contents []*gui.Env - // Defaults to image.Black +type box struct { + Contents []*gui.Env Background color.Color - // Defaults to an even split - Split func(int, int) []int - // Defaults to 0 - Gap int + Split SplitFunc + Gap int vertical bool } -func NewBox(env gui.Env, contents []*gui.Env, options ...func(*Box)) gui.Env { - ret := &Box{ +// NewBox creates a familiar flexbox-like list layout. +// It can be horizontal or vertical. +func NewBox(env gui.Env, contents []*gui.Env, options ...func(*box)) gui.Env { + ret := &box{ Background: image.Black, Contents: contents, - Split: evenSplit, + Split: EvenSplit, } for _, f := range options { f(ret) @@ -49,33 +36,38 @@ func NewBox(env gui.Env, contents []*gui.Env, options ...func(*Box)) gui.Env { return env } -func BoxVertical(b *Box) { +// BoxVertical changes the otherwise horizontal Box to be vertical. +func BoxVertical(b *box) { b.vertical = true } -func BoxBackground(c color.Color) func(*Box) { - return func(grid *Box) { +// BoxBackground changes the background of the box to a uniform color. +func BoxBackground(c color.Color) func(*box) { + return func(grid *box) { grid.Background = c } } -func BoxSplit(split func(int, int) []int) func(*Box) { - return func(grid *Box) { +// BoxSplit changes the way the space is divided among the elements. +func BoxSplit(split SplitFunc) func(*box) { + return func(grid *box) { grid.Split = split } } -func BoxGap(gap int) func(*Box) { - return func(grid *Box) { +// BoxGap changes the box gap. +// The gap is identical everywhere (top, left, bottom, right). +func BoxGap(gap int) func(*box) { + return func(grid *box) { grid.Gap = gap } } -func (g *Box) Redraw(drw draw.Image, bounds image.Rectangle) { +func (g *box) Redraw(drw draw.Image, bounds image.Rectangle) { draw.Draw(drw, bounds, image.NewUniform(g.Background), image.ZP, draw.Src) } -func (g *Box) Lay(bounds image.Rectangle) []image.Rectangle { +func (g *box) Lay(bounds image.Rectangle) []image.Rectangle { items := len(g.Contents) ret := make([]image.Rectangle, 0, items) if g.vertical { diff --git a/layout/grid.go b/layout/grid.go index 1831794..9c79343 100644 --- a/layout/grid.go +++ b/layout/grid.go @@ -8,23 +8,23 @@ import ( "github.com/faiface/gui" ) -// Grid represents a simple grid layout. -// Do not edit properties directly, use the constructor instead. -type Grid struct { +type grid struct { Contents [][]*gui.Env Background color.Color Gap int - SplitX func(int, int) []int - SplitY func(int, int) []int + SplitX SplitFunc + SplitY SplitFunc } -func NewGrid(env gui.Env, contents [][]*gui.Env, options ...func(*Grid)) gui.Env { - ret := &Grid{ +// NewGrid creates a familiar flexbox-like grid layout. +// Each row can be a different length. +func NewGrid(env gui.Env, contents [][]*gui.Env, options ...func(*grid)) gui.Env { + ret := &grid{ Background: image.Black, Gap: 0, Contents: contents, - SplitX: evenSplit, - SplitY: evenSplit, + SplitX: EvenSplit, + SplitY: EvenSplit, } for _, f := range options { f(ret) @@ -40,35 +40,40 @@ func NewGrid(env gui.Env, contents [][]*gui.Env, options ...func(*Grid)) gui.Env return env } -func GridBackground(c color.Color) func(*Grid) { - return func(grid *Grid) { +// GridBackground changes the background of the grid to a uniform color. +func GridBackground(c color.Color) func(*grid) { + return func(grid *grid) { grid.Background = c } } -func GridGap(g int) func(*Grid) { - return func(grid *Grid) { +// GridGap changes the grid gap. +// The gap is identical everywhere (top, left, bottom, right). +func GridGap(g int) func(*grid) { + return func(grid *grid) { grid.Gap = g } } -func GridSplitX(split func(int, int) []int) func(*Grid) { - return func(grid *Grid) { +// GridSplitX changes the way the space is divided among the columns in each row. +func GridSplitX(split SplitFunc) func(*grid) { + return func(grid *grid) { grid.SplitX = split } } -func GridSplitY(split func(int, int) []int) func(*Grid) { - return func(grid *Grid) { +// GridSplitY changes the way the space is divided among the rows. +func GridSplitY(split SplitFunc) func(*grid) { + return func(grid *grid) { grid.SplitY = split } } -func (g *Grid) Redraw(drw draw.Image, bounds image.Rectangle) { +func (g *grid) Redraw(drw draw.Image, bounds image.Rectangle) { draw.Draw(drw, bounds, image.NewUniform(g.Background), image.ZP, draw.Src) } -func (g *Grid) Lay(bounds image.Rectangle) []image.Rectangle { +func (g *grid) Lay(bounds image.Rectangle) []image.Rectangle { gap := g.Gap ret := make([]image.Rectangle, 0) rows := len(g.Contents) diff --git a/layout/split.go b/layout/split.go new file mode 100644 index 0000000..4bf00a7 --- /dev/null +++ b/layout/split.go @@ -0,0 +1,19 @@ +package layout + +// SplitFunc represents a way to split a space among a number of elements. +// The space 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 + +// EvenSplit implements SplitFunc 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 { + ret := make([]int, 0, elements) + for elements > 0 { + v := width / elements + width -= v + elements -= 1 + ret = append(ret, v) + } + return ret +} -- cgit v1.2.3 From 0cc6367a881ab7dba48ace7b111e0eb1151c10bb Mon Sep 17 00:00:00 2001 From: Clement Benard Date: Fri, 12 Jul 2019 17:23:28 +0200 Subject: Better separation between Layout and Mux --- examples/layout/layout | Bin 0 -> 5571064 bytes examples/layout/main.go | 146 +++++++++++++++--------------------------------- layout/box.go | 13 ++--- layout/grid.go | 21 +++---- layout/layout.go | 9 ++- layout/mux.go | 43 ++++++++++++-- layout/split.go | 9 ++- 7 files changed, 114 insertions(+), 127 deletions(-) create mode 100755 examples/layout/layout diff --git a/examples/layout/layout b/examples/layout/layout new file mode 100755 index 0000000..08d9e00 Binary files /dev/null and b/examples/layout/layout differ diff --git a/examples/layout/main.go b/examples/layout/main.go index 7e40ea4..3a2b7be 100644 --- a/examples/layout/main.go +++ b/examples/layout/main.go @@ -1,10 +1,7 @@ package main import ( - "image" - "image/draw" "log" - "time" "github.com/faiface/gui" "github.com/faiface/gui/layout" @@ -14,54 +11,6 @@ import ( "golang.org/x/image/font/gofont/goregular" ) -func Blinker(env gui.Env) { - defer func() { - if recover() != nil { - log.Print("recovered blinker") - } - }() - - var r image.Rectangle - var visible bool = true - - redraw := func() func(draw.Image) image.Rectangle { - return func(drw draw.Image) image.Rectangle { - if visible { - draw.Draw(drw, r, image.White, image.ZP, draw.Src) - } else { - draw.Draw(drw, r, &image.Uniform{colornames.Firebrick}, image.ZP, draw.Src) - } - return r - } - } - - // first we draw a white rectangle - env.Draw() <- redraw() - func() { - for event := range env.Events() { - switch event := event.(type) { - case win.MoDown: - if event.Point.In(r) { - go func() { - for i := 0; i < 3; i++ { - visible = false - env.Draw() <- redraw() - time.Sleep(time.Second / 3) - visible = true - env.Draw() <- redraw() - time.Sleep(time.Second / 3) - } - }() - } - case gui.Resize: - log.Print(event) - r = event.Rectangle - env.Draw() <- redraw() - } - } - }() -} - func run() { face, err := TTFToFace(goregular.TTF, 18) if err != nil { @@ -77,39 +26,41 @@ func run() { ButtonDown: colornames.Grey, } w, err := win.New(win.Title("gui test")) // win.Resizable(), - if err != nil { panic(err) } + mux, env := gui.NewMux(w) var ( top gui.Env left, right gui.Env bottomLeft, bottom, bottomRight gui.Env ) - layout.NewGrid( + layout.NewMux( mux.MakeEnv(), - [][]*gui.Env{ - {&top}, - {&left, &right}, - {&bottomLeft, &bottom, &bottomRight}, - }, - layout.GridGap(10), - layout.GridBackground(colornames.Sandybrown), - layout.GridSplitY(func(els int, width int) []int { - ret := make([]int, els) - total := 0 - for i := 0; i < els; i++ { - if i == els-1 { - ret[i] = width - total - } else { - v := (width - total) / 2 - ret[i] = v - total += v + layout.NewGrid( + [][]*gui.Env{ + {&top}, + {&left, &right}, + {&bottomLeft, &bottom, &bottomRight}, + }, + layout.GridGap(10), + layout.GridBackground(colornames.Sandybrown), + layout.GridSplitY(func(els int, width int) []int { + ret := make([]int, els) + total := 0 + for i := 0; i < els; i++ { + if i == els-1 { + ret[i] = width - total + } else { + v := (width - total) / 2 + ret[i] = v + total += v + } } - } - return ret - }), + return ret + }), + ), ) go Blinker(right) go Blinker(left) @@ -118,18 +69,18 @@ func run() { var ( b1, b2, b3, b4, b5, b6 gui.Env ) - layout.NewBox( - top, - []*gui.Env{ - &b1, &b2, &b3, - }, - layout.BoxGap(10), - layout.BoxBackground(colornames.Lightblue), + layout.NewMux(top, + layout.NewBox( + []*gui.Env{ + &b1, &b2, &b3, + }, + layout.BoxGap(10), + layout.BoxBackground(colornames.Lightblue), + ), ) go Blinker(b1) go Blinker(b2) box := layout.NewBox( - b3, []*gui.Env{ &b4, &b5, &b6, }, @@ -139,24 +90,17 @@ func run() { layout.BoxSplit(func(els int, width int) []int { ret := make([]int, els) total := 0 - for i := 0; i < els; i++ { - if i == els-1 { - ret[i] = width - total - } else { - v := (width - total) / 2 - ret[i] = v - total += v - } + for i := 0; i < els-1; i++ { + v := (width - total) / 2 + ret[i] = v + total += v } + ret[els-1] = width - total return ret }), ) + layout.NewMux(b3, box) log.Print(box) - // go func() { - // for v := range box.Events() { - // log.Print("box: ", v) - // } - // }() go Blinker(b4) go Blinker(b5) @@ -165,13 +109,15 @@ func run() { var ( btn1, btn2, btn3 gui.Env ) - layout.NewGrid( + layout.NewMux( bottom, - [][]*gui.Env{ - {&btn1, &btn2, &btn3}, - }, - layout.GridGap(4), - layout.GridBackground(colornames.Darkgrey), + layout.NewGrid( + [][]*gui.Env{ + {&btn1, &btn2, &btn3}, + }, + layout.GridGap(4), + layout.GridBackground(colornames.Darkgrey), + ), ) btn := func(env gui.Env, name string) { Button(env, theme, name, func() { diff --git a/layout/box.go b/layout/box.go index 01a1143..13c7b2b 100644 --- a/layout/box.go +++ b/layout/box.go @@ -19,7 +19,7 @@ type box struct { // NewBox creates a familiar flexbox-like list layout. // It can be horizontal or vertical. -func NewBox(env gui.Env, contents []*gui.Env, options ...func(*box)) gui.Env { +func NewBox(contents []*gui.Env, options ...func(*box)) Layout { ret := &box{ Background: image.Black, Contents: contents, @@ -28,12 +28,7 @@ func NewBox(env gui.Env, contents []*gui.Env, options ...func(*box)) gui.Env { for _, f := range options { f(ret) } - - mux, env := NewMux(env, ret) - for _, item := range contents { - *item = mux.MakeEnv() - } - return env + return ret } // BoxVertical changes the otherwise horizontal Box to be vertical. @@ -67,6 +62,10 @@ func (g *box) Redraw(drw draw.Image, bounds image.Rectangle) { draw.Draw(drw, bounds, image.NewUniform(g.Background), image.ZP, draw.Src) } +func (g *box) Items() []*gui.Env { + return g.Contents +} + func (g *box) Lay(bounds image.Rectangle) []image.Rectangle { items := len(g.Contents) ret := make([]image.Rectangle, 0, items) diff --git a/layout/grid.go b/layout/grid.go index 9c79343..c90821a 100644 --- a/layout/grid.go +++ b/layout/grid.go @@ -18,7 +18,7 @@ type grid struct { // NewGrid creates a familiar flexbox-like grid layout. // Each row can be a different length. -func NewGrid(env gui.Env, contents [][]*gui.Env, options ...func(*grid)) gui.Env { +func NewGrid(contents [][]*gui.Env, options ...func(*grid)) Layout { ret := &grid{ Background: image.Black, Gap: 0, @@ -29,15 +29,7 @@ func NewGrid(env gui.Env, contents [][]*gui.Env, options ...func(*grid)) gui.Env for _, f := range options { f(ret) } - - mux, env := NewMux(env, ret) - for _, row := range contents { - for _, item := range row { - *item = mux.MakeEnv() - } - } - - return env + return ret } // GridBackground changes the background of the grid to a uniform color. @@ -73,6 +65,15 @@ func (g *grid) Redraw(drw draw.Image, bounds image.Rectangle) { draw.Draw(drw, bounds, image.NewUniform(g.Background), image.ZP, draw.Src) } +func (g *grid) Items() []*gui.Env { + // 32 should be more than enough for most grids + ret := make([]*gui.Env, 0, 32) + for _, row := range g.Contents { + ret = append(ret, row...) + } + return ret +} + func (g *grid) Lay(bounds image.Rectangle) []image.Rectangle { gap := g.Gap ret := make([]image.Rectangle, 0) diff --git a/layout/layout.go b/layout/layout.go index 987ee24..dfacca1 100644 --- a/layout/layout.go +++ b/layout/layout.go @@ -3,17 +3,22 @@ package layout import ( "image" "image/draw" + + "github.com/faiface/gui" ) // Layout represents any graphical layout // -// A Layout needs to be able to redraw itself with the Redraw method. -// Redraw() only draws the background or frame of the Layout, not the childs. +// Items returns the Layout's childs in whatever order. // // 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. type Layout interface { + Items() []*gui.Env Lay(image.Rectangle) []image.Rectangle Redraw(draw.Image, image.Rectangle) } diff --git a/layout/mux.go b/layout/mux.go index 208664c..6c7ddaa 100644 --- a/layout/mux.go +++ b/layout/mux.go @@ -1,6 +1,7 @@ package layout import ( + "fmt" "image" "image/draw" "sync" @@ -24,9 +25,15 @@ type Mux struct { 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 and underlying Layout -// for modifying the gui.Resize events. to the childs. +// 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. +// Also, you cannot make Envs manually, you must use FillLayout. func NewMux(env gui.Env, l Layout) (mux *Mux, master gui.Env) { drawChan := make(chan func(draw.Image) image.Rectangle) mux = &Mux{ @@ -34,7 +41,7 @@ func NewMux(env gui.Env, l Layout) (mux *Mux, master gui.Env) { draw: drawChan, } master, masterIn := mux.makeEnv(true) - events := make(chan gui.Event, 0) + events := make(chan gui.Event) mux.evIn = events go func() { for d := range drawChan { @@ -85,12 +92,36 @@ func NewMux(env gui.Env, l Layout) (mux *Mux, master gui.Env) { } mux.mu.Unlock() }() + + err := mux.FillLayout() + if err != nil { + panic(err) + } return } -func (mux *Mux) MakeEnv() gui.Env { - env, _ := mux.makeEnv(false) - return env +// FillLayout uses the mux to fill the Layout's Envs with suitable Envs. +// It's called automatically on NewMux, but you can call it again to fill +// the Envs that became nil, to change a child in the Layout. +func (mux *Mux) FillLayout() error { + nilptrs := 0 + filleditems := 0 + // err + for _, en := range mux.layout.Items() { + if en == nil { + nilptrs += 1 + continue + } + if *en != nil { + filleditems += 1 + continue + } + *en, _ = mux.makeEnv(false) + } + if nilptrs > 0 { + return fmt.Errorf("Mux: %d already filled and %d nil pointers", filleditems, nilptrs) + } + return nil } type muxEnv struct { diff --git a/layout/split.go b/layout/split.go index 4bf00a7..fb6d36e 100644 --- a/layout/split.go +++ b/layout/split.go @@ -1,13 +1,18 @@ package layout +import "fmt" + // SplitFunc represents a way to split a space among a number of elements. -// The space of the returned slice must be equal to the 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 -// EvenSplit implements SplitFunc to split a space (almost) evenly among the elements. +// 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) for elements > 0 { v := width / elements -- cgit v1.2.3 From 1b03644d1e0e1be27cdd22732f893d03c0e8421d Mon Sep 17 00:00:00 2001 From: Clement Benard Date: Fri, 12 Jul 2019 17:32:57 +0200 Subject: Somehow added the wrong file, fix that --- examples/layout/blinker.go | 60 +++++++++++++++++++++++++++++++++++++++++++++ examples/layout/layout | Bin 5571064 -> 0 bytes 2 files changed, 60 insertions(+) create mode 100644 examples/layout/blinker.go delete mode 100755 examples/layout/layout diff --git a/examples/layout/blinker.go b/examples/layout/blinker.go new file mode 100644 index 0000000..76dbd95 --- /dev/null +++ b/examples/layout/blinker.go @@ -0,0 +1,60 @@ +package main + +import ( + "image" + "image/draw" + "log" + "time" + + "github.com/faiface/gui" + "github.com/faiface/gui/win" + "golang.org/x/image/colornames" +) + +func Blinker(env gui.Env) { + defer func() { + if recover() != nil { + log.Print("recovered blinker") + } + }() + + var r image.Rectangle + var visible bool = true + + redraw := func() func(draw.Image) image.Rectangle { + return func(drw draw.Image) image.Rectangle { + if visible { + draw.Draw(drw, r, image.White, image.ZP, draw.Src) + } else { + draw.Draw(drw, r, &image.Uniform{colornames.Firebrick}, image.ZP, draw.Src) + } + return r + } + } + + // first we draw a white rectangle + env.Draw() <- redraw() + func() { + for event := range env.Events() { + switch event := event.(type) { + case win.MoDown: + if event.Point.In(r) { + go func() { + for i := 0; i < 3; i++ { + visible = false + env.Draw() <- redraw() + time.Sleep(time.Second / 3) + visible = true + env.Draw() <- redraw() + time.Sleep(time.Second / 3) + } + }() + } + case gui.Resize: + log.Print(event) + r = event.Rectangle + env.Draw() <- redraw() + } + } + }() +} diff --git a/examples/layout/layout b/examples/layout/layout deleted file mode 100755 index 08d9e00..0000000 Binary files a/examples/layout/layout and /dev/null differ -- cgit v1.2.3 From 736e35bd9b0c8f4f9649557e4b7a3085b4bdbe63 Mon Sep 17 00:00:00 2001 From: Clement Benard Date: Mon, 15 Jul 2019 15:44:34 +0200 Subject: Easier and more idiomatic Layout initializing --- examples/layout/blinker.go | 1 - examples/layout/button.go | 2 - examples/layout/main.go | 90 +++++++++++++++++++++++++----------------- layout/box.go | 93 +++++++++++++++---------------------------- layout/doc.go | 10 +++++ layout/grid.go | 98 +++++++++++++--------------------------------- layout/layout.go | 5 --- layout/mux.go | 41 +++++-------------- 8 files changed, 133 insertions(+), 207 deletions(-) create mode 100644 layout/doc.go diff --git a/examples/layout/blinker.go b/examples/layout/blinker.go index 76dbd95..b096a73 100644 --- a/examples/layout/blinker.go +++ b/examples/layout/blinker.go @@ -51,7 +51,6 @@ func Blinker(env gui.Env) { }() } case gui.Resize: - log.Print(event) r = event.Rectangle env.Draw() <- redraw() } diff --git a/examples/layout/button.go b/examples/layout/button.go index 6c907e3..0693e06 100644 --- a/examples/layout/button.go +++ b/examples/layout/button.go @@ -4,7 +4,6 @@ import ( "image" "image/color" "image/draw" - "log" "github.com/faiface/gui" "github.com/faiface/gui/win" @@ -39,7 +38,6 @@ func Button(env gui.Env, theme *Theme, text string, action func()) { switch e := e.(type) { case gui.Resize: r = e.Rectangle - log.Print("button ", e) env.Draw() <- redraw(r, over, pressed) case win.MoDown: diff --git a/examples/layout/main.go b/examples/layout/main.go index 3a2b7be..c0055ee 100644 --- a/examples/layout/main.go +++ b/examples/layout/main.go @@ -1,7 +1,10 @@ package main import ( + "image" + "image/draw" "log" + "time" "github.com/faiface/gui" "github.com/faiface/gui/layout" @@ -31,6 +34,20 @@ func run() { } mux, env := gui.NewMux(w) + + go func() { + // Hack for non-reparenting window managers (I think) + e := mux.MakeEnv() + for { + time.Sleep(time.Second / 5) + e.Draw() <- func(drw draw.Image) image.Rectangle { + r := image.Rect(0, 0, 10, 10) + draw.Draw(drw, r, image.Transparent, image.ZP, draw.Over) + return r + } + } + }() + var ( top gui.Env left, right gui.Env @@ -38,15 +55,15 @@ func run() { ) layout.NewMux( mux.MakeEnv(), - layout.NewGrid( - [][]*gui.Env{ - {&top}, - {&left, &right}, - {&bottomLeft, &bottom, &bottomRight}, - }, - layout.GridGap(10), - layout.GridBackground(colornames.Sandybrown), - layout.GridSplitY(func(els int, width int) []int { + []*gui.Env{ + &top, + &left, &right, + &bottomLeft, &bottom, &bottomRight}, + layout.Grid{ + Rows: []int{1, 2, 3}, + Gap: 10, + Background: colornames.Sandybrown, + SplitY: func(els int, width int) []int { ret := make([]int, els) total := 0 for i := 0; i < els; i++ { @@ -59,8 +76,8 @@ func run() { } } return ret - }), - ), + }, + }, ) go Blinker(right) go Blinker(left) @@ -70,24 +87,21 @@ func run() { b1, b2, b3, b4, b5, b6 gui.Env ) layout.NewMux(top, - layout.NewBox( - []*gui.Env{ - &b1, &b2, &b3, - }, - layout.BoxGap(10), - layout.BoxBackground(colornames.Lightblue), - ), + []*gui.Env{&b1, &b2, &b3}, + layout.Box{ + Length: 3, + Gap: 10, + Background: colornames.Lightblue, + }, ) go Blinker(b1) go Blinker(b2) - box := layout.NewBox( - []*gui.Env{ - &b4, &b5, &b6, - }, - layout.BoxVertical, - layout.BoxBackground(colornames.Pink), - layout.BoxGap(4), - layout.BoxSplit(func(els int, width int) []int { + box := layout.Box{ + Length: 4, + Vertical: true, + Gap: 4, + Background: colornames.Pink, + Split: func(els int, width int) []int { ret := make([]int, els) total := 0 for i := 0; i < els-1; i++ { @@ -97,10 +111,15 @@ func run() { } ret[els-1] = width - total return ret - }), + }, + } + + layout.NewMux(b3, + []*gui.Env{ + &b4, &b5, &b6, + }, + box, ) - layout.NewMux(b3, box) - log.Print(box) go Blinker(b4) go Blinker(b5) @@ -111,13 +130,12 @@ func run() { ) layout.NewMux( bottom, - layout.NewGrid( - [][]*gui.Env{ - {&btn1, &btn2, &btn3}, - }, - layout.GridGap(4), - layout.GridBackground(colornames.Darkgrey), - ), + []*gui.Env{&btn1, &btn2, &btn3}, + layout.Grid{ + Rows: []int{2, 1}, + Background: colornames.Darkgrey, + Gap: 4, + }, ) btn := func(env gui.Env, name string) { Button(env, theme, name, func() { diff --git a/layout/box.go b/layout/box.go index 13c7b2b..24f0c0d 100644 --- a/layout/box.go +++ b/layout/box.go @@ -4,84 +4,53 @@ import ( "image" "image/color" "image/draw" - - "github.com/faiface/gui" ) -type box struct { - Contents []*gui.Env +type Box struct { + // Number of child elements + Length int + // Background changes the background of the Box to a uniform color. Background color.Color - Split SplitFunc - Gap int - - vertical bool -} - -// NewBox creates a familiar flexbox-like list layout. -// It can be horizontal or vertical. -func NewBox(contents []*gui.Env, options ...func(*box)) Layout { - ret := &box{ - Background: image.Black, - Contents: contents, - Split: EvenSplit, - } - for _, f := range options { - f(ret) - } - return ret -} + // 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 -// BoxVertical changes the otherwise horizontal Box to be vertical. -func BoxVertical(b *box) { - b.vertical = true + // Vertical changes the otherwise horizontal Box to be vertical. + Vertical bool } -// BoxBackground changes the background of the box to a uniform color. -func BoxBackground(c color.Color) func(*box) { - return func(grid *box) { - grid.Background = c +func (b Box) Redraw(drw draw.Image, bounds image.Rectangle) { + col := b.Background + if col == nil { + col = image.Black } -} -// BoxSplit changes the way the space is divided among the elements. -func BoxSplit(split SplitFunc) func(*box) { - return func(grid *box) { - grid.Split = split - } + draw.Draw(drw, bounds, image.NewUniform(col), image.ZP, draw.Src) } -// BoxGap changes the box gap. -// The gap is identical everywhere (top, left, bottom, right). -func BoxGap(gap int) func(*box) { - return func(grid *box) { - grid.Gap = gap +func (b Box) Lay(bounds image.Rectangle) []image.Rectangle { + items := b.Length + gap := b.Gap + split := b.Split + if split == nil { + split = EvenSplit } -} - -func (g *box) Redraw(drw draw.Image, bounds image.Rectangle) { - draw.Draw(drw, bounds, image.NewUniform(g.Background), image.ZP, draw.Src) -} - -func (g *box) Items() []*gui.Env { - return g.Contents -} - -func (g *box) Lay(bounds image.Rectangle) []image.Rectangle { - items := len(g.Contents) ret := make([]image.Rectangle, 0, items) - if g.vertical { - spl := g.Split(items, bounds.Dy()-(g.Gap*(items+1))) - Y := bounds.Min.Y + g.Gap + 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+g.Gap, Y, bounds.Max.X-g.Gap, Y+item)) - Y += item + g.Gap + ret = append(ret, image.Rect(bounds.Min.X+gap, Y, bounds.Max.X-gap, Y+item)) + Y += item + gap } } else { - spl := g.Split(items, bounds.Dx()-(g.Gap*(items+1))) - X := bounds.Min.X + g.Gap + 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+g.Gap, X+item, bounds.Max.Y-g.Gap)) - X += item + g.Gap + 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/doc.go b/layout/doc.go new file mode 100644 index 0000000..8305d43 --- /dev/null +++ b/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/layout/grid.go b/layout/grid.go index c90821a..f3cced6 100644 --- a/layout/grid.go +++ b/layout/grid.go @@ -4,92 +4,50 @@ import ( "image" "image/color" "image/draw" - - "github.com/faiface/gui" ) -type grid struct { - Contents [][]*gui.Env +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 int - SplitX SplitFunc - SplitY SplitFunc -} - -// NewGrid creates a familiar flexbox-like grid layout. -// Each row can be a different length. -func NewGrid(contents [][]*gui.Env, options ...func(*grid)) Layout { - ret := &grid{ - Background: image.Black, - Gap: 0, - Contents: contents, - SplitX: EvenSplit, - SplitY: EvenSplit, - } - for _, f := range options { - f(ret) - } - return ret -} - -// GridBackground changes the background of the grid to a uniform color. -func GridBackground(c color.Color) func(*grid) { - return func(grid *grid) { - grid.Background = c - } -} - -// GridGap changes the grid gap. -// The gap is identical everywhere (top, left, bottom, right). -func GridGap(g int) func(*grid) { - return func(grid *grid) { - grid.Gap = g - } + // 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 } -// GridSplitX changes the way the space is divided among the columns in each row. -func GridSplitX(split SplitFunc) func(*grid) { - return func(grid *grid) { - grid.SplitX = split +func (g Grid) Redraw(drw draw.Image, bounds image.Rectangle) { + col := g.Background + if col == nil { + col = image.Black } + draw.Draw(drw, bounds, image.NewUniform(col), image.ZP, draw.Src) } -// GridSplitY changes the way the space is divided among the rows. -func GridSplitY(split SplitFunc) func(*grid) { - return func(grid *grid) { - grid.SplitY = split +func (g Grid) Lay(bounds image.Rectangle) []image.Rectangle { + gap := g.Gap + rows := g.Rows + splitX := g.SplitX + if splitX == nil { + splitX = EvenSplit } -} - -func (g *grid) Redraw(drw draw.Image, bounds image.Rectangle) { - draw.Draw(drw, bounds, image.NewUniform(g.Background), image.ZP, draw.Src) -} - -func (g *grid) Items() []*gui.Env { - // 32 should be more than enough for most grids - ret := make([]*gui.Env, 0, 32) - for _, row := range g.Contents { - ret = append(ret, row...) + splitY := g.SplitY + if splitY == nil { + splitY = EvenSplit } - return ret -} -func (g *grid) Lay(bounds image.Rectangle) []image.Rectangle { - gap := g.Gap ret := make([]image.Rectangle, 0) - rows := len(g.Contents) - - rowsH := g.SplitY(rows, bounds.Dy()-(g.Gap*(rows+1))) - + rowsH := splitY(len(rows), bounds.Dy()-(gap*(len(rows)+1))) X := gap + bounds.Min.X Y := gap + bounds.Min.Y - for y, row := range g.Contents { - cols := len(row) + for y, cols := range rows { h := rowsH[y] - colsW := g.SplitX(cols, bounds.Dx()-(g.Gap*(cols+1))) + colsW := splitX(cols, bounds.Dx()-(gap*(cols+1))) X = gap + bounds.Min.X - for x := range row { - w := colsW[x] + for _, w := range colsW { ret = append(ret, image.Rect(X, Y, X+w, Y+h)) X += gap + w } diff --git a/layout/layout.go b/layout/layout.go index dfacca1..8b8e039 100644 --- a/layout/layout.go +++ b/layout/layout.go @@ -3,14 +3,10 @@ package layout import ( "image" "image/draw" - - "github.com/faiface/gui" ) // Layout represents any graphical layout // -// Items returns the Layout's childs in whatever order. -// // 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. @@ -18,7 +14,6 @@ import ( // // Redraw only draws the background or frame of the Layout, not the childs. type Layout interface { - Items() []*gui.Env Lay(image.Rectangle) []image.Rectangle Redraw(draw.Image, image.Rectangle) } diff --git a/layout/mux.go b/layout/mux.go index 6c7ddaa..2789878 100644 --- a/layout/mux.go +++ b/layout/mux.go @@ -1,9 +1,9 @@ package layout import ( - "fmt" "image" "image/draw" + "log" "sync" "github.com/faiface/gui" @@ -33,8 +33,7 @@ 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. -// Also, you cannot make Envs manually, you must use FillLayout. -func NewMux(env gui.Env, l Layout) (mux *Mux, master gui.Env) { +func NewMux(env gui.Env, envs []*gui.Env, l Layout) (mux *Mux, master gui.Env) { drawChan := make(chan func(draw.Image) image.Rectangle) mux = &Mux{ layout: l, @@ -63,6 +62,12 @@ func NewMux(env gui.Env, l Layout) (mux *Mux, master gui.Env) { mux.mu.Lock() if resize, ok := e.(gui.Resize); ok { mux.lastResize = resize + 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)) + mux.mu.Unlock() + continue + } rect := resize.Rectangle @@ -73,7 +78,6 @@ func NewMux(env gui.Env, l Layout) (mux *Mux, master gui.Env) { } // Send appropriate resize Events to childs - lay := mux.layout.Lay(rect) for i, eventsIn := range mux.eventsIns { resize.Rectangle = lay[i] eventsIn <- resize @@ -93,35 +97,10 @@ func NewMux(env gui.Env, l Layout) (mux *Mux, master gui.Env) { mux.mu.Unlock() }() - err := mux.FillLayout() - if err != nil { - panic(err) - } - return -} - -// FillLayout uses the mux to fill the Layout's Envs with suitable Envs. -// It's called automatically on NewMux, but you can call it again to fill -// the Envs that became nil, to change a child in the Layout. -func (mux *Mux) FillLayout() error { - nilptrs := 0 - filleditems := 0 - // err - for _, en := range mux.layout.Items() { - if en == nil { - nilptrs += 1 - continue - } - if *en != nil { - filleditems += 1 - continue - } + for _, en := range envs { *en, _ = mux.makeEnv(false) } - if nilptrs > 0 { - return fmt.Errorf("Mux: %d already filled and %d nil pointers", filleditems, nilptrs) - } - return nil + return } type muxEnv struct { -- cgit v1.2.3 From 8b70878ccc7fe324f3647e56503a37f3780f9d41 Mon Sep 17 00:00:00 2001 From: Clement Benard Date: Mon, 15 Jul 2019 16:19:19 +0200 Subject: now working --- examples/layout/blinker.go | 7 ++----- examples/layout/layout | Bin 0 -> 5564792 bytes examples/layout/main.go | 2 +- layout/mux.go | 3 +-- 4 files changed, 4 insertions(+), 8 deletions(-) create mode 100755 examples/layout/layout diff --git a/examples/layout/blinker.go b/examples/layout/blinker.go index b096a73..8c380c3 100644 --- a/examples/layout/blinker.go +++ b/examples/layout/blinker.go @@ -40,11 +40,8 @@ func Blinker(env gui.Env) { case win.MoDown: if event.Point.In(r) { go func() { - for i := 0; i < 3; i++ { - visible = false - env.Draw() <- redraw() - time.Sleep(time.Second / 3) - visible = true + for i := 0; i < 6; i++ { + visible = !visible env.Draw() <- redraw() time.Sleep(time.Second / 3) } diff --git a/examples/layout/layout b/examples/layout/layout new file mode 100755 index 0000000..0d51021 Binary files /dev/null and b/examples/layout/layout differ diff --git a/examples/layout/main.go b/examples/layout/main.go index c0055ee..c672e63 100644 --- a/examples/layout/main.go +++ b/examples/layout/main.go @@ -97,7 +97,7 @@ func run() { go Blinker(b1) go Blinker(b2) box := layout.Box{ - Length: 4, + Length: 3, Vertical: true, Gap: 4, Background: colornames.Pink, diff --git a/layout/mux.go b/layout/mux.go index 2789878..d56d266 100644 --- a/layout/mux.go +++ b/layout/mux.go @@ -62,6 +62,7 @@ func NewMux(env gui.Env, envs []*gui.Env, l Layout) (mux *Mux, master gui.Env) { 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(mux.eventsIns) { log.Printf("Lay of %T has %d elements while mux has %d, skipping\n", l, len(lay), len(envs)) @@ -69,8 +70,6 @@ func NewMux(env gui.Env, envs []*gui.Env, l Layout) (mux *Mux, master gui.Env) { continue } - rect := resize.Rectangle - // Redraw self mux.draw <- func(drw draw.Image) image.Rectangle { mux.layout.Redraw(drw, rect) -- cgit v1.2.3 From 8d183ef96a57e3a2f42c0cb4ec0ab4c256e0d47e Mon Sep 17 00:00:00 2001 From: Clement Benard Date: Wed, 7 Aug 2019 16:02:33 +0200 Subject: Made the layout package actually usable --- examples/layout/blinker.go | 37 +++++++++---- examples/layout/button.go | 7 +++ examples/layout/card.go | 24 ++++++++ examples/layout/label.go | 35 ++++++++++++ examples/layout/layout | Bin 5564792 -> 5614432 bytes examples/layout/main.go | 103 +++++++++++++++++++++-------------- layout/box.go | 57 ------------------- layout/grid.go | 95 +++++++++++++++++++++++++------- layout/intercepter.go | 43 +++++++++++++++ layout/layout.go | 9 +-- layout/mux.go | 16 ++---- layout/scroller.go | 133 +++++++++++++++++++++++++++++++++++++++++++++ layout/split.go | 6 +- 13 files changed, 420 insertions(+), 145 deletions(-) create mode 100644 examples/layout/card.go create mode 100644 examples/layout/label.go delete mode 100644 layout/box.go create mode 100644 layout/intercepter.go create mode 100644 layout/scroller.go diff --git a/examples/layout/blinker.go b/examples/layout/blinker.go index 8c380c3..65dce43 100644 --- a/examples/layout/blinker.go +++ b/examples/layout/blinker.go @@ -2,13 +2,15 @@ package main import ( "image" + "image/color" "image/draw" "log" + "math/rand" + "sync" "time" "github.com/faiface/gui" "github.com/faiface/gui/win" - "golang.org/x/image/colornames" ) func Blinker(env gui.Env) { @@ -17,23 +19,33 @@ func Blinker(env gui.Env) { log.Print("recovered blinker") } }() - - var r image.Rectangle - var visible bool = true - - redraw := func() func(draw.Image) image.Rectangle { + buf := make([]byte, 3) + rand.Read(buf) + defaultColor := image.NewUniform(color.RGBA{buf[0], buf[1], buf[2], 255}) + rand.Read(buf) + blinkColor := image.NewUniform(color.RGBA{buf[0], buf[1], buf[2], 255}) + redraw := func(r image.Rectangle, visible bool) func(draw.Image) image.Rectangle { return func(drw draw.Image) image.Rectangle { + if r == image.ZR { + return r + } if visible { - draw.Draw(drw, r, image.White, image.ZP, draw.Src) + draw.Draw(drw, r, defaultColor, image.ZP, draw.Src) } else { - draw.Draw(drw, r, &image.Uniform{colornames.Firebrick}, image.ZP, draw.Src) + draw.Draw(drw, r, blinkColor, image.ZP, draw.Src) } return r } } + var mu sync.Mutex + var ( + r image.Rectangle + visible bool = true + ) + // first we draw a white rectangle - env.Draw() <- redraw() + // env.Draw() <- redraw(b) func() { for event := range env.Events() { switch event := event.(type) { @@ -41,15 +53,18 @@ func Blinker(env gui.Env) { if event.Point.In(r) { go func() { for i := 0; i < 6; i++ { + mu.Lock() visible = !visible - env.Draw() <- redraw() + env.Draw() <- redraw(r, visible) + mu.Unlock() + time.Sleep(time.Second / 3) } }() } case gui.Resize: r = event.Rectangle - env.Draw() <- redraw() + env.Draw() <- redraw(r, visible) } } }() diff --git a/examples/layout/button.go b/examples/layout/button.go index 0693e06..cf13c3d 100644 --- a/examples/layout/button.go +++ b/examples/layout/button.go @@ -40,6 +40,13 @@ func Button(env gui.Env, theme *Theme, text string, action func()) { r = e.Rectangle env.Draw() <- redraw(r, over, pressed) + case win.MoMove: + nover := e.Point.In(r) + if nover != over { + over = nover + env.Draw() <- redraw(r, over, pressed) + } + case win.MoDown: newPressed := e.Point.In(r) if newPressed != pressed { diff --git a/examples/layout/card.go b/examples/layout/card.go new file mode 100644 index 0000000..501e4e3 --- /dev/null +++ b/examples/layout/card.go @@ -0,0 +1,24 @@ +package main + +import ( + "github.com/faiface/gui" + "github.com/faiface/gui/layout" + "golang.org/x/image/colornames" +) + +func Card(env gui.Env, theme *Theme, title, content string) { + box := layout.Grid{ + Rows: []int{1, 1}, + // Flip: true, + // Gap: 4, + Background: colornames.Pink, + } + fields := makeEnvPtr(2) + layout.NewMux(env, + fields, + box, + ) + go Label(*fields[0], theme, title, colornames.Lightgray) + go Label(*fields[1], theme, content, colornames.Slategray) + // go Blinker(*fields[1]) +} diff --git a/examples/layout/label.go b/examples/layout/label.go new file mode 100644 index 0000000..f46f25f --- /dev/null +++ b/examples/layout/label.go @@ -0,0 +1,35 @@ +package main + +import ( + "image" + "image/color" + "image/draw" + + "github.com/faiface/gui" +) + +func Label(env gui.Env, theme *Theme, text string, colr color.Color) { + textImg := MakeTextImage(text, theme.Face, theme.Text) + + redraw := func(r image.Rectangle) func(draw.Image) image.Rectangle { + return func(drw draw.Image) image.Rectangle { + draw.Draw(drw, r, &image.Uniform{colr}, image.ZP, draw.Src) + DrawLeftCentered(drw, r.Add(image.Pt(5, 0)), textImg, draw.Over) + return r + } + } + + var ( + r image.Rectangle + ) + + for e := range env.Events() { + switch e := e.(type) { + case gui.Resize: + r = e.Rectangle + env.Draw() <- redraw(r) + } + } + + close(env.Draw()) +} diff --git a/examples/layout/layout b/examples/layout/layout index 0d51021..e7e9c5b 100755 Binary files a/examples/layout/layout and b/examples/layout/layout differ diff --git a/examples/layout/main.go b/examples/layout/main.go index c672e63..83a170d 100644 --- a/examples/layout/main.go +++ b/examples/layout/main.go @@ -1,9 +1,11 @@ package main import ( + "fmt" "image" "image/draw" "log" + "os" "time" "github.com/faiface/gui" @@ -14,6 +16,14 @@ import ( "golang.org/x/image/font/gofont/goregular" ) +func makeEnvPtr(n int) []*gui.Env { + elsp := make([]*gui.Env, n) + for i := 0; i < len(elsp); i++ { + elsp[i] = new(gui.Env) + } + return elsp +} + func run() { face, err := TTFToFace(goregular.TTF, 18) if err != nil { @@ -26,7 +36,8 @@ func run() { Text: colornames.Black, Highlight: colornames.Blueviolet, ButtonUp: colornames.Lightgrey, - ButtonDown: colornames.Grey, + ButtonOver: colornames.Grey, + ButtonDown: colornames.Dimgrey, } w, err := win.New(win.Title("gui test")) // win.Resizable(), if err != nil { @@ -39,7 +50,7 @@ func run() { // Hack for non-reparenting window managers (I think) e := mux.MakeEnv() for { - time.Sleep(time.Second / 5) + time.Sleep(time.Second / 10) e.Draw() <- func(drw draw.Image) image.Rectangle { r := image.Rect(0, 0, 10, 10) draw.Draw(drw, r, image.Transparent, image.ZP, draw.Over) @@ -60,21 +71,21 @@ func run() { &left, &right, &bottomLeft, &bottom, &bottomRight}, layout.Grid{ - Rows: []int{1, 2, 3}, - Gap: 10, - Background: colornames.Sandybrown, - SplitY: func(els int, width int) []int { + Rows: []int{1, 2, 3}, + Gap: 10, + Margin: -6, + Border: 1, + // Flip: true, + BorderColor: image.White, + Background: colornames.Sandybrown, + SplitRows: func(els int, width int) []int { ret := make([]int, els) total := 0 - for i := 0; i < els; i++ { - if i == els-1 { - ret[i] = width - total - } else { - v := (width - total) / 2 - ret[i] = v - total += v - } + for i := 0; i < els-1; i++ { + ret[i] = (width - total) / 2 + total += ret[i] } + ret[els-1] = width - total return ret }, }, @@ -83,22 +94,36 @@ func run() { go Blinker(left) go Blinker(bottomRight) - var ( - b1, b2, b3, b4, b5, b6 gui.Env - ) + subGrid := makeEnvPtr(3) layout.NewMux(top, - []*gui.Env{&b1, &b2, &b3}, - layout.Box{ - Length: 3, + subGrid, + layout.Grid{ + Rows: []int{len(subGrid)}, Gap: 10, Background: colornames.Lightblue, }, ) - go Blinker(b1) - go Blinker(b2) - box := layout.Box{ - Length: 3, - Vertical: true, + + elsp := makeEnvPtr(100) + scrl := &layout.Scroller{ + Background: colornames.Red, + Length: len(elsp), + Gap: 2, + ChildHeight: 80, + } + layout.NewMux(*subGrid[0], + elsp, + scrl, + ) + for i, el := range elsp { + // go Blinker(*el) + go Card(*el, theme, "hello", fmt.Sprintf("I'm card #%d", i)) + } + + go Blinker(*subGrid[1]) + box := layout.Grid{ + Rows: []int{3}, + Flip: true, Gap: 4, Background: colornames.Pink, Split: func(els int, width int) []int { @@ -113,28 +138,25 @@ func run() { return ret }, } - - layout.NewMux(b3, - []*gui.Env{ - &b4, &b5, &b6, - }, + blinkers := makeEnvPtr(3) + layout.NewMux(*subGrid[2], + blinkers, box, ) - go Blinker(b4) - go Blinker(b5) - go Blinker(b6) + go Blinker(*blinkers[0]) + go Blinker(*blinkers[1]) + go Blinker(*blinkers[2]) - var ( - btn1, btn2, btn3 gui.Env - ) + btns := makeEnvPtr(3) layout.NewMux( bottom, - []*gui.Env{&btn1, &btn2, &btn3}, + btns, layout.Grid{ Rows: []int{2, 1}, Background: colornames.Darkgrey, Gap: 4, + Flip: true, }, ) btn := func(env gui.Env, name string) { @@ -142,15 +164,16 @@ func run() { log.Print(name) }) } - go btn(btn1, "Hey") - go btn(btn2, "Ho") - go btn(btn3, "Hu") + go btn(*btns[0], "Hey") + go btn(*btns[1], "Ho") + go btn(*btns[2], "Hu") // we use the master env now, w is used by the mux for event := range env.Events() { switch event.(type) { case win.WiClose: close(env.Draw()) + os.Exit(0) } } } 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 } -- cgit v1.2.3