From b28b3e1838dba1bc58f7ededccd632671a7bd5d0 Mon Sep 17 00:00:00 2001 From: Sander Schobers Date: Fri, 15 May 2020 14:20:07 +0200 Subject: [PATCH] Added Resources abstraction. --- addons/fs/afero.go | 76 +++++++++++++++++++++++++++++++++++++++++++++ allg5ui/renderer.go | 27 +++++++++++++--- io.go | 49 +++++++++++++++++++++++++++++ sdlui/renderer.go | 40 +++++++++++++++++------- ui/context.go | 10 +++--- ui/copyresources.go | 56 +++++++++++++++++++++++++++++++++ ui/imagesource.go | 44 ++++++++++++++++---------- ui/osresources.go | 34 ++++++++++++++++++++ ui/renderer.go | 4 +++ ui/resources.go | 13 ++++++++ 10 files changed, 316 insertions(+), 37 deletions(-) create mode 100644 addons/fs/afero.go create mode 100644 ui/copyresources.go create mode 100644 ui/osresources.go create mode 100644 ui/resources.go diff --git a/addons/fs/afero.go b/addons/fs/afero.go new file mode 100644 index 0000000..8094c76 --- /dev/null +++ b/addons/fs/afero.go @@ -0,0 +1,76 @@ +package fs + +import ( + "io" + "os" + "path/filepath" + + "github.com/spf13/afero" + "opslag.de/schobers/zntg" + "opslag.de/schobers/zntg/ui" +) + +type aferoResources struct { + dir string + fs afero.Fs + copy *zntg.Dir +} + +var _ ui.Resources = &aferoResources{} + +// NewAferoResources provides resources from a afero file system. The prefix is used as a prefix of the temporary directory. +func NewAferoResources(fs afero.Fs, prefix string) (ui.Resources, error) { + return NewAferoFallbackResources("", fs, prefix) +} + +// NewAferoFallbackResources provides resources from the directory first and uses afero file system as a fallback if the resource in the directory doesn't exist. The prefix is used as a prefix of the temporary directory. +func NewAferoFallbackResources(dir string, fs afero.Fs, prefix string) (ui.Resources, error) { + copy, err := zntg.NewTempDir(prefix) + if err != nil { + return nil, err + } + return &aferoResources{dir, fs, copy}, nil +} + +func (r *aferoResources) fetchAferoResource(name string) (string, error) { + path := r.copy.FilePath(name) + if !zntg.FileExists(path) { + src, err := r.fs.Open(name) + if err != nil { + return "", err + } + defer src.Close() + + err = r.copy.Write(name, src) + if err != nil { + return "", err + } + } + return path, nil +} + +func (r *aferoResources) openAferoResource(name string) (io.ReadCloser, error) { return r.fs.Open(name) } + +func (r *aferoResources) Destroy() error { return r.copy.Destroy() } + +func (r *aferoResources) FetchResource(name string) (string, error) { + if r.dir == "" { + return r.fetchAferoResource(name) + } + path := filepath.Join(r.dir, name) + if zntg.FileExists(path) { + return path, nil + } + return r.fetchAferoResource(name) +} + +func (r *aferoResources) OpenResource(name string) (io.ReadCloser, error) { + if r.dir == "" { + return r.openAferoResource(name) + } + path := filepath.Join(r.dir, name) + if zntg.FileExists(path) { + return os.Open(path) + } + return r.openAferoResource(name) +} diff --git a/allg5ui/renderer.go b/allg5ui/renderer.go index bf08db6..f1f631d 100644 --- a/allg5ui/renderer.go +++ b/allg5ui/renderer.go @@ -47,7 +47,7 @@ func NewRenderer(w, h int, opts allg5.NewDisplayOptions) (*Renderer, error) { }) clean = nil - return &Renderer{disp, eq, nil, map[string]*font{}, user, ui.KeyState{}, ui.KeyModifierNone, ui.MouseCursorDefault}, nil + return &Renderer{disp, eq, nil, map[string]*font{}, user, &ui.OSResources{}, ui.KeyState{}, ui.KeyModifierNone, ui.MouseCursorDefault}, nil } // Renderer implements ui.Renderer using Allegro 5. @@ -57,6 +57,7 @@ type Renderer struct { unh func(allg5.Event) ft map[string]*font user *allg5.UserEventSource + res ui.Resources keys ui.KeyState modifiers ui.KeyModifier @@ -153,6 +154,7 @@ func (r *Renderer) Destroy() error { } r.ft = nil r.disp.Destroy() + r.res.Destroy() return nil } @@ -182,10 +184,14 @@ func (r *Renderer) CreateTexture(source ui.ImageSource) (ui.Texture, error) { } func (r *Renderer) CreateTextureGo(im image.Image, source bool) (ui.Texture, error) { - return r.createTexture(ui.ImageGoSource{im}, true) + return r.createTexture(ui.ImageSourceGo{im}, true) } func (r *Renderer) CreateTexturePath(path string, source bool) (ui.Texture, error) { + path, err := r.res.FetchResource(path) + if err != nil { + return nil, err + } bmp, err := allg5.LoadBitmap(path) if err != nil { return nil, err @@ -250,7 +256,11 @@ func (r *Renderer) Rectangle(rect geom.RectangleF32, c color.Color, thickness fl } func (r *Renderer) RegisterFont(name, path string, size int) error { - var f, err = allg5.LoadTTFFont(path, size) + path, err := r.res.FetchResource(path) + if err != nil { + return err + } + font, err := allg5.LoadTTFFont(path, size) if err != nil { return err } @@ -258,7 +268,7 @@ func (r *Renderer) RegisterFont(name, path string, size int) error { if prev != nil { prev.Destroy() } - r.ft[name] = newFont(f) + r.ft[name] = newFont(font) return nil } @@ -281,6 +291,8 @@ func (r *Renderer) RenderToDisplay() { r.disp.SetAsTarget() } +func (r *Renderer) Resources() ui.Resources { return r.res } + func (r *Renderer) Size() geom.PointF32 { return geom.PtF32(float32(r.disp.Width()), float32(r.disp.Height())) } @@ -294,6 +306,13 @@ func (r *Renderer) SetMouseCursor(c ui.MouseCursor) { r.cursor = c } +func (r *Renderer) SetResourceProvider(factory func() ui.Resources) { + if r.res != nil { + r.res.Destroy() + } + r.res = factory() +} + func (r *Renderer) SetUnhandledEventHandler(handler func(allg5.Event)) { r.unh = handler } diff --git a/io.go b/io.go index 9b3dbb6..f45470b 100644 --- a/io.go +++ b/io.go @@ -7,6 +7,7 @@ import ( _ "image/jpeg" // decoding of JPEG "image/png" "io" + "io/ioutil" "os" "path/filepath" ) @@ -47,6 +48,29 @@ func DecodeJSON(path string, value interface{}) error { return err } +// Dir is a convenience struct for representing a path to a directory. +type Dir struct { + Path string +} + +// FilePath returns the path of a file with the specified name in the directory. +func (d *Dir) FilePath(name string) string { + return filepath.Join(d.Path, name) +} + +// Write writes the content of the reader into a file with the specified name. +func (d *Dir) Write(name string, r io.Reader) error { + path := d.FilePath(name) + dir := filepath.Dir(path) + os.MkdirAll(dir, 0777) + return EncodeFile(path, r, CopyReader) +} + +// Destroy removes the directory. +func (d *Dir) Destroy() error { + return os.RemoveAll(d.Path) +} + // EncoderFn describes a generic encoder. type EncoderFn func(io.Writer, interface{}) error @@ -70,6 +94,14 @@ func EncodePNG(path string, im image.Image) error { return EncodeFile(path, im, PNGEncoder) } +// FileExists returns if file exists on specified path. +func FileExists(path string) bool { + if _, err := os.Stat(path); os.IsNotExist(err) { + return false + } + return true +} + var _ DecoderFn = ImageDecoder // ImageDecoder is a generic image decoder. @@ -91,6 +123,15 @@ var _ EncoderFn = PNGEncoder // PNGEncoder is a generic PNG encoder. func PNGEncoder(w io.Writer, value interface{}) error { return png.Encode(w, value.(image.Image)) } +// NewTempDir creates a temporary directory. +func NewTempDir(prefix string) (*Dir, error) { + path, err := ioutil.TempDir("", prefix) + if nil != err { + return nil, err + } + return &Dir{path}, nil +} + // UserDir gives back the user configuration directory with given name. func UserDir(name string) (string, error) { config, err := os.UserConfigDir() @@ -113,3 +154,11 @@ func UserFile(app, name string) (string, error) { } return filepath.Join(dir, name), nil } + +var _ EncoderFn = CopyReader + +// CopyReader copies the provided value to the output. +func CopyReader(w io.Writer, value interface{}) error { + _, err := io.Copy(w, value.(io.Reader)) + return err +} diff --git a/sdlui/renderer.go b/sdlui/renderer.go index fbdffc4..3b2e580 100644 --- a/sdlui/renderer.go +++ b/sdlui/renderer.go @@ -19,10 +19,11 @@ import ( var errNotImplemented = errors.New(`not implemented`) type Renderer struct { - window *sdl.Window - renderer *sdl.Renderer - refresh uint32 - fonts map[string]*Font + window *sdl.Window + renderer *sdl.Renderer + refresh uint32 + fonts map[string]*Font + resources ui.Resources mouse geom.PointF32 cursor ui.MouseCursor @@ -84,11 +85,12 @@ func NewRenderer(title string, width, height int32, opts NewRendererOptions) (*R clean = nil return &Renderer{ - window: window, - renderer: renderer, - refresh: refresh, - fonts: map[string]*Font{}, - cursors: map[sdl.SystemCursor]*sdl.Cursor{}, + window: window, + renderer: renderer, + refresh: refresh, + fonts: map[string]*Font{}, + resources: &ui.OSResources{}, + cursors: map[sdl.SystemCursor]*sdl.Cursor{}, }, nil } @@ -210,6 +212,7 @@ func (r *Renderer) Destroy() error { r.window.Destroy() ttf.Quit() sdl.Quit() + r.resources.Destroy() return nil } @@ -253,11 +256,11 @@ func (r *Renderer) CreateTexture(source ui.ImageSource) (ui.Texture, error) { } func (r *Renderer) CreateTextureGo(m image.Image, source bool) (ui.Texture, error) { - return r.createTexture(ui.ImageGoSource{Image: m}, source) + return r.createTexture(ui.ImageSourceGo{Image: m}, source) } func (r *Renderer) CreateTexturePath(path string, source bool) (ui.Texture, error) { - return r.createTexture(ui.ImageFileSource(path), source) + return r.createTexture(ui.ImageSourceResource{Resources: r.resources, Name: path}, source) } func (r *Renderer) CreateTextureTarget(w, h float32) (ui.Texture, error) { @@ -331,6 +334,10 @@ func (r *Renderer) Rectangle(rect geom.RectangleF32, c color.Color, thickness fl } func (r *Renderer) RegisterFont(name, path string, size int) error { + path, err := r.resources.FetchResource(path) + if err != nil { + return err + } font, err := ttf.OpenFont(path, size) if err != nil { return err @@ -420,6 +427,17 @@ func (r *Renderer) TextAlign(p geom.PointF32, font string, color color.Color, te } } +// Resources + +func (r *Renderer) Resources() ui.Resources { return r.resources } + +func (r *Renderer) SetResourceProvider(factory func() ui.Resources) { + if r.resources != nil { + r.resources.Destroy() + } + r.resources = factory() +} + // Texture func (r *Renderer) Image() image.Image { return nil } diff --git a/ui/context.go b/ui/context.go index f8303ef..29096d0 100644 --- a/ui/context.go +++ b/ui/context.go @@ -3,10 +3,10 @@ package ui type Context interface { Animate() HasQuit() bool - Textures() *Textures Quit() Renderer() Renderer Style() *Style + Textures() *Textures } var _ Context = &context{} @@ -32,7 +32,9 @@ func (c *context) HasQuit() bool { } } -func (c *context) Textures() *Textures { return c.textures } +func (c *context) Renderer() Renderer { return c.r } + +func (c *context) Style() *Style { return c.style } func (c *context) Quit() { if !c.HasQuit() { @@ -40,9 +42,7 @@ func (c *context) Quit() { } } -func (c *context) Renderer() Renderer { return c.r } - -func (c *context) Style() *Style { return c.style } +func (c *context) Textures() *Textures { return c.textures } // Handle implement EventTarget diff --git a/ui/copyresources.go b/ui/copyresources.go new file mode 100644 index 0000000..07e11a3 --- /dev/null +++ b/ui/copyresources.go @@ -0,0 +1,56 @@ +package ui + +import ( + "io" + "os" + + "opslag.de/schobers/zntg" +) + +var _ Resources = &CopyResources{} + +// CopyResources copies and opens resources to a temporary directory. +type CopyResources struct { + Source Resources + + copy *zntg.Dir +} + +// NewCopyResource creates a proxy that copied resources first to disk. +func NewCopyResource(prefix string, source Resources) (*CopyResources, error) { + copy, err := zntg.NewTempDir(prefix) + if nil != err { + return nil, err + } + return &CopyResources{source, copy}, nil +} + +// FetchResource copies the file from the source to disk and returns the path to it. +func (r *CopyResources) FetchResource(name string) (string, error) { + path := r.copy.FilePath(name) + if !zntg.FileExists(path) { + src, err := r.Source.OpenResource(name) + if err != nil { + return "", err + } + defer src.Close() + + err = r.copy.Write(name, src) + if nil != err { + return "", err + } + } + return path, nil +} + +// OpenResource opens the (copied) resource on disk. +func (r *CopyResources) OpenResource(name string) (io.ReadCloser, error) { + path := r.copy.FilePath(name) + src, err := os.Open(path) + return src, err +} + +// Destroy destroy the copy of the resources. +func (r *CopyResources) Destroy() error { + return r.copy.Destroy() +} diff --git a/ui/imagesource.go b/ui/imagesource.go index e19f08d..adab905 100644 --- a/ui/imagesource.go +++ b/ui/imagesource.go @@ -2,36 +2,46 @@ package ui import ( "image" - "os" + + "opslag.de/schobers/zntg" ) type ImageSource interface { CreateImage() (image.Image, error) } -type ImageFileSource string +type ImageSourceFile string -var _ ImageSource = ImageFileSource("") +var _ ImageSource = ImageSourceFile("") -func (s ImageFileSource) CreateImage() (image.Image, error) { - f, err := os.Open(string(s)) - if err != nil { - return nil, err - } - defer f.Close() - m, _, err := image.Decode(f) - if err != nil { - return nil, err - } - return m, nil +func (s ImageSourceFile) CreateImage() (image.Image, error) { + return zntg.DecodeImage(string(s)) } -type ImageGoSource struct { +type ImageSourceGo struct { image.Image } -var _ ImageSource = ImageGoSource{} +var _ ImageSource = ImageSourceGo{} -func (s ImageGoSource) CreateImage() (image.Image, error) { +func (s ImageSourceGo) CreateImage() (image.Image, error) { return s.Image, nil } + +type ImageSourceResource struct { + Resources Resources + Name string +} + +func (s ImageSourceResource) CreateImage() (image.Image, error) { + src, err := s.Resources.OpenResource(s.Name) + if err != nil { + return nil, err + } + defer src.Close() + value, err := zntg.ImageDecoder(src) + if err != nil { + return nil, err + } + return value.(image.Image), nil +} diff --git a/ui/osresources.go b/ui/osresources.go new file mode 100644 index 0000000..6572644 --- /dev/null +++ b/ui/osresources.go @@ -0,0 +1,34 @@ +package ui + +import ( + "io" + "os" +) + +var _ Resources = &OSResources{} + +// DefaultResources returns the default Resources implementation (OSResources). +func DefaultResources() Resources { + return &OSResources{} +} + +// OSResources is Resources implementation that uses the default file system directly. +type OSResources struct { +} + +// FetchResource checks if file is available and returns the specified path. +func (r *OSResources) FetchResource(name string) (string, error) { + _, err := os.Stat(name) + if err != nil { + return "", err + } + return name, nil +} + +// OpenResource opens the specified file on disk. +func (r *OSResources) OpenResource(name string) (io.ReadCloser, error) { + return os.Open(name) +} + +// Destroy does nothing. +func (r *OSResources) Destroy() error { return nil } diff --git a/ui/renderer.go b/ui/renderer.go index 3e43463..85dc636 100644 --- a/ui/renderer.go +++ b/ui/renderer.go @@ -35,4 +35,8 @@ type Renderer interface { Target() Texture Text(p geom.PointF32, font string, color color.Color, text string) TextAlign(p geom.PointF32, font string, color color.Color, text string, align HorizontalAlignment) + + // Resources + Resources() Resources + SetResourceProvider(factory func() Resources) } diff --git a/ui/resources.go b/ui/resources.go new file mode 100644 index 0000000..c8da887 --- /dev/null +++ b/ui/resources.go @@ -0,0 +1,13 @@ +package ui + +import "io" + +// Resources is an abstraction on resources. +type Resources interface { + // FetchResource should fetch the resource with the specified name and return a path (on disk) where the resource can be accessed. + FetchResource(name string) (string, error) + // OpenResource should open the resource with the specified name. The user is responsible for closing the resource. + OpenResource(name string) (io.ReadCloser, error) + // Destroy can be used for cleaning up at the end of the applications lifetime. + Destroy() error +}