zntg/sdlui/renderer.go

448 lines
11 KiB
Go
Raw Normal View History

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 {
2020-05-15 12:20:07 +00:00
window *sdl.Window
renderer *sdl.Renderer
refresh uint32
fonts map[string]*Font
resources ui.Resources
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{
2020-05-15 12:20:07 +00:00
window: window,
renderer: renderer,
refresh: refresh,
fonts: map[string]*Font{},
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) {
r.renderer.Present()
waitOrPoll := func() sdl.Event {
if wait {
return sdl.WaitEvent()
}
return sdl.PollEvent()
}
cursor := r.cursor
for event := waitOrPoll(); event != nil; event = sdl.PollEvent() {
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_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))
}
}
}
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 {
for _, f := range r.fonts {
f.Close()
}
r.renderer.Destroy()
r.window.Destroy()
ttf.Quit()
sdl.Quit()
2020-05-15 12:20:07 +00:00
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) createTexture(source ui.ImageSource, keepSource bool) (ui.Texture, error) {
m, err := source.CreateImage()
if err != nil {
return nil, err
}
rgba := RGBAImage(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
}
if keepSource {
return &TextureImageSource{&Texture{texture}, source}, nil
}
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) {
2020-05-15 12:20:07 +00:00
return r.createTexture(ui.ImageSourceGo{Image: m}, source)
}
func (r *Renderer) CreateTexturePath(path string, source bool) (ui.Texture, error) {
2020-05-15 12:20:07 +00:00
return r.createTexture(ui.ImageSourceResource{Resources: r.resources, Name: path}, source)
}
func (r *Renderer) CreateTextureTarget(w, h float32) (ui.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) DefaultTarget() ui.Texture { return r }
func (r *Renderer) DrawTexture(t ui.Texture, p geom.PointF32) {
r.DrawTextureOptions(t, p, ui.DrawOptions{})
}
func (r *Renderer) DrawTextureOptions(t ui.Texture, p geom.PointF32, opts ui.DrawOptions) {
texture, ok := t.(sdlTexture)
if !ok {
return
}
if opts.Tint != nil {
texture.SetColor(opts.Tint)
}
width, height, err := texture.Size()
if err != nil {
return
}
dst := RectPtr(int32(p.X), int32(p.Y), width, height)
if opts.Scale != nil {
dst.W = int32(float32(width) * opts.Scale.X)
dst.H = int32(float32(height) * opts.Scale.Y)
}
r.renderer.Copy(texture.Native(), RectPtr(0, 0, width, height), dst)
}
func (r *Renderer) FillRectangle(rect geom.RectangleF32, c color.Color) {
r.SetDrawColorGo(c)
r.renderer.FillRect(SDLRectanglePtr(rect))
}
func (r *Renderer) Font(name string) ui.Font {
return r.fonts[name]
}
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) RegisterFont(name, path string, size int) error {
2020-05-15 12:20:07 +00:00
path, err := r.resources.FetchResource(path)
if err != nil {
return err
}
font, err := ttf.OpenFont(path, size)
if err != nil {
return err
}
r.fonts[name] = &Font{font}
return nil
}
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(p geom.PointF32, font string, color color.Color, text string) {
f := r.Font(font).(*Font)
surface, err := f.RenderUTF8Blended(text, ColorSDL(color))
if err != nil {
return
}
defer surface.Free()
texture, err := r.renderer.CreateTextureFromSurface(surface)
if err != nil {
return
}
defer texture.Destroy()
r.DrawTexture(&Texture{texture}, p)
}
func (r *Renderer) TextAlign(p geom.PointF32, font string, color color.Color, text string, align ui.HorizontalAlignment) {
switch align {
case ui.AlignLeft:
r.Text(p, font, color, text)
case ui.AlignCenter:
width := r.Font(font).(*Font).WidthOf(text)
r.Text(p.Add2D(-.5*width, 0), font, color, text)
case ui.AlignRight:
width := r.Font(font).(*Font).WidthOf(text)
r.Text(p.Add2D(-width, 0), font, color, text)
}
}
2020-05-15 12:20:07 +00:00
// 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 }
func (r *Renderer) Height() float32 { return r.Size().Y }
func (r *Renderer) Width() float32 { return r.Size().X }