diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000000000000000000000000000000000000..80975cae7ea38cf76d699b3bb3c4e54661ad9c0a
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,75 @@
+override	CPPFLAGS	+= --std=c++1z
+override	CPPFLAGS	+= -MMD -MP
+override	CPPFLAGS	+= -I./include
+override	CPPFLAGS	+= $(shell cat .cxxflags 2> /dev/null | xargs)
+
+ARFLAGS	:= $(ARFLAGS)c
+
+PREFIX	:= $(DESTDIR)/usr/local
+INCDIR	:= $(PREFIX)/include
+LIBDIR	:= $(PREFIX)/lib
+
+SRCDIR	:= ./source
+TEMPDIR	:= temp
+DISTDIR	:= out
+TARGET	:= libsimple_interactive.a
+OUT		:= $(DISTDIR)/$(TARGET)
+SOURCES	:= $(shell find -wholename "$(SRCDIR)/*.cpp")
+HEADERS	:= $(shell find -wholename "$(SRCDIR)/*.hpp" && find -wholename "$(SRCDIR)/*.h")
+INCLUDE	:= $(HEADERS:$(SRCDIR)/%=$(INCDIR)/%)
+INCDIRS	:= $(shell dirname $(INCLUDE))
+OBJECTS	:= $(SOURCES:$(SRCDIR)/%.cpp=$(TEMPDIR)/%.o)
+OBJDIRS	:= $(shell dirname $(OBJECTS))
+DEPENDS	:= $(OBJECTS:.o=.d)
+
+
+$(OUT): $(OBJECTS) | $(DISTDIR)
+	$(AR) $(ARFLAGS) $@ $^
+
+$(TEMPDIR)/%.o: $(SRCDIR)/%.cpp | $(TEMPDIR)
+	@mkdir -p $(@D)
+	$(CXX) $(CPPFLAGS) $(CXXFLAGS) -o $@ -c $<
+
+$(TEMPDIR):
+	@mkdir $@
+
+$(DISTDIR):
+	@mkdir $@
+
+clean:
+	@rm $(DEPENDS) 2> /dev/null || true
+	@rm $(OBJECTS) 2> /dev/null || true
+	@rmdir -p $(OBJDIRS) 2> /dev/null || true
+	@echo Temporaries cleaned!
+
+distclean: clean
+	@rm $(OUT) 2> /dev/null || true
+	@rmdir $(DISTDIR) 2> /dev/null || true
+	@echo All clean!
+
+install: $(OUT) $(INCLUDE) | $(LIBDIR)
+	cp $(OUT) $(LIBDIR)/$(TARGET)
+	@echo Install complete!
+
+$(LIBDIR):
+	@mkdir $@
+
+$(INCDIR)/%.h: $(SRCDIR)/%.h
+	@mkdir -p $(@D)
+	cp $< $@
+
+$(INCDIR)/%.hpp: $(SRCDIR)/%.hpp
+	@mkdir -p $(@D)
+	cp $< $@
+
+uninstall:
+	-rm $(INCLUDE)
+	@rmdir -p $(INCDIRS) 2> /dev/null || true
+	-rm $(LIBDIR)/$(TARGET)
+	@rmdir $(LIBDIR) 2> /dev/null || true
+	@echo Uninstall complete!
+
+-include $(DEPENDS)
+
+.PRECIOUS : $(OBJECTS)
+.PHONY : clean distclean install uninstall
diff --git a/examples/00_press_any_key.cpp b/examples/00_press_any_key.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..40230b717e80dc75e91f1316df611bc1813d6558
--- /dev/null
+++ b/examples/00_press_any_key.cpp
@@ -0,0 +1,40 @@
+#include <cstdio>
+#include <thread>
+#include <chrono>
+
+#include "simple/interactive/initializer.h"
+#include "simple/interactive/event.h"
+#include "simple/support/function_utils.hpp"
+#include "simple/support/misc.hpp"
+#include "common/sdl_input_grabber.h"
+
+#include "common/sdl_input_grabber.cpp"
+
+using namespace simple::interactive;
+using namespace std::chrono_literals;
+
+int main() try
+{
+
+	initializer init;
+
+	sdl_input_grabber input_grabber;
+	std::puts("Press any key!");
+	while(true)
+		if (auto e = next_event(); !e || !std::holds_alternative<key_pressed>(*e))
+			std::this_thread::sleep_for(20ms);
+		else break;
+
+	return 0;
+}
+catch(...)
+{
+	if(errno)
+		std::perror("ERROR");
+
+	const char* sdl_error = SDL_GetError();
+	if(*sdl_error)
+		std::puts(sdl_error);
+
+	throw;
+}
diff --git a/examples/01_mouse_around.cpp b/examples/01_mouse_around.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..58e29b45d1e467930c8e4308cbbbc8a4612fbc61
--- /dev/null
+++ b/examples/01_mouse_around.cpp
@@ -0,0 +1,7 @@
+#include "simple/interactive/initializer.h"
+
+#if SDL_VERSION_ATLEAST(2,0,4)
+#include "versions/01_mouse_around_capture.cpp"
+#else
+#include "versions/01_mouse_around_grab.cpp"
+#endif
diff --git a/examples/Makefile b/examples/Makefile
new file mode 100644
index 0000000000000000000000000000000000000000..e5e5b8d90534b0334fd8d7260d4a5a1245fb9d74
--- /dev/null
+++ b/examples/Makefile
@@ -0,0 +1,57 @@
+
+override CPPFLAGS	+= --std=c++1z
+override CPPFLAGS	+= -MMD -MP
+override CPPFLAGS	+= -I../source -I../include
+override CPPFLAGS	+= $(shell cat ../.cxxflags 2> /dev/null | xargs)
+override LDFLAGS	+= -L../out/ -L../lib/
+override LDFLAGS	+= $(shell cat .ldflags 2> /dev/null | xargs)
+
+override LDARCH		+= $(shell cat .ldarch 2> /dev/null | xargs)
+ifeq ($(strip $(LDARCH)),)
+override LDLIBS		+= -lsimple_sdlcore
+else
+override LDLIBS		+= $(LDARCH)
+endif
+
+override LDLIBS		+= -lsimple_interactive -lSDL2main -lSDL2
+
+TEMPDIR	:= temp
+DISTDIR	:= out
+
+SOURCES	:= $(shell echo *.cpp)
+TARGETS	:= $(SOURCES:%.cpp=$(DISTDIR)/%)
+OBJECTS	:= $(SOURCES:%.cpp=$(TEMPDIR)/%.o)
+DEPENDS	:= $(OBJECTS:.o=.d)
+
+build: make_parent $(TARGETS)
+
+make_parent:
+	make -C ..
+
+$(DISTDIR)/%: $(TEMPDIR)/%.o ../out/libsimple_interactive.a $(LDARCH) | $(DISTDIR)
+	$(CXX) $(LDFLAGS) $< $(LDLIBS) -o $@
+
+$(TEMPDIR)/%.o: %.cpp | $(TEMPDIR)
+	$(CXX) $(CPPFLAGS) $(CXXFLAGS) -o $@ -c $<
+
+$(TEMPDIR):
+	@mkdir $@
+
+$(DISTDIR):
+	@mkdir $@
+
+clean:
+	@rm $(DEPENDS) 2> /dev/null || true
+	@rm $(OBJECTS) 2> /dev/null || true
+	@rmdir $(TEMPDIR) 2> /dev/null || true
+	@echo Temporaries cleaned!
+
+distclean: clean
+	@rm $(TARGETS) 2> /dev/null || true
+	@rmdir $(DISTDIR) 2> /dev/null || true
+	@echo All clean!
+
+-include $(DEPENDS)
+
+.PRECIOUS : $(OBJECTS)
+.PHONY : clean distclean make_parent
diff --git a/examples/common/sdl_input_grabber.cpp b/examples/common/sdl_input_grabber.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..fa749553954cf659e7fde918fe8fecf494c05e6c
--- /dev/null
+++ b/examples/common/sdl_input_grabber.cpp
@@ -0,0 +1,19 @@
+#include "sdl_input_grabber.h"
+#include "simple/sdlcore/utils.hpp"
+
+sdl_input_grabber::sdl_input_grabber() :
+	video_init( simple::sdlcore::system_flag::video ),
+	window(SDL_CreateWindow("input grabber", 0,0,0,0, SDL_WINDOW_BORDERLESS | SDL_WINDOW_INPUT_FOCUS | SDL_WINDOW_MOUSE_FOCUS))
+{
+	simple::sdlcore::utils::throw_error(window.get());
+}
+
+void sdl_input_grabber::grab() noexcept
+{
+	SDL_SetWindowGrab(window.get(), SDL_TRUE);
+}
+
+void sdl_input_grabber::release() noexcept
+{
+	SDL_SetWindowGrab(window.get(), SDL_FALSE);
+}
diff --git a/examples/common/sdl_input_grabber.h b/examples/common/sdl_input_grabber.h
new file mode 100644
index 0000000000000000000000000000000000000000..24411d26b92cde06313fc5a5089f8dde4dc3921a
--- /dev/null
+++ b/examples/common/sdl_input_grabber.h
@@ -0,0 +1,26 @@
+#ifndef SIMPLE_INTERACTIVE_EXAMPLES_SDL_INPUT_GRABBER_H
+#define SIMPLE_INTERACTIVE_EXAMPLES_SDL_INPUT_GRABBER_H
+
+#include <memory>
+#include "simple/sdlcore/initializer.h"
+#include "simple/support/enum_flags_operators.hpp"
+
+class sdl_input_grabber
+{
+	struct sdl_window_del
+	{
+		void operator()(SDL_Window * w) noexcept
+		{
+			SDL_DestroyWindow(w);
+		}
+	};
+
+	simple::sdlcore::initializer video_init;
+	std::unique_ptr<SDL_Window, sdl_window_del> window;
+	public:
+	sdl_input_grabber();
+	void grab() noexcept;
+	void release() noexcept;
+};
+
+#endif /* end of include guard */
diff --git a/examples/versions/01_mouse_around_capture.cpp b/examples/versions/01_mouse_around_capture.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..125bf49e46adb3c7e635623da6c036874c267a60
--- /dev/null
+++ b/examples/versions/01_mouse_around_capture.cpp
@@ -0,0 +1,98 @@
+#include <cstdio>
+#include <thread>
+#include <chrono>
+#include <string>
+
+#include "simple/interactive/initializer.h"
+#include "simple/interactive/event.h"
+#include "simple/support/function_utils.hpp"
+#include "simple/support/misc.hpp"
+#include "../common/sdl_input_grabber.h"
+
+#include "../common/sdl_input_grabber.cpp"
+
+using namespace simple::interactive;
+using namespace std::chrono_literals;
+using namespace std::string_literals;
+
+void render_screen(int2 size, int2 cursor_position, char bg, char cursor, bool break_lines = false);
+
+int main(int argc, char const* argv[]) try
+{
+
+	using simple::support::ston;
+	const int screen_size_x = argc > 1 ? ston<int>(argv[1]) : 79;
+	const int screen_size_y = argc > 2 ? ston<int>(argv[2]) : 23;
+	const std::chrono::milliseconds frametime(argc > 3 ? ston<int>(argv[3]) : 16);
+	const bool break_lines = argc > 4 ? "false"s != argv[4] : true;
+
+	const float2 screen_size{float(screen_size_x), float(screen_size_y)};
+
+	initializer init;
+	sdl_input_grabber input_grabber;
+	require_mouse_capture(true);
+
+	float2 cursor_position{};
+
+	bool run = true;
+	while(run)
+	{
+		while(auto e = next_event()) std::visit( simple::support::overloaded{
+			[&cursor_position, &screen_size](const mouse_motion& event)
+			{
+				cursor_position = screen_size * event.screen_normalized_position().value();
+			},
+			[&run](mouse_up)
+			{
+				run = false;
+			},
+			[&run](key_pressed)
+			{
+				run = false;
+			},
+			[](auto) { }
+		}, *e);
+
+		std::puts("\nPress any key or click to quit.");
+		render_screen(int2(screen_size), int2(cursor_position), ' ', '*', break_lines);
+		std::this_thread::sleep_for(frametime);
+	}
+	std::puts("");
+	return 0;
+}
+catch(...)
+{
+	if(errno)
+		std::perror("ERROR");
+
+	const char* sdl_error = SDL_GetError();
+	if(*sdl_error)
+		std::puts(sdl_error);
+
+	throw;
+}
+
+void render_screen(int2 size, int2 cursor_position, char bg, char cursor, bool break_lines)
+{
+	static std::string buffer;
+	int linear_size = size.y() * size.x();
+	if(break_lines)
+		linear_size += size.y() - 1;
+	buffer.resize(linear_size);
+
+	auto row = buffer.begin();
+	for(int y = 0; y < size.y(); ++y)
+	{
+		row = std::fill_n(row, size.x(), bg);
+		if(break_lines && y != (size.y() - 1))
+			*row++ = '\n';
+	}
+
+	int pitch = break_lines ? size.x() + 1 : size.x();
+	int linear_cursor_position = cursor_position.y() * pitch + cursor_position.x();
+	buffer[linear_cursor_position] = cursor;
+
+	std::fputs(buffer.c_str(), stdout);
+	std::fflush(stdout);
+}
+
diff --git a/examples/versions/01_mouse_around_grab.cpp b/examples/versions/01_mouse_around_grab.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..78e633a273e30a50ed757c04adc1a0dece8641b4
--- /dev/null
+++ b/examples/versions/01_mouse_around_grab.cpp
@@ -0,0 +1,103 @@
+#include <cstdio>
+#include <thread>
+#include <chrono>
+#include <string>
+
+#include "simple/interactive/initializer.h"
+#include "simple/interactive/event.h"
+#include "simple/support/function_utils.hpp"
+#include "simple/support/misc.hpp"
+#include "../common/sdl_input_grabber.h"
+
+#include "../common/sdl_input_grabber.cpp"
+
+using namespace simple::interactive;
+using namespace std::chrono_literals;
+using namespace std::string_literals;
+
+void render_screen(int2 size, int2 cursor_position, char bg, char cursor, bool break_lines = false);
+
+int main(int argc, char const* argv[]) try
+{
+
+	using simple::support::ston;
+	const int screen_size_x = argc > 1 ? ston<int>(argv[1]) : 79;
+	const int screen_size_y = argc > 2 ? ston<int>(argv[2]) : 23;
+	const std::chrono::milliseconds frametime(argc > 3 ? ston<int>(argv[3]) : 16);
+	const bool break_lines = argc > 4 ? "false"s != argv[4] : true;
+
+	const float2 screen_size{float(screen_size_x), float(screen_size_y)};
+
+	initializer init;
+	sdl_input_grabber input_grabber;
+	input_grabber.grab();
+	relative_mouse_mode(true);
+
+	float2 cursor_position{};
+
+	bool run = true;
+	while(run)
+	{
+		while(auto e = next_event()) std::visit( simple::support::overloaded{
+			[&cursor_position, &screen_size](const mouse_motion& event)
+			{
+				// using value_or here since SDL sends motion events with invalid window id until window gains focus for the first time,
+				// and screen_normalized_motion selects the screen/display based on the window id.
+				const float2 motion = event.screen_normalized_motion().value_or(float2{});
+				cursor_position += screen_size * motion;
+				cursor_position.clamp(float2::zero(), screen_size - 1);
+			},
+			[&run](mouse_up)
+			{
+				run = false;
+			},
+			[&run](key_pressed)
+			{
+				run = false;
+			},
+			[](auto) { }
+		}, *e);
+
+		std::puts("\nPress any key or click to quit.");
+		render_screen(int2(screen_size), int2(cursor_position), ' ', '*', break_lines);
+		std::this_thread::sleep_for(frametime);
+	}
+	std::puts("");
+	return 0;
+}
+catch(...)
+{
+	if(errno)
+		std::perror("ERROR");
+
+	const char* sdl_error = SDL_GetError();
+	if(*sdl_error)
+		std::puts(sdl_error);
+
+	throw;
+}
+
+void render_screen(int2 size, int2 cursor_position, char bg, char cursor, bool break_lines)
+{
+	static std::string buffer;
+	int linear_size = size.y() * size.x();
+	if(break_lines)
+		linear_size += size.y() - 1;
+	buffer.resize(linear_size);
+
+	auto row = buffer.begin();
+	for(int y = 0; y < size.y(); ++y)
+	{
+		row = std::fill_n(row, size.x(), bg);
+		if(break_lines && y != (size.y() - 1))
+			*row++ = '\n';
+	}
+
+	int pitch = break_lines ? size.x() + 1 : size.x();
+	int linear_cursor_position = cursor_position.y() * pitch + cursor_position.x();
+	buffer[linear_cursor_position] = cursor;
+
+	std::fputs(buffer.c_str(), stdout);
+	std::fflush(stdout);
+}
+
diff --git a/source/simple/interactive/codes.cpp b/source/simple/interactive/codes.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..4b547beb524fb225e8ddac6d931610f86ec9a051
--- /dev/null
+++ b/source/simple/interactive/codes.cpp
@@ -0,0 +1,16 @@
+#include "codes.h"
+#include "simple/support/enum.hpp"
+
+using namespace simple::interactive;
+using simple::support::to_integer;
+
+scancode to_scancode(keycode code) noexcept
+{
+	return static_cast<scancode>(SDL_GetScancodeFromKey(to_integer(code)));
+}
+
+keycode to_keycode(scancode code) noexcept
+{
+	return static_cast<keycode>(
+		SDL_GetKeyFromScancode(static_cast<SDL_Scancode>(to_integer(code))) );
+}
diff --git a/source/simple/interactive/codes.h b/source/simple/interactive/codes.h
new file mode 100644
index 0000000000000000000000000000000000000000..3c969a8217625d90b68d7b142184108f32246769
--- /dev/null
+++ b/source/simple/interactive/codes.h
@@ -0,0 +1,500 @@
+#ifndef SIMPLE_INTERACTIVE_CODES_H
+#define SIMPLE_INTERACTIVE_CODES_H
+#include <SDL2/SDL.h>
+
+namespace simple::interactive
+{
+
+	enum class keycode : SDL_Keycode
+	{
+		_0 = SDLK_0,
+		_1 = SDLK_1,
+		_2 = SDLK_2,
+		_3 = SDLK_3,
+		_4 = SDLK_4,
+		_5 = SDLK_5,
+		_6 = SDLK_6,
+		_7 = SDLK_7,
+		_8 = SDLK_8,
+		_9 = SDLK_9,
+		a = SDLK_a,
+		ac_back = SDLK_AC_BACK,
+		ac_bookmarks = SDLK_AC_BOOKMARKS,
+		ac_forward = SDLK_AC_FORWARD,
+		ac_home = SDLK_AC_HOME,
+		ac_refresh = SDLK_AC_REFRESH,
+		ac_search = SDLK_AC_SEARCH,
+		ac_stop = SDLK_AC_STOP,
+		again = SDLK_AGAIN,
+		alterase = SDLK_ALTERASE,
+		ampersand = SDLK_AMPERSAND,
+		application = SDLK_APPLICATION,
+		asterisk = SDLK_ASTERISK,
+		at = SDLK_AT,
+		audiomute = SDLK_AUDIOMUTE,
+		audionext = SDLK_AUDIONEXT,
+		audioplay = SDLK_AUDIOPLAY,
+		audioprev = SDLK_AUDIOPREV,
+		audiostop = SDLK_AUDIOSTOP,
+		b = SDLK_b,
+		backquote = SDLK_BACKQUOTE,
+		backslash = SDLK_BACKSLASH,
+		backspace = SDLK_BACKSPACE,
+		brightnessdown = SDLK_BRIGHTNESSDOWN,
+		brightnessup = SDLK_BRIGHTNESSUP,
+		c = SDLK_c,
+		calculator = SDLK_CALCULATOR,
+		cancel = SDLK_CANCEL,
+		capslock = SDLK_CAPSLOCK,
+		caret = SDLK_CARET,
+		clear = SDLK_CLEAR,
+		clearagain = SDLK_CLEARAGAIN,
+		colon = SDLK_COLON,
+		comma = SDLK_COMMA,
+		computer = SDLK_COMPUTER,
+		copy = SDLK_COPY,
+		crsel = SDLK_CRSEL,
+		currencysubunit = SDLK_CURRENCYSUBUNIT,
+		currencyunit = SDLK_CURRENCYUNIT,
+		cut = SDLK_CUT,
+		d = SDLK_d,
+		decimalseparator = SDLK_DECIMALSEPARATOR,
+		del = SDLK_DELETE,
+		displayswitch = SDLK_DISPLAYSWITCH,
+		dollar = SDLK_DOLLAR,
+		down = SDLK_DOWN,
+		e = SDLK_e,
+		eject = SDLK_EJECT,
+		end = SDLK_END,
+		equals = SDLK_EQUALS,
+		escape = SDLK_ESCAPE,
+		exclaim = SDLK_EXCLAIM,
+		execute = SDLK_EXECUTE,
+		exsel = SDLK_EXSEL,
+		f = SDLK_f,
+		f1 = SDLK_F1,
+		f10 = SDLK_F10,
+		f11 = SDLK_F11,
+		f12 = SDLK_F12,
+		f13 = SDLK_F13,
+		f14 = SDLK_F14,
+		f15 = SDLK_F15,
+		f16 = SDLK_F16,
+		f17 = SDLK_F17,
+		f18 = SDLK_F18,
+		f19 = SDLK_F19,
+		f2 = SDLK_F2,
+		f20 = SDLK_F20,
+		f21 = SDLK_F21,
+		f22 = SDLK_F22,
+		f23 = SDLK_F23,
+		f24 = SDLK_F24,
+		f3 = SDLK_F3,
+		f4 = SDLK_F4,
+		f5 = SDLK_F5,
+		f6 = SDLK_F6,
+		f7 = SDLK_F7,
+		f8 = SDLK_F8,
+		f9 = SDLK_F9,
+		find = SDLK_FIND,
+		g = SDLK_g,
+		greater = SDLK_GREATER,
+		h = SDLK_h,
+		hash = SDLK_HASH,
+		help = SDLK_HELP,
+		SDLK_HOME,
+		i = SDLK_i,
+		insert = SDLK_INSERT,
+		j = SDLK_j,
+		k = SDLK_k,
+		kbdillumdown = SDLK_KBDILLUMDOWN,
+		kbdillumtoggle = SDLK_KBDILLUMTOGGLE,
+		kbdillumup = SDLK_KBDILLUMUP,
+		kp_0 = SDLK_KP_0,
+		kp_00 = SDLK_KP_00,
+		kp_000 = SDLK_KP_000,
+		kp_1 = SDLK_KP_1,
+		kp_2 = SDLK_KP_2,
+		kp_3 = SDLK_KP_3,
+		kp_4 = SDLK_KP_4,
+		kp_5 = SDLK_KP_5,
+		kp_6 = SDLK_KP_6,
+		kp_7 = SDLK_KP_7,
+		kp_8 = SDLK_KP_8,
+		kp_9 = SDLK_KP_9,
+		kp_a = SDLK_KP_A,
+		kp_ampersand = SDLK_KP_AMPERSAND,
+		kp_at = SDLK_KP_AT,
+		kp_b = SDLK_KP_B,
+		kp_backspace = SDLK_KP_BACKSPACE,
+		kp_binary = SDLK_KP_BINARY,
+		kp_c = SDLK_KP_C,
+		kp_clear = SDLK_KP_CLEAR,
+		kp_clearentry = SDLK_KP_CLEARENTRY,
+		kp_colon = SDLK_KP_COLON,
+		kp_comma = SDLK_KP_COMMA,
+		kp_d = SDLK_KP_D,
+		kp_dblampersand = SDLK_KP_DBLAMPERSAND,
+		kp_dblverticalbar = SDLK_KP_DBLVERTICALBAR,
+		kp_decimal = SDLK_KP_DECIMAL,
+		kp_divide = SDLK_KP_DIVIDE,
+		kp_e = SDLK_KP_E,
+		kp_enter = SDLK_KP_ENTER,
+		kp_equals = SDLK_KP_EQUALS,
+		kp_equalsas400 = SDLK_KP_EQUALSAS400,
+		kp_exclam = SDLK_KP_EXCLAM,
+		kp_f = SDLK_KP_F,
+		kp_greater = SDLK_KP_GREATER,
+		kp_hash = SDLK_KP_HASH,
+		kp_hexadecimal = SDLK_KP_HEXADECIMAL,
+		kp_leftbrace = SDLK_KP_LEFTBRACE,
+		kp_leftparen = SDLK_KP_LEFTPAREN,
+		kp_less = SDLK_KP_LESS,
+		kp_memadd = SDLK_KP_MEMADD,
+		kp_memclear = SDLK_KP_MEMCLEAR,
+		kp_memdivide = SDLK_KP_MEMDIVIDE,
+		kp_memmultiply = SDLK_KP_MEMMULTIPLY,
+		kp_memrecall = SDLK_KP_MEMRECALL,
+		kp_memstore = SDLK_KP_MEMSTORE,
+		kp_memsubtract = SDLK_KP_MEMSUBTRACT,
+		kp_minus = SDLK_KP_MINUS,
+		kp_multiply = SDLK_KP_MULTIPLY,
+		kp_octal = SDLK_KP_OCTAL,
+		kp_percent = SDLK_KP_PERCENT,
+		kp_period = SDLK_KP_PERIOD,
+		kp_plus = SDLK_KP_PLUS,
+		kp_plusminus = SDLK_KP_PLUSMINUS,
+		kp_power = SDLK_KP_POWER,
+		kp_rightbrace = SDLK_KP_RIGHTBRACE,
+		kp_rightparen = SDLK_KP_RIGHTPAREN,
+		kp_space = SDLK_KP_SPACE,
+		kp_tab = SDLK_KP_TAB,
+		kp_verticalbar = SDLK_KP_VERTICALBAR,
+		kp_xor = SDLK_KP_XOR,
+		l = SDLK_l,
+		lalt = SDLK_LALT,
+		lctrl = SDLK_LCTRL,
+		left = SDLK_LEFT,
+		leftbracket = SDLK_LEFTBRACKET,
+		leftparen = SDLK_LEFTPAREN,
+		less = SDLK_LESS,
+		lgui = SDLK_LGUI,
+		lshift = SDLK_LSHIFT,
+		m = SDLK_m,
+		mail = SDLK_MAIL,
+		mediaselect = SDLK_MEDIASELECT,
+		menu = SDLK_MENU,
+		minus = SDLK_MINUS,
+		mode = SDLK_MODE,
+		mute = SDLK_MUTE,
+		n = SDLK_n,
+		numlockclear = SDLK_NUMLOCKCLEAR,
+		o = SDLK_o,
+		oper = SDLK_OPER,
+		out = SDLK_OUT,
+		p = SDLK_p,
+		pagedown = SDLK_PAGEDOWN,
+		pageup = SDLK_PAGEUP,
+		paste = SDLK_PASTE,
+		pause = SDLK_PAUSE,
+		percent = SDLK_PERCENT,
+		period = SDLK_PERIOD,
+		plus = SDLK_PLUS,
+		power = SDLK_POWER,
+		printscreen = SDLK_PRINTSCREEN,
+		prior = SDLK_PRIOR,
+		q = SDLK_q,
+		question = SDLK_QUESTION,
+		quote = SDLK_QUOTE,
+		quotedbl = SDLK_QUOTEDBL,
+		r = SDLK_r,
+		ralt = SDLK_RALT,
+		rctrl = SDLK_RCTRL,
+		enter = SDLK_RETURN,
+		return2 = SDLK_RETURN2,
+		rgui = SDLK_RGUI,
+		right = SDLK_RIGHT,
+		rightbracket = SDLK_RIGHTBRACKET,
+		rightparen = SDLK_RIGHTPAREN,
+		rshift = SDLK_RSHIFT,
+		s = SDLK_s,
+		scrolllock = SDLK_SCROLLLOCK,
+		select = SDLK_SELECT,
+		semicolon = SDLK_SEMICOLON,
+		separator = SDLK_SEPARATOR,
+		slash = SDLK_SLASH,
+		sleep = SDLK_SLEEP,
+		space = SDLK_SPACE,
+		stop = SDLK_STOP,
+		sysreq = SDLK_SYSREQ,
+		t = SDLK_t,
+		tab = SDLK_TAB,
+		thousandsseparator = SDLK_THOUSANDSSEPARATOR,
+		u = SDLK_u,
+		underscore = SDLK_UNDERSCORE,
+		undo = SDLK_UNDO,
+		unknown = SDLK_UNKNOWN,
+		up = SDLK_UP,
+		v = SDLK_v,
+		volumedown = SDLK_VOLUMEDOWN,
+		volumeup = SDLK_VOLUMEUP,
+		w = SDLK_w,
+		www = SDLK_WWW,
+		x = SDLK_x,
+		y = SDLK_y,
+		z = SDLK_z,
+	};
+
+	enum class scancode : std::underlying_type_t<SDL_Scancode>
+	{
+		_0 = SDL_SCANCODE_0,
+		_1 = SDL_SCANCODE_1,
+		_2 = SDL_SCANCODE_2,
+		_3 = SDL_SCANCODE_3,
+		_4 = SDL_SCANCODE_4,
+		_5 = SDL_SCANCODE_5,
+		_6 = SDL_SCANCODE_6,
+		_7 = SDL_SCANCODE_7,
+		_8 = SDL_SCANCODE_8,
+		_9 = SDL_SCANCODE_9,
+		a = SDL_SCANCODE_A,
+		ac_back = SDL_SCANCODE_AC_BACK,
+		ac_bookmarks = SDL_SCANCODE_AC_BOOKMARKS,
+		ac_forward = SDL_SCANCODE_AC_FORWARD,
+		ac_home = SDL_SCANCODE_AC_HOME,
+		ac_refresh = SDL_SCANCODE_AC_REFRESH,
+		ac_search = SDL_SCANCODE_AC_SEARCH,
+		ac_stop = SDL_SCANCODE_AC_STOP,
+		again = SDL_SCANCODE_AGAIN,
+		alterase = SDL_SCANCODE_ALTERASE,
+		apostrophe = SDL_SCANCODE_APOSTROPHE,
+		application = SDL_SCANCODE_APPLICATION,
+		audiomute = SDL_SCANCODE_AUDIOMUTE,
+		audionext = SDL_SCANCODE_AUDIONEXT,
+		audioplay = SDL_SCANCODE_AUDIOPLAY,
+		audioprev = SDL_SCANCODE_AUDIOPREV,
+		audiostop = SDL_SCANCODE_AUDIOSTOP,
+		b = SDL_SCANCODE_B,
+		backslash = SDL_SCANCODE_BACKSLASH,
+		backspace = SDL_SCANCODE_BACKSPACE,
+		brightnessdown = SDL_SCANCODE_BRIGHTNESSDOWN,
+		brightnessup = SDL_SCANCODE_BRIGHTNESSUP,
+		c = SDL_SCANCODE_C,
+		calculator = SDL_SCANCODE_CALCULATOR,
+		cancel = SDL_SCANCODE_CANCEL,
+		capslock = SDL_SCANCODE_CAPSLOCK,
+		clear = SDL_SCANCODE_CLEAR,
+		clearagain = SDL_SCANCODE_CLEARAGAIN,
+		comma = SDL_SCANCODE_COMMA,
+		computer = SDL_SCANCODE_COMPUTER,
+		copy = SDL_SCANCODE_COPY,
+		crsel = SDL_SCANCODE_CRSEL,
+		currencysubunit = SDL_SCANCODE_CURRENCYSUBUNIT,
+		currencyunit = SDL_SCANCODE_CURRENCYUNIT,
+		cut = SDL_SCANCODE_CUT,
+		d = SDL_SCANCODE_D,
+		decimalseparator = SDL_SCANCODE_DECIMALSEPARATOR,
+		del = SDL_SCANCODE_DELETE,
+		displayswitch = SDL_SCANCODE_DISPLAYSWITCH,
+		down = SDL_SCANCODE_DOWN,
+		e = SDL_SCANCODE_E,
+		eject = SDL_SCANCODE_EJECT,
+		end = SDL_SCANCODE_END,
+		equals = SDL_SCANCODE_EQUALS,
+		escape = SDL_SCANCODE_ESCAPE,
+		execute = SDL_SCANCODE_EXECUTE,
+		exsel = SDL_SCANCODE_EXSEL,
+		f = SDL_SCANCODE_F,
+		f1 = SDL_SCANCODE_F1,
+		f10 = SDL_SCANCODE_F10,
+		f11 = SDL_SCANCODE_F11,
+		f12 = SDL_SCANCODE_F12,
+		f13 = SDL_SCANCODE_F13,
+		f14 = SDL_SCANCODE_F14,
+		f15 = SDL_SCANCODE_F15,
+		f16 = SDL_SCANCODE_F16,
+		f17 = SDL_SCANCODE_F17,
+		f18 = SDL_SCANCODE_F18,
+		f19 = SDL_SCANCODE_F19,
+		f2 = SDL_SCANCODE_F2,
+		f20 = SDL_SCANCODE_F20,
+		f21 = SDL_SCANCODE_F21,
+		f22 = SDL_SCANCODE_F22,
+		f23 = SDL_SCANCODE_F23,
+		f24 = SDL_SCANCODE_F24,
+		f3 = SDL_SCANCODE_F3,
+		f4 = SDL_SCANCODE_F4,
+		f5 = SDL_SCANCODE_F5,
+		f6 = SDL_SCANCODE_F6,
+		f7 = SDL_SCANCODE_F7,
+		f8 = SDL_SCANCODE_F8,
+		f9 = SDL_SCANCODE_F9,
+		find = SDL_SCANCODE_FIND,
+		g = SDL_SCANCODE_G,
+		grave = SDL_SCANCODE_GRAVE,
+		h = SDL_SCANCODE_H,
+		help = SDL_SCANCODE_HELP,
+		home = SDL_SCANCODE_HOME,
+		i = SDL_SCANCODE_I,
+		insert = SDL_SCANCODE_INSERT,
+		international1 = SDL_SCANCODE_INTERNATIONAL1,
+		international2 = SDL_SCANCODE_INTERNATIONAL2,
+		international3 = SDL_SCANCODE_INTERNATIONAL3,
+		international4 = SDL_SCANCODE_INTERNATIONAL4,
+		international5 = SDL_SCANCODE_INTERNATIONAL5,
+		international6 = SDL_SCANCODE_INTERNATIONAL6,
+		international7 = SDL_SCANCODE_INTERNATIONAL7,
+		international8 = SDL_SCANCODE_INTERNATIONAL8,
+		international9 = SDL_SCANCODE_INTERNATIONAL9,
+		j = SDL_SCANCODE_J,
+		k = SDL_SCANCODE_K,
+		kbdillumdown = SDL_SCANCODE_KBDILLUMDOWN,
+		kbdillumtoggle = SDL_SCANCODE_KBDILLUMTOGGLE,
+		kbdillumup = SDL_SCANCODE_KBDILLUMUP,
+		kp_0 = SDL_SCANCODE_KP_0,
+		kp_00 = SDL_SCANCODE_KP_00,
+		kp_000 = SDL_SCANCODE_KP_000,
+		kp_1 = SDL_SCANCODE_KP_1,
+		kp_2 = SDL_SCANCODE_KP_2,
+		kp_3 = SDL_SCANCODE_KP_3,
+		kp_4 = SDL_SCANCODE_KP_4,
+		kp_5 = SDL_SCANCODE_KP_5,
+		kp_6 = SDL_SCANCODE_KP_6,
+		kp_7 = SDL_SCANCODE_KP_7,
+		kp_8 = SDL_SCANCODE_KP_8,
+		kp_9 = SDL_SCANCODE_KP_9,
+		kp_a = SDL_SCANCODE_KP_A,
+		kp_ampersand = SDL_SCANCODE_KP_AMPERSAND,
+		kp_at = SDL_SCANCODE_KP_AT,
+		kp_b = SDL_SCANCODE_KP_B,
+		kp_backspace = SDL_SCANCODE_KP_BACKSPACE,
+		kp_binary = SDL_SCANCODE_KP_BINARY,
+		kp_c = SDL_SCANCODE_KP_C,
+		kp_clear = SDL_SCANCODE_KP_CLEAR,
+		kp_clearentry = SDL_SCANCODE_KP_CLEARENTRY,
+		kp_colon = SDL_SCANCODE_KP_COLON,
+		kp_comma = SDL_SCANCODE_KP_COMMA,
+		kp_d = SDL_SCANCODE_KP_D,
+		kp_dblampersand = SDL_SCANCODE_KP_DBLAMPERSAND,
+		kp_dblverticalbar = SDL_SCANCODE_KP_DBLVERTICALBAR,
+		kp_decimal = SDL_SCANCODE_KP_DECIMAL,
+		kp_divide = SDL_SCANCODE_KP_DIVIDE,
+		kp_e = SDL_SCANCODE_KP_E,
+		kp_enter = SDL_SCANCODE_KP_ENTER,
+		kp_equals = SDL_SCANCODE_KP_EQUALS,
+		kp_equalsas400 = SDL_SCANCODE_KP_EQUALSAS400,
+		kp_exclam = SDL_SCANCODE_KP_EXCLAM,
+		kp_f = SDL_SCANCODE_KP_F,
+		kp_greater = SDL_SCANCODE_KP_GREATER,
+		kp_hash = SDL_SCANCODE_KP_HASH,
+		kp_hexadecimal = SDL_SCANCODE_KP_HEXADECIMAL,
+		kp_leftbrace = SDL_SCANCODE_KP_LEFTBRACE,
+		kp_leftparen = SDL_SCANCODE_KP_LEFTPAREN,
+		kp_less = SDL_SCANCODE_KP_LESS,
+		kp_memadd = SDL_SCANCODE_KP_MEMADD,
+		kp_memclear = SDL_SCANCODE_KP_MEMCLEAR,
+		kp_memdivide = SDL_SCANCODE_KP_MEMDIVIDE,
+		kp_memmultiply = SDL_SCANCODE_KP_MEMMULTIPLY,
+		kp_memrecall = SDL_SCANCODE_KP_MEMRECALL,
+		kp_memstore = SDL_SCANCODE_KP_MEMSTORE,
+		kp_memsubtract = SDL_SCANCODE_KP_MEMSUBTRACT,
+		kp_minus = SDL_SCANCODE_KP_MINUS,
+		kp_multiply = SDL_SCANCODE_KP_MULTIPLY,
+		kp_octal = SDL_SCANCODE_KP_OCTAL,
+		kp_percent = SDL_SCANCODE_KP_PERCENT,
+		kp_period = SDL_SCANCODE_KP_PERIOD,
+		kp_plus = SDL_SCANCODE_KP_PLUS,
+		kp_plusminus = SDL_SCANCODE_KP_PLUSMINUS,
+		kp_power = SDL_SCANCODE_KP_POWER,
+		kp_rightbrace = SDL_SCANCODE_KP_RIGHTBRACE,
+		kp_rightparen = SDL_SCANCODE_KP_RIGHTPAREN,
+		kp_space = SDL_SCANCODE_KP_SPACE,
+		kp_tab = SDL_SCANCODE_KP_TAB,
+		kp_verticalbar = SDL_SCANCODE_KP_VERTICALBAR,
+		kp_xor = SDL_SCANCODE_KP_XOR,
+		l = SDL_SCANCODE_L,
+		lalt = SDL_SCANCODE_LALT,
+		lang1 = SDL_SCANCODE_LANG1,
+		lang2 = SDL_SCANCODE_LANG2,
+		lang3 = SDL_SCANCODE_LANG3,
+		lang4 = SDL_SCANCODE_LANG4,
+		lang5 = SDL_SCANCODE_LANG5,
+		lang6 = SDL_SCANCODE_LANG6,
+		lang7 = SDL_SCANCODE_LANG7,
+		lang8 = SDL_SCANCODE_LANG8,
+		lang9 = SDL_SCANCODE_LANG9,
+		lctrl = SDL_SCANCODE_LCTRL,
+		left = SDL_SCANCODE_LEFT,
+		leftbracket = SDL_SCANCODE_LEFTBRACKET,
+		lgui = SDL_SCANCODE_LGUI,
+		// well these don't exist... wiki lies!!
+		// lockingcapslock = SDL_SCANCODE_LOCKINGCAPSLOCK,
+		// lockingnumlock = SDL_SCANCODE_LOCKINGNUMLOCK,
+		// lockingscrolllock = SDL_SCANCODE_LOCKINGSCROLLLOCK,
+		lshift = SDL_SCANCODE_LSHIFT,
+		m = SDL_SCANCODE_M,
+		mail = SDL_SCANCODE_MAIL,
+		mediaselect = SDL_SCANCODE_MEDIASELECT,
+		menu = SDL_SCANCODE_MENU,
+		minus = SDL_SCANCODE_MINUS,
+		mode = SDL_SCANCODE_MODE,
+		mute = SDL_SCANCODE_MUTE,
+		n = SDL_SCANCODE_N,
+		nonusbackslash = SDL_SCANCODE_NONUSBACKSLASH,
+		nonushash = SDL_SCANCODE_NONUSHASH,
+		numlockclear = SDL_SCANCODE_NUMLOCKCLEAR,
+		o = SDL_SCANCODE_O,
+		oper = SDL_SCANCODE_OPER,
+		out = SDL_SCANCODE_OUT,
+		p = SDL_SCANCODE_P,
+		pagedown = SDL_SCANCODE_PAGEDOWN,
+		pageup = SDL_SCANCODE_PAGEUP,
+		paste = SDL_SCANCODE_PASTE,
+		pause = SDL_SCANCODE_PAUSE,
+		period = SDL_SCANCODE_PERIOD,
+		power = SDL_SCANCODE_POWER,
+		printscreen = SDL_SCANCODE_PRINTSCREEN,
+		prior = SDL_SCANCODE_PRIOR,
+		q = SDL_SCANCODE_Q,
+		r = SDL_SCANCODE_R,
+		ralt = SDL_SCANCODE_RALT,
+		rctrl = SDL_SCANCODE_RCTRL,
+		enter = SDL_SCANCODE_RETURN,
+		return2 = SDL_SCANCODE_RETURN2,
+		rgui = SDL_SCANCODE_RGUI,
+		right = SDL_SCANCODE_RIGHT,
+		rightbracket = SDL_SCANCODE_RIGHTBRACKET,
+		rshift = SDL_SCANCODE_RSHIFT,
+		s = SDL_SCANCODE_S,
+		scrolllock = SDL_SCANCODE_SCROLLLOCK,
+		select = SDL_SCANCODE_SELECT,
+		semicolon = SDL_SCANCODE_SEMICOLON,
+		separator = SDL_SCANCODE_SEPARATOR,
+		slash = SDL_SCANCODE_SLASH,
+		sleep = SDL_SCANCODE_SLEEP,
+		space = SDL_SCANCODE_SPACE,
+		stop = SDL_SCANCODE_STOP,
+		sysreq = SDL_SCANCODE_SYSREQ,
+		t = SDL_SCANCODE_T,
+		tab = SDL_SCANCODE_TAB,
+		thousandsseparator = SDL_SCANCODE_THOUSANDSSEPARATOR,
+		u = SDL_SCANCODE_U,
+		undo = SDL_SCANCODE_UNDO,
+		unknown = SDL_SCANCODE_UNKNOWN,
+		up = SDL_SCANCODE_UP,
+		v = SDL_SCANCODE_V,
+		volumedown = SDL_SCANCODE_VOLUMEDOWN,
+		volumeup = SDL_SCANCODE_VOLUMEUP,
+		w = SDL_SCANCODE_W,
+		www = SDL_SCANCODE_WWW,
+		x = SDL_SCANCODE_X,
+		y = SDL_SCANCODE_Y,
+		z = SDL_SCANCODE_Z,
+	};
+
+	scancode to_scancode(keycode) noexcept;
+	keycode to_keycode(scancode) noexcept;
+
+} // namespace simple::interactive
+
+#endif /* end of include guard */
diff --git a/source/simple/interactive/event.cpp b/source/simple/interactive/event.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..37a7a58533624419af01996aeb3158f0fca02058
--- /dev/null
+++ b/source/simple/interactive/event.cpp
@@ -0,0 +1,185 @@
+#include "event.h"
+#include "simple/sdlcore/utils.hpp"
+
+namespace simple::interactive
+{
+
+	std::optional<event> next_event() noexcept
+	{
+		SDL_Event event;
+		while(SDL_PollEvent(&event))
+		{
+			switch(event.type)
+			{
+				case SDL_KEYDOWN:
+					return key_pressed
+					{
+						event.key.windowID,
+						std::chrono::milliseconds(event.key.timestamp),
+						static_cast<keycode>(event.key.keysym.sym),
+						static_cast<scancode>(event.key.keysym.scancode),
+						static_cast<keystate>(event.key.state),
+						event.key.repeat
+					};
+				case SDL_KEYUP:
+					return key_released
+					{
+						event.key.windowID,
+						std::chrono::milliseconds(event.key.timestamp),
+						static_cast<keycode>(event.key.keysym.sym),
+						static_cast<scancode>(event.key.keysym.scancode),
+						static_cast<keystate>(event.key.state),
+						event.key.repeat
+					};
+				case SDL_MOUSEBUTTONDOWN:
+					return mouse_down
+					{
+						event.button.windowID,
+						std::chrono::milliseconds(event.button.timestamp),
+						event.button.which,
+						{event.button.x, event.button.y},
+						static_cast<mouse_button>(event.button.button),
+						static_cast<keystate>(event.button.state),
+						event.button.clicks
+					};
+				case SDL_MOUSEBUTTONUP:
+					return mouse_up
+					{
+						event.button.windowID,
+						std::chrono::milliseconds(event.button.timestamp),
+						event.button.which,
+						{event.button.x, event.button.y},
+						static_cast<mouse_button>(event.button.button),
+						static_cast<keystate>(event.button.state),
+#if SDL_VERSION_ATLEAST(2,0,2)
+						event.button.clicks
+#endif
+					};
+				case SDL_MOUSEMOTION:
+					return mouse_motion
+					{
+						event.motion.windowID,
+						std::chrono::milliseconds(event.motion.timestamp),
+						event.motion.which,
+						{event.motion.x, event.motion.y},
+						{event.motion.xrel, event.motion.yrel},
+						static_cast<mouse_button_mask>(event.motion.state),
+					};
+				case SDL_MOUSEWHEEL:
+					return mouse_wheel
+					{
+						event.wheel.windowID,
+						std::chrono::milliseconds(event.wheel.timestamp),
+						event.wheel.which,
+						{event.wheel.x, event.wheel.y},
+#if SDL_VERSION_ATLEAST(2,0,4)
+						static_cast<wheel_direction>(event.wheel.direction),
+#endif
+					};
+			}
+		}
+		return std::nullopt;
+	}
+
+#if SDL_VERSION_ATLEAST(2,0,4)
+	int2 mouse_wheel::motion() const noexcept
+	{
+		return data.direction == wheel_direction::flipped ? -data.position : data.position;
+	}
+#endif
+
+	bool relative_mouse_mode() noexcept
+	{
+		return SDL_GetRelativeMouseMode();
+	}
+
+	bool relative_mouse_mode(bool enable) noexcept
+	{
+		return !sdlcore::utils::check_error(SDL_SetRelativeMouseMode(SDL_bool(enable)));
+	}
+
+	void require_mouse_mode(bool enable) noexcept
+	{
+		sdlcore::utils::throw_error(SDL_SetRelativeMouseMode(SDL_bool(enable)));
+	}
+
+	std::optional<float2> window_normalized(uint32_t window_id, int2 position) noexcept
+	{
+		float2 result = static_cast<float2>(position);
+
+		auto window = SDL_GetWindowFromID(window_id);
+		if(!window)
+			return std::nullopt;
+
+		int2 window_size;
+		SDL_GetWindowSize(window, &window_size.x(), &window_size.y());
+		if( std::any_of(window_size.begin(), window_size.end(), [](auto c){ return c == 0;}) )
+			return std::nullopt;
+
+		result /= static_cast<float2>(window_size);
+		return result;
+	}
+
+	std::optional<float2> window_normalized_position(const common_mouse_data& data) noexcept
+	{
+		return window_normalized(data.window_id, data.position);
+	}
+
+	std::optional<float2> window_normalized_motion(const mouse_motion_data& data) noexcept
+	{
+		return window_normalized(data.window_id, data.motion);
+	}
+
+	std::optional<float2> screen_normalized(uint32_t window_id, int2 position, bool absolute = true) noexcept
+	{
+		float2 result = static_cast<float2>(position);
+
+		auto window = SDL_GetWindowFromID(window_id);
+		if(!window)
+			return std::nullopt;
+
+		if(absolute)
+		{
+			int2 window_position;
+			SDL_GetWindowPosition(window, &window_position.x(), &window_position.y());
+			result += static_cast<float2>(window_position);
+		}
+
+		SDL_DisplayMode mode;
+		if(SDL_GetCurrentDisplayMode(SDL_GetWindowDisplayIndex(window), &mode) < 0)
+			return std::nullopt;
+
+		const int2 screen_size{mode.w, mode.h};
+
+		result /= static_cast<float2>(screen_size);
+		return result;
+	}
+
+	std::optional<float2> screen_normalized_position(const common_mouse_data& data) noexcept
+	{
+		return screen_normalized(data.window_id, data.position);
+	}
+
+	std::optional<float2> screen_normalized_motion(const mouse_motion_data& data) noexcept
+	{
+		return screen_normalized(data.window_id, data.motion, false);
+	}
+
+#if SDL_VERSION_ATLEAST(2,0,4)
+
+	// better to use expected<bool, error>
+	bool mouse_capture(bool enable) noexcept
+	{
+		SDL_PumpEvents();
+		return !sdlcore::utils::check_error(SDL_CaptureMouse(SDL_bool(enable)));
+	}
+
+	void require_mouse_capture(bool enable)
+	{
+		SDL_PumpEvents();
+		sdlcore::utils::throw_error(SDL_CaptureMouse(SDL_bool(enable)));
+	}
+
+#endif
+
+} // namespace simple::interactive
diff --git a/source/simple/interactive/event.h b/source/simple/interactive/event.h
new file mode 100644
index 0000000000000000000000000000000000000000..033388368cc1d586297a4b4606fe3f4a1e17a0cb
--- /dev/null
+++ b/source/simple/interactive/event.h
@@ -0,0 +1,168 @@
+#ifndef SIMPLE_INTERACTIVE_EVENT_H
+#define SIMPLE_INTERACTIVE_EVENT_H
+#include "codes.h"
+#include <chrono>
+#include <variant>
+#include <optional>
+#include "simple/geom/vector.hpp"
+
+namespace simple::interactive
+{
+	using int2 = simple::geom::vector<int, 2>;
+	using float2 = simple::geom::vector<float, 2>;
+
+	enum class keystate : uint8_t
+	{
+		pressed = SDL_PRESSED,
+		released = SDL_RELEASED
+	};
+
+#if SDL_VERSION_ATLEAST(2,0,4)
+	enum class wheel_direction : uint32_t
+	{
+		normal = SDL_MOUSEWHEEL_NORMAL,
+		flipped = SDL_MOUSEWHEEL_FLIPPED
+	};
+#endif
+
+	enum class mouse_button : uint8_t
+	{
+		left = SDL_BUTTON_LEFT,
+		right = SDL_BUTTON_RIGHT,
+		middle = SDL_BUTTON_MIDDLE,
+		x1 = SDL_BUTTON_X1,
+		x2 = SDL_BUTTON_X2
+	};
+
+	enum class mouse_button_mask : uint32_t
+	{
+		left = SDL_BUTTON_LMASK,
+		right = SDL_BUTTON_RMASK,
+		middle = SDL_BUTTON_MMASK,
+		x1 = SDL_BUTTON_X1MASK,
+		x2 = SDL_BUTTON_X2MASK
+	};
+	using ::operator |;
+	using ::operator &;
+	using ::operator &&;
+
+	constexpr uint32_t touch_mouse_id = SDL_TOUCH_MOUSEID;
+
+	struct common_data
+	{
+		uint32_t window_id;
+		std::chrono::milliseconds timestamp;
+	};
+
+	struct key_data : public common_data
+	{
+		enum keycode keycode;
+		enum scancode scancode;
+		keystate state;
+		uint8_t repeat;
+	};
+
+	struct common_mouse_data : public common_data
+	{
+		uint32_t mouse_id;
+		int2 position;
+	};
+
+	struct mouse_button_data : public common_mouse_data
+	{
+		mouse_button button;
+		keystate state;
+#if SDL_VERSION_ATLEAST(2,0,2)
+		uint8_t clicks;
+#endif
+	};
+
+	struct mouse_motion_data : public common_mouse_data
+	{
+		int2 motion;
+		mouse_button_mask button_state;
+	};
+
+	struct mouse_wheel_data : public common_mouse_data
+	{
+#if SDL_VERSION_ATLEAST(2,0,4)
+		wheel_direction direction;
+#endif
+	};
+
+	struct key_event
+	{
+		const key_data data;
+	};
+
+	struct key_pressed : key_event
+	{};
+
+	struct key_released : key_event
+	{};
+
+	std::optional<float2> window_normalized_position(const common_mouse_data& data) noexcept;
+	std::optional<float2> screen_normalized_position(const common_mouse_data& data) noexcept;
+	std::optional<float2> window_normalized_motion(const mouse_motion_data& data) noexcept;
+	std::optional<float2> screen_normalized_motion(const mouse_motion_data& data) noexcept;
+
+	template<typename event_data>
+	struct mouse_position_event
+	{
+		const event_data data;
+		auto window_normalized_position() const noexcept { return interactive::window_normalized_position(data); }
+		auto screen_normalized_position() const noexcept { return interactive::screen_normalized_position(data); }
+	};
+
+	struct mouse_motion : public mouse_position_event<mouse_motion_data>
+	{
+		auto window_normalized_motion() const noexcept { return interactive::window_normalized_motion(data); }
+		auto screen_normalized_motion() const noexcept { return interactive::screen_normalized_motion(data); }
+	};
+
+	struct mouse_wheel
+	{
+		const mouse_wheel_data data;
+#if SDL_VERSION_ATLEAST(2,0,4)
+		int2 motion() const noexcept;
+#endif
+	};
+
+	struct mouse_button_event : public mouse_position_event<mouse_button_data>
+	{
+	};
+
+	struct mouse_down : public mouse_button_event
+	{};
+
+	struct mouse_up : public mouse_button_event
+	{};
+
+	using event = std::variant<
+		key_pressed
+		,key_released
+		,mouse_down
+		,mouse_up
+		,mouse_motion
+		,mouse_wheel
+	>;
+
+	std::optional<event> next_event() noexcept;
+
+	// better to use expected<bool, error>
+	bool relative_mouse_mode() noexcept;
+	bool relative_mouse_mode(bool enable) noexcept;
+	void require_relative_mouse_mode(bool enable);
+
+#if SDL_VERSION_ATLEAST(2,0,4)
+	// better to use expected<bool, error>
+	bool mouse_capture(bool enable) noexcept;
+	void require_mouse_capture(bool enable);
+#endif
+
+} // namespace simple::interactive
+
+template<> struct simple::support::define_enum_flags_operators<simple::interactive::mouse_button_mask>
+	: std::true_type {};
+
+#endif /* end of include guard */
diff --git a/source/simple/interactive/initializer.cpp b/source/simple/interactive/initializer.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..1948f89c760f2a334f7c2aa32685ed71c1d4e058
--- /dev/null
+++ b/source/simple/interactive/initializer.cpp
@@ -0,0 +1,10 @@
+#include "initializer.h"
+
+using namespace simple;
+using namespace simple::interactive;
+
+using flag = sdlcore::system_flag;
+
+initializer::initializer() :
+	sdlcore::initializer(flag::event | flag::haptic | flag::joystick | flag::game_controller)
+{}
diff --git a/source/simple/interactive/initializer.h b/source/simple/interactive/initializer.h
new file mode 100644
index 0000000000000000000000000000000000000000..bb2e166971b02eb79e4b2249c267ac0a4a8a4919
--- /dev/null
+++ b/source/simple/interactive/initializer.h
@@ -0,0 +1,18 @@
+#ifndef SIMPLE_GRAPHICAL_INITIALIZER_H
+#define SIMPLE_GRAPHICAL_INITIALIZER_H
+#include <memory>
+#include "simple/sdlcore/initializer.h"
+#include "simple/support/enum_flags_operators.hpp"
+
+namespace simple::interactive
+{
+
+	class initializer : private sdlcore::initializer
+	{
+		public:
+		initializer();
+	};
+
+} // namespace simple::graphical
+
+#endif /* end of include guard */