459 lines
11 KiB
Go
459 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()
|
|
}
|
|
|
|
func (r *Renderer) Stamp() float64 {
|
|
return allg5.GetTime()
|
|
}
|
|
|
|
// 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) Line(p, q geom.PointF32, color color.Color, thickness float32) {
|
|
allg5.DrawLine(p.X, p.Y, q.X, q.Y, newColor(color), thickness)
|
|
}
|
|
|
|
func (r *Renderer) Location() geom.Point {
|
|
x, y := r.disp.Position()
|
|
return geom.Pt(x, y)
|
|
}
|
|
|
|
func (r *Renderer) Move(to geom.Point) {
|
|
r.disp.SetPosition(to.X, to.Y)
|
|
}
|
|
|
|
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)
|
|
}
|