Sander Schobers
cdfb863ab0
Added Action{,s}. List of actions that can be used to defer cleanup code (see NewRenderer implementations). Added TextInputEvent (replaces the old KeyPressEvent) and added to new events KeyDown & KeyUp. Added VSync to NewRendererOptions. Removed IconScale from button. Added ImageSource interface that replaces the Image/Texture method on the Texture interface. This makes converting back a texture to an image optional (since this is atypical for a hardware texture for instance). Added new KeyModifier: OSCommand (Windows/Command key). Added KeyState that can keep the state of keys (pressed or not). Added KeyEnter, representing the Enter key. Changed signatures of CreateTexture methods in Renderer. Changed signatures of icon related method (removed factories). Basic example now depends on sdlgui.
372 lines
9.1 KiB
Go
372 lines
9.1 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.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
|
|
|
|
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()
|
|
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.ImageGoSource{im}, true)
|
|
}
|
|
|
|
func (r *Renderer) CreateTexturePath(path string, source bool) (ui.Texture, error) {
|
|
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 {
|
|
var f, err = allg5.LoadTTFFont(path, size)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var prev = r.ft[name]
|
|
if prev != nil {
|
|
prev.Destroy()
|
|
}
|
|
r.ft[name] = newFont(f)
|
|
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) 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) 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)))
|
|
}
|