Sander Schobers
8560204c39
- The Allegro (alui) implementation only provides emulation of the event (comparing the position every time other events are handled).
397 lines
9.9 KiB
Go
397 lines
9.9 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.Resources
|
|
|
|
dispPos geom.Point
|
|
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()
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
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) (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.RectangleF32) {
|
|
r.DrawTextureOptions(texture, p, ui.DrawOptions{})
|
|
}
|
|
|
|
func (r *Renderer) DrawTextureOptions(texture ui.Texture, p geom.RectangleF32, opts ui.DrawOptions) {
|
|
bmp := r.mustGetBitmap(texture)
|
|
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 := r.mustGetBitmap(texture)
|
|
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) 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) 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(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)
|
|
}
|
|
|
|
// 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)
|
|
}
|