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) 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) (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{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 := 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) SetPosition(p geom.PointF32) { r.disp.SetPosition(int(p.X), int(p.Y)) } 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) }