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 }