diff options
| author | faiface <faiface2202@gmail.com> | 2019-05-06 00:42:28 +0200 |
|---|---|---|
| committer | faiface <faiface2202@gmail.com> | 2019-05-06 00:42:28 +0200 |
| commit | ff5293e4cbf7b116715fce5b5e91f907ca41a3de (patch) | |
| tree | c8422d8b370059935f518ec4d5a95e77fd8394d1 | |
| parent | 2357ae3c4c1f905dd3f75dea19d48d301252a4af (diff) | |
| download | gui-ff5293e4cbf7b116715fce5b5e91f907ca41a3de.zip | |
examples: add imageviewer
| -rw-r--r-- | examples/imageviewer/browser.go | 181 | ||||
| -rw-r--r-- | examples/imageviewer/button.go | 61 | ||||
| -rw-r--r-- | examples/imageviewer/main.go | 66 | ||||
| -rw-r--r-- | examples/imageviewer/splits.go | 112 | ||||
| -rw-r--r-- | examples/imageviewer/theme.go | 19 | ||||
| -rw-r--r-- | examples/imageviewer/util.go | 104 | ||||
| -rw-r--r-- | examples/imageviewer/viewer.go | 65 |
7 files changed, 608 insertions, 0 deletions
diff --git a/examples/imageviewer/browser.go b/examples/imageviewer/browser.go new file mode 100644 index 0000000..087e596 --- /dev/null +++ b/examples/imageviewer/browser.go @@ -0,0 +1,181 @@ +package main + +import ( + "image" + "image/color" + "image/draw" + "os" + "path/filepath" + + "github.com/faiface/gui" + "golang.org/x/image/math/fixed" +) + +func Browser(env gui.Env, theme *Theme, dir string, cd <-chan string, view chan<- string) { + reload := func(dir string) (names []string, lineHeight int, namesImage *image.RGBA) { + names = nil + + filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if path == dir { + return nil + } + rel, err := filepath.Rel(dir, path) + if err != nil { + return nil + } + if info.IsDir() { + names = append(names, rel+string(filepath.Separator)) + return filepath.SkipDir + } + names = append(names, rel) + return nil + }) + + var images []image.Image + for _, name := range names { + images = append(images, DrawText(name, theme.Face, theme.Text)) + } + + const inset = 4 + + var width int + for _, img := range images { + if img.Bounds().Dx() > width { + width = img.Bounds().Inset(-inset).Dx() + } + } + + metrics := theme.Face.Metrics() + lineHeight = (metrics.Height + 2*fixed.I(inset)).Ceil() + height := lineHeight * len(names) + + namesImage = image.NewRGBA(image.Rect(0, 0, width+2*inset, height+2*inset)) + for i := range images { + r := image.Rect( + 0, lineHeight*i, + width, lineHeight*(i+1), + ) + DrawLeftCentered(namesImage, r.Inset(inset), images[i], draw.Over) + } + + return names, lineHeight, namesImage + } + + redraw := func(r image.Rectangle, selected int, position image.Point, lineHeight int, namesImage image.Image) func(draw.Image) image.Rectangle { + return func(drw draw.Image) image.Rectangle { + draw.Draw(drw, r, &image.Uniform{theme.Background}, image.ZP, draw.Src) + draw.Draw(drw, r, namesImage, position, draw.Over) + if selected >= 0 { + highlightR := image.Rect( + namesImage.Bounds().Min.X, + namesImage.Bounds().Min.Y+lineHeight*selected, + namesImage.Bounds().Max.X, + namesImage.Bounds().Min.Y+lineHeight*(selected+1), + ) + highlightR = highlightR.Sub(position).Add(r.Min) + draw.DrawMask( + drw, highlightR.Intersect(r), + &image.Uniform{theme.Highlight}, image.ZP, + &image.Uniform{color.Alpha{64}}, image.ZP, + draw.Over, + ) + } + return r + } + } + + names, lineHeight, namesImage := reload(dir) + + var ( + r image.Rectangle + position = image.ZP + selected = -1 + ) + + for { + select { + case path := <-cd: + if filepath.IsAbs(path) { + dir = path + } else { + dir = filepath.Join(dir, path) + } + names, lineHeight, namesImage = reload(dir) + position = image.ZP + selected = -1 + env.Draw() <- redraw(r, selected, position, lineHeight, namesImage) + + case e, ok := <-env.Events(): + if !ok { + close(env.Draw()) + return + } + + var ( + minX, minY, maxX, maxY int + x, y int + ) + + switch { + case e.Matches("resize/%d/%d/%d/%d", &minX, &minY, &maxX, &maxY): + r = image.Rect(minX, minY, maxX, maxY) + env.Draw() <- redraw(r, selected, position, lineHeight, namesImage) + + case e.Matches("mo/down/%d/%d", &x, &y): + if !image.Pt(x, y).In(r) { + continue + } + click := image.Pt(x, y).Sub(r.Min).Add(position) + i := click.Y / lineHeight + if i < 0 || i >= len(names) { + continue + } + if selected == i { + func() { + path := filepath.Join(dir, names[selected]) + f, err := os.Open(path) + if err != nil { + return + } + defer f.Close() + info, err := f.Stat() + if err != nil { + return + } + if info.IsDir() { + dir = path + names, lineHeight, namesImage = reload(dir) + position = image.ZP + selected = -1 + env.Draw() <- redraw(r, selected, position, lineHeight, namesImage) + } else { + view <- path + } + }() + } else { + selected = i + env.Draw() <- redraw(r, selected, position, lineHeight, namesImage) + } + + case e.Matches("mo/scroll/%d/%d", &y, &x): + newP := position.Sub(image.Pt(int(x*16), int(y*16))) + if newP.X > namesImage.Bounds().Max.X-r.Dx() { + newP.X = namesImage.Bounds().Max.X - r.Dx() + } + if newP.Y > namesImage.Bounds().Max.Y-r.Dy() { + newP.Y = namesImage.Bounds().Max.Y - r.Dy() + } + if newP.X < namesImage.Bounds().Min.X { + newP.X = namesImage.Bounds().Min.X + } + if newP.Y < namesImage.Bounds().Min.Y { + newP.Y = namesImage.Bounds().Min.Y + } + if newP != position { + position = newP + env.Draw() <- redraw(r, selected, position, lineHeight, namesImage) + } + } + } + } +} diff --git a/examples/imageviewer/button.go b/examples/imageviewer/button.go new file mode 100644 index 0000000..0078774 --- /dev/null +++ b/examples/imageviewer/button.go @@ -0,0 +1,61 @@ +package main + +import ( + "image" + "image/color" + "image/draw" + + "github.com/faiface/gui" +) + +func Button(env gui.Env, theme *Theme, text string, action func()) { + textImg := DrawText(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() { + var x, y, minX, minY, maxX, maxY int + + switch { + case e.Matches("resize/%d/%d/%d/%d", &minX, &minY, &maxX, &maxY): + r = image.Rect(minX, minY, maxX, maxY) + env.Draw() <- redraw(r, over, pressed) + + case e.Matches("mo/down/%d/%d/left", &x, &y): + newPressed := image.Pt(x, y).In(r) + if newPressed != pressed { + pressed = newPressed + env.Draw() <- redraw(r, over, pressed) + } + + case e.Matches("mo/up/%d/%d/left", &x, &y): + if pressed { + action() + pressed = false + env.Draw() <- redraw(r, over, pressed) + } + } + } + + close(env.Draw()) +} diff --git a/examples/imageviewer/main.go b/examples/imageviewer/main.go new file mode 100644 index 0000000..056bf1f --- /dev/null +++ b/examples/imageviewer/main.go @@ -0,0 +1,66 @@ +package main + +import ( + "os/user" + + "github.com/faiface/gui" + "github.com/faiface/gui/win" + "github.com/faiface/mainthread" + "github.com/golang/image/font/gofont/goregular" + "golang.org/x/image/colornames" +) + +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("Image Viewer"), win.Size(900, 600), win.Resizable()) + if err != nil { + panic(err) + } + + mux, env := gui.NewMux(w) + + cd := make(chan string) + view := make(chan string) + + go Browser(FixedBottom(FixedLeft(mux.MakeEnv(), 300), 30), theme, ".", cd, view) + go Viewer(FixedRight(mux.MakeEnv(), 300), theme, view) + + go Button(EvenHorizontal(FixedTop(FixedLeft(mux.MakeEnv(), 300), 30), 0, 1, 3), theme, "Dir Up", func() { + cd <- ".." + }) + go Button(EvenHorizontal(FixedTop(FixedLeft(mux.MakeEnv(), 300), 30), 1, 2, 3), theme, "Refresh", func() { + cd <- "." + }) + go Button(EvenHorizontal(FixedTop(FixedLeft(mux.MakeEnv(), 300), 30), 2, 3, 3), theme, "Home", func() { + user, err := user.Current() + if err != nil { + return + } + cd <- user.HomeDir + }) + + for e := range env.Events() { + switch { + case e.Matches("wi/close"): + close(env.Draw()) + } + } +} + +func main() { + mainthread.Run(run) +} diff --git a/examples/imageviewer/splits.go b/examples/imageviewer/splits.go new file mode 100644 index 0000000..041a770 --- /dev/null +++ b/examples/imageviewer/splits.go @@ -0,0 +1,112 @@ +package main + +import ( + "image" + "image/draw" + + "github.com/faiface/gui" +) + +type envPair struct { + events <-chan gui.Event + draw chan<- func(draw.Image) image.Rectangle +} + +func (ep *envPair) Events() <-chan gui.Event { return ep.events } +func (ep *envPair) Draw() chan<- func(draw.Image) image.Rectangle { return ep.draw } + +func FixedLeft(env gui.Env, maxX int) gui.Env { + out, in := gui.MakeEventsChan() + + go func() { + for e := range env.Events() { + var minX, minY, dummy, maxY int + switch { + case e.Matches("resize/%d/%d/%d/%d", &minX, &minY, &dummy, &maxY): + in <- gui.Eventf("resize/%d/%d/%d/%d", minX, minY, maxX, maxY) + default: + in <- e + } + } + close(in) + }() + + return &envPair{out, env.Draw()} +} + +func FixedRight(env gui.Env, minX int) gui.Env { + out, in := gui.MakeEventsChan() + + go func() { + for e := range env.Events() { + var dummy, minY, maxX, maxY int + switch { + case e.Matches("resize/%d/%d/%d/%d", &dummy, &minY, &maxX, &maxY): + in <- gui.Eventf("resize/%d/%d/%d/%d", minX, minY, maxX, maxY) + default: + in <- e + } + } + close(in) + }() + + return &envPair{out, env.Draw()} +} + +func FixedTop(env gui.Env, maxY int) gui.Env { + out, in := gui.MakeEventsChan() + + go func() { + for e := range env.Events() { + var minX, minY, maxX, dummy int + switch { + case e.Matches("resize/%d/%d/%d/%d", &minX, &minY, &maxX, &dummy): + in <- gui.Eventf("resize/%d/%d/%d/%d", minX, minY, maxX, maxY) + default: + in <- e + } + } + close(in) + }() + + return &envPair{out, env.Draw()} +} + +func FixedBottom(env gui.Env, minY int) gui.Env { + out, in := gui.MakeEventsChan() + + go func() { + for e := range env.Events() { + var minX, dummy, maxX, maxY int + switch { + case e.Matches("resize/%d/%d/%d/%d", &minX, &dummy, &maxX, &maxY): + in <- gui.Eventf("resize/%d/%d/%d/%d", minX, minY, maxX, maxY) + default: + in <- e + } + } + close(in) + }() + + return &envPair{out, env.Draw()} +} + +func EvenHorizontal(env gui.Env, minI, maxI, n int) gui.Env { + out, in := gui.MakeEventsChan() + + go func() { + for e := range env.Events() { + var minX, minY, maxX, maxY int + switch { + case e.Matches("resize/%d/%d/%d/%d", &minX, &minY, &maxX, &maxY): + minX, maxX := minX+(maxX-minX)*minI/n, minX+(maxX-minX)*maxI/n + in <- gui.Eventf("resize/%d/%d/%d/%d", minX, minY, maxX, maxY) + default: + in <- e + } + } + close(in) + }() + + return &envPair{out, env.Draw()} +} diff --git a/examples/imageviewer/theme.go b/examples/imageviewer/theme.go new file mode 100644 index 0000000..e37c0c0 --- /dev/null +++ b/examples/imageviewer/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/imageviewer/util.go b/examples/imageviewer/util.go new file mode 100644 index 0000000..94f25fb --- /dev/null +++ b/examples/imageviewer/util.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 DrawText(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/examples/imageviewer/viewer.go b/examples/imageviewer/viewer.go new file mode 100644 index 0000000..e783d78 --- /dev/null +++ b/examples/imageviewer/viewer.go @@ -0,0 +1,65 @@ +package main + +import ( + "image" + "image/draw" + "os" + + "github.com/faiface/gui" + + _ "image/gif" + _ "image/jpeg" + _ "image/png" + + _ "golang.org/x/image/bmp" +) + +func Viewer(env gui.Env, theme *Theme, view <-chan string) { + redraw := func(r image.Rectangle, img image.Image) func(draw.Image) image.Rectangle { + return func(drw draw.Image) image.Rectangle { + draw.Draw(drw, r, &image.Uniform{theme.Empty}, image.ZP, draw.Src) + DrawCentered(drw, r, img, draw.Over) + return r + } + } + + invalid := DrawText("Invalid image", theme.Face, theme.Text) + + var ( + r image.Rectangle + img image.Image + ) + + for { + select { + case path := <-view: + func() { + f, err := os.Open(path) + if err != nil { + img = invalid + return + } + defer f.Close() + img, _, err = image.Decode(f) + if err != nil { + img = invalid + return + } + }() + env.Draw() <- redraw(r, img) + + case e, ok := <-env.Events(): + if !ok { + close(env.Draw()) + return + } + + var minX, minY, maxX, maxY int + switch { + case e.Matches("resize/%d/%d/%d/%d", &minX, &minY, &maxX, &maxY): + r = image.Rect(minX, minY, maxX, maxY) + env.Draw() <- redraw(r, img) + } + } + } +} |