diff options
| author | Clement Benard <contact@clementbenard.com> | 2019-07-04 16:28:51 +0200 |
|---|---|---|
| committer | Clement Benard <contact@clementbenard.com> | 2019-07-04 16:28:51 +0200 |
| commit | 3a216b96b6a7c80275a2516e7de82d9b2ffc96df (patch) | |
| tree | 676a7983048b7bbc35f2ffe7d685bb65683660f7 | |
| parent | ed00d80d15daf82492cace0e51cc5584f0aae736 (diff) | |
| download | gui-3a216b96b6a7c80275a2516e7de82d9b2ffc96df.zip | |
added layout basics
| -rw-r--r-- | examples/layout/button.go | 64 | ||||
| -rw-r--r-- | examples/layout/main.go | 128 | ||||
| -rw-r--r-- | examples/layout/theme.go | 19 | ||||
| -rw-r--r-- | examples/layout/utils.go | 104 | ||||
| -rw-r--r-- | layout/fixedgrid/fixedgrid.go | 88 | ||||
| -rw-r--r-- | layout/layout.go | 165 |
6 files changed, 568 insertions, 0 deletions
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 +} |