diff options
| author | Clement Benard <contact@clementbenard.com> | 2019-07-09 10:59:38 +0200 |
|---|---|---|
| committer | Clement Benard <contact@clementbenard.com> | 2019-07-09 10:59:38 +0200 |
| commit | 1223e277009005337243ca991cb54dd75bf723a7 (patch) | |
| tree | aa4863dc2a08649c825fd28843940ef5d60beba1 | |
| parent | 3a216b96b6a7c80275a2516e7de82d9b2ffc96df (diff) | |
| download | gui-1223e277009005337243ca991cb54dd75bf723a7.zip | |
Layout system remaking
| -rwxr-xr-x | examples/layout/layout | bin | 0 -> 5570192 bytes | |||
| -rw-r--r-- | examples/layout/main.go | 122 | ||||
| -rw-r--r-- | layout/box.go | 99 | ||||
| -rw-r--r-- | layout/fixedgrid/fixedgrid.go | 88 | ||||
| -rw-r--r-- | layout/grid.go | 92 | ||||
| -rw-r--r-- | layout/layout.go | 168 | ||||
| -rw-r--r-- | layout/mux.go | 150 |
7 files changed, 446 insertions, 273 deletions
diff --git a/examples/layout/layout b/examples/layout/layout Binary files differnew file mode 100755 index 0000000..050fbdf --- /dev/null +++ b/examples/layout/layout 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 +} |