diff --git a/game/events.py b/game/events.py
new file mode 100644
index 0000000..fe212f0
--- /dev/null
+++ b/game/events.py
@@ -0,0 +1,54 @@
+# Copyright (C) 2022 RozK
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from engine import (
+ buffer,
+ EVENT_FOCUS_IN, EVENT_FOCUS_OUT,
+ EVENT_KEY_PRESS, EVENT_KEY_RELEASE,
+ EVENT_BUTTON_PRESS, EVENT_BUTTON_RELEASE,
+ EVENT_MOTION,
+ Event, create_events, destroy_events, consume_events)
+
+_max_type = max(
+ EVENT_FOCUS_IN, EVENT_FOCUS_OUT,
+ EVENT_KEY_PRESS, EVENT_KEY_RELEASE,
+ EVENT_BUTTON_PRESS, EVENT_BUTTON_RELEASE,
+ EVENT_MOTION)
+
+_max_events = 64
+
+class Events:
+ __slots__ = '_display', '_events', '_buffer', '_handlers'
+
+ def __init__(self, display):
+ self._display = display
+ self._events = create_events(display)
+ self._buffer = buffer(Event, _max_events)
+ self._handlers = [[] for _ in range(_max_type + 1)]
+
+ def __del__(self):
+ destroy_events(self._display, self._events)
+
+ def register(self, type, handler):
+ assert type <= _max_type
+ self._handlers[type].append(handler)
+
+ def update(self):
+ nevents = consume_events(self._events, self._buffer, _max_events)
+ if nevents:
+ for event in self._buffer[:nevents]:
+ data = event.data
+ for handler in self._handlers[event.type]:
+ handler(data)
diff --git a/game/keyboard.py b/game/keyboard.py
new file mode 100644
index 0000000..729df97
--- /dev/null
+++ b/game/keyboard.py
@@ -0,0 +1,27 @@
+# Copyright (C) 2022 RozK
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from engine import EVENT_KEY_PRESS
+
+class Keyboard:
+ __slots__ = 'quit'
+
+ def __init__(self, events):
+ self.quit = False
+ events.register(EVENT_KEY_PRESS, self.key_press_handler)
+
+ def key_press_handler(self, data):
+ if data.key.character == 'q':
+ self.quit = True
diff --git a/game/mouse.py b/game/mouse.py
new file mode 100644
index 0000000..acedbf4
--- /dev/null
+++ b/game/mouse.py
@@ -0,0 +1,52 @@
+# Copyright (C) 2022 RozK
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from engine import (
+ EVENT_BUTTON_PRESS, EVENT_BUTTON_RELEASE, EVENT_MOTION, BUTTON_LEFT, BUTTON_WHEEL_UP, BUTTON_WHEEL_DOWN)
+
+class Mouse:
+ __slots__ = 'buttons', 'wheel', 'wheel_min', 'position', 'drag'
+
+ def __init__(self, events, wheel = 0, wheel_min = None):
+ self.buttons = 0
+ self.wheel = wheel
+ self.wheel_min = wheel_min
+ self.position = None
+ self.drag = (0, 0)
+ events.register(EVENT_BUTTON_PRESS, self.button_press_handler)
+ events.register(EVENT_BUTTON_RELEASE, self.button_release_handler)
+ events.register(EVENT_MOTION, self.motion_handler)
+
+ def button_press_handler(self, data):
+ button = data.button.index
+ self.buttons |= 1 << button
+ if button == BUTTON_WHEEL_UP:
+ self.wheel -= 1
+ if self.wheel_min is not None and self.wheel < self.wheel_min:
+ self.wheel = self.wheel_min
+ elif button == BUTTON_WHEEL_DOWN:
+ self.wheel += 1
+
+ def button_release_handler(self, data):
+ self.buttons &= ~(1 << data.button.index)
+
+ def motion_handler(self, data):
+ new_x = data.motion.x
+ new_y = data.motion.y
+ if self.position and (self.buttons & (1 << BUTTON_LEFT)):
+ prev_x, prev_y = self.position
+ prev_dx, prev_dy = self.drag
+ self.drag = (prev_dx + (new_x - prev_x), prev_dy + (new_y - prev_y))
+ self.position = (new_x, new_y)