zntg/sdlui/renderer.go

566 lines
14 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
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 {
2022-05-01 10:40:09 +00:00
Borderless bool
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)
2022-05-01 10:40:09 +00:00
if opts.Borderless {
windowFlags |= sdl.WINDOW_BORDERLESS
}
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,
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)
}
func (r *Renderer) Stamp() float64 {
return .001 * float64(sdl.GetTicks())
}
// Lifetime
func (r *Renderer) Destroy() error {
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) 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) {
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) {
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))
}
2021-03-23 11:31:31 +00:00
func (r *Renderer) Line(p, q geom.PointF32, color color.Color, thickness float32) {
r.SetDrawColorGo(color)
2021-06-04 15:17:41 +00:00
r.renderer.DrawLineF(p.X, p.Y, q.X, q.Y)
2021-03-23 11:31:31 +00:00
}
func (r *Renderer) Location() geom.Point {
x, y := r.renderer.GetPosition()
return geom.Pt(int(x), int(y))
}
func (r *Renderer) Move(to geom.Point) {
r.renderer.SetPosition(int32(to.X), int32(to.Y))
}
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)
}
2021-06-01 19:14:51 +00:00
func (r *Renderer) WindowHandle() uintptr {
info, err := r.window.GetWMInfo()
if err != nil {
return 0
}
switch info.Subsystem {
case sdl.SYSWM_COCOA:
return uintptr(info.GetCocoaInfo().Window)
case sdl.SYSWM_DIRECTFB:
return uintptr(info.GetDFBInfo().Window)
case sdl.SYSWM_UIKIT:
return uintptr(info.GetUIKitInfo().Window)
case sdl.SYSWM_WINDOWS:
return uintptr(info.GetWindowsInfo().Window)
case sdl.SYSWM_X11:
return uintptr(info.GetX11Info().Window)
}
return 0
}
2020-05-15 12:20:07 +00:00
// Resources
func (r *Renderer) Resources() ui.Resources { return r.resources }
func (r *Renderer) SetResourceProvider(resources ui.Resources) {
2020-05-15 12:20:07 +00:00
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
}
2020-05-15 12:20:07 +00:00
}
// 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 }