Sander Schobers
8560204c39
- The Allegro (alui) implementation only provides emulation of the event (comparing the position every time other events are handled).
455 lines
12 KiB
Go
455 lines
12 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.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{
|
|
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) {
|
|
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_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))
|
|
}
|
|
}
|
|
}
|
|
|
|
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 := 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.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) {
|
|
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 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
|
|
}
|
|
width, height, err := texture.Size()
|
|
if err != nil {
|
|
return
|
|
}
|
|
r.drawTexture(texture, Rect(0, 0, width, height), 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
|
|
}
|
|
width, height, err := texture.Size()
|
|
if err != nil {
|
|
return
|
|
}
|
|
r.drawTexture(texture, Rect(0, 0, width, height), Rect(int32(dst.X), int32(dst.Y), width, height), 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, p geom.PointF32, color color.Color, text string) {
|
|
f := 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.DrawTexturePoint(&Texture{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)
|
|
}
|
|
}
|
|
|
|
// 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 }
|