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) 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) } // 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 := 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 } 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) { 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) { 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 } 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, 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) } // 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 }