422 lines
10 KiB
Go
422 lines
10 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
|
||
|
fonts map[string]*Font
|
||
|
|
||
|
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,
|
||
|
fonts: map[string]*Font{},
|
||
|
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
|
||
|
|
||
|
// TODO: simulate ui.MouseEnter & ui.MouseLeave?
|
||
|
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.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:
|
||
|
t.Handle(&ui.MouseMoveEvent{MouseEvent: ui.MouseEvent{EventBase: eventBase(e), X: float32(e.X), Y: float32(e.Y)}, MouseWheel: 0})
|
||
|
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()
|
||
|
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) {
|
||
|
return r.createTexture(ui.ImageGoSource{Image: m}, source)
|
||
|
}
|
||
|
|
||
|
func (r *Renderer) CreateTexturePath(path string, source bool) (ui.Texture, error) {
|
||
|
return r.createTexture(ui.ImageFileSource(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 {
|
||
|
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)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 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 }
|