From 37ae98d81b793395081a366ac2d99256458beb51 Mon Sep 17 00:00:00 2001 From: David Carrell Date: Tue, 23 Jun 2020 17:12:08 -0500 Subject: [PATCH] Abstract away remaining ebiten references (#409) * 337 - remove ebiten from character selection * 337 - abstract d2input away from ebiten implementation * WIP 337 - remove ebiten use from d2ui * 337 - fix accidental left->right change * 337 - fix ui button selection bugs * 337 - fix textbox bugs * 337 - fix scrollbar bugs * 337 - address PR comments * 337 - fix invalid hero selection bug Co-authored-by: David Carrell --- CONTRIBUTORS | 1 + d2core/d2input/d2input.go | 140 ++------------------ d2core/d2input/ebiten/ebiten_input.go | 156 +++++++++++++++++++++++ d2core/d2input/input_manager.go | 71 ++++++----- d2core/d2input/key.go | 138 ++++++++++++++++++++ d2core/d2ui/d2ui.go | 154 ++++++++++------------ d2core/d2ui/scrollbar.go | 4 +- d2core/d2ui/textbox.go | 72 ++++++----- d2game/d2gamescreen/character_select.go | 58 ++++----- d2game/d2gamescreen/main_menu.go | 30 +++-- d2game/d2gamescreen/select_hero_class.go | 15 +-- main.go | 3 + 12 files changed, 506 insertions(+), 336 deletions(-) create mode 100644 d2core/d2input/ebiten/ebiten_input.go create mode 100644 d2core/d2input/key.go diff --git a/CONTRIBUTORS b/CONTRIBUTORS index c5e0cb9f..3900c606 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -17,6 +17,7 @@ Intyre Gürkan Kaymak Maxime "malavv" Lavigne Ripolak +dafe * DIABLO2 LOGO Jose Pardilla (th3-prophetman) diff --git a/d2core/d2input/d2input.go b/d2core/d2input/d2input.go index 85814136..15c0268b 100644 --- a/d2core/d2input/d2input.go +++ b/d2core/d2input/d2input.go @@ -2,8 +2,6 @@ package d2input import ( "errors" - - "github.com/hajimehoshi/ebiten" ) var ( @@ -19,136 +17,6 @@ const ( PriorityHigh ) -type Key int - -//noinspection GoUnusedConst -const ( - Key0 = Key(ebiten.Key0) - Key1 = Key(ebiten.Key1) - Key2 = Key(ebiten.Key2) - Key3 = Key(ebiten.Key3) - Key4 = Key(ebiten.Key4) - Key5 = Key(ebiten.Key5) - Key6 = Key(ebiten.Key6) - Key7 = Key(ebiten.Key7) - Key8 = Key(ebiten.Key8) - Key9 = Key(ebiten.Key9) - KeyA = Key(ebiten.KeyA) - KeyB = Key(ebiten.KeyB) - KeyC = Key(ebiten.KeyC) - KeyD = Key(ebiten.KeyD) - KeyE = Key(ebiten.KeyE) - KeyF = Key(ebiten.KeyF) - KeyG = Key(ebiten.KeyG) - KeyH = Key(ebiten.KeyH) - KeyI = Key(ebiten.KeyI) - KeyJ = Key(ebiten.KeyJ) - KeyK = Key(ebiten.KeyK) - KeyL = Key(ebiten.KeyL) - KeyM = Key(ebiten.KeyM) - KeyN = Key(ebiten.KeyN) - KeyO = Key(ebiten.KeyO) - KeyP = Key(ebiten.KeyP) - KeyQ = Key(ebiten.KeyQ) - KeyR = Key(ebiten.KeyR) - KeyS = Key(ebiten.KeyS) - KeyT = Key(ebiten.KeyT) - KeyU = Key(ebiten.KeyU) - KeyV = Key(ebiten.KeyV) - KeyW = Key(ebiten.KeyW) - KeyX = Key(ebiten.KeyX) - KeyY = Key(ebiten.KeyY) - KeyZ = Key(ebiten.KeyZ) - KeyApostrophe = Key(ebiten.KeyApostrophe) - KeyBackslash = Key(ebiten.KeyBackslash) - KeyBackspace = Key(ebiten.KeyBackspace) - KeyCapsLock = Key(ebiten.KeyCapsLock) - KeyComma = Key(ebiten.KeyComma) - KeyDelete = Key(ebiten.KeyDelete) - KeyDown = Key(ebiten.KeyDown) - KeyEnd = Key(ebiten.KeyEnd) - KeyEnter = Key(ebiten.KeyEnter) - KeyEqual = Key(ebiten.KeyEqual) - KeyEscape = Key(ebiten.KeyEscape) - KeyF1 = Key(ebiten.KeyF1) - KeyF2 = Key(ebiten.KeyF2) - KeyF3 = Key(ebiten.KeyF3) - KeyF4 = Key(ebiten.KeyF4) - KeyF5 = Key(ebiten.KeyF5) - KeyF6 = Key(ebiten.KeyF6) - KeyF7 = Key(ebiten.KeyF7) - KeyF8 = Key(ebiten.KeyF8) - KeyF9 = Key(ebiten.KeyF9) - KeyF10 = Key(ebiten.KeyF10) - KeyF11 = Key(ebiten.KeyF11) - KeyF12 = Key(ebiten.KeyF12) - KeyGraveAccent = Key(ebiten.KeyGraveAccent) - KeyHome = Key(ebiten.KeyHome) - KeyInsert = Key(ebiten.KeyInsert) - KeyKP0 = Key(ebiten.KeyKP0) - KeyKP1 = Key(ebiten.KeyKP1) - KeyKP2 = Key(ebiten.KeyKP2) - KeyKP3 = Key(ebiten.KeyKP3) - KeyKP4 = Key(ebiten.KeyKP4) - KeyKP5 = Key(ebiten.KeyKP5) - KeyKP6 = Key(ebiten.KeyKP6) - KeyKP7 = Key(ebiten.KeyKP7) - KeyKP8 = Key(ebiten.KeyKP8) - KeyKP9 = Key(ebiten.KeyKP9) - KeyKPAdd = Key(ebiten.KeyKPAdd) - KeyKPDecimal = Key(ebiten.KeyKPDecimal) - KeyKPDivide = Key(ebiten.KeyKPDivide) - KeyKPEnter = Key(ebiten.KeyKPEnter) - KeyKPEqual = Key(ebiten.KeyKPEqual) - KeyKPMultiply = Key(ebiten.KeyKPMultiply) - KeyKPSubtract = Key(ebiten.KeyKPSubtract) - KeyLeft = Key(ebiten.KeyLeft) - KeyLeftBracket = Key(ebiten.KeyLeftBracket) - KeyMenu = Key(ebiten.KeyMenu) - KeyMinus = Key(ebiten.KeyMinus) - KeyNumLock = Key(ebiten.KeyNumLock) - KeyPageDown = Key(ebiten.KeyPageDown) - KeyPageUp = Key(ebiten.KeyPageUp) - KeyPause = Key(ebiten.KeyPause) - KeyPeriod = Key(ebiten.KeyPeriod) - KeyPrintScreen = Key(ebiten.KeyPrintScreen) - KeyRight = Key(ebiten.KeyRight) - KeyRightBracket = Key(ebiten.KeyRightBracket) - KeyScrollLock = Key(ebiten.KeyScrollLock) - KeySemicolon = Key(ebiten.KeySemicolon) - KeySlash = Key(ebiten.KeySlash) - KeySpace = Key(ebiten.KeySpace) - KeyTab = Key(ebiten.KeyTab) - KeyUp = Key(ebiten.KeyUp) - KeyAlt = Key(ebiten.KeyAlt) - KeyControl = Key(ebiten.KeyControl) - KeyShift = Key(ebiten.KeyShift) -) - -type KeyMod int - -const ( - KeyModAlt = 1 << iota - KeyModControl - KeyModShift -) - -type MouseButton int - -const ( - MouseButtonLeft = MouseButton(ebiten.MouseButtonLeft) - MouseButtonMiddle = MouseButton(ebiten.MouseButtonMiddle) - MouseButtonRight = MouseButton(ebiten.MouseButtonRight) -) - -type MouseButtonMod int - -const ( - MouseButtonModLeft MouseButtonMod = 1 << iota - MouseButtonModMiddle - MouseButtonModRight -) - type HandlerEvent struct { KeyMod KeyMod ButtonMod MouseButtonMod @@ -159,6 +27,8 @@ type HandlerEvent struct { type KeyEvent struct { HandlerEvent Key Key + // Duration represents the number of frames this key has been pressed for + Duration int } type KeyCharsEvent struct { @@ -211,6 +81,12 @@ type MouseMoveHandler interface { var singleton inputManager +func Initialize(inputService InputService) { + singleton = inputManager{ + inputService: inputService, + } +} + func Advance(elapsed float64) error { return singleton.advance(elapsed) } diff --git a/d2core/d2input/ebiten/ebiten_input.go b/d2core/d2input/ebiten/ebiten_input.go new file mode 100644 index 00000000..12205b17 --- /dev/null +++ b/d2core/d2input/ebiten/ebiten_input.go @@ -0,0 +1,156 @@ +package ebiten + +import ( + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2input" + "github.com/hajimehoshi/ebiten" + "github.com/hajimehoshi/ebiten/inpututil" +) + +var ( + keyToEbiten = map[d2input.Key]ebiten.Key{ + d2input.Key0: ebiten.Key0, + d2input.Key1: ebiten.Key1, + d2input.Key2: ebiten.Key2, + d2input.Key3: ebiten.Key3, + d2input.Key4: ebiten.Key4, + d2input.Key5: ebiten.Key5, + d2input.Key6: ebiten.Key6, + d2input.Key7: ebiten.Key7, + d2input.Key8: ebiten.Key8, + d2input.Key9: ebiten.Key9, + d2input.KeyA: ebiten.KeyA, + d2input.KeyB: ebiten.KeyB, + d2input.KeyC: ebiten.KeyC, + d2input.KeyD: ebiten.KeyD, + d2input.KeyE: ebiten.KeyE, + d2input.KeyF: ebiten.KeyF, + d2input.KeyG: ebiten.KeyG, + d2input.KeyH: ebiten.KeyH, + d2input.KeyI: ebiten.KeyI, + d2input.KeyJ: ebiten.KeyJ, + d2input.KeyK: ebiten.KeyK, + d2input.KeyL: ebiten.KeyL, + d2input.KeyM: ebiten.KeyM, + d2input.KeyN: ebiten.KeyN, + d2input.KeyO: ebiten.KeyO, + d2input.KeyP: ebiten.KeyP, + d2input.KeyQ: ebiten.KeyQ, + d2input.KeyR: ebiten.KeyR, + d2input.KeyS: ebiten.KeyS, + d2input.KeyT: ebiten.KeyT, + d2input.KeyU: ebiten.KeyU, + d2input.KeyV: ebiten.KeyV, + d2input.KeyW: ebiten.KeyW, + d2input.KeyX: ebiten.KeyX, + d2input.KeyY: ebiten.KeyY, + d2input.KeyZ: ebiten.KeyZ, + d2input.KeyApostrophe: ebiten.KeyApostrophe, + d2input.KeyBackslash: ebiten.KeyBackslash, + d2input.KeyBackspace: ebiten.KeyBackspace, + d2input.KeyCapsLock: ebiten.KeyCapsLock, + d2input.KeyComma: ebiten.KeyComma, + d2input.KeyDelete: ebiten.KeyDelete, + d2input.KeyDown: ebiten.KeyDown, + d2input.KeyEnd: ebiten.KeyEnd, + d2input.KeyEnter: ebiten.KeyEnter, + d2input.KeyEqual: ebiten.KeyEqual, + d2input.KeyEscape: ebiten.KeyEscape, + d2input.KeyF1: ebiten.KeyF1, + d2input.KeyF2: ebiten.KeyF2, + d2input.KeyF3: ebiten.KeyF3, + d2input.KeyF4: ebiten.KeyF4, + d2input.KeyF5: ebiten.KeyF5, + d2input.KeyF6: ebiten.KeyF6, + d2input.KeyF7: ebiten.KeyF7, + d2input.KeyF8: ebiten.KeyF8, + d2input.KeyF9: ebiten.KeyF9, + d2input.KeyF10: ebiten.KeyF10, + d2input.KeyF11: ebiten.KeyF11, + d2input.KeyF12: ebiten.KeyF12, + d2input.KeyGraveAccent: ebiten.KeyGraveAccent, + d2input.KeyHome: ebiten.KeyHome, + d2input.KeyInsert: ebiten.KeyInsert, + d2input.KeyKP0: ebiten.KeyKP0, + d2input.KeyKP1: ebiten.KeyKP1, + d2input.KeyKP2: ebiten.KeyKP2, + d2input.KeyKP3: ebiten.KeyKP3, + d2input.KeyKP4: ebiten.KeyKP4, + d2input.KeyKP5: ebiten.KeyKP5, + d2input.KeyKP6: ebiten.KeyKP6, + d2input.KeyKP7: ebiten.KeyKP7, + d2input.KeyKP8: ebiten.KeyKP8, + d2input.KeyKP9: ebiten.KeyKP9, + d2input.KeyKPAdd: ebiten.KeyKPAdd, + d2input.KeyKPDecimal: ebiten.KeyKPDecimal, + d2input.KeyKPDivide: ebiten.KeyKPDivide, + d2input.KeyKPEnter: ebiten.KeyKPEnter, + d2input.KeyKPEqual: ebiten.KeyKPEqual, + d2input.KeyKPMultiply: ebiten.KeyKPMultiply, + d2input.KeyKPSubtract: ebiten.KeyKPSubtract, + d2input.KeyLeft: ebiten.KeyLeft, + d2input.KeyLeftBracket: ebiten.KeyLeftBracket, + d2input.KeyMenu: ebiten.KeyMenu, + d2input.KeyMinus: ebiten.KeyMinus, + d2input.KeyNumLock: ebiten.KeyNumLock, + d2input.KeyPageDown: ebiten.KeyPageDown, + d2input.KeyPageUp: ebiten.KeyPageUp, + d2input.KeyPause: ebiten.KeyPause, + d2input.KeyPeriod: ebiten.KeyPeriod, + d2input.KeyPrintScreen: ebiten.KeyPrintScreen, + d2input.KeyRight: ebiten.KeyRight, + d2input.KeyRightBracket: ebiten.KeyRightBracket, + d2input.KeyScrollLock: ebiten.KeyScrollLock, + d2input.KeySemicolon: ebiten.KeySemicolon, + d2input.KeySlash: ebiten.KeySlash, + d2input.KeySpace: ebiten.KeySpace, + d2input.KeyTab: ebiten.KeyTab, + d2input.KeyUp: ebiten.KeyUp, + d2input.KeyAlt: ebiten.KeyAlt, + d2input.KeyControl: ebiten.KeyControl, + d2input.KeyShift: ebiten.KeyShift, + } + mouseButtonToEbiten = map[d2input.MouseButton]ebiten.MouseButton{ + d2input.MouseButtonLeft: ebiten.MouseButtonLeft, + d2input.MouseButtonMiddle: ebiten.MouseButtonMiddle, + d2input.MouseButtonRight: ebiten.MouseButtonRight, + } +) + +// InputService provides an abstraction on ebiten to support handling input events +type InputService struct{} + +func (is InputService) CursorPosition() (x int, y int) { + return ebiten.CursorPosition() +} + +func (is InputService) InputChars() []rune { + return ebiten.InputChars() +} + +func (is InputService) IsKeyPressed(key d2input.Key) bool { + return ebiten.IsKeyPressed(keyToEbiten[key]) +} + +func (is InputService) IsKeyJustPressed(key d2input.Key) bool { + return inpututil.IsKeyJustPressed(keyToEbiten[key]) +} + +func (is InputService) IsKeyJustReleased(key d2input.Key) bool { + return inpututil.IsKeyJustReleased(keyToEbiten[key]) +} + +func (is InputService) IsMouseButtonPressed(button d2input.MouseButton) bool { + return ebiten.IsMouseButtonPressed(mouseButtonToEbiten[button]) +} + +func (is InputService) IsMouseButtonJustPressed(button d2input.MouseButton) bool { + return inpututil.IsMouseButtonJustPressed(mouseButtonToEbiten[button]) +} + +func (is InputService) IsMouseButtonJustReleased(button d2input.MouseButton) bool { + return inpututil.IsMouseButtonJustReleased(mouseButtonToEbiten[button]) +} + +func (is InputService) KeyPressDuration(key d2input.Key) int { + return inpututil.KeyPressDuration(keyToEbiten[key]) +} diff --git a/d2core/d2input/input_manager.go b/d2core/d2input/input_manager.go index 6f0abf3c..f917b095 100644 --- a/d2core/d2input/input_manager.go +++ b/d2core/d2input/input_manager.go @@ -2,9 +2,6 @@ package d2input import ( "sort" - - "github.com/hajimehoshi/ebiten" - "github.com/hajimehoshi/ebiten/inpututil" ) type handlerEntry struct { @@ -26,9 +23,22 @@ func (lel handlerEntryList) Less(i, j int) bool { return lel[i].priority > lel[j].priority } +type InputService interface { + CursorPosition() (x int, y int) + InputChars() []rune + IsKeyPressed(key Key) bool + IsKeyJustPressed(key Key) bool + IsKeyJustReleased(key Key) bool + IsMouseButtonPressed(button MouseButton) bool + IsMouseButtonJustPressed(button MouseButton) bool + IsMouseButtonJustReleased(button MouseButton) bool + KeyPressDuration(key Key) int +} + type inputManager struct { - cursorX int - cursorY int + inputService InputService + cursorX int + cursorY int buttonMod MouseButtonMod keyMod KeyMod @@ -36,28 +46,28 @@ type inputManager struct { entries handlerEntryList } -func (im *inputManager) advance(elapsed float64) error { - cursorX, cursorY := ebiten.CursorPosition() +func (im *inputManager) advance(_ float64) error { + cursorX, cursorY := im.inputService.CursorPosition() im.keyMod = 0 - if ebiten.IsKeyPressed(ebiten.KeyAlt) { + if im.inputService.IsKeyPressed(KeyAlt) { im.keyMod |= KeyModAlt } - if ebiten.IsKeyPressed(ebiten.KeyControl) { + if im.inputService.IsKeyPressed(KeyControl) { im.keyMod |= KeyModControl } - if ebiten.IsKeyPressed(ebiten.KeyShift) { + if im.inputService.IsKeyPressed(KeyShift) { im.keyMod |= KeyModShift } im.buttonMod = 0 - if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) { + if im.inputService.IsMouseButtonPressed(MouseButtonLeft) { im.buttonMod |= MouseButtonModLeft } - if ebiten.IsMouseButtonPressed(ebiten.MouseButtonMiddle) { + if im.inputService.IsMouseButtonPressed(MouseButtonMiddle) { im.buttonMod |= MouseButtonModMiddle } - if ebiten.IsMouseButtonPressed(ebiten.MouseButtonRight) { + if im.inputService.IsMouseButtonPressed(MouseButtonRight) { im.buttonMod |= MouseButtonModRight } @@ -68,9 +78,9 @@ func (im *inputManager) advance(elapsed float64) error { cursorY, } - for key := ebiten.Key0; key < ebiten.KeyMax; key++ { - if inpututil.IsKeyJustPressed(key) { - event := KeyEvent{eventBase, Key(key)} + for key := keyMin; key < keyMax; key++ { + if im.inputService.IsKeyJustPressed(key) { + event := KeyEvent{HandlerEvent: eventBase, Key: key} im.propagate(func(handler Handler) bool { if l, ok := handler.(KeyDownHandler); ok { return l.OnKeyDown(event) @@ -80,8 +90,8 @@ func (im *inputManager) advance(elapsed float64) error { }) } - if ebiten.IsKeyPressed(key) { - event := KeyEvent{eventBase, Key(key)} + if im.inputService.IsKeyPressed(key) { + event := KeyEvent{HandlerEvent: eventBase, Key: key, Duration: im.inputService.KeyPressDuration(key)} im.propagate(func(handler Handler) bool { if l, ok := handler.(KeyRepeatHandler); ok { return l.OnKeyRepeat(event) @@ -91,8 +101,8 @@ func (im *inputManager) advance(elapsed float64) error { }) } - if inpututil.IsKeyJustReleased(key) { - event := KeyEvent{eventBase, Key(key)} + if im.inputService.IsKeyJustReleased(key) { + event := KeyEvent{HandlerEvent: eventBase, Key: key} im.propagate(func(handler Handler) bool { if l, ok := handler.(KeyUpHandler); ok { return l.OnKeyUp(event) @@ -103,7 +113,7 @@ func (im *inputManager) advance(elapsed float64) error { } } - if chars := ebiten.InputChars(); len(chars) > 0 { + if chars := im.inputService.InputChars(); len(chars) > 0 { event := KeyCharsEvent{eventBase, chars} im.propagate(func(handler Handler) bool { if l, ok := handler.(KeyCharsHandler); ok { @@ -114,8 +124,8 @@ func (im *inputManager) advance(elapsed float64) error { }) } - for button := ebiten.MouseButtonLeft; button < ebiten.MouseButtonMiddle; button++ { - if inpututil.IsMouseButtonJustPressed(button) { + for button := mouseButtonMin; button < mouseButtonMax; button++ { + if im.inputService.IsMouseButtonJustPressed(button) { event := MouseEvent{eventBase, MouseButton(button)} im.propagate(func(handler Handler) bool { if l, ok := handler.(MouseButtonDownHandler); ok { @@ -126,24 +136,21 @@ func (im *inputManager) advance(elapsed float64) error { }) } - for button := ebiten.MouseButtonLeft; button < ebiten.MouseButtonMiddle; button++ { - if ebiten.IsMouseButtonPressed(button) { + if im.inputService.IsMouseButtonJustReleased(button) { event := MouseEvent{eventBase, MouseButton(button)} im.propagate(func(handler Handler) bool { - if l, ok := handler.(MouseButtonRepeatHandler); ok { - return l.OnMouseButtonRepeat(event) + if l, ok := handler.(MouseButtonUpHandler); ok { + return l.OnMouseButtonUp(event) } return false }) } - } - - if inpututil.IsMouseButtonJustReleased(button) { + if im.inputService.IsMouseButtonPressed(button) { event := MouseEvent{eventBase, MouseButton(button)} im.propagate(func(handler Handler) bool { - if l, ok := handler.(MouseButtonUpHandler); ok { - return l.OnMouseButtonUp(event) + if l, ok := handler.(MouseButtonRepeatHandler); ok { + return l.OnMouseButtonRepeat(event) } return false diff --git a/d2core/d2input/key.go b/d2core/d2input/key.go new file mode 100644 index 00000000..f735fa7d --- /dev/null +++ b/d2core/d2input/key.go @@ -0,0 +1,138 @@ +package d2input + +// Key is the physical key of keyboard input +type Key int + +const ( + Key0 Key = iota + Key1 + Key2 + Key3 + Key4 + Key5 + Key6 + Key7 + Key8 + Key9 + KeyA + KeyB + KeyC + KeyD + KeyE + KeyF + KeyG + KeyH + KeyI + KeyJ + KeyK + KeyL + KeyM + KeyN + KeyO + KeyP + KeyQ + KeyR + KeyS + KeyT + KeyU + KeyV + KeyW + KeyX + KeyY + KeyZ + KeyApostrophe + KeyBackslash + KeyBackspace + KeyCapsLock + KeyComma + KeyDelete + KeyDown + KeyEnd + KeyEnter + KeyEqual + KeyEscape + KeyF1 + KeyF2 + KeyF3 + KeyF4 + KeyF5 + KeyF6 + KeyF7 + KeyF8 + KeyF9 + KeyF10 + KeyF11 + KeyF12 + KeyGraveAccent + KeyHome + KeyInsert + KeyKP0 + KeyKP1 + KeyKP2 + KeyKP3 + KeyKP4 + KeyKP5 + KeyKP6 + KeyKP7 + KeyKP8 + KeyKP9 + KeyKPAdd + KeyKPDecimal + KeyKPDivide + KeyKPEnter + KeyKPEqual + KeyKPMultiply + KeyKPSubtract + KeyLeft + KeyLeftBracket + KeyMenu + KeyMinus + KeyNumLock + KeyPageDown + KeyPageUp + KeyPause + KeyPeriod + KeyPrintScreen + KeyRight + KeyRightBracket + KeyScrollLock + KeySemicolon + KeySlash + KeySpace + KeyTab + KeyUp + KeyAlt + KeyControl + KeyShift + + keyMin = Key0 + keyMax = KeyShift +) + +type KeyMod int + +const ( + KeyModAlt KeyMod = 1 << iota + KeyModControl + KeyModShift +) + +// MouseButton is the physical button for mouse input +type MouseButton int + +const ( + MouseButtonLeft MouseButton = iota + MouseButtonMiddle + MouseButtonRight + + mouseButtonMin = MouseButtonLeft + mouseButtonMax = MouseButtonRight +) + +type MouseButtonMod int + +const ( + MouseButtonModLeft MouseButtonMod = 1 << iota + MouseButtonModMiddle + MouseButtonModRight +) diff --git a/d2core/d2ui/d2ui.go b/d2core/d2ui/d2ui.go index d61b4908..363d6879 100644 --- a/d2core/d2ui/d2ui.go +++ b/d2core/d2ui/d2ui.go @@ -1,10 +1,12 @@ package d2ui import ( + "log" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2audio" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2input" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2render" - "github.com/hajimehoshi/ebiten" ) // CursorButton represents a mouse button @@ -17,122 +19,106 @@ const ( CursorButtonRight CursorButton = 2 ) -var widgets []Widget -var cursorButtons CursorButton -var pressedIndex int -var CursorX int -var CursorY int +type UI struct { + widgets []Widget + cursorButtons CursorButton // TODO (carrelld) convert dependent code and remove + CursorX int // TODO (carrelld) convert dependent code and remove + CursorY int // TODO (carrelld) convert dependent code and remove + pressedWidget Widget +} + +var singleton UI var clickSfx d2audio.SoundEffect -var waitForLeftMouseUp bool func Initialize() { - pressedIndex = -1 - clickSfx, _ = d2audio.LoadSoundEffect(d2resource.SFXButtonClick) - waitForLeftMouseUp = false + sfx, err := d2audio.LoadSoundEffect(d2resource.SFXButtonClick) + if err != nil { + log.Fatalf("failed to initialize ui: %v", err) + } + clickSfx = sfx + + d2input.BindHandler(&singleton) } // Reset resets the state of the UI manager. Typically called for new screens func Reset() { - widgets = make([]Widget, 0) - pressedIndex = -1 - waitForLeftMouseUp = true + singleton.widgets = nil + singleton.pressedWidget = nil } // AddWidget adds a widget to the UI manager func AddWidget(widget Widget) { - widgets = append(widgets, widget) + d2input.BindHandler(widget) + singleton.widgets = append(singleton.widgets, widget) } -func WaitForMouseRelease() { - waitForLeftMouseUp = true +func (u *UI) OnMouseButtonUp(event d2input.MouseEvent) bool { + singleton.CursorX, singleton.CursorY = event.X, event.Y + if event.Button == d2input.MouseButtonLeft { + singleton.cursorButtons |= CursorButtonLeft + // activate previously pressed widget if cursor is still hovering + w := singleton.pressedWidget + if w != nil && contains(w, singleton.CursorX, singleton.CursorY) && w.GetVisible() && w.GetEnabled() { + w.Activate() + } + // unpress all widgets that are pressed + for _, w := range singleton.widgets { + w.SetPressed(false) + } + } + return false +} + +func (u *UI) OnMouseButtonDown(event d2input.MouseEvent) bool { + singleton.CursorX, singleton.CursorY = event.X, event.Y + if event.Button == d2input.MouseButtonLeft { + // find and press a widget on screen + singleton.pressedWidget = nil + for _, w := range singleton.widgets { + if contains(w, singleton.CursorX, singleton.CursorY) && w.GetVisible() && w.GetEnabled() { + w.SetPressed(true) + singleton.pressedWidget = w + clickSfx.Play() + break + } + } + } + if event.Button == d2input.MouseButtonRight { + singleton.cursorButtons |= CursorButtonRight + } + return false } // Render renders all of the UI elements func Render(target d2render.Surface) { - for _, widget := range widgets { + for _, widget := range singleton.widgets { if widget.GetVisible() { widget.Render(target) } } } +// contains determines whether a given x,y coordinate lands within a Widget +func contains(w Widget, x, y int) bool { + wx, wy := w.GetPosition() + ww, wh := w.GetSize() + return x >= wx && x <= wx+ww && y >= wy && y <= wy+wh +} + // Update updates all of the UI elements func Advance(elapsed float64) { - for _, widget := range widgets { + for _, widget := range singleton.widgets { if widget.GetVisible() { widget.Advance(elapsed) } } - - cursorButtons = 0 - if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) { - if !waitForLeftMouseUp { - cursorButtons |= CursorButtonLeft - } - } else { - if waitForLeftMouseUp { - waitForLeftMouseUp = false - } - } - if ebiten.IsMouseButtonPressed(ebiten.MouseButtonRight) { - cursorButtons |= CursorButtonRight - } - CursorX, CursorY = ebiten.CursorPosition() - if CursorButtonPressed(CursorButtonLeft) { - found := false - for i, widget := range widgets { - if !widget.GetVisible() || !widget.GetEnabled() { - continue - } - wx, wy := widget.GetPosition() - ww, wh := widget.GetSize() - if CursorX >= wx && CursorX <= wx+ww && CursorY >= wy && CursorY <= wy+wh { - widget.SetPressed(true) - if pressedIndex == -1 { - found = true - pressedIndex = i - clickSfx.Play() - } else if pressedIndex > -1 && pressedIndex != i { - widgets[i].SetPressed(false) - } else { - found = true - } - } else { - widget.SetPressed(false) - } - } - if !found { - if pressedIndex > -1 { - widgets[pressedIndex].SetPressed(false) - } else { - pressedIndex = -2 - } - } - } else { - if pressedIndex > -1 { - widget := widgets[pressedIndex] - wx, wy := widget.GetPosition() - ww, wh := widget.GetSize() - if CursorX >= wx && CursorX <= wx+ww && CursorY >= wy && CursorY <= wy+wh { - widget.Activate() - } - } else { - for _, widget := range widgets { - if !widget.GetVisible() || !widget.GetEnabled() { - continue - } - widget.SetPressed(false) - } - } - pressedIndex = -1 - } } // CursorButtonPressed determines if the specified button has been pressed func CursorButtonPressed(button CursorButton) bool { - return cursorButtons&button > 0 + return singleton.cursorButtons&button > 0 } -func KeyPressed(key ebiten.Key) bool { - return ebiten.IsKeyPressed(key) +func CursorPosition() (x, y int) { + return singleton.CursorX, singleton.CursorY } diff --git a/d2core/d2ui/scrollbar.go b/d2core/d2ui/scrollbar.go index 215353e9..eb405089 100644 --- a/d2core/d2ui/scrollbar.go +++ b/d2core/d2ui/scrollbar.go @@ -4,7 +4,6 @@ import ( "github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2render" - "github.com/hajimehoshi/ebiten" ) type Scrollbar struct { @@ -52,7 +51,7 @@ func (v Scrollbar) getBarPosition() int { } func (v *Scrollbar) Activate() { - _, my := ebiten.CursorPosition() + _, my := CursorPosition() barPosition := v.getBarPosition() if my <= v.y+barPosition+15 { if v.currentOffset > 0 { @@ -65,6 +64,7 @@ func (v *Scrollbar) Activate() { v.lastDirChange = 1 } } + if v.onActivate != nil { v.onActivate() } diff --git a/d2core/d2ui/textbox.go b/d2core/d2ui/textbox.go index 42fdf96a..0d4df609 100644 --- a/d2core/d2ui/textbox.go +++ b/d2core/d2ui/textbox.go @@ -4,15 +4,15 @@ import ( "strings" "time" - "github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset" - "github.com/OpenDiablo2/OpenDiablo2/d2core/d2render" - - "github.com/hajimehoshi/ebiten/inpututil" - "github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource" - "github.com/hajimehoshi/ebiten" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2input" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2render" ) +// TextBox with cursor focus +var focusedTextBox *TextBox + // TextBox represents a text input box type TextBox struct { text string @@ -29,7 +29,7 @@ type TextBox struct { func CreateTextbox() TextBox { animation, _ := d2asset.LoadAnimation(d2resource.TextBox2, d2resource.PaletteUnits) bgSprite, _ := LoadSprite(animation) - result := TextBox{ + tb := TextBox{ filter: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", bgSprite: bgSprite, textLabel: CreateLabel(d2resource.FontFormal11, d2resource.PaletteUnits), @@ -37,29 +37,15 @@ func CreateTextbox() TextBox { enabled: true, visible: true, } - result.lineBar.SetText("_") - return result + tb.lineBar.SetText("_") + + return tb } func (v *TextBox) SetFilter(filter string) { v.filter = filter } -func repeatingKeyPressed(key ebiten.Key) bool { - const ( - delay = 30 - interval = 3 - ) - d := inpututil.KeyPressDuration(key) - if d == 1 { - return true - } - if d >= delay && (d-delay)%interval == 0 { - return true - } - return false -} - func (v *TextBox) Render(target d2render.Surface) { if !v.visible { return @@ -71,21 +57,47 @@ func (v *TextBox) Render(target d2render.Surface) { } } -func (v *TextBox) Advance(elapsed float64) { - if !v.visible || !v.enabled { - return +func (v *TextBox) OnKeyChars(event d2input.KeyCharsEvent) bool { + if !(focusedTextBox == v) || !v.visible || !v.enabled { + return false } - newText := string(ebiten.InputChars()) + newText := string(event.Chars) if len(newText) > 0 { v.text += newText v.SetText(v.text) + return true } - if repeatingKeyPressed(ebiten.KeyBackspace) { + return false +} + +func (v *TextBox) OnKeyRepeat(event d2input.KeyEvent) bool { + if event.Key == d2input.KeyBackspace && debounceEvents(event.Duration) { if len(v.text) >= 1 { v.text = v.text[:len(v.text)-1] } v.SetText(v.text) } + return false +} + +func debounceEvents(numFrames int) bool { + const ( + delay = 30 + interval = 3 + ) + if numFrames == 1 { + return true + } + if numFrames >= delay && (numFrames-delay)%interval == 0 { + return true + } + return false +} + +func (v *TextBox) Advance(_ float64) { + if !v.visible || !v.enabled { + return + } } func (v *TextBox) Update() { @@ -164,5 +176,5 @@ func (v *TextBox) OnActivated(callback func()) { } func (v *TextBox) Activate() { - //no op + focusedTextBox = v } diff --git a/d2game/d2gamescreen/character_select.go b/d2game/d2gamescreen/character_select.go index ed8b6871..5f43b6e1 100644 --- a/d2game/d2gamescreen/character_select.go +++ b/d2game/d2gamescreen/character_select.go @@ -6,24 +6,20 @@ import ( "os" "strings" - "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapentity" - - "github.com/OpenDiablo2/OpenDiablo2/d2game/d2player" - - "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2client" - "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2client/d2clientconnectiontype" - - "github.com/hajimehoshi/ebiten" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2input" "github.com/OpenDiablo2/OpenDiablo2/d2common" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource" - "github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2audio" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2inventory" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapentity" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2render" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2screen" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2ui" + "github.com/OpenDiablo2/OpenDiablo2/d2game/d2player" + "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2client" + "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2client/d2clientconnectiontype" ) type CharacterSelect struct { @@ -46,7 +42,6 @@ type CharacterSelect struct { characterImage [8]*d2mapentity.Player gameStates []*d2player.PlayerState selectedCharacter int - mouseButtonPressed bool showDeleteConfirmation bool connectionType d2clientconnectiontype.ClientConnectionType connectionHost string @@ -62,6 +57,7 @@ func CreateCharacterSelect(connectionType d2clientconnectiontype.ClientConnectio func (v *CharacterSelect) OnLoad() error { d2audio.PlayBGM(d2resource.BGMTitle) + d2input.BindHandler(v) animation, _ := d2asset.LoadAnimation(d2resource.CharacterSelectionBackground, d2resource.PaletteSky) v.background, _ = d2ui.LoadSprite(animation) @@ -222,32 +218,32 @@ func (v *CharacterSelect) moveSelectionBox() { v.d2HeroTitle.SetText(v.gameStates[v.selectedCharacter].HeroName) } -func (v *CharacterSelect) Advance(tickTime float64) error { +func (v *CharacterSelect) OnMouseButtonDown(event d2input.MouseEvent) bool { if !v.showDeleteConfirmation { - if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) { - if !v.mouseButtonPressed { - v.mouseButtonPressed = true - mx, my := ebiten.CursorPosition() - bw := 272 - bh := 92 - localMouseX := mx - 37 - localMouseY := my - 86 - if localMouseX > 0 && localMouseX < bw*2 && localMouseY >= 0 && localMouseY < bh*4 { - adjustY := localMouseY / bh - selectedIndex := adjustY * 2 - if localMouseX > bw { - selectedIndex += 1 - } - if (v.charScrollbar.GetCurrentOffset()*2)+selectedIndex < len(v.gameStates) { - v.selectedCharacter = (v.charScrollbar.GetCurrentOffset() * 2) + selectedIndex - v.moveSelectionBox() - } + if event.Button == d2input.MouseButtonLeft { + mx, my := event.X, event.Y + bw := 272 + bh := 92 + localMouseX := mx - 37 + localMouseY := my - 86 + if localMouseX > 0 && localMouseX < bw*2 && localMouseY >= 0 && localMouseY < bh*4 { + adjustY := localMouseY / bh + selectedIndex := adjustY * 2 + if localMouseX > bw { + selectedIndex += 1 + } + if (v.charScrollbar.GetCurrentOffset()*2)+selectedIndex < len(v.gameStates) { + v.selectedCharacter = (v.charScrollbar.GetCurrentOffset() * 2) + selectedIndex + v.moveSelectionBox() } } - } else { - v.mouseButtonPressed = false + return true } } + return false +} + +func (v *CharacterSelect) Advance(tickTime float64) error { for _, hero := range v.characterImage { if hero != nil { hero.AnimatedComposite.Advance(tickTime) diff --git a/d2game/d2gamescreen/main_menu.go b/d2game/d2gamescreen/main_menu.go index 8c2089fa..80dc3c80 100644 --- a/d2game/d2gamescreen/main_menu.go +++ b/d2game/d2gamescreen/main_menu.go @@ -8,6 +8,8 @@ import ( "os/exec" "runtime" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2input" + "github.com/OpenDiablo2/OpenDiablo2/d2game/d2player" "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2client/d2clientconnectiontype" @@ -238,6 +240,9 @@ func (v *MainMenu) OnLoad() error { } else { v.SetScreenMode(ScreenModeMainMenu) } + + d2input.BindHandler(v) + return nil } @@ -343,23 +348,17 @@ func (v *MainMenu) Advance(tickTime float64) error { v.diabloLogoRight.Advance(tickTime) } - switch v.screenMode { - case ScreenModeTrademark: - if d2ui.CursorButtonPressed(d2ui.CursorButtonLeft) { - if v.leftButtonHeld { - return nil - } - d2ui.WaitForMouseRelease() - v.SetScreenMode(ScreenModeMainMenu) - v.leftButtonHeld = true - } else { - v.leftButtonHeld = false - } - } - return nil } +func (v *MainMenu) OnMouseButtonDown(event d2input.MouseEvent) bool { + if v.screenMode == ScreenModeTrademark && event.Button == d2input.MouseButtonLeft { + v.SetScreenMode(ScreenModeMainMenu) + return true + } + return false +} + func (v *MainMenu) SetScreenMode(screenMode MainMenuScreenMode) { v.screenMode = screenMode isMainMenu := screenMode == ScreenModeMainMenu @@ -379,6 +378,9 @@ func (v *MainMenu) SetScreenMode(screenMode MainMenuScreenMode) { v.btnTcpIpHostGame.SetVisible(isTcpIp) v.btnTcpIpJoinGame.SetVisible(isTcpIp) v.tcpJoinGameEntry.SetVisible(isServerIp) + if isServerIp { + v.tcpJoinGameEntry.Activate() + } v.btnServerIpOk.SetVisible(isServerIp) v.btnServerIpCancel.SetVisible(isServerIp) } diff --git a/d2game/d2gamescreen/select_hero_class.go b/d2game/d2gamescreen/select_hero_class.go index 974d513d..a43b2f1f 100644 --- a/d2game/d2gamescreen/select_hero_class.go +++ b/d2game/d2gamescreen/select_hero_class.go @@ -472,17 +472,10 @@ func (v *SelectHeroClass) Advance(tickTime float64) error { canSelect = false } } - allIdle := true - for heroType, data := range v.heroRenderInfo { - if allIdle && data.Stance != d2enum.HeroStanceIdle { - allIdle = false - } + for heroType, _ := range v.heroRenderInfo { v.updateHeroSelectionHover(heroType, canSelect) } - if v.selectedHero != d2enum.HeroNone && allIdle { - v.selectedHero = d2enum.HeroNone - } - v.okButton.SetEnabled(len(v.heroNameTextbox.GetText()) >= 2) + v.okButton.SetEnabled(len(v.heroNameTextbox.GetText()) >= 2 && v.selectedHero != d2enum.HeroNone) return nil } @@ -509,12 +502,12 @@ func (v *SelectHeroClass) updateHeroSelectionHover(hero d2enum.Hero, canSelect b if renderInfo.Stance == d2enum.HeroStanceSelected { return } - mouseX := d2ui.CursorX - mouseY := d2ui.CursorY + mouseX, mouseY := d2ui.CursorPosition() b := renderInfo.SelectionBounds mouseHover := (mouseX >= b.Min.X) && (mouseX <= b.Min.X+b.Max.X) && (mouseY >= b.Min.Y) && (mouseY <= b.Min.Y+b.Max.Y) if mouseHover && d2ui.CursorButtonPressed(d2ui.CursorButtonLeft) { v.heroNameTextbox.SetVisible(true) + v.heroNameTextbox.Activate() v.okButton.SetVisible(true) v.expansionCheckbox.SetVisible(true) v.hardcoreCheckbox.SetVisible(true) diff --git a/main.go b/main.go index a4473818..c610068c 100644 --- a/main.go +++ b/main.go @@ -25,6 +25,7 @@ import ( "github.com/OpenDiablo2/OpenDiablo2/d2core/d2config" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2gui" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2input" + ebiten_input "github.com/OpenDiablo2/OpenDiablo2/d2core/d2input/ebiten" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2inventory" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2render" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2render/ebiten" @@ -113,6 +114,8 @@ func initialize() error { config := d2config.Get() d2resource.LanguageCode = config.Language + d2input.Initialize(ebiten_input.InputService{}) + renderer, err := ebiten.CreateRenderer() if err != nil { return err