zntg/allg5ui/renderer.go
Sander Schobers 0f03760e66 Added Resize & SetIcon to Renderer.
Refactored Size (on Renderer) to return geom.Point instead of geom.PointF32.
Refactored Width and Height (on Texture) to return int instead of float32.

Refactored texture dimensions to be represented by ints instead of float32s.
2020-12-13 07:40:19 +01:00

442 lines
11 KiB
Go

package allg5ui
import (
"image"
"image/color"
"math"
"unicode"
"opslag.de/schobers/zntg"
"opslag.de/schobers/allg5"
"opslag.de/schobers/geom"
"opslag.de/schobers/zntg/ui"
)
var _ ui.Renderer = &Renderer{}
func NewRenderer(w, h int, opts allg5.NewDisplayOptions) (*Renderer, error) {
var clean zntg.Actions
defer func() { clean.Do() }()
var err = allg5.Init(allg5.InitAll)
if err != nil {
return nil, err
}
disp, err := allg5.NewDisplay(w, h, opts)
if err != nil {
return nil, err
}
clean = clean.Add(disp.Destroy)
eq, err := allg5.NewEventQueue()
if err != nil {
return nil, err
}
clean = clean.Add(eq.Destroy)
user := allg5.NewUserEventSource()
eq.RegisterKeyboard()
eq.RegisterMouse()
eq.RegisterDisplay(disp)
eq.RegisterUserEvents(user)
allg5.CaptureNewBitmapFlags().Mutate(func(m allg5.FlagMutation) {
m.Set(allg5.NewBitmapFlagMinLinear)
})
clean = nil
return &Renderer{disp, eq, nil, user, &ui.OSResources{}, dispPos(disp), ui.KeyState{}, ui.KeyModifierNone, ui.MouseCursorDefault}, nil
}
// Renderer implements ui.Renderer using Allegro 5.
type Renderer struct {
disp *allg5.Display
eq *allg5.EventQueue
unh func(allg5.Event)
user *allg5.UserEventSource
res ui.PhysicalResources
dispPos geom.Point
keys ui.KeyState
modifiers ui.KeyModifier
cursor ui.MouseCursor
}
// Renderer implementation (events)
func (r *Renderer) PushEvents(t ui.EventTarget, wait bool) bool {
r.disp.Flip()
var ev = eventWait(r.eq, wait)
if ev == nil {
return false
}
cursor := r.cursor
r.cursor = ui.MouseCursorDefault
var unhandled bool
for ev != nil {
switch e := ev.(type) {
case *allg5.DisplayCloseEvent:
t.Handle(&ui.DisplayCloseEvent{EventBase: eventBase(e)})
case *allg5.DisplayResizeEvent:
t.Handle(&ui.DisplayResizeEvent{EventBase: eventBase(e), Bounds: geom.RectF32(float32(e.X), float32(e.Y), float32(e.X+e.Width), float32(e.Y+e.Height))})
case *allg5.KeyCharEvent:
if r.modifiers&ui.KeyModifierControl == ui.KeyModifierNone && !unicode.IsControl(e.UnicodeCharacter) {
t.Handle(&ui.TextInputEvent{EventBase: eventBase(e), Character: e.UnicodeCharacter})
} else {
unhandled = true
}
case *allg5.KeyDownEvent:
key := key(e.KeyCode)
r.keys[key] = true
r.modifiers = r.keys.Modifiers()
t.Handle(&ui.KeyDownEvent{EventBase: eventBase(e), Key: key, Modifiers: r.modifiers})
case *allg5.KeyUpEvent:
key := key(e.KeyCode)
r.keys[key] = false
r.modifiers = r.keys.Modifiers()
t.Handle(&ui.KeyUpEvent{EventBase: eventBase(e), Key: key, Modifiers: r.modifiers})
case *allg5.MouseButtonDownEvent:
t.Handle(&ui.MouseButtonDownEvent{MouseEvent: mouseEvent(e.MouseEvent), Button: ui.MouseButton(e.Button)})
case *allg5.MouseButtonUpEvent:
t.Handle(&ui.MouseButtonUpEvent{MouseEvent: mouseEvent(e.MouseEvent), Button: ui.MouseButton(e.Button)})
case *allg5.MouseEnterEvent:
t.Handle(&ui.MouseLeaveEvent{MouseEvent: mouseEvent(e.MouseEvent)})
case *allg5.MouseLeaveEvent:
t.Handle(&ui.MouseLeaveEvent{MouseEvent: mouseEvent(e.MouseEvent)})
case *allg5.MouseMoveEvent:
t.Handle(&ui.MouseMoveEvent{MouseEvent: mouseEvent(e.MouseEvent), MouseWheel: float32(e.DeltaZ)})
case *allg5.UserEvent:
t.Handle(&ui.RefreshEvent{EventBase: eventBase(e)})
default:
if r.unh != nil {
r.unh(e)
}
unhandled = true
}
ev = r.eq.Get()
}
dispPos := dispPos(r.disp)
if dispPos != r.dispPos {
r.dispPos = dispPos
w := r.disp.Width()
h := r.disp.Height()
t.Handle(&ui.DisplayMoveEvent{Bounds: r.dispPos.RectRel2D(w, h).ToF32()})
}
if !unhandled && cursor != r.cursor {
switch r.cursor {
case ui.MouseCursorNone:
r.disp.SetMouseCursor(allg5.MouseCursorNone)
case ui.MouseCursorDefault:
r.disp.SetMouseCursor(allg5.MouseCursorDefault)
case ui.MouseCursorNotAllowed:
r.disp.SetMouseCursor(allg5.MouseCursorUnavailable)
case ui.MouseCursorPointer:
r.disp.SetMouseCursor(allg5.MouseCursorLink)
case ui.MouseCursorText:
r.disp.SetMouseCursor(allg5.MouseCursorEdit)
}
}
return true
}
func (r *Renderer) RegisterRecorder(rec *allg5.Recorder) {
r.eq.RegisterRecorder(rec)
}
func (r *Renderer) Refresh() {
r.user.EmitEvent()
}
// Renderer implementation (lifetime)
func (r *Renderer) Destroy() error {
r.user.Destroy()
r.eq.Destroy()
r.disp.Destroy()
r.res.Destroy()
return nil
}
// Renderer implementation (drawing)
func (r *Renderer) Clear(c color.Color) {
allg5.ClearToColor(newColor(c))
}
func (r *Renderer) CreateFontPath(path string, size int) (ui.Font, error) {
path, err := r.res.FetchResource(path)
if err != nil {
return nil, err
}
f, err := allg5.LoadTTFFont(path, size)
if err != nil {
return nil, err
}
return &font{f}, nil
}
func (r *Renderer) createTexture(source ui.ImageSource, keepSource bool) (*texture, error) {
im, err := source.CreateImage()
if err != nil {
return nil, err
}
bmp, err := allg5.NewBitmapFromImage(im, true)
if err != nil {
return nil, err
}
if keepSource {
return &texture{bmp, source}, nil
}
return &texture{bmp, nil}, nil
}
func (r *Renderer) CreateTexture(source ui.ImageSource) (ui.Texture, error) {
return r.createTexture(source, true)
}
func (r *Renderer) CreateTextureGo(im image.Image, source bool) (ui.Texture, error) {
return r.createTexture(ui.ImageSourceGo{Image: im}, source)
}
func (r *Renderer) CreateTexturePath(path string, source bool) (ui.Texture, error) {
resourcePath, err := r.res.FetchResource(path)
if err != nil {
return nil, err
}
bmp, err := allg5.LoadBitmap(resourcePath)
if err != nil {
return nil, err
}
if source {
return &texture{bmp, ui.ImageSourceResource{Resources: r.res, Name: path}}, nil
}
return &texture{bmp, nil}, nil
}
func (r *Renderer) CreateTextureTarget(w, h float32) (ui.Texture, error) {
bmp, err := allg5.NewVideoBitmap(int(w), int(h))
if err != nil {
return nil, err
}
return &texture{bmp, nil}, nil
}
func (r *Renderer) DefaultTarget() ui.Texture {
return &texture{r.disp.Target(), nil}
}
func (r *Renderer) Display() *allg5.Display { return r.disp }
func (r *Renderer) DrawTexture(texture ui.Texture, p geom.RectangleF32) {
r.DrawTextureOptions(texture, p, ui.DrawOptions{})
}
func (r *Renderer) DrawTextureOptions(texture ui.Texture, p geom.RectangleF32, opts ui.DrawOptions) {
bmp, ok := r.mustGetSubBitmap(texture, opts.Source)
if ok {
defer bmp.Destroy()
}
x, y := snap(p.Min)
var o allg5.DrawOptions
if opts.Tint != nil {
tint := newColor(opts.Tint)
o.Tint = &tint
}
w, h := p.Dx(), p.Dy()
bmpW, bmpH := float32(bmp.Width()), float32(bmp.Height())
if w != bmpW || h != bmpH {
o.Scale = &allg5.Scale{Horizontal: w / bmpW, Vertical: h / bmpH}
}
bmp.DrawOptions(x, y, o)
}
func (r *Renderer) DrawTexturePoint(texture ui.Texture, p geom.PointF32) {
bmp := r.mustGetBitmap(texture)
x, y := snap(p)
bmp.Draw(x, y)
}
func (r *Renderer) DrawTexturePointOptions(texture ui.Texture, p geom.PointF32, opts ui.DrawOptions) {
bmp, ok := r.mustGetSubBitmap(texture, opts.Source)
if ok {
defer bmp.Destroy()
}
var o allg5.DrawOptions
if opts.Tint != nil {
tint := newColor(opts.Tint)
o.Tint = &tint
}
x, y := snap(p)
bmp.DrawOptions(x, y, o)
}
func (r *Renderer) FillRectangle(rect geom.RectangleF32, c color.Color) {
allg5.DrawFilledRectangle(rect.Min.X, rect.Min.Y, rect.Max.X, rect.Max.Y, newColor(c))
}
func (r *Renderer) mustGetBitmap(t ui.Texture) *allg5.Bitmap {
texture, ok := t.(*texture)
if !ok {
panic("texture must be created on same renderer")
}
return texture.bmp
}
func (r *Renderer) mustGetSubBitmap(t ui.Texture, source *geom.RectangleF32) (*allg5.Bitmap, bool) {
texture, ok := t.(*texture)
if !ok {
panic("texture must be created on same renderer")
}
if source == nil {
return texture.bmp, false
}
src := source.ToInt()
return texture.bmp.Sub(src.Min.X, src.Min.Y, src.Dx(), src.Dy()), true
}
func (r *Renderer) Rectangle(rect geom.RectangleF32, c color.Color, thickness float32) {
minX, minY := snap(rect.Min)
maxX, maxY := snap(rect.Max)
allg5.DrawRectangle(minX, minY, maxX, maxY, newColor(c), thickness)
}
func (r *Renderer) RenderTo(texture ui.Texture) {
bmp := r.mustGetBitmap(texture)
bmp.SetAsTarget()
}
func (r *Renderer) RenderToDisplay() {
r.disp.SetAsTarget()
}
func (r *Renderer) Resize(width, height int) {
r.disp.Resize(width, height)
}
func (r *Renderer) Resources() ui.Resources { return r.res }
func (r *Renderer) Size() geom.Point {
return geom.Pt(r.disp.Width(), r.disp.Height())
}
func (r *Renderer) SetIcon(source ui.ImageSource) {
texture, err := r.createTexture(source, false)
if err != nil {
return
}
defer texture.Destroy()
r.disp.SetIcon(texture.bmp)
}
func (r *Renderer) SetMouseCursor(c ui.MouseCursor) {
r.cursor = c
}
func (r *Renderer) SetPosition(p geom.PointF32) { r.disp.SetPosition(int(p.X), int(p.Y)) }
func (r *Renderer) SetResourceProvider(res ui.Resources) {
if r.res != nil {
r.res.Destroy()
}
if phys, ok := res.(ui.PhysicalResources); ok {
r.res = phys
} else {
copy, err := ui.NewCopyResources("allg5ui", res, false)
if err != nil {
return
}
r.res = copy
}
}
func (r *Renderer) SetUnhandledEventHandler(handler func(allg5.Event)) {
r.unh = handler
}
func (r *Renderer) SetWindowTitle(t string) {
r.disp.SetWindowTitle(t)
}
func (r *Renderer) Target() ui.Texture {
return &texture{allg5.CurrentTarget(), nil}
}
func (r *Renderer) text(f ui.Font, p geom.PointF32, c color.Color, t string, align allg5.HorizontalAlignment) {
font, ok := f.(*font)
if !ok {
return
}
x, y := snap(p)
font.Draw(x, y, newColor(c), align, t)
}
func (r *Renderer) Text(font ui.Font, p geom.PointF32, c color.Color, t string) {
r.text(font, p, c, t, allg5.AlignLeft)
}
func (r *Renderer) TextAlign(font ui.Font, p geom.PointF32, c color.Color, t string, align ui.HorizontalAlignment) {
var alignment = allg5.AlignLeft
switch align {
case ui.AlignCenter:
alignment = allg5.AlignCenter
case ui.AlignRight:
alignment = allg5.AlignRight
}
r.text(font, p, c, t, alignment)
}
func (r *Renderer) TextTexture(font ui.Font, color color.Color, text string) (ui.Texture, error) {
return ui.TextTexture(r, font, color, text)
}
// Utility functions
func eventWait(eq *allg5.EventQueue, wait bool) allg5.Event {
if wait {
return eq.GetWait()
}
return eq.Get()
}
func eventBase(e allg5.Event) ui.EventBase {
return ui.EventBase{StampInSeconds: e.Stamp()}
}
func keyModifiers(mods allg5.KeyMod) ui.KeyModifier {
var m ui.KeyModifier
if mods&allg5.KeyModShift == allg5.KeyModShift {
m |= ui.KeyModifierShift
} else if mods&allg5.KeyModCtrl == allg5.KeyModCtrl {
m |= ui.KeyModifierControl
} else if mods&allg5.KeyModAlt == allg5.KeyModAlt {
m |= ui.KeyModifierAlt
}
return m
}
func mouseEvent(e allg5.MouseEvent) ui.MouseEvent {
return ui.MouseEvent{EventBase: eventBase(e), X: float32(e.X), Y: float32(e.Y)}
}
func newColor(c color.Color) allg5.Color {
if c == nil {
return newColor(color.Black)
}
return allg5.NewColorGo(c)
}
func snap(p geom.PointF32) (float32, float32) {
return float32(math.Round(float64(p.X))), float32(math.Round(float64(p.Y)))
}
func dispPos(disp *allg5.Display) geom.Point {
x, y := disp.Position()
return geom.Pt(x, y)
}