Sander Schobers
cdfb863ab0
Added Action{,s}. List of actions that can be used to defer cleanup code (see NewRenderer implementations). Added TextInputEvent (replaces the old KeyPressEvent) and added to new events KeyDown & KeyUp. Added VSync to NewRendererOptions. Removed IconScale from button. Added ImageSource interface that replaces the Image/Texture method on the Texture interface. This makes converting back a texture to an image optional (since this is atypical for a hardware texture for instance). Added new KeyModifier: OSCommand (Windows/Command key). Added KeyState that can keep the state of keys (pressed or not). Added KeyEnter, representing the Enter key. Changed signatures of CreateTexture methods in Renderer. Changed signatures of icon related method (removed factories). Basic example now depends on sdlgui.
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 }
|