Sander Schobers
0f03760e66
Refactored Size (on Renderer) to return geom.Point instead of geom.PointF32. Refactored Width and Height (on Texture) to return int instead of float32. Refactored texture dimensions to be represented by ints instead of float32s.
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() int { return r.Size().Y }
|
|
|
|
func (r *Renderer) Width() int { return r.Size().X }
|