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 --- layout/box.go | 99 +++++++++++++++++++++++++ layout/fixedgrid/fixedgrid.go | 88 ---------------------- layout/grid.go | 92 +++++++++++++++++++++++ layout/layout.go | 168 +++--------------------------------------- layout/mux.go | 150 +++++++++++++++++++++++++++++++++++++ 5 files changed, 352 insertions(+), 245 deletions(-) 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 (limited to 'layout') 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