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 --- 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 ++- 7 files changed, 265 insertions(+), 94 deletions(-) delete mode 100644 layout/box.go create mode 100644 layout/intercepter.go create mode 100644 layout/scroller.go (limited to 'layout') 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