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

49
io.go
View File

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

View File

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

View File

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

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 (
"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
}

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

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
}