mirror of
https://github.com/OpenDiablo2/OpenDiablo2
synced 2024-11-02 09:17:19 -04:00
ca45be0948
* adds error handling, returns early from funcs where it makes sense * fixes build errors * merge ballresin PR with upstream * adds error handling, returns early from funcs where it makes sense * fixes build errors * merge ballresin PR with upstream Co-authored-by: dknuth <dknuth0101@gmail.com>
465 lines
12 KiB
Go
465 lines
12 KiB
Go
package d2ui
|
|
|
|
import (
|
|
"fmt"
|
|
"image"
|
|
"log"
|
|
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource"
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2gui"
|
|
)
|
|
|
|
// ButtonType defines the type of button
|
|
type ButtonType int
|
|
|
|
// ButtonType constants
|
|
const (
|
|
ButtonTypeWide ButtonType = 1
|
|
ButtonTypeMedium ButtonType = 2
|
|
ButtonTypeNarrow ButtonType = 3
|
|
ButtonTypeCancel ButtonType = 4
|
|
ButtonTypeTall ButtonType = 5
|
|
ButtonTypeShort ButtonType = 6
|
|
ButtonTypeOkCancel ButtonType = 7
|
|
|
|
// Game UI
|
|
|
|
ButtonTypeSkill ButtonType = 7
|
|
ButtonTypeRun ButtonType = 8
|
|
ButtonTypeMenu ButtonType = 9
|
|
ButtonTypeGoldCoin ButtonType = 10
|
|
ButtonTypeClose ButtonType = 11
|
|
ButtonTypeSecondaryInvHand ButtonType = 12
|
|
ButtonTypeMinipanelCharacter ButtonType = 13
|
|
ButtonTypeMinipanelInventory ButtonType = 14
|
|
ButtonTypeMinipanelSkill ButtonType = 15
|
|
ButtonTypeMinipanelAutomap ButtonType = 16
|
|
ButtonTypeMinipanelMessage ButtonType = 17
|
|
ButtonTypeMinipanelQuest ButtonType = 18
|
|
ButtonTypeMinipanelMen ButtonType = 19
|
|
ButtonTypeSquareClose ButtonType = 20
|
|
)
|
|
|
|
const (
|
|
greyAlpha100 = 0x646464ff
|
|
lightGreyAlpha75 = 0x808080c3
|
|
)
|
|
|
|
// ButtonLayout defines the type of buttons
|
|
type ButtonLayout struct {
|
|
ResourceName string
|
|
PaletteName string
|
|
FontPath string
|
|
XSegments int
|
|
YSegments int
|
|
BaseFrame int
|
|
DisabledFrame int
|
|
ClickableRect *image.Rectangle
|
|
TextOffset int
|
|
Toggleable bool
|
|
AllowFrameChange bool
|
|
}
|
|
|
|
const (
|
|
buttonWideSegmentsX = 2
|
|
buttonWideSegmentsY = 1
|
|
buttonWideDisabledFrame = -1
|
|
buttonWideTextOffset = 1
|
|
|
|
buttonShortSegmentsX = 1
|
|
buttonShortSegmentsY = 1
|
|
buttonShortDisabledFrame = -1
|
|
buttonShortTextOffset = -1
|
|
|
|
buttonMediumSegmentsX = 1
|
|
buttonMediumSegmentsY = 1
|
|
|
|
buttonTallSegmentsX = 1
|
|
buttonTallSegmentsY = 1
|
|
buttonTallTextOffset = 5
|
|
|
|
buttonOkCancelSegmentsX = 1
|
|
buttonOkCancelSegmentsY = 1
|
|
buttonOkCancelDisabledFrame = -1
|
|
|
|
buttonBuySellSegmentsX = 1
|
|
buttonBuySellSegmentsY = 1
|
|
buttonBuySellDisabledFrame = 1
|
|
|
|
buttonRunSegmentsX = 1
|
|
buttonRunSegmentsY = 1
|
|
buttonRunDisabledFrame = -1
|
|
|
|
pressedButtonOffset = 2
|
|
)
|
|
|
|
func getButtonLayouts() map[ButtonType]ButtonLayout {
|
|
return map[ButtonType]ButtonLayout{
|
|
ButtonTypeWide: {
|
|
XSegments: buttonWideSegmentsX,
|
|
YSegments: buttonWideSegmentsY,
|
|
DisabledFrame: buttonWideDisabledFrame,
|
|
TextOffset: buttonWideTextOffset,
|
|
ResourceName: d2resource.WideButtonBlank,
|
|
PaletteName: d2resource.PaletteUnits,
|
|
FontPath: d2resource.FontExocet10,
|
|
AllowFrameChange: true,
|
|
},
|
|
ButtonTypeShort: {
|
|
XSegments: buttonShortSegmentsX,
|
|
YSegments: buttonShortSegmentsY,
|
|
DisabledFrame: buttonShortDisabledFrame,
|
|
TextOffset: buttonShortTextOffset,
|
|
ResourceName: d2resource.ShortButtonBlank,
|
|
PaletteName: d2resource.PaletteUnits,
|
|
FontPath: d2resource.FontRediculous,
|
|
AllowFrameChange: true,
|
|
},
|
|
ButtonTypeMedium: {
|
|
XSegments: buttonMediumSegmentsX,
|
|
YSegments: buttonMediumSegmentsY,
|
|
ResourceName: d2resource.MediumButtonBlank,
|
|
PaletteName: d2resource.PaletteUnits,
|
|
FontPath: d2resource.FontExocet10,
|
|
AllowFrameChange: true,
|
|
},
|
|
ButtonTypeTall: {
|
|
XSegments: buttonTallSegmentsX,
|
|
YSegments: buttonTallSegmentsY,
|
|
TextOffset: buttonTallTextOffset,
|
|
ResourceName: d2resource.TallButtonBlank,
|
|
PaletteName: d2resource.PaletteUnits,
|
|
FontPath: d2resource.FontExocet10,
|
|
AllowFrameChange: true,
|
|
},
|
|
ButtonTypeOkCancel: {
|
|
XSegments: buttonOkCancelSegmentsX,
|
|
YSegments: buttonOkCancelSegmentsY,
|
|
DisabledFrame: buttonOkCancelDisabledFrame,
|
|
ResourceName: d2resource.CancelButton,
|
|
PaletteName: d2resource.PaletteUnits,
|
|
FontPath: d2resource.FontRediculous,
|
|
AllowFrameChange: true,
|
|
},
|
|
ButtonTypeRun: {
|
|
XSegments: buttonRunSegmentsX,
|
|
YSegments: buttonRunSegmentsY,
|
|
DisabledFrame: buttonRunDisabledFrame,
|
|
ResourceName: d2resource.RunButton,
|
|
PaletteName: d2resource.PaletteSky,
|
|
Toggleable: true,
|
|
FontPath: d2resource.FontRediculous,
|
|
AllowFrameChange: true,
|
|
},
|
|
ButtonTypeSquareClose: {
|
|
XSegments: buttonBuySellSegmentsX,
|
|
YSegments: buttonBuySellSegmentsY,
|
|
DisabledFrame: buttonBuySellDisabledFrame,
|
|
ResourceName: d2resource.BuySellButton,
|
|
PaletteName: d2resource.PaletteUnits,
|
|
Toggleable: true,
|
|
FontPath: d2resource.Font30,
|
|
AllowFrameChange: true,
|
|
BaseFrame: 10,
|
|
},
|
|
}
|
|
}
|
|
|
|
var _ Widget = &Button{} // static check to ensure button implements widget
|
|
|
|
// Button defines a standard wide UI button
|
|
type Button struct {
|
|
manager *UIManager
|
|
buttonLayout ButtonLayout
|
|
normalSurface d2interface.Surface
|
|
pressedSurface d2interface.Surface
|
|
toggledSurface d2interface.Surface
|
|
pressedToggledSurface d2interface.Surface
|
|
disabledSurface d2interface.Surface
|
|
x int
|
|
y int
|
|
width int
|
|
height int
|
|
onClick func()
|
|
enabled bool
|
|
visible bool
|
|
pressed bool
|
|
toggled bool
|
|
}
|
|
|
|
// NewButton creates an instance of Button
|
|
func (ui *UIManager) NewButton(buttonType ButtonType, text string) *Button {
|
|
btn := &Button{
|
|
width: 0,
|
|
height: 0,
|
|
visible: true,
|
|
enabled: true,
|
|
pressed: false,
|
|
}
|
|
|
|
buttonLayout := getButtonLayouts()[buttonType]
|
|
btn.buttonLayout = buttonLayout
|
|
lbl := ui.NewLabel(buttonLayout.FontPath, d2resource.PaletteUnits)
|
|
|
|
lbl.SetText(text)
|
|
lbl.Color[0] = d2util.Color(greyAlpha100)
|
|
lbl.Alignment = d2gui.HorizontalAlignCenter
|
|
|
|
buttonSprite, err := ui.NewSprite(buttonLayout.ResourceName, buttonLayout.PaletteName)
|
|
if err != nil {
|
|
log.Print(err)
|
|
return nil
|
|
}
|
|
|
|
for i := 0; i < buttonLayout.XSegments; i++ {
|
|
w, _, err := buttonSprite.GetFrameSize(i)
|
|
if err != nil {
|
|
log.Print(err)
|
|
return nil
|
|
}
|
|
btn.width += w
|
|
}
|
|
|
|
for i := 0; i < buttonLayout.YSegments; i++ {
|
|
_, h, err := buttonSprite.GetFrameSize(i * buttonLayout.YSegments)
|
|
if err != nil {
|
|
log.Print(err)
|
|
return nil
|
|
}
|
|
|
|
btn.height += h
|
|
}
|
|
|
|
btn.normalSurface, err = ui.renderer.NewSurface(btn.width, btn.height, d2enum.FilterNearest)
|
|
if err != nil {
|
|
log.Print(err)
|
|
return nil
|
|
}
|
|
|
|
buttonSprite.SetPosition(0, 0)
|
|
buttonSprite.SetEffect(d2enum.DrawEffectModulate)
|
|
|
|
ui.addWidget(btn) // important that this comes before renderFrames!
|
|
|
|
btn.renderFrames(buttonSprite, &buttonLayout, lbl)
|
|
|
|
return btn
|
|
}
|
|
|
|
func (v *Button) renderFrames(btnSprite *Sprite, btnLayout *ButtonLayout, label *Label) {
|
|
var err error
|
|
totalButtonTypes := btnSprite.GetFrameCount() / (btnLayout.XSegments * btnLayout.YSegments)
|
|
|
|
err = btnSprite.RenderSegmented(v.normalSurface, btnLayout.XSegments, btnLayout.YSegments, btnLayout.BaseFrame)
|
|
|
|
if err != nil {
|
|
fmt.Printf("failed to render button normalSurface, err: %v\n", err)
|
|
}
|
|
|
|
_, labelHeight := label.GetSize()
|
|
textY := half(v.height - labelHeight)
|
|
xOffset := half(v.width)
|
|
|
|
label.SetPosition(xOffset, textY)
|
|
label.Render(v.normalSurface)
|
|
|
|
if btnLayout.AllowFrameChange {
|
|
frameOffset := 0
|
|
xSeg, ySeg, baseFrame := btnLayout.XSegments, btnLayout.YSegments, btnLayout.BaseFrame
|
|
|
|
totalButtonTypes--
|
|
if totalButtonTypes > 0 { // button has more than one type
|
|
frameOffset++
|
|
|
|
v.pressedSurface, err = v.manager.renderer.NewSurface(v.width, v.height,
|
|
d2enum.FilterNearest)
|
|
if err != nil {
|
|
log.Print(err)
|
|
}
|
|
|
|
err = btnSprite.RenderSegmented(v.pressedSurface, xSeg, ySeg, baseFrame+frameOffset)
|
|
if err != nil {
|
|
fmt.Printf("failed to render button pressedSurface, err: %v\n", err)
|
|
}
|
|
|
|
label.SetPosition(xOffset-pressedButtonOffset, textY+pressedButtonOffset)
|
|
label.Render(v.pressedSurface)
|
|
}
|
|
|
|
if btnLayout.ResourceName == d2resource.BuySellButton {
|
|
// Without returning early, the button UI gets all subsequent (unrelated) frames stacked on top
|
|
// Only 2 frames from this sprite are applicable to the button in question
|
|
// The presentation is incorrect without this hack
|
|
return
|
|
}
|
|
|
|
totalButtonTypes--
|
|
if totalButtonTypes > 0 { // button has more than two types
|
|
frameOffset++
|
|
|
|
v.toggledSurface, err = v.manager.renderer.NewSurface(v.width, v.height,
|
|
d2enum.FilterNearest)
|
|
if err != nil {
|
|
log.Print(err)
|
|
}
|
|
|
|
err = btnSprite.RenderSegmented(v.pressedSurface, xSeg, ySeg, baseFrame+frameOffset)
|
|
if err != nil {
|
|
fmt.Printf("failed to render button toggledSurface, err: %v\n", err)
|
|
}
|
|
|
|
label.SetPosition(xOffset, textY)
|
|
label.Render(v.toggledSurface)
|
|
}
|
|
|
|
totalButtonTypes--
|
|
if totalButtonTypes > 0 { // button has more than three types
|
|
frameOffset++
|
|
|
|
v.pressedToggledSurface, err = v.manager.renderer.NewSurface(v.width, v.height,
|
|
d2enum.FilterNearest)
|
|
if err != nil {
|
|
log.Print(err)
|
|
}
|
|
|
|
err = btnSprite.RenderSegmented(v.pressedSurface, xSeg, ySeg, baseFrame+frameOffset)
|
|
if err != nil {
|
|
fmt.Printf("failed to render button pressedToggledSurface, err: %v\n", err)
|
|
}
|
|
|
|
label.SetPosition(xOffset, textY)
|
|
label.Render(v.pressedToggledSurface)
|
|
}
|
|
|
|
if btnLayout.DisabledFrame != -1 {
|
|
v.disabledSurface, err = v.manager.renderer.NewSurface(v.width, v.height,
|
|
d2enum.FilterNearest)
|
|
if err != nil {
|
|
log.Print(err)
|
|
}
|
|
|
|
err = btnSprite.RenderSegmented(v.disabledSurface, xSeg, ySeg, btnLayout.DisabledFrame)
|
|
if err != nil {
|
|
fmt.Printf("failed to render button disabledSurface, err: %v\n", err)
|
|
}
|
|
|
|
label.SetPosition(xOffset, textY)
|
|
label.Render(v.disabledSurface)
|
|
}
|
|
}
|
|
}
|
|
|
|
// bindManager binds the button to the UI manager
|
|
func (v *Button) bindManager(manager *UIManager) {
|
|
v.manager = manager
|
|
}
|
|
|
|
// OnActivated defines the callback handler for the activate event
|
|
func (v *Button) OnActivated(callback func()) {
|
|
v.onClick = callback
|
|
}
|
|
|
|
// Activate calls the on activated callback handler, if any
|
|
func (v *Button) Activate() {
|
|
if v.onClick == nil {
|
|
return
|
|
}
|
|
|
|
v.onClick()
|
|
}
|
|
|
|
// Render renders the button
|
|
func (v *Button) Render(target d2interface.Surface) error {
|
|
target.PushFilter(d2enum.FilterNearest)
|
|
defer target.Pop()
|
|
|
|
target.PushTranslation(v.x, v.y)
|
|
defer target.Pop()
|
|
|
|
var err error
|
|
|
|
switch {
|
|
case !v.enabled:
|
|
target.PushColor(d2util.Color(lightGreyAlpha75))
|
|
defer target.Pop()
|
|
err = target.Render(v.disabledSurface)
|
|
case v.toggled && v.pressed:
|
|
err = target.Render(v.pressedToggledSurface)
|
|
case v.pressed:
|
|
err = target.Render(v.pressedSurface)
|
|
case v.toggled:
|
|
err = target.Render(v.toggledSurface)
|
|
default:
|
|
err = target.Render(v.normalSurface)
|
|
}
|
|
|
|
if err != nil {
|
|
fmt.Printf("failed to render button surface, err: %v\n", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Toggle negates the toggled state of the button
|
|
func (v *Button) Toggle() {
|
|
v.toggled = !v.toggled
|
|
}
|
|
|
|
// Advance advances the button state
|
|
func (v *Button) Advance(_ float64) error {
|
|
return nil
|
|
}
|
|
|
|
// GetEnabled returns the enabled state
|
|
func (v *Button) GetEnabled() bool {
|
|
return v.enabled
|
|
}
|
|
|
|
// SetEnabled sets the enabled state
|
|
func (v *Button) SetEnabled(enabled bool) {
|
|
v.enabled = enabled
|
|
}
|
|
|
|
// GetSize returns the size of the button
|
|
func (v *Button) GetSize() (width, height int) {
|
|
return v.width, v.height
|
|
}
|
|
|
|
// SetPosition moves the button
|
|
func (v *Button) SetPosition(x, y int) {
|
|
v.x = x
|
|
v.y = y
|
|
}
|
|
|
|
// GetPosition returns the location of the button
|
|
func (v *Button) GetPosition() (x, y int) {
|
|
return v.x, v.y
|
|
}
|
|
|
|
// GetVisible returns the visibility of the button
|
|
func (v *Button) GetVisible() bool {
|
|
return v.visible
|
|
}
|
|
|
|
// SetVisible sets the visibility of the button
|
|
func (v *Button) SetVisible(visible bool) {
|
|
v.visible = visible
|
|
}
|
|
|
|
// GetPressed returns the pressed state of the button
|
|
func (v *Button) GetPressed() bool {
|
|
return v.pressed
|
|
}
|
|
|
|
// SetPressed sets the pressed state of the button
|
|
func (v *Button) SetPressed(pressed bool) {
|
|
v.pressed = pressed
|
|
}
|
|
|
|
func half(n int) int {
|
|
return n / 2
|
|
}
|