diff options
| author | Sam Anthony <sam@samanthony.xyz> | 2024-08-17 14:05:07 -0400 |
|---|---|---|
| committer | Sam Anthony <sam@samanthony.xyz> | 2024-08-17 14:05:07 -0400 |
| commit | 32f1b20c3e93457dec949e7e426fd3ab17dc3d5c (patch) | |
| tree | 201321e995e9f74cab877dac64e74eb654946f1b | |
| parent | ee1ecb5c17ebe98d18dc62390ecb6c09f648a52e (diff) | |
| parent | 8d183ef96a57e3a2f42c0cb4ec0ab4c256e0d47e (diff) | |
| download | gui-32f1b20c3e93457dec949e7e426fd3ab17dc3d5c.zip | |
Merge remote-tracking branch 'keitio/master'
layout
| -rw-r--r-- | examples/layout/blinker.go | 71 | ||||
| -rw-r--r-- | examples/layout/button.go | 69 | ||||
| -rw-r--r-- | examples/layout/card.go | 24 | ||||
| -rw-r--r-- | examples/layout/label.go | 35 | ||||
| -rwxr-xr-x | examples/layout/layout | bin | 0 -> 5614432 bytes | |||
| -rw-r--r-- | examples/layout/main.go | 183 | ||||
| -rw-r--r-- | examples/layout/theme.go | 19 | ||||
| -rw-r--r-- | examples/layout/utils.go | 104 | ||||
| -rw-r--r-- | layout/doc.go | 10 | ||||
| -rw-r--r-- | layout/grid.go | 115 | ||||
| -rw-r--r-- | layout/intercepter.go | 43 | ||||
| -rw-r--r-- | layout/layout.go | 20 | ||||
| -rw-r--r-- | layout/mux.go | 172 | ||||
| -rw-r--r-- | layout/scroller.go | 133 | ||||
| -rw-r--r-- | layout/split.go | 26 |
15 files changed, 1024 insertions, 0 deletions
diff --git a/examples/layout/blinker.go b/examples/layout/blinker.go new file mode 100644 index 0000000..65dce43 --- /dev/null +++ b/examples/layout/blinker.go @@ -0,0 +1,71 @@ +package main + +import ( + "image" + "image/color" + "image/draw" + "log" + "math/rand" + "sync" + "time" + + "github.com/faiface/gui" + "github.com/faiface/gui/win" +) + +func Blinker(env gui.Env) { + defer func() { + if recover() != nil { + log.Print("recovered blinker") + } + }() + 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, defaultColor, image.ZP, draw.Src) + } else { + 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(b) + func() { + for event := range env.Events() { + switch event := event.(type) { + case win.MoDown: + if event.Point.In(r) { + go func() { + for i := 0; i < 6; i++ { + mu.Lock() + visible = !visible + env.Draw() <- redraw(r, visible) + mu.Unlock() + + time.Sleep(time.Second / 3) + } + }() + } + case gui.Resize: + r = event.Rectangle + env.Draw() <- redraw(r, visible) + } + } + }() +} diff --git a/examples/layout/button.go b/examples/layout/button.go new file mode 100644 index 0000000..cf13c3d --- /dev/null +++ b/examples/layout/button.go @@ -0,0 +1,69 @@ +package main + +import ( + "image" + "image/color" + "image/draw" + + "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 + 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 { + 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/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 Binary files differnew file mode 100755 index 0000000..e7e9c5b --- /dev/null +++ b/examples/layout/layout diff --git a/examples/layout/main.go b/examples/layout/main.go new file mode 100644 index 0000000..83a170d --- /dev/null +++ b/examples/layout/main.go @@ -0,0 +1,183 @@ +package main + +import ( + "fmt" + "image" + "image/draw" + "log" + "os" + "time" + + "github.com/faiface/gui" + "github.com/faiface/gui/layout" + "github.com/faiface/gui/win" + "github.com/faiface/mainthread" + "golang.org/x/image/colornames" + "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 { + panic(err) + } + theme := &Theme{ + Face: face, + Background: colornames.White, + Empty: colornames.Darkgrey, + Text: colornames.Black, + Highlight: colornames.Blueviolet, + ButtonUp: colornames.Lightgrey, + ButtonOver: colornames.Grey, + ButtonDown: colornames.Dimgrey, + } + w, err := win.New(win.Title("gui test")) // win.Resizable(), + if err != nil { + panic(err) + } + + mux, env := gui.NewMux(w) + + go func() { + // Hack for non-reparenting window managers (I think) + e := mux.MakeEnv() + for { + 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) + return r + } + } + }() + + var ( + top gui.Env + left, right gui.Env + bottomLeft, bottom, bottomRight gui.Env + ) + layout.NewMux( + mux.MakeEnv(), + []*gui.Env{ + &top, + &left, &right, + &bottomLeft, &bottom, &bottomRight}, + layout.Grid{ + 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-1; i++ { + ret[i] = (width - total) / 2 + total += ret[i] + } + ret[els-1] = width - total + return ret + }, + }, + ) + go Blinker(right) + go Blinker(left) + go Blinker(bottomRight) + + subGrid := makeEnvPtr(3) + layout.NewMux(top, + subGrid, + layout.Grid{ + Rows: []int{len(subGrid)}, + Gap: 10, + Background: colornames.Lightblue, + }, + ) + + 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 { + ret := make([]int, els) + total := 0 + for i := 0; i < els-1; i++ { + v := (width - total) / 2 + ret[i] = v + total += v + } + ret[els-1] = width - total + return ret + }, + } + blinkers := makeEnvPtr(3) + layout.NewMux(*subGrid[2], + blinkers, + box, + ) + + go Blinker(*blinkers[0]) + go Blinker(*blinkers[1]) + go Blinker(*blinkers[2]) + + btns := makeEnvPtr(3) + layout.NewMux( + bottom, + btns, + layout.Grid{ + Rows: []int{2, 1}, + Background: colornames.Darkgrey, + Gap: 4, + Flip: true, + }, + ) + btn := func(env gui.Env, name string) { + Button(env, theme, name, func() { + log.Print(name) + }) + } + 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) + } + } +} + +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/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 new file mode 100644 index 0000000..3ff8112 --- /dev/null +++ b/layout/grid.go @@ -0,0 +1,115 @@ +package layout + +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 + // Background represents the background of the grid as a uniform color. + Background color.Color + // Gap represents the grid gap, equal on all sides. + Gap int + // 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) { + col := g.Background + if col == nil { + 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.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 + splitMain := g.Split + if splitMain == nil { + splitMain = EvenSplit + } + splitSec := g.SplitRows + if splitSec == nil { + splitSec = 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) + + // 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 := splitMain(cols, W-(gap*(cols+1))-margin*2) + X = gap + mX + margin + for _, w := range colsW { + 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 + } + + return ret +} 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 new file mode 100644 index 0000000..4d8e616 --- /dev/null +++ b/layout/layout.go @@ -0,0 +1,20 @@ +package layout + +import ( + "image" +) + +// Layout represents any graphical layout +// +// 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. +// +// 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 + Intercepter +} diff --git a/layout/mux.go b/layout/mux.go new file mode 100644 index 0000000..8eb19d6 --- /dev/null +++ b/layout/mux.go @@ -0,0 +1,172 @@ +package layout + +import ( + "image" + "image/draw" + "log" + "sync" + + "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 { + mu sync.Mutex + lastResize gui.Event + eventsIns []chan<- gui.Event + draw chan<- func(draw.Image) image.Rectangle + + 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 an underlying Layout +// for modifying the gui.Resize events sent to the childs. +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, + draw: drawChan, + } + master, masterIn := mux.makeEnv(true) + events := make(chan gui.Event) + go func() { + for d := range drawChan { + env.Draw() <- d + } + close(env.Draw()) + }() + + 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 + lay := mux.layout.Lay(rect) + 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 + } + + // Send appropriate resize Events to childs + 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() + }() + + for _, en := range envs { + *en, _ = mux.makeEnv(false) + } + return +} + +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) (env gui.Env, eventsIn chan<- gui.Event) { + eventsOut, eventsIn := gui.MakeEventsChan() + drawChan := make(chan func(draw.Image) image.Rectangle) + env = &muxEnv{eventsOut, drawChan} + + 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() + } + + 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() + } + }() + + return env, eventsIn +} 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 new file mode 100644 index 0000000..db04225 --- /dev/null +++ b/layout/split.go @@ -0,0 +1,26 @@ +package layout + +import "fmt" + +// SplitFunc represents a way to split a space among a 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 + +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, elements) + for elements > 0 { + v := width / elements + width -= v + elements -= 1 + ret[elements] = v + } + return ret +} |