Rework the UI framework

Interface is now Driver, and Widget is now a set of interfaces with a
struct per widget type. This should make it easier to add other types.
This commit is contained in:
2020-03-24 20:21:55 +00:00
parent bcee07e8f7
commit 69971b2825
14 changed files with 791 additions and 669 deletions

View File

@@ -4,12 +4,16 @@ import (
"github.com/hajimehoshi/ebiten"
)
var (
SpeedDivisor = 2
)
type animation []*ebiten.Image
func (a animation) image(step int) *ebiten.Image {
func (a animation) image(tick int) *ebiten.Image {
if len(a) == 0 {
return nil
}
return a[step%len(a)]
return a[(tick/SpeedDivisor)%len(a)]
}

136
internal/ui/buttons.go Normal file
View File

@@ -0,0 +1,136 @@
package ui
import (
"image"
"code.ur.gs/lupine/ordoor/internal/assetstore"
"code.ur.gs/lupine/ordoor/internal/menus"
)
func init() {
registerBuilder(menus.TypeSimpleButton, registerSimpleButton)
registerBuilder(menus.TypeInvokeButton, registerInvokeButton)
registerBuilder(menus.TypeMainButton, registerMainButton)
}
// A button without hover animation
type button struct {
path string
baseSpr *assetstore.Sprite
clickSpr *assetstore.Sprite
frozenSpr *assetstore.Sprite
clickImpl
freezeImpl
hoverImpl
}
// A button with hover animation
type mainButton struct {
hoverAnim animation
button
}
func registerSimpleButton(d *Driver, r *menus.Record) error {
return registerButton(d, r, r.SpriteId[0])
}
func registerInvokeButton(d *Driver, r *menus.Record) error {
return registerButton(d, r, r.Share)
}
func registerMainButton(d *Driver, r *menus.Record) error {
sprites, err := d.menu.Sprites(r.Share, 3) // base, pressed, disabled
if err != nil {
return err
}
hovers, err := d.menu.Images(r.SpriteId[0], r.DrawType)
if err != nil {
return err
}
btn := &mainButton{
hoverAnim: animation(hovers),
button: button{
path: r.Path(),
baseSpr: sprites[0],
clickSpr: sprites[1],
frozenSpr: sprites[2],
hoverImpl: hoverImpl{text: r.Desc},
},
}
d.clickables = append(d.clickables, btn)
d.freezables = append(d.freezables, btn)
d.hoverables = append(d.hoverables, btn)
d.paintables = append(d.paintables, btn)
return nil
}
func registerButton(d *Driver, r *menus.Record, spriteId int) error {
sprites, err := d.menu.Sprites(spriteId, 3) // base, pressed, disabled
if err != nil {
return err
}
btn := &button{
path: r.Path(),
baseSpr: sprites[0],
clickSpr: sprites[1],
frozenSpr: sprites[2],
hoverImpl: hoverImpl{text: r.Desc},
}
d.clickables = append(d.clickables, btn)
d.freezables = append(d.freezables, btn)
d.hoverables = append(d.hoverables, btn)
d.paintables = append(d.paintables, btn)
return nil
}
func (b *button) id() string {
return b.path
}
func (b *button) bounds() image.Rectangle {
return b.baseSpr.Rect
}
func (b *button) mouseDownState() bool {
if b.isFrozen() {
return false
}
return b.clickImpl.mouseDownState()
}
func (b *button) registerMouseClick() {
if !b.isFrozen() {
b.clickImpl.registerMouseClick()
}
}
func (b *button) regions(tick int) []region {
if b.isFrozen() {
return oneRegion(b.bounds().Min, b.frozenSpr.Image)
}
if b.mouseDownState() {
return oneRegion(b.bounds().Min, b.clickSpr.Image)
}
return oneRegion(b.bounds().Min, b.baseSpr.Image)
}
func (m *mainButton) regions(tick int) []region {
if !m.isFrozen() && !m.mouseDownState() && m.hoverState() {
return oneRegion(m.bounds().Min, m.hoverAnim.image(tick))
}
return m.button.regions(tick)
}

284
internal/ui/driver.go Normal file
View File

@@ -0,0 +1,284 @@
package ui
import (
"fmt"
"image"
"log"
"github.com/hajimehoshi/ebiten"
"github.com/hajimehoshi/ebiten/ebitenutil"
"code.ur.gs/lupine/ordoor/internal/assetstore"
"code.ur.gs/lupine/ordoor/internal/menus"
)
func init() {
// These menu types don't need driving, so we can ignore them
registerBuilder(menus.TypeMenu, nil) // Menus are just containers
// FIXME: these need further investigation / implementation
registerBuilder(menus.TypeOverlay, nil)
registerBuilder(menus.TypeSlider, nil)
}
const (
OriginalX = 640.0
OriginalY = 480.0
)
var (
// Widgets register their builder here
widgetBuilders = map[menus.MenuType]builderFunc{}
)
// Used to add widgets to a driver
type builderFunc func(d *Driver, r *menus.Record) error
func registerBuilder(t menus.MenuType, f builderFunc) {
if _, ok := widgetBuilders[t]; ok {
panic(fmt.Sprintf("A builder for menu type %v already exists", t))
}
widgetBuilders[t] = f
}
// Driver acts as an interface between the main loop and the widgets specified
// in a menu.
//
// Menu assets assume a 640x480 screen; Driver is responsible for scaling to the
// actual screen size when drawing.
type Driver struct {
Name string
menu *assetstore.Menu
// UI elements we need to drive
clickables []clickable
freezables []freezable
hoverables []hoverable
paintables []paintable
valueables []valueable
// The cursor in two different coordinate spaces: original, and screen-scaled
cursorOrig image.Point
cursorScaled image.Point
// These two matrices are used for scaling between the two
orig2native ebiten.GeoM
native2orig ebiten.GeoM
ticks int // Used in animation effects
tooltip string
}
func NewDriver(menu *assetstore.Menu) (*Driver, error) {
driver := &Driver{
Name: menu.Name,
menu: menu,
}
for _, record := range menu.Records() {
if err := driver.addRecord(record); err != nil {
return nil, err
}
}
return driver, nil
}
func (d *Driver) Value(id string, into *string) error {
for _, valueable := range d.valueables {
if valueable.id() == id {
*into = valueable.value()
return nil
}
}
return fmt.Errorf("Couldn't find valueable widget %q", id)
}
func (d *Driver) SetValue(id, value string) error {
for _, valueable := range d.valueables {
if valueable.id() == id {
valueable.setValue(value)
return nil
}
}
return fmt.Errorf("Couldn't find valueable widget %q", id)
}
func (d *Driver) ValueBool(id string, into *bool) error {
var vStr string
if err := d.Value(id, &vStr); err != nil {
return err
}
*into = vStr == "1"
return nil
}
func (d *Driver) SetValueBool(id string, value bool) error {
vStr := "0"
if value {
vStr = "1"
}
return d.SetValue(id, vStr)
}
func (d *Driver) SetFreeze(id string, value bool) error {
for _, freezable := range d.freezables {
if freezable.id() == id {
freezable.setFreezeState(value)
return nil
}
}
return fmt.Errorf("Couldn't find clickable widget %q", id)
}
func (d *Driver) OnClick(id string, f func()) error {
for _, clickable := range d.clickables {
if clickable.id() == id {
clickable.onClick(f)
return nil
}
}
return fmt.Errorf("Couldn't find clickable widget %q", id)
}
func (d *Driver) Update(screenX, screenY int) error {
// This will be updated while processing hovers
d.tooltip = ""
d.ticks += 1
// Update translation matrices
d.orig2native.Reset()
d.orig2native.Scale(float64(screenX)/OriginalX, float64(screenY)/OriginalY)
d.native2orig = d.orig2native
d.native2orig.Invert()
// Update original and scaled mouse coordinates
mouseX, mouseY := ebiten.CursorPosition()
d.cursorScaled = image.Pt(mouseX, mouseY)
mnX, mnY := d.native2orig.Apply(float64(mouseX), float64(mouseY))
d.cursorOrig = image.Pt(int(mnX), int(mnY))
// Dispatch notifications to our widgets
for _, hoverable := range d.hoverables {
inBounds := d.cursorOrig.In(hoverable.bounds())
d.hoverStartEvent(hoverable, inBounds)
d.hoverEndEvent(hoverable, inBounds)
if hoverable.hoverState() && hoverable.tooltip() != "" {
d.tooltip = hoverable.tooltip()
}
}
mouseIsDown := ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft)
for _, clickable := range d.clickables {
inBounds := d.cursorOrig.In(clickable.bounds())
mouseWasDown := clickable.mouseDownState()
d.mouseDownEvent(clickable, inBounds, mouseWasDown, mouseIsDown)
d.mouseClickEvent(clickable, inBounds, mouseWasDown, mouseIsDown)
d.mouseUpEvent(clickable, inBounds, mouseWasDown, mouseIsDown)
}
return nil
}
func (d *Driver) Draw(screen *ebiten.Image) error {
var do ebiten.DrawImageOptions
for _, paint := range d.paintables {
for _, region := range paint.regions(d.ticks) {
x, y := d.orig2native.Apply(float64(region.offset.X), float64(region.offset.Y))
do.GeoM = d.orig2native
do.GeoM.Translate(x, y)
if err := screen.DrawImage(region.image, &do); err != nil {
return err
}
}
}
if d.tooltip != "" {
x, y := d.cursorScaled.X+16, d.cursorScaled.Y-16
ebitenutil.DebugPrintAt(screen, d.tooltip, x, y)
}
return nil
}
func (d *Driver) addRecord(record *menus.Record) error {
log.Printf("Adding record: %#+v", record)
handler, ok := widgetBuilders[record.Type]
if !ok {
return fmt.Errorf("UI driver encountered unknown menu record: %#+v", record)
}
if handler != nil {
if err := handler(d, record); err != nil {
return err
}
}
// Recursively add all children of this record
for _, record := range record.Children {
if err := d.addRecord(record); err != nil {
return err
}
}
return nil
}
func (d *Driver) hoverStartEvent(h hoverable, inBounds bool) {
if inBounds && !h.hoverState() {
log.Printf("hoverable false -> true")
h.setHoverState(true)
}
}
func (d *Driver) hoverEndEvent(h hoverable, inBounds bool) {
if !inBounds && h.hoverState() {
log.Printf("hoverable true -> false")
h.setHoverState(false)
}
}
func (d *Driver) mouseDownEvent(c clickable, inBounds, wasDown, isDown bool) {
if inBounds && !wasDown && isDown {
log.Printf("mouse down false -> true")
c.setMouseDownState(true)
}
}
func (d *Driver) mouseClickEvent(c clickable, inBounds, wasDown, isDown bool) {
if inBounds && wasDown && !isDown {
log.Printf("mouse click")
c.registerMouseClick()
}
}
func (d *Driver) mouseUpEvent(c clickable, inBounds, wasDown, isDown bool) {
if inBounds {
if wasDown && !isDown {
log.Printf("mouse down true -> false")
c.setMouseDownState(false)
}
} else {
if wasDown {
log.Printf("mouse down true -> false")
c.setMouseDownState(false)
}
}
}

View File

@@ -1,175 +0,0 @@
package ui
import (
"fmt"
"image"
"log"
"github.com/hajimehoshi/ebiten"
"github.com/hajimehoshi/ebiten/ebitenutil"
"code.ur.gs/lupine/ordoor/internal/assetstore"
"code.ur.gs/lupine/ordoor/internal/menus"
)
// type Interface encapsulates a user interface, providing a means to track UI
// state, draw the interface, and execute code when the widgets are interacted
// with.
//
// The graphics for UI elements were all created with a 640x480 resolution in
// mind. The interface transparently scales them all to the current screen size
// to compensate.
type Interface struct {
Name string
menu *assetstore.Menu
static []*noninteractive
ticks int
widgets []*Widget
}
func NewInterface(menu *assetstore.Menu) (*Interface, error) {
iface := &Interface{
Name: menu.Name,
menu: menu,
}
for _, record := range menu.Records() {
if err := iface.addRecord(record); err != nil {
return nil, err
}
}
return iface, nil
}
// Find a widget by its hierarchical ID path
func (i *Interface) Widget(path string) (*Widget, error) {
for _, widget := range i.widgets {
if path == widget.path {
return widget, nil
}
}
return nil, fmt.Errorf("Couldn't find widget %v", path)
}
func (i *Interface) Update(screenX, screenY int) error {
// Used in animation effects
i.ticks += 1
mousePos := i.getMousePos(screenX, screenY)
// Iterate through all widgets, update mouse state
for _, widget := range i.widgets {
if widget.disabled {
continue // No activity for disabled widgets
}
mouseIsOver := mousePos.In(widget.Bounds)
widget.hovering(mouseIsOver)
widget.mouseButton(mouseIsOver && ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft))
}
return nil
}
func (i *Interface) Draw(screen *ebiten.Image) error {
var tooltip string // Draw this last, so it's on top of everything
mousePt := i.getMousePos(screen.Size())
geo := i.scale(screen.Size())
do := &ebiten.DrawImageOptions{GeoM: geo}
for _, s := range i.static {
if image := s.image(i.ticks); image != nil {
do.GeoM.Translate(geo.Apply(float64(s.bounds.Min.X), float64(s.bounds.Min.Y)))
if err := screen.DrawImage(image, do); err != nil {
return err
}
do.GeoM = geo
}
if mousePt.In(s.bounds) && s.tooltip != "" {
tooltip = s.tooltip
}
}
for _, widget := range i.widgets {
img, err := widget.Image(i.ticks / 2)
if err != nil {
return err
}
if img == nil {
continue
}
do.GeoM.Translate(geo.Apply(float64(widget.Bounds.Min.X), float64(widget.Bounds.Min.Y)))
if err := screen.DrawImage(img, do); err != nil {
return err
}
do.GeoM = geo
if widget.hoverState && widget.Tooltip != "" {
tooltip = widget.Tooltip
}
}
if tooltip != "" {
cX, cY := ebiten.CursorPosition()
ebitenutil.DebugPrintAt(screen, tooltip, cX+16, cY-16)
}
return nil
}
func (i *Interface) addRecord(record *menus.Record) error {
log.Printf("Adding record: %#+v", record)
handler, ok := setupHandlers[record.Type]
if !ok {
return fmt.Errorf("ui.interface: encountered unknown menu record: %#+v", record)
}
if handler != nil {
if err := handler(i, record); err != nil {
return err
}
}
// Recursively add all children
for _, record := range record.Children {
if err := i.addRecord(record); err != nil {
return err
}
}
return nil
}
// Works out how much we have to scale the current screen by to draw correctly
func (i *Interface) scale(w, h int) ebiten.GeoM {
var geo ebiten.GeoM
geo.Scale(float64(w)/640.0, float64(h)/480.0)
return geo
}
func (i *Interface) unscale(w, h int) ebiten.GeoM {
geo := i.scale(w, h)
geo.Invert()
return geo
}
// Returns the current position of the mouse in 640x480 coordinates. Needs the
// actual size of the screen to do so.
func (i *Interface) getMousePos(w, h int) image.Point {
cX, cY := ebiten.CursorPosition()
geo := i.unscale(w, h)
sX, sY := geo.Apply(float64(cX), float64(cY))
return image.Pt(int(sX), int(sY))
}

136
internal/ui/interfaces.go Normal file
View File

@@ -0,0 +1,136 @@
package ui
import (
"image"
"github.com/hajimehoshi/ebiten"
)
type region struct {
offset image.Point
image *ebiten.Image
}
func oneRegion(offset image.Point, image *ebiten.Image) []region {
return []region{{offset: offset, image: image}}
}
type idable interface {
id() string
}
// Clickable can be clicked by the left button of a mouse. Specify code to run
// with OnClick().
type clickable interface {
idable
bounds() image.Rectangle
onClick(f func())
// These are used to drive the state of the item
mouseDownState() bool
setMouseDownState(bool)
registerMouseClick()
}
// This implements the clickable interface except id(), bounds(), and registerMouseClick()
type clickImpl struct {
f func()
mouseDown bool
}
func (c *clickImpl) onClick(f func()) {
c.f = f
}
func (c *clickImpl) mouseDownState() bool {
return c.mouseDown
}
func (c *clickImpl) setMouseDownState(down bool) {
c.mouseDown = down
}
func (c *clickImpl) registerMouseClick() {
if c.f != nil {
c.f()
}
}
// Freezable represents a widget that can be enabled or disabled
type freezable interface {
idable
isFrozen() bool
setFreezeState(bool)
}
// This implements the freezable interface except id()
type freezeImpl struct {
frozen bool
}
func (f *freezeImpl) isFrozen() bool {
return f.frozen
}
func (f *freezeImpl) setFreezeState(frozen bool) {
f.frozen = frozen
}
// Hoverable can be hovered over by the mouse cursor.
//
// If something can be hovered, it can have a tooltip, so that is implemented
// here too.
type hoverable interface {
bounds() image.Rectangle
tooltip() string
// These are used to drive the state of the item
hoverState() bool
setHoverState(bool)
}
// Implements the hoverable interface with the exception of bounds()
type hoverImpl struct {
hovering bool
text string
}
func (h *hoverImpl) tooltip() string {
return h.text
}
func (h *hoverImpl) hoverState() bool {
return h.hovering
}
func (h *hoverImpl) setHoverState(hovering bool) {
h.hovering = hovering
}
// Paintable encapsulates one or more regions to be painted to the screen
type paintable interface {
regions(tick int) []region
}
// Valueable encapsulates the idea of an element with a value. Only strings are
// supported - #dealwithit for bools, ints, etc
type valueable interface {
idable
value() string
setValue(string)
}
type valueImpl struct {
str string
}
func (v *valueImpl) value() string {
return v.str
}
func (v *valueImpl) setValue(value string) {
v.str = value
}

View File

@@ -1,20 +1,97 @@
package ui
import (
"github.com/hajimehoshi/ebiten"
"image"
"code.ur.gs/lupine/ordoor/internal/menus"
)
func init() {
registerBuilder(menus.TypeStatic, registerStatic)
registerBuilder(menus.TypeHypertext, registerHypertext)
registerBuilder(menus.TypeAnimationSample, registerAnimation)
}
// A non-interactive element is not a widget; it merely displays some pixels and
// may optionally have a tooltip for display within bounds.
//
// For non-animated non-interactive elements, just give them a single frame.
type noninteractive struct {
bounds image.Rectangle
frames animation
tooltip string
frames animation
rect image.Rectangle
hoverImpl
}
func (n *noninteractive) image(step int) *ebiten.Image {
return n.frames.image(step)
func registerStatic(d *Driver, r *menus.Record) error {
// FIXME: SpriteID takes precedence over SHARE if present, but is that right?
spriteId := r.Share
if len(r.SpriteId) > 0 && r.SpriteId[0] != -1 {
spriteId = r.SpriteId[0]
}
sprite, err := d.menu.Sprite(spriteId)
if err != nil {
return err
}
ni := &noninteractive{
frames: animation{sprite.Image},
hoverImpl: hoverImpl{text: r.Desc},
rect: sprite.Rect,
}
d.hoverables = append(d.hoverables, ni)
d.paintables = append(d.paintables, ni)
return nil
}
func registerHypertext(d *Driver, r *menus.Record) error {
sprite, err := d.menu.Sprite(r.Share)
if err != nil {
return err
}
ni := &noninteractive{
frames: nil,
hoverImpl: hoverImpl{text: r.Desc},
rect: sprite.Rect,
}
d.hoverables = append(d.hoverables, ni)
return nil
}
// An animation is a non-interactive element that displays something in a loop
func registerAnimation(d *Driver, r *menus.Record) error {
sprite, err := d.menu.Sprite(r.SpriteId[0])
if err != nil {
return err
}
frames, err := d.menu.Images(r.SpriteId[0], r.DrawType)
if err != nil {
return err
}
ani := &noninteractive{
frames: animation(frames),
hoverImpl: hoverImpl{text: r.Desc},
rect: sprite.Rect,
}
d.hoverables = append(d.hoverables, ani)
d.paintables = append(d.paintables, ani)
return nil
}
func (n *noninteractive) bounds() image.Rectangle {
return n.rect
}
func (n *noninteractive) regions(tick int) []region {
return oneRegion(n.bounds().Min, n.frames.image(tick))
}

62
internal/ui/selectors.go Normal file
View File

@@ -0,0 +1,62 @@
package ui
import (
"code.ur.gs/lupine/ordoor/internal/menus"
)
func init() {
registerBuilder(menus.TypeCheckbox, registerCheckbox)
}
type checkbox struct {
button
valueImpl
}
// A checkbox has 3 sprites, and 3 states: unchecked, checked, disabled.
func registerCheckbox(d *Driver, r *menus.Record) error {
sprites, err := d.menu.Sprites(r.Share, 3) // unchecked, disabled, checked
if err != nil {
return err
}
checkbox := &checkbox{
button: button{
path: r.Path(),
baseSpr: sprites[0], // unchecked
clickSpr: sprites[2], // checked
frozenSpr: sprites[1],
hoverImpl: hoverImpl{text: r.Desc},
},
valueImpl: valueImpl{str: "0"},
}
d.clickables = append(d.clickables, checkbox)
d.freezables = append(d.freezables, checkbox)
d.hoverables = append(d.hoverables, checkbox)
d.paintables = append(d.paintables, checkbox)
d.valueables = append(d.valueables, checkbox)
return nil
}
func (c *checkbox) registerMouseClick() {
if c.value() == "1" { // Click disables
c.setValue("0")
} else { // Click enables
c.setValue("1")
}
}
func (c *checkbox) regions(tick int) []region {
if c.isFrozen() {
return oneRegion(c.bounds().Min, c.frozenSpr.Image)
}
if c.value() == "1" {
return oneRegion(c.bounds().Min, c.clickSpr.Image)
}
return oneRegion(c.bounds().Min, c.baseSpr.Image)
}

View File

@@ -1,229 +0,0 @@
package ui
import (
"github.com/hajimehoshi/ebiten"
"code.ur.gs/lupine/ordoor/internal/menus"
)
// Setup handlers know how to handle each type of widget.
// TODO: it might be better to have a Widget interface and different structs for
// each type of widget, but let's see how far we can push this model.
var setupHandlers = map[menus.MenuType]func(i *Interface, r *menus.Record) error{
menus.TypeStatic: handleStatic,
menus.TypeMenu: nil,
menus.TypeButton: handleButton,
menus.TypeInvokeButton: handleInvokeButton,
menus.TypeOverlay: nil, // FIXME: What's it for?
menus.TypeHypertext: handleHypertext,
menus.TypeCheckbox: handleCheckbox,
menus.TypeAnimationSample: handleAnimation,
menus.TypeMainButton: handleMainButton,
menus.TypeSlider: nil, // FIXME: handle this
}
func handleStatic(i *Interface, record *menus.Record) error {
spriteId := record.Share
// FIXME: SpriteID takes precedence over SHARE if present, but is that right?
if len(record.SpriteId) > 0 && record.SpriteId[0] != -1 {
spriteId = record.SpriteId[0]
}
sprite, err := i.menu.Sprite(spriteId)
if err != nil {
return err
}
static := &noninteractive{
bounds: sprite.Rect,
frames: animation{sprite.Image},
tooltip: record.Desc,
}
i.static = append(i.static, static)
return nil
}
// A hypertext is static, but we should only take the bounds from "SHARE", not
// display anything.
func handleHypertext(i *Interface, record *menus.Record) error {
sprite, err := i.menu.Sprite(record.Share)
if err != nil {
return err
}
static := &noninteractive{
bounds: sprite.Rect,
frames: nil,
tooltip: record.Desc,
}
i.static = append(i.static, static)
return nil
}
// A checkbox has 3 sprites, and 3 states: unchecked, checked, disabled.
func handleCheckbox(i *Interface, record *menus.Record) error {
widget, err := i.widgetFromRecord(record, record.Share)
if err != nil {
return err
}
unchecked := widget.sprite
disabled, err := i.menu.Sprite(record.Share + 1)
if err != nil {
return err
}
checked, err := i.menu.Sprite(record.Share + 2)
if err != nil {
return err
}
widget.Value = "0"
widget.OnMouseClick = func() {
if widget.Value == "1" { // Click disables
widget.Value = "0"
} else { // Click enables
widget.Value = "1"
}
}
widget.disabledImage = disabled.Image
widget.valueToImage = func() *ebiten.Image {
if widget.Value == "1" {
return checked.Image
}
return unchecked.Image
}
i.widgets = append(i.widgets, widget)
return nil
}
// An animation is a non-interactive element that displays something in a loop
func handleAnimation(i *Interface, record *menus.Record) error {
sprite, err := i.menu.Sprite(record.SpriteId[0])
if err != nil {
return err
}
frames, err := i.menu.Images(record.SpriteId[0], record.DrawType)
if err != nil {
return err
}
ani := &noninteractive{
bounds: sprite.Rect,
frames: animation(frames),
tooltip: record.Desc,
}
i.static = append(i.static, ani)
return nil
}
func handleButton(i *Interface, record *menus.Record) error {
spriteId := record.SpriteId[0]
widget, err := i.widgetFromRecord(record, spriteId)
if err != nil {
return err
}
pressed, err := i.menu.Sprite(spriteId + 1)
if err != nil {
return err
}
disabled, err := i.menu.Sprite(spriteId + 2)
if err != nil {
return err
}
widget.mouseButtonDownImage = pressed.Image
widget.disabledImage = disabled.Image
i.widgets = append(i.widgets, widget)
return nil
}
func handleInvokeButton(i *Interface, record *menus.Record) error {
widget, err := i.widgetFromRecord(record, record.Share)
if err != nil {
return err
}
pressed, err := i.menu.Sprite(record.Share + 1)
if err != nil {
return err
}
disabled, err := i.menu.Sprite(record.Share + 2)
if err != nil {
return err
}
widget.mouseButtonDownImage = pressed.Image
widget.disabledImage = disabled.Image
i.widgets = append(i.widgets, widget)
return nil
}
// A main button is quite complex. It has 3 main sprites and a hover animation
func handleMainButton(i *Interface, record *menus.Record) error {
widget, err := i.widgetFromRecord(record, record.Share)
if err != nil {
return err
}
pressed, err := i.menu.Sprite(record.Share + 1)
if err != nil {
return err
}
disabled, err := i.menu.Sprite(record.Share + 2)
if err != nil {
return err
}
hovers, err := i.menu.Images(record.SpriteId[0], record.DrawType)
if err != nil {
return err
}
widget.mouseButtonDownImage = pressed.Image
widget.disabledImage = disabled.Image
widget.hoverAnimation = animation(hovers)
i.widgets = append(i.widgets, widget)
return nil
}
// Widgets need a bounding box determined by a sprite. Different widgets specify
// their sprites in different attributes, so pass in the right sprite externally
func (i *Interface) widgetFromRecord(record *menus.Record, spriteId int) (*Widget, error) {
sprite, err := i.menu.Sprite(spriteId)
if err != nil {
return nil, err
}
widget := &Widget{
Bounds: sprite.Rect,
Tooltip: record.Desc,
path: record.Path(),
record: record,
sprite: sprite,
}
return widget, nil
}

View File

@@ -1,109 +0,0 @@
package ui
import (
"image"
"github.com/hajimehoshi/ebiten"
"code.ur.gs/lupine/ordoor/internal/assetstore"
"code.ur.gs/lupine/ordoor/internal/menus"
)
// Widget represents an interactive area of the screen. Backgrounds and other
// non-interactive areas are not widgets.
type Widget struct {
// Position on the screen in original (i.e., unscaled) coordinates
Bounds image.Rectangle
Tooltip string
Value string // #dealwithit for bools and ints and so on :p
OnHoverEnter func()
OnHoverLeave func()
// Mouse up can happen without a click taking place if, for instance, the
// mouse cursor leaves the bounds while still pressed.
OnMouseDown func()
OnMouseClick func()
OnMouseUp func()
disabled bool
disabledImage *ebiten.Image
// These are expected to have the same dimensions as the Bounds
hoverAnimation animation
hoverState bool
// FIXME: We assume right mouse button isn't needed here
// TODO: down, up, and click hooks.
mouseButtonDownImage *ebiten.Image
mouseButtonState bool
path string
record *menus.Record
sprite *assetstore.Sprite
valueToImage func() *ebiten.Image
}
func (w *Widget) Disable() {
w.hovering(false)
w.mouseButton(false)
w.disabled = true
}
func (w *Widget) hovering(value bool) {
if w.OnHoverEnter != nil && !w.hoverState && value {
w.OnHoverEnter()
}
if w.OnHoverLeave != nil && w.hoverState && !value {
w.OnHoverLeave()
}
w.hoverState = value
return
}
func (w *Widget) mouseButton(value bool) {
if w.OnMouseDown != nil && !w.mouseButtonState && value {
w.OnMouseDown()
}
if w.mouseButtonState && !value {
if w.OnMouseClick != nil && w.hoverState {
w.OnMouseClick()
}
if w.OnMouseUp != nil {
w.OnMouseUp()
}
}
w.mouseButtonState = value
}
func (w *Widget) Image(aniStep int) (*ebiten.Image, error) {
if w.disabled {
if w.disabledImage != nil {
return w.disabledImage, nil
}
return w.sprite.Image, nil
}
if w.mouseButtonDownImage != nil && w.hoverState && w.mouseButtonState {
return w.mouseButtonDownImage, nil
}
if w.hoverState && w.hoverAnimation != nil {
return w.hoverAnimation.image(aniStep), nil
}
if w.valueToImage != nil {
return w.valueToImage(), nil
}
return w.sprite.Image, nil
}