Sander Schobers
44220c8f9a
Refactored Size (on Renderer) to return geom.Point instead of geom.PointF32.
524 lines
13 KiB
Go
524 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) createSurface(source ui.ImageSource) (*sdl.Surface, 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
|
|
}
|
|
return surface, nil
|
|
}
|
|
|
|
func (r *Renderer) createTexture(source ui.ImageSource, keepSource bool) (ui.Texture, error) {
|
|
surface, err := r.createSurface(source)
|
|
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) Resize(width, height int) {
|
|
r.window.SetSize(int32(width), int32(height))
|
|
}
|
|
|
|
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) SetIcon(source ui.ImageSource) {
|
|
window := r.window
|
|
if window == nil {
|
|
return
|
|
}
|
|
surface, err := r.createSurface(source)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer surface.Free()
|
|
window.SetIcon(surface)
|
|
}
|
|
|
|
func (r *Renderer) SetMouseCursor(c ui.MouseCursor) { r.cursor = c }
|
|
|
|
func (r *Renderer) Size() geom.Point {
|
|
w, h, err := r.renderer.GetOutputSize()
|
|
if err != nil {
|
|
return geom.ZeroPt
|
|
}
|
|
return geom.Pt(int(w), int(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(resources ui.Resources) {
|
|
if r.resources != nil {
|
|
r.resources.Destroy()
|
|
}
|
|
|
|
if physical, ok := resources.(ui.PhysicalResources); ok {
|
|
r.resources = physical
|
|
} else {
|
|
copy, err := ui.NewCopyResources("sdlui", resources, false)
|
|
if err != nil {
|
|
return
|
|
}
|
|
r.resources = copy
|
|
}
|
|
}
|
|
|
|
// 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 }
|