Sander Schobers
67e73a8671
PhysicalResources derives from Resources and exposes FetchResource. Made dependency specific resource addons. Extended the available resource options (fallback, path, refactored copy). NewRenderer provides DefaultResources to the created renderer.
494 lines
13 KiB
Go
494 lines
13 KiB
Go
package sdlui
|
|
|
|
import (
|
|
"errors"
|
|
"image"
|
|
"image/color"
|
|
_ "image/jpeg" // add JPEG for CreateSurfacePath
|
|
_ "image/png" // add PNG for CreateSurfacePath
|
|
"math"
|
|
"unsafe"
|
|
|
|
"github.com/veandco/go-sdl2/sdl"
|
|
"github.com/veandco/go-sdl2/ttf"
|
|
"opslag.de/schobers/geom"
|
|
"opslag.de/schobers/zntg"
|
|
"opslag.de/schobers/zntg/ui"
|
|
)
|
|
|
|
var errNotImplemented = errors.New(`not implemented`)
|
|
|
|
type Renderer struct {
|
|
window *sdl.Window
|
|
renderer *sdl.Renderer
|
|
refresh uint32
|
|
resources ui.PhysicalResources
|
|
|
|
mouse geom.PointF32
|
|
cursor ui.MouseCursor
|
|
cursors map[sdl.SystemCursor]*sdl.Cursor
|
|
}
|
|
|
|
var _ ui.Renderer = &Renderer{}
|
|
var _ ui.Texture = &Renderer{}
|
|
|
|
type NewRendererOptions struct {
|
|
Location sdl.Point
|
|
Resizable bool
|
|
VSync bool
|
|
}
|
|
|
|
func NewRenderer(title string, width, height int32, opts NewRendererOptions) (*Renderer, error) {
|
|
var clean zntg.Actions
|
|
defer func() { clean.Do() }()
|
|
|
|
if err := sdl.Init(sdl.INIT_EVERYTHING); err != nil {
|
|
return nil, err
|
|
}
|
|
clean = clean.Add(sdl.Quit)
|
|
|
|
if err := ttf.Init(); err != nil {
|
|
return nil, err
|
|
}
|
|
clean = clean.Add(ttf.Quit)
|
|
|
|
if opts.VSync {
|
|
sdl.SetHint(sdl.HINT_RENDER_VSYNC, "1")
|
|
}
|
|
sdl.SetHint(sdl.HINT_RENDER_SCALE_QUALITY, "1")
|
|
windowFlags := uint32(sdl.WINDOW_SHOWN)
|
|
if opts.Resizable {
|
|
windowFlags |= sdl.WINDOW_RESIZABLE
|
|
}
|
|
window, err := sdl.CreateWindow(title, opts.Location.X, opts.Location.Y, width, height, windowFlags)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
clean = clean.AddErr(window.Destroy)
|
|
|
|
rendererFlags := uint32(sdl.RENDERER_ACCELERATED)
|
|
if opts.VSync {
|
|
rendererFlags |= sdl.RENDERER_PRESENTVSYNC
|
|
}
|
|
renderer, err := sdl.CreateRenderer(window, -1, rendererFlags)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
renderer.SetDrawBlendMode(sdl.BLENDMODE_BLEND)
|
|
clean = clean.AddErr(renderer.Destroy)
|
|
|
|
refresh := sdl.RegisterEvents(1)
|
|
if refresh == math.MaxUint32 {
|
|
return nil, errors.New("couldn't register user event")
|
|
}
|
|
clean = nil
|
|
|
|
return &Renderer{
|
|
window: window,
|
|
renderer: renderer,
|
|
refresh: refresh,
|
|
resources: &ui.OSResources{},
|
|
cursors: map[sdl.SystemCursor]*sdl.Cursor{},
|
|
}, nil
|
|
}
|
|
|
|
// Events
|
|
|
|
func (r *Renderer) WindowBounds() geom.RectangleF32 {
|
|
x, y := r.window.GetPosition()
|
|
w, h := r.window.GetSize()
|
|
return geom.RectF32(float32(x), float32(y), float32(x+w), float32(y+h))
|
|
}
|
|
|
|
func (r *Renderer) PushEvents(t ui.EventTarget, wait bool) bool {
|
|
r.renderer.Present()
|
|
|
|
waitOrPoll := func() sdl.Event {
|
|
if wait {
|
|
return sdl.WaitEvent()
|
|
}
|
|
return sdl.PollEvent()
|
|
}
|
|
|
|
cursor := r.cursor
|
|
var pushed bool
|
|
for event := waitOrPoll(); event != nil; event = sdl.PollEvent() {
|
|
pushed = true
|
|
r.cursor = ui.MouseCursorDefault
|
|
var unhandled bool
|
|
|
|
switch e := event.(type) {
|
|
case *sdl.WindowEvent:
|
|
switch e.Event {
|
|
case sdl.WINDOWEVENT_CLOSE:
|
|
t.Handle(&ui.DisplayCloseEvent{EventBase: eventBase(e)})
|
|
case sdl.WINDOWEVENT_MOVED:
|
|
t.Handle(&ui.DisplayMoveEvent{EventBase: eventBase(e), Bounds: r.WindowBounds()})
|
|
case sdl.WINDOWEVENT_RESIZED:
|
|
t.Handle(&ui.DisplayResizeEvent{EventBase: eventBase(e), Bounds: r.WindowBounds()})
|
|
case sdl.WINDOWEVENT_ENTER:
|
|
t.Handle(&ui.MouseEnterEvent{MouseEvent: ui.MouseEvent{EventBase: eventBase(e), X: r.mouse.X, Y: r.mouse.Y}})
|
|
case sdl.WINDOWEVENT_LEAVE:
|
|
t.Handle(&ui.MouseLeaveEvent{MouseEvent: ui.MouseEvent{EventBase: eventBase(e), X: r.mouse.X, Y: r.mouse.Y}})
|
|
default:
|
|
unhandled = true
|
|
}
|
|
|
|
case *sdl.KeyboardEvent:
|
|
if e.Type == sdl.KEYDOWN {
|
|
t.Handle(&ui.KeyDownEvent{EventBase: eventBase(e), Key: key(e.Keysym.Sym), Modifiers: keyModifiers(e.Keysym.Mod)})
|
|
} else if e.Type == sdl.KEYUP {
|
|
t.Handle(&ui.KeyDownEvent{EventBase: eventBase(e), Key: 0, Modifiers: 0})
|
|
} else {
|
|
unhandled = true
|
|
}
|
|
case *sdl.TextInputEvent:
|
|
if e.Type == sdl.TEXTINPUT {
|
|
text := e.GetText()
|
|
for _, character := range text {
|
|
t.Handle(&ui.TextInputEvent{
|
|
EventBase: eventBase(e),
|
|
Character: character,
|
|
})
|
|
}
|
|
} else {
|
|
unhandled = true
|
|
}
|
|
|
|
case *sdl.MouseButtonEvent:
|
|
if e.Type == sdl.MOUSEBUTTONDOWN {
|
|
t.Handle(&ui.MouseButtonDownEvent{MouseEvent: mouseEvent(e, e.X, e.Y), Button: mouseButton(e.Button)})
|
|
} else {
|
|
t.Handle(&ui.MouseButtonUpEvent{MouseEvent: mouseEvent(e, e.X, e.Y), Button: mouseButton(e.Button)})
|
|
}
|
|
case *sdl.MouseMotionEvent:
|
|
r.mouse = geom.PtF32(float32(e.X), float32(e.Y))
|
|
t.Handle(&ui.MouseMoveEvent{MouseEvent: ui.MouseEvent{EventBase: eventBase(e), X: r.mouse.X, Y: r.mouse.Y}, MouseWheel: 0})
|
|
case *sdl.MouseWheelEvent:
|
|
t.Handle(&ui.MouseMoveEvent{MouseEvent: ui.MouseEvent{EventBase: eventBase(e), X: r.mouse.X, Y: r.mouse.Y}, MouseWheel: float32(e.Y)})
|
|
case *sdl.UserEvent:
|
|
if r.refresh == e.Type {
|
|
t.Handle(&ui.RefreshEvent{EventBase: eventBase(e)})
|
|
} else {
|
|
unhandled = true
|
|
}
|
|
default:
|
|
unhandled = true // not handled by EventTarget.Handle
|
|
}
|
|
|
|
if unhandled {
|
|
r.cursor = cursor
|
|
}
|
|
}
|
|
|
|
if r.cursor != cursor {
|
|
switch r.cursor {
|
|
case ui.MouseCursorDefault:
|
|
sdl.SetCursor(r.SystemCursor(sdl.SYSTEM_CURSOR_ARROW))
|
|
case ui.MouseCursorNotAllowed:
|
|
sdl.SetCursor(r.SystemCursor(sdl.SYSTEM_CURSOR_NO))
|
|
case ui.MouseCursorPointer:
|
|
sdl.SetCursor(r.SystemCursor(sdl.SYSTEM_CURSOR_HAND))
|
|
case ui.MouseCursorText:
|
|
sdl.SetCursor(r.SystemCursor(sdl.SYSTEM_CURSOR_IBEAM))
|
|
}
|
|
}
|
|
return pushed
|
|
}
|
|
|
|
func (r *Renderer) Refresh() {
|
|
windowID, _ := r.window.GetID()
|
|
e := &sdl.UserEvent{
|
|
Type: r.refresh,
|
|
WindowID: windowID,
|
|
}
|
|
sdl.PushEvent(e)
|
|
}
|
|
|
|
// Lifetime
|
|
|
|
func (r *Renderer) Destroy() error {
|
|
r.renderer.Destroy()
|
|
r.window.Destroy()
|
|
ttf.Quit()
|
|
sdl.Quit()
|
|
r.resources.Destroy()
|
|
return nil
|
|
}
|
|
|
|
// Drawing
|
|
|
|
func (r *Renderer) Clear(c color.Color) {
|
|
if c == color.Transparent {
|
|
return
|
|
}
|
|
r.SetDrawColorGo(c)
|
|
r.renderer.Clear()
|
|
}
|
|
|
|
func (r *Renderer) CreateFontPath(path string, size int) (ui.Font, error) {
|
|
path, err := r.resources.FetchResource(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
font, err := ttf.OpenFont(path, size)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &Font{font}, nil
|
|
}
|
|
|
|
func (r *Renderer) createTexture(source ui.ImageSource, keepSource bool) (ui.Texture, error) {
|
|
m, err := source.CreateImage()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rgba := NRGBAImage(m)
|
|
width := int32(rgba.Bounds().Dx())
|
|
height := int32(rgba.Bounds().Dy())
|
|
surface, err := sdl.CreateRGBSurfaceWithFormatFrom(
|
|
unsafe.Pointer(&rgba.Pix[0]),
|
|
width, height, 32, int32(rgba.Stride), sdl.PIXELFORMAT_ABGR8888)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer surface.Free()
|
|
texture, err := r.renderer.CreateTextureFromSurface(surface)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
texture.SetBlendMode(sdl.BLENDMODE_BLEND)
|
|
if keepSource {
|
|
return &TextureImageSource{&Texture{texture}, source}, nil
|
|
}
|
|
return &Texture{texture}, nil
|
|
}
|
|
|
|
func (r *Renderer) createTextureTarget(w, h float32) (*Texture, error) {
|
|
format, err := r.window.GetPixelFormat()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
texture, err := r.renderer.CreateTexture(format, sdl.TEXTUREACCESS_TARGET, int32(w), int32(h))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
texture.SetBlendMode(sdl.BLENDMODE_BLEND)
|
|
return &Texture{texture}, nil
|
|
}
|
|
|
|
func (r *Renderer) CreateTexture(source ui.ImageSource) (ui.Texture, error) {
|
|
return r.createTexture(source, true)
|
|
}
|
|
|
|
func (r *Renderer) CreateTextureGo(m image.Image, source bool) (ui.Texture, error) {
|
|
return r.createTexture(ui.ImageSourceGo{Image: m}, source)
|
|
}
|
|
|
|
func (r *Renderer) CreateTexturePath(path string, source bool) (ui.Texture, error) {
|
|
return r.createTexture(ui.ImageSourceResource{Resources: r.resources, Name: path}, source)
|
|
}
|
|
|
|
func (r *Renderer) CreateTextureTarget(w, h float32) (ui.Texture, error) {
|
|
return r.createTextureTarget(w, h)
|
|
}
|
|
|
|
func (r *Renderer) DefaultTarget() ui.Texture { return r }
|
|
|
|
func (r *Renderer) drawTexture(t sdlTexture, src, dst sdl.Rect, opts ui.DrawOptions) {
|
|
if opts.Tint != nil {
|
|
t.SetColor(opts.Tint)
|
|
}
|
|
r.renderer.Copy(t.Native(), &src, &dst)
|
|
}
|
|
|
|
func (r *Renderer) DrawTexture(t ui.Texture, dst geom.RectangleF32) {
|
|
r.DrawTextureOptions(t, dst, ui.DrawOptions{})
|
|
}
|
|
|
|
func (r *Renderer) DrawTextureOptions(t ui.Texture, dst geom.RectangleF32, opts ui.DrawOptions) {
|
|
texture, ok := t.(sdlTexture)
|
|
if !ok {
|
|
return
|
|
}
|
|
var source sdl.Rect
|
|
if opts.Source != nil {
|
|
source = RectAbs(int32(opts.Source.Min.X), int32(opts.Source.Min.Y), int32(opts.Source.Max.X), int32(opts.Source.Max.Y))
|
|
} else {
|
|
width, height, err := texture.Size()
|
|
if err != nil {
|
|
return
|
|
}
|
|
source = Rect(0, 0, width, height)
|
|
}
|
|
r.drawTexture(texture, source, RectAbs(int32(dst.Min.X), int32(dst.Min.Y), int32(dst.Max.X), int32(dst.Max.Y)), opts)
|
|
}
|
|
|
|
func (r *Renderer) DrawTexturePoint(t ui.Texture, dst geom.PointF32) {
|
|
r.DrawTexturePointOptions(t, dst, ui.DrawOptions{})
|
|
}
|
|
|
|
func (r *Renderer) DrawTexturePointOptions(t ui.Texture, dst geom.PointF32, opts ui.DrawOptions) {
|
|
texture, ok := t.(sdlTexture)
|
|
if !ok {
|
|
return
|
|
}
|
|
var source, destination sdl.Rect
|
|
if opts.Source != nil {
|
|
source = RectAbs(int32(opts.Source.Min.X), int32(opts.Source.Min.Y), int32(opts.Source.Max.X), int32(opts.Source.Max.Y))
|
|
destination = Rect(int32(dst.X), int32(dst.Y), source.W, source.H)
|
|
} else {
|
|
width, height, err := texture.Size()
|
|
if err != nil {
|
|
return
|
|
}
|
|
source = Rect(0, 0, width, height)
|
|
destination = Rect(int32(dst.X), int32(dst.Y), width, height)
|
|
}
|
|
r.drawTexture(texture, source, destination, opts)
|
|
}
|
|
|
|
func (r *Renderer) FillRectangle(rect geom.RectangleF32, c color.Color) {
|
|
r.SetDrawColorGo(c)
|
|
r.renderer.FillRect(SDLRectanglePtr(rect))
|
|
}
|
|
|
|
func (r *Renderer) Rectangle(rect geom.RectangleF32, c color.Color, thickness float32) {
|
|
r.SetDrawColorGo(c)
|
|
if rect.Dx() == 0 { // SDL doesn't draw a 1 px wide line when Dx() == 0 && thickness == 1
|
|
offset := int32(rect.Min.X - .5*thickness)
|
|
for thick := int32(thickness); thick > 0; thick-- {
|
|
r.renderer.DrawLine(offset, int32(rect.Min.Y), offset, int32(rect.Max.Y))
|
|
offset++
|
|
}
|
|
} else if rect.Dy() == 0 {
|
|
offset := int32(rect.Min.Y - .5*thickness)
|
|
for thick := int32(thickness); thick > 0; thick-- {
|
|
r.renderer.DrawLine(int32(rect.Min.X), offset, int32(rect.Max.X), offset)
|
|
offset++
|
|
}
|
|
} else {
|
|
for thick := int32(thickness); thick > 0; thick-- {
|
|
r.renderer.DrawRect(SDLRectanglePtr(rect))
|
|
rect = rect.Inset(1)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (r *Renderer) RenderTo(t ui.Texture) {
|
|
texture, ok := t.(sdlTexture)
|
|
if ok {
|
|
err := r.renderer.SetRenderTarget(texture.Native())
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
} else {
|
|
r.RenderToDisplay()
|
|
}
|
|
}
|
|
|
|
func (r *Renderer) RenderToDisplay() {
|
|
r.renderer.SetRenderTarget(nil)
|
|
}
|
|
|
|
func (r *Renderer) SetDrawColor(c sdl.Color) {
|
|
r.renderer.SetDrawColor(c.R, c.G, c.B, c.A)
|
|
}
|
|
|
|
func (r *Renderer) SetDrawColorGo(c color.Color) {
|
|
r.SetDrawColor(ColorSDL(c))
|
|
}
|
|
|
|
func (r *Renderer) SetMouseCursor(c ui.MouseCursor) { r.cursor = c }
|
|
|
|
func (r *Renderer) Size() geom.PointF32 {
|
|
w, h, err := r.renderer.GetOutputSize()
|
|
if err != nil {
|
|
return geom.PtF32(geom.NaN32(), geom.NaN32())
|
|
}
|
|
return geom.PtF32(float32(w), float32(h))
|
|
}
|
|
|
|
func (r *Renderer) SystemCursor(id sdl.SystemCursor) *sdl.Cursor {
|
|
if cursor, ok := r.cursors[id]; ok {
|
|
return cursor
|
|
}
|
|
cursor := sdl.CreateSystemCursor(id)
|
|
r.cursors[id] = cursor
|
|
return cursor
|
|
}
|
|
|
|
func (r *Renderer) Target() ui.Texture {
|
|
target := r.renderer.GetRenderTarget()
|
|
if target == nil {
|
|
return r
|
|
}
|
|
return &Texture{target}
|
|
}
|
|
|
|
func (r *Renderer) text(font ui.Font, color color.Color, text string) (*Texture, error) {
|
|
f, ok := font.(*Font)
|
|
if !ok {
|
|
return nil, errors.New("font not created with renderer")
|
|
}
|
|
surface, err := f.RenderUTF8Blended(text, ColorSDL(color))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer surface.Free()
|
|
texture, err := r.renderer.CreateTextureFromSurface(surface)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &Texture{texture}, nil
|
|
}
|
|
|
|
func (r *Renderer) Text(font ui.Font, p geom.PointF32, color color.Color, text string) {
|
|
texture, err := r.text(font, color, text)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer texture.Destroy()
|
|
r.DrawTexturePoint(texture, p)
|
|
}
|
|
|
|
func (r *Renderer) TextAlign(font ui.Font, p geom.PointF32, color color.Color, text string, align ui.HorizontalAlignment) {
|
|
switch align {
|
|
case ui.AlignLeft:
|
|
r.Text(font, p, color, text)
|
|
case ui.AlignCenter:
|
|
width := font.WidthOf(text)
|
|
r.Text(font, p.Add2D(-.5*width, 0), color, text)
|
|
case ui.AlignRight:
|
|
width := font.WidthOf(text)
|
|
r.Text(font, p.Add2D(-width, 0), color, text)
|
|
}
|
|
}
|
|
|
|
func (r *Renderer) TextTexture(font ui.Font, color color.Color, text string) (ui.Texture, error) {
|
|
return r.text(font, color, text)
|
|
}
|
|
|
|
// 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()
|
|
}
|
|
resources, err := ui.NewCopyResources("sdlui", factory(), false)
|
|
if err != nil {
|
|
return
|
|
}
|
|
r.resources = resources
|
|
}
|
|
|
|
// Texture
|
|
|
|
func (r *Renderer) Image() image.Image { return nil }
|
|
|
|
func (r *Renderer) Height() float32 { return r.Size().Y }
|
|
|
|
func (r *Renderer) Width() float32 { return r.Size().X }
|