Skip to content

Commit

Permalink
Add MouseState class
Browse files Browse the repository at this point in the history
This class represents the state of the mouse. It is designed to wrap
around specific API code so that it can be easily mocked in tests. This
commit also extracts the `KeyBitState` struct from the KeyboardState
class so that it can be shared between various input state classes.
  • Loading branch information
meisekimiu committed Sep 22, 2024
1 parent 507ac14 commit a44ce11
Show file tree
Hide file tree
Showing 7 changed files with 275 additions and 16 deletions.
13 changes: 13 additions & 0 deletions src/input/common/ButtonBitState.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#ifndef GL_ADAGIO_BUTTONBITSTATE_H
#define GL_ADAGIO_BUTTONBITSTATE_H

namespace Adagio::Input {
// Encodes the state of a digital button, shared by various input state objects.
struct ButtonBitState {
bool isDown: 1;
bool isPressed: 1;
bool isReleased: 1;
};
}

#endif //GL_ADAGIO_BUTTONBITSTATE_H
18 changes: 9 additions & 9 deletions src/input/keyboard/KeyboardState.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ void Adagio::KeyboardState::setHandler(Adagio::KeyboardHandler *handle) {
bool Adagio::KeyboardState::isKeyDown(Adagio::keycode key) const {
auto it = keys.find(key);
if (it != keys.end()) {
return it->second.keyDown;
return it->second.isDown;
}
return false;
}
Expand All @@ -19,15 +19,15 @@ bool Adagio::KeyboardState::isKeyUp(Adagio::keycode key) const {
bool Adagio::KeyboardState::hasKeyPressStarted(Adagio::keycode key) const {
auto it = keys.find(key);
if (it != keys.end()) {
return it->second.keyPressed;
return it->second.isPressed;
}
return false;
}

bool Adagio::KeyboardState::hasKeyPressEnded(Adagio::keycode key) const {
auto it = keys.find(key);
if (it != keys.end()) {
return it->second.keyReleased;
return it->second.isReleased;
}
return false;
}
Expand Down Expand Up @@ -58,18 +58,18 @@ void Adagio::KeyboardState::updateTextBuffer() {

void Adagio::KeyboardState::checkKnownKeys() {
for (auto &keyState: keys) {
keyState.second.keyPressed = false;
keyState.second.keyReleased = keyState.second.keyDown && handler->isKeyUp(keyState.first);
keyState.second.keyDown = handler->isKeyDown(keyState.first);
keyState.second.isPressed = false;
keyState.second.isReleased = keyState.second.isDown && handler->isKeyUp(keyState.first);
keyState.second.isDown = handler->isKeyDown(keyState.first);
}
}

void Adagio::KeyboardState::scanForNewKeyPresses() {
keycode newKey = handler->getNextKey();
while (newKey != 0) {
keys[newKey].keyDown = true;
keys[newKey].keyPressed = true;
keys[newKey].keyReleased = false;
keys[newKey].isDown = true;
keys[newKey].isPressed = true;
keys[newKey].isReleased = false;
newKey = handler->getNextKey();
}
}
Expand Down
9 changes: 2 additions & 7 deletions src/input/keyboard/KeyboardState.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include <unordered_map>
#include <cstdint>
#include "KeyboardHandler.h"
#include "../common/ButtonBitState.h"

namespace Adagio {
class KeyboardState {
Expand All @@ -25,15 +26,9 @@ namespace Adagio {
void update();

private:
struct KeyBitState {
bool keyDown: 1;
bool keyPressed: 1;
bool keyReleased: 1;
};

char textBuffer[8]{0, 0, 0, 0, 0, 0, 0, 0};
KeyboardHandler *handler{nullptr};
std::unordered_map<keycode, KeyBitState> keys;
std::unordered_map<keycode, Input::ButtonBitState> keys;

void scanForNewKeyPresses();

Expand Down
25 changes: 25 additions & 0 deletions src/input/mouse/MouseHandler.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#ifndef GL_ADAGIO_MOUSEHANDLER_H
#define GL_ADAGIO_MOUSEHANDLER_H

#include <cstdint>
#include "../../math/Vector2.h"

namespace Adagio {
typedef std::uint8_t MouseButton;

typedef std::int32_t MouseCursor;

struct MouseHandler {
virtual bool isMouseButtonDown(MouseButton) = 0;

virtual bool isMouseButtonUp(MouseButton) = 0;

virtual Vector2i getMouseCoords() = 0;

virtual void setMouseCoords(Vector2i) = 0;

virtual void setMouseCursor(MouseCursor) = 0;
};
}

#endif //GL_ADAGIO_MOUSEHANDLER_H
67 changes: 67 additions & 0 deletions src/input/mouse/MouseState.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#include "MouseState.h"

Adagio::MouseState::MouseState() {
for (auto &button: buttonStates) {
button.isDown = false;
button.isPressed = false;
button.isReleased = false;
}
}

void Adagio::MouseState::setHandler(Adagio::MouseHandler *h) {
handler = h;
}

void Adagio::MouseState::update() {
updateCoords();
updateButtonStates();
}

void Adagio::MouseState::updateCoords() {
Vector2i prevCoords = cursorCoords;
cursorCoords = handler->getMouseCoords();
cursorDelta = cursorCoords - prevCoords;
}

void Adagio::MouseState::updateButtonStates() {
for (Input::ButtonBitState &button: buttonStates) {
const long buttonIndex = &button - buttonStates;
const bool isDown = handler->isMouseButtonDown(buttonIndex);
button.isPressed = !button.isPressed && isDown;
button.isReleased = !button.isReleased && handler->isMouseButtonUp(buttonIndex);
button.isDown = isDown;
}
}

Adagio::Vector2i Adagio::MouseState::getCursorCoords() const {
return cursorCoords;
}

Adagio::Vector2i Adagio::MouseState::getCursorDelta() const {
return cursorDelta;
}

bool Adagio::MouseState::isButtonDown(MouseButton btn) const {
return buttonStates[btn].isDown;
}

bool Adagio::MouseState::isButtonUp(MouseButton btn) const {
return !buttonStates[btn].isDown;
}

bool Adagio::MouseState::hasClickStarted(MouseButton btn) const {
return buttonStates[btn].isPressed;
}

bool Adagio::MouseState::hasClickEnded(Adagio::MouseButton btn) const {
return buttonStates[btn].isReleased;
}

void Adagio::MouseState::setCursorCoords(const Vector2i &coords) const {
handler->setMouseCoords(coords);
}

void Adagio::MouseState::setCursor(Adagio::MouseCursor c) const {
handler->setMouseCursor(c);
}

46 changes: 46 additions & 0 deletions src/input/mouse/MouseState.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#ifndef GL_ADAGIO_MOUSESTATE_H
#define GL_ADAGIO_MOUSESTATE_H

#define MAX_MOUSE_BUTTONS 8

#include "MouseHandler.h"
#include "../common/ButtonBitState.h"

namespace Adagio {
class MouseState {
public:
MouseState();

void setHandler(MouseHandler *);

[[nodiscard]] Vector2i getCursorCoords() const;

[[nodiscard]] Vector2i getCursorDelta() const;

[[nodiscard]] bool isButtonDown(MouseButton) const;

[[nodiscard]] bool isButtonUp(MouseButton) const;

[[nodiscard]] bool hasClickStarted(MouseButton) const;

[[nodiscard]] bool hasClickEnded(MouseButton) const;

void setCursorCoords(const Vector2i &) const;

void setCursor(MouseCursor) const;

void update();

private:
MouseHandler *handler{nullptr};
Vector2i cursorCoords;
Vector2i cursorDelta;
Input::ButtonBitState buttonStates[MAX_MOUSE_BUTTONS]{};

void updateButtonStates();

void updateCoords();
};
}

#endif //GL_ADAGIO_MOUSESTATE_H
113 changes: 113 additions & 0 deletions test/input/mouse.test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
#include <catch2/catch.hpp>
#include "../../src/input/mouse/MouseState.h"
#include <unordered_map>

struct MockMouse : public Adagio::MouseHandler {
Adagio::Vector2i coords{0, 0};
std::unordered_map<Adagio::MouseButton, bool> buttons;
int cursor{0};

bool isMouseButtonDown(Adagio::MouseButton btn) override {
return buttons[btn];
}

bool isMouseButtonUp(Adagio::MouseButton btn) override {
return !buttons[btn];
}

Adagio::Vector2i getMouseCoords() override {
return coords;
}

void setMouseCoords(Adagio::Vector2i c) override {
coords = c;
}

void setMouseCursor(Adagio::MouseCursor i) override {
cursor = i;
}

void setMockCoords(int x, int y) {
coords.x = x;
coords.y = y;
}

void pressButton(Adagio::MouseButton i) {
buttons[i] = true;
}

void releaseButton(Adagio::MouseButton i) {
buttons[i] = false;
}
};

TEST_CASE("MouseState", "[input][mouse]") {
Adagio::MouseState mouseState;
MockMouse mouse;
mouseState.setHandler(&mouse);

SECTION("It can get mouse coords") {
mouse.setMockCoords(1336, 1235);
auto nonUpdatedCoords = mouseState.getCursorCoords();
mouseState.update();
mouse.setMockCoords(1337, 1234);
mouseState.update();
auto result = mouseState.getCursorCoords();
auto delta = mouseState.getCursorDelta();
REQUIRE(nonUpdatedCoords.x == 0);
REQUIRE(nonUpdatedCoords.y == 0);
REQUIRE(result.x == 1337);
REQUIRE(result.y == 1234);
REQUIRE(delta.x == 1);
REQUIRE(delta.y == -1);
}

SECTION("It can query an empty mouse button state") {
for (char i = 0; i < 3; i++) {
REQUIRE_FALSE(mouseState.isButtonDown(i));
REQUIRE(mouseState.isButtonUp(i));
REQUIRE_FALSE(mouseState.hasClickStarted(i));
REQUIRE_FALSE(mouseState.hasClickEnded(i));
}
}

SECTION("It can detect a mouse button press") {
for (char i = 0; i < 3; i++) {
mouse.pressButton(i);
mouseState.update();
REQUIRE(mouseState.isButtonDown(i));
REQUIRE(mouseState.hasClickStarted(i));
REQUIRE_FALSE(mouseState.isButtonUp(i));
REQUIRE_FALSE(mouseState.hasClickEnded(i));
mouseState.update();
REQUIRE_FALSE(mouseState.hasClickStarted(i));
mouse.releaseButton(i);
}
}

SECTION("It can detect a mouse button release") {
for (char i = 0; i < 3; i++) {
mouse.pressButton(i);
mouseState.update();
mouse.releaseButton(i);
mouseState.update();
REQUIRE_FALSE(mouseState.isButtonDown(i));
REQUIRE_FALSE(mouseState.hasClickStarted(i));
REQUIRE(mouseState.isButtonUp(i));
REQUIRE(mouseState.hasClickEnded(i));
mouseState.update();
REQUIRE_FALSE(mouseState.hasClickEnded(i));
}
}

SECTION("It can set mouse cursor") {
mouseState.setCursor(1337);
REQUIRE(mouse.cursor == 1337);
}

SECTION("It can set mouse coords") {
mouseState.setCursorCoords({123, 456});
REQUIRE(mouse.coords.x == 123);
REQUIRE(mouse.coords.y == 456);
}
}

0 comments on commit a44ce11

Please sign in to comment.