zntg/allg5ui/renderer.go

391 lines
9.5 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, map[string]*font{}, user, &ui.OSResources{}, 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)
ft map[string]*font
user *allg5.UserEventSource
res ui.Resources
keys ui.KeyState
modifiers ui.KeyModifier
cursor ui.MouseCursor
}
// Renderer implementation (events)
func (r *Renderer) PushEvents(t ui.EventTarget, wait bool) {
r.disp.Flip()
var ev = eventWait(r.eq, wait)
if ev == nil {
return
}
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()
}
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)
}
}
}
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()
for _, f := range r.ft {
f.Destroy()
}
r.ft = nil
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) createTexture(source ui.ImageSource, keepSource bool) (ui.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{im}, true)
}
func (r *Renderer) CreateTexturePath(path string, source bool) (ui.Texture, error) {
path, err := r.res.FetchResource(path)
if err != nil {
return nil, err
}
bmp, err := allg5.LoadBitmap(path)
if err != nil {
return nil, err
}
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.PointF32) {
bmp := r.mustGetBitmap(texture)
x, y := snap(p)
bmp.Draw(x, y)
}
func (r *Renderer) DrawTextureOptions(texture ui.Texture, p geom.PointF32, opts ui.DrawOptions) {
bmp := r.mustGetBitmap(texture)
var o allg5.DrawOptions
if opts.Tint != nil {
tint := newColor(opts.Tint)
o.Tint = &tint
}
if opts.Scale != nil {
o.Scale = &allg5.Scale{Horizontal: opts.Scale.X, Vertical: opts.Scale.Y}
}
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) Font(name string) ui.Font {
return r.ft[name]
}
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) 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) RegisterFont(name, path string, size int) error {
path, err := r.res.FetchResource(path)
if err != nil {
return err
}
font, err := allg5.LoadTTFFont(path, size)
if err != nil {
return err
}
var prev = r.ft[name]
if prev != nil {
prev.Destroy()
}
r.ft[name] = newFont(font)
return nil
}
func (r *Renderer) RegisterFonts(path string, fonts ...FontDefinition) error {
for _, f := range fonts {
err := r.RegisterFont(path, f.Name, f.Size)
if err != nil {
return err
}
}
return nil
}
func (r *Renderer) RenderTo(texture ui.Texture) {
bmp := r.mustGetBitmap(texture)
bmp.SetAsTarget()
}
func (r *Renderer) RenderToDisplay() {
r.disp.SetAsTarget()
}
func (r *Renderer) Resources() ui.Resources { return r.res }
func (r *Renderer) Size() geom.PointF32 {
return geom.PtF32(float32(r.disp.Width()), float32(r.disp.Height()))
}
func (r *Renderer) SetIcon(texture ui.Texture) {
bmp := r.mustGetBitmap(texture)
r.disp.SetIcon(bmp)
}
func (r *Renderer) SetMouseCursor(c ui.MouseCursor) {
r.cursor = c
}
func (r *Renderer) SetResourceProvider(factory func() ui.Resources) {
if r.res != nil {
r.res.Destroy()
}
r.res = factory()
}
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(p geom.PointF32, font string, c color.Color, t string, align allg5.HorizontalAlignment) {
var f = r.ft[font]
if f == nil {
return
}
x, y := snap(p)
f.Draw(x, y, newColor(c), align, t)
}
func (r *Renderer) Text(p geom.PointF32, font string, c color.Color, t string) {
r.text(p, font, c, t, allg5.AlignLeft)
}
func (r *Renderer) TextAlign(p geom.PointF32, font string, 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(p, font, c, t, alignment)
}
// 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)))
}