Added Resources abstraction.

This commit is contained in:
Sander Schobers 2020-05-15 14:20:07 +02:00
parent a0660a9650
commit b28b3e1838
10 changed files with 316 additions and 37 deletions

76
addons/fs/afero.go Normal file
View File

@ -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)
}

View File

@ -47,7 +47,7 @@ func NewRenderer(w, h int, opts allg5.NewDisplayOptions) (*Renderer, error) {
}) })
clean = nil 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. // Renderer implements ui.Renderer using Allegro 5.
@ -57,6 +57,7 @@ type Renderer struct {
unh func(allg5.Event) unh func(allg5.Event)
ft map[string]*font ft map[string]*font
user *allg5.UserEventSource user *allg5.UserEventSource
res ui.Resources
keys ui.KeyState keys ui.KeyState
modifiers ui.KeyModifier modifiers ui.KeyModifier
@ -153,6 +154,7 @@ func (r *Renderer) Destroy() error {
} }
r.ft = nil r.ft = nil
r.disp.Destroy() r.disp.Destroy()
r.res.Destroy()
return nil 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) { 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) { 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) bmp, err := allg5.LoadBitmap(path)
if err != nil { if err != nil {
return nil, err 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 { 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 { if err != nil {
return err return err
} }
@ -258,7 +268,7 @@ func (r *Renderer) RegisterFont(name, path string, size int) error {
if prev != nil { if prev != nil {
prev.Destroy() prev.Destroy()
} }
r.ft[name] = newFont(f) r.ft[name] = newFont(font)
return nil return nil
} }
@ -281,6 +291,8 @@ func (r *Renderer) RenderToDisplay() {
r.disp.SetAsTarget() r.disp.SetAsTarget()
} }
func (r *Renderer) Resources() ui.Resources { return r.res }
func (r *Renderer) Size() geom.PointF32 { func (r *Renderer) Size() geom.PointF32 {
return geom.PtF32(float32(r.disp.Width()), float32(r.disp.Height())) 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 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)) { func (r *Renderer) SetUnhandledEventHandler(handler func(allg5.Event)) {
r.unh = handler r.unh = handler
} }

49
io.go
View File

@ -7,6 +7,7 @@ import (
_ "image/jpeg" // decoding of JPEG _ "image/jpeg" // decoding of JPEG
"image/png" "image/png"
"io" "io"
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
) )
@ -47,6 +48,29 @@ func DecodeJSON(path string, value interface{}) error {
return err 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. // EncoderFn describes a generic encoder.
type EncoderFn func(io.Writer, interface{}) error type EncoderFn func(io.Writer, interface{}) error
@ -70,6 +94,14 @@ func EncodePNG(path string, im image.Image) error {
return EncodeFile(path, im, PNGEncoder) 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 var _ DecoderFn = ImageDecoder
// ImageDecoder is a generic image decoder. // ImageDecoder is a generic image decoder.
@ -91,6 +123,15 @@ var _ EncoderFn = PNGEncoder
// PNGEncoder is a generic PNG encoder. // PNGEncoder is a generic PNG encoder.
func PNGEncoder(w io.Writer, value interface{}) error { return png.Encode(w, value.(image.Image)) } 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. // UserDir gives back the user configuration directory with given name.
func UserDir(name string) (string, error) { func UserDir(name string) (string, error) {
config, err := os.UserConfigDir() config, err := os.UserConfigDir()
@ -113,3 +154,11 @@ func UserFile(app, name string) (string, error) {
} }
return filepath.Join(dir, name), nil 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
}

View File

@ -23,6 +23,7 @@ type Renderer struct {
renderer *sdl.Renderer renderer *sdl.Renderer
refresh uint32 refresh uint32
fonts map[string]*Font fonts map[string]*Font
resources ui.Resources
mouse geom.PointF32 mouse geom.PointF32
cursor ui.MouseCursor cursor ui.MouseCursor
@ -88,6 +89,7 @@ func NewRenderer(title string, width, height int32, opts NewRendererOptions) (*R
renderer: renderer, renderer: renderer,
refresh: refresh, refresh: refresh,
fonts: map[string]*Font{}, fonts: map[string]*Font{},
resources: &ui.OSResources{},
cursors: map[sdl.SystemCursor]*sdl.Cursor{}, cursors: map[sdl.SystemCursor]*sdl.Cursor{},
}, nil }, nil
} }
@ -210,6 +212,7 @@ func (r *Renderer) Destroy() error {
r.window.Destroy() r.window.Destroy()
ttf.Quit() ttf.Quit()
sdl.Quit() sdl.Quit()
r.resources.Destroy()
return nil 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) { 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) { 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) { 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 { 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) font, err := ttf.OpenFont(path, size)
if err != nil { if err != nil {
return err 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 // Texture
func (r *Renderer) Image() image.Image { return nil } func (r *Renderer) Image() image.Image { return nil }

View File

@ -3,10 +3,10 @@ package ui
type Context interface { type Context interface {
Animate() Animate()
HasQuit() bool HasQuit() bool
Textures() *Textures
Quit() Quit()
Renderer() Renderer Renderer() Renderer
Style() *Style Style() *Style
Textures() *Textures
} }
var _ Context = &context{} 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() { func (c *context) Quit() {
if !c.HasQuit() { if !c.HasQuit() {
@ -40,9 +42,7 @@ func (c *context) Quit() {
} }
} }
func (c *context) Renderer() Renderer { return c.r } func (c *context) Textures() *Textures { return c.textures }
func (c *context) Style() *Style { return c.style }
// Handle implement EventTarget // Handle implement EventTarget

56
ui/copyresources.go Normal file
View File

@ -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()
}

View File

@ -2,36 +2,46 @@ package ui
import ( import (
"image" "image"
"os"
"opslag.de/schobers/zntg"
) )
type ImageSource interface { type ImageSource interface {
CreateImage() (image.Image, error) CreateImage() (image.Image, error)
} }
type ImageFileSource string type ImageSourceFile string
var _ ImageSource = ImageFileSource("") var _ ImageSource = ImageSourceFile("")
func (s ImageFileSource) CreateImage() (image.Image, error) { func (s ImageSourceFile) CreateImage() (image.Image, error) {
f, err := os.Open(string(s)) return zntg.DecodeImage(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
} }
type ImageGoSource struct { type ImageSourceGo struct {
image.Image 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 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
}

34
ui/osresources.go Normal file
View File

@ -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 }

View File

@ -35,4 +35,8 @@ type Renderer interface {
Target() Texture Target() Texture
Text(p geom.PointF32, font string, color color.Color, text string) Text(p geom.PointF32, font string, color color.Color, text string)
TextAlign(p geom.PointF32, font string, color color.Color, text string, align HorizontalAlignment) TextAlign(p geom.PointF32, font string, color color.Color, text string, align HorizontalAlignment)
// Resources
Resources() Resources
SetResourceProvider(factory func() Resources)
} }

13
ui/resources.go Normal file
View File

@ -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
}