diff --git a/demos/combustion_demo/src/combustion_demo.cpp b/demos/combustion_demo/src/combustion_demo.cpp
index 12764cf1d6e2dcf5ed6d1069ddbd4e6bf9d030b4..e7d284866a64b1e3bae7f9a3b597cd8b72342338 100644
--- a/demos/combustion_demo/src/combustion_demo.cpp
+++ b/demos/combustion_demo/src/combustion_demo.cpp
@@ -31,7 +31,8 @@
 #include "phx/display/display_system_openvr.hpp"
 #include "phx/display/display_system_window.hpp"
 #include "phx/input/device_system.hpp"
-#include "phx/input/input_system.hpp"
+#include "phx/input/keyboard.hpp"
+#include "phx/input/mouse.hpp"
 #include "phx/rendering/auxiliary/splash_screen.hpp"
 #include "phx/rendering/components/mesh_handle.hpp"
 #include "phx/rendering/components/mesh_render_settings.hpp"
@@ -66,11 +67,13 @@ int main(int, char**) {
   assimp_loader->SetProgressUpdateCallback(
       [splash](float progress) { splash->SetLoadProgress(progress); });
 
-  phx::InputSystem* input_system = engine->GetSystem<phx::InputSystem>();
-
-  input_system->AddKeyPressCallback([&engine](char key) {
-    if (key == 'q') engine->Stop();
-  });
+  phx::Keyboard* keyboard =
+      engine->GetSystem<phx::DeviceSystem>()->GetDevices<phx::Keyboard>()[0];
+  keyboard->RegisterKeySignal(
+      [&engine](char key, phx::Keyboard::KeyEvent key_event, int) {
+        if (key_event != phx::Keyboard::KEY_PRESSED) return;
+        if (key == 'q') engine->Stop();
+      });
 
   auto right_controller_entities = scene->GetEntitiesWithComponents<
       phx::RuntimeComponent<phx::RIGHT_CONTROLLER>>();
@@ -83,8 +86,7 @@ int main(int, char**) {
   }
 
   auto handle = std::async([&right_interaction_behavior, &scene,
-                            rendering_system, splash, input_system,
-                            openvr_system]() {
+                            rendering_system, splash, openvr_system]() {
     auto model_surface_entity = phx::SceneLoader::InsertModelIntoScene(
         "models/combustion/combustion_opt.stl", scene.get());
     auto surface_transform =
@@ -204,7 +206,9 @@ int main(int, char**) {
   }
 
   if (!openvr_system) {
-    camera->AddComponent<DesktopNavigationBehavior>(input_system);
+    phx::Mouse* mouse =
+        engine->GetSystem<phx::DeviceSystem>()->GetDevices<phx::Mouse>()[0];
+    camera->AddComponent<DesktopNavigationBehavior>(mouse);
     camera->GetFirstComponent<phx::Transform>()->Translate(
         glm::vec3(0.0f, 1.0f, 1.0f));
   }
diff --git a/demos/combustion_demo/src/desktop_navigation_behavior.cpp b/demos/combustion_demo/src/desktop_navigation_behavior.cpp
index 0491b8c21da54044d239d8ebce43394ab9c54027..55e1a74e1f34b81e60c0fdcfc287fd152368a95f 100644
--- a/demos/combustion_demo/src/desktop_navigation_behavior.cpp
+++ b/demos/combustion_demo/src/desktop_navigation_behavior.cpp
@@ -32,15 +32,12 @@ SUPPRESS_WARNINGS_END
 #include "phx/core/entity.hpp"
 #include "phx/rendering/components/transform.hpp"
 
-DesktopNavigationBehavior::DesktopNavigationBehavior(
-    phx::InputSystem* input_system)
-    : input_system_(input_system) {
-  input_system_->AddMouseMoveCallback(
-      [this](int x, int y) { OnMouseMove(x, y); });
-  input_system_->AddMousePressCallback(
-      [this](unsigned btn) { OnMousePress(btn); });
-  input_system_->AddMouseReleaseCallback(
-      [this](unsigned btn) { OnMouseRelease(btn); });
+DesktopNavigationBehavior::DesktopNavigationBehavior(phx::Mouse* mouse) {
+  mouse->RegisterMoveSignal([this](int x, int y) { OnMouseMove(x, y); });
+  mouse->RegisterButtonSignal(
+      [this](phx::Mouse::ButtonId id, phx::Mouse::ButtonEvent event) {
+        OnMouseButton(id, event);
+      });
 }
 
 void DesktopNavigationBehavior::OnUpdate() {
@@ -76,26 +73,27 @@ void DesktopNavigationBehavior::OnMouseMove(int x, int y) {
   accumulated_mouse_pos_ += glm::ivec2{x, y};
 }
 
-void DesktopNavigationBehavior::OnMousePress(unsigned btn) {
-  if (btn == 1) {
-    rotation_mode_ = true;
-  }
-  if (btn == 2) {
-    strafe_mode_ = true;
-  }
-  if (btn == 3) {
-    translation_mode_ = true;
-  }
-}
-
-void DesktopNavigationBehavior::OnMouseRelease(unsigned btn) {
-  if (btn == 1) {
-    rotation_mode_ = false;
-  }
-  if (btn == 2) {
-    strafe_mode_ = false;
-  }
-  if (btn == 3) {
-    translation_mode_ = false;
+void DesktopNavigationBehavior::OnMouseButton(phx::Mouse::ButtonId id,
+                                              phx::Mouse::ButtonEvent event) {
+  if (event == phx::Mouse::BUTTON_PRESSED) {
+    if (id == phx::Mouse::LEFT_BUTTON) {
+      rotation_mode_ = true;
+    }
+    if (id == phx::Mouse::MIDDLE_BUTTON) {
+      strafe_mode_ = true;
+    }
+    if (id == phx::Mouse::RIGHT_BUTTON) {
+      translation_mode_ = true;
+    }
+  } else {
+    if (id == phx::Mouse::LEFT_BUTTON) {
+      rotation_mode_ = false;
+    }
+    if (id == phx::Mouse::MIDDLE_BUTTON) {
+      strafe_mode_ = false;
+    }
+    if (id == phx::Mouse::RIGHT_BUTTON) {
+      translation_mode_ = false;
+    }
   }
 }
diff --git a/demos/combustion_demo/src/desktop_navigation_behavior.hpp b/demos/combustion_demo/src/desktop_navigation_behavior.hpp
index c394669cc307d679652fc69e9c4442bb20843625..1f03704f1006b5d9481976f29665835aa88e39e4 100644
--- a/demos/combustion_demo/src/desktop_navigation_behavior.hpp
+++ b/demos/combustion_demo/src/desktop_navigation_behavior.hpp
@@ -29,12 +29,12 @@ SUPPRESS_WARNINGS_BEGIN
 #include "glm/glm.hpp"
 SUPPRESS_WARNINGS_END
 
-#include "phx/input/input_system.hpp"
+#include "phx/input/mouse.hpp"
 #include "phx/scripting/behavior.hpp"
 
 class DesktopNavigationBehavior : public phx::Behavior {
  public:
-  explicit DesktopNavigationBehavior(phx::InputSystem* input_system);
+  explicit DesktopNavigationBehavior(phx::Mouse* mouse);
   DesktopNavigationBehavior(const DesktopNavigationBehavior& that) = default;
   DesktopNavigationBehavior(DesktopNavigationBehavior&& temp) = default;
   virtual ~DesktopNavigationBehavior() = default;
@@ -45,11 +45,7 @@ class DesktopNavigationBehavior : public phx::Behavior {
 
   void OnUpdate() override;
   void OnMouseMove(int x, int y);
-  void OnMousePress(unsigned btn);
-  void OnMouseRelease(unsigned btn);
-
- protected:
-  phx::InputSystem* input_system_;
+  void OnMouseButton(phx::Mouse::ButtonId id, phx::Mouse::ButtonEvent event);
 
  private:
   glm::ivec2 accumulated_mouse_pos_ = glm::ivec2(0);
diff --git a/demos/viewer/src/viewer.cpp b/demos/viewer/src/viewer.cpp
index a43834aba9cadeb24c0425e9c2373a7b4026f78d..173e5ae2e44988c3eb7ceb0c0b9352a91f2350b6 100644
--- a/demos/viewer/src/viewer.cpp
+++ b/demos/viewer/src/viewer.cpp
@@ -34,7 +34,7 @@
 #include "phx/display/display_system_window.hpp"
 #include "phx/display/window.hpp"
 #include "phx/input/device_system.hpp"
-#include "phx/input/input_system.hpp"
+#include "phx/input/keyboard.hpp"
 #include "phx/rendering/auxiliary/splash_screen.hpp"
 #include "phx/rendering/components/light.hpp"
 #include "phx/rendering/components/transform.hpp"
@@ -68,14 +68,18 @@ int main(int, char**) {
   assimp_loader->SetProgressUpdateCallback(
       [splash](float progress) { splash->SetLoadProgress(progress); });
 
-  phx::InputSystem* input_system = engine->GetSystem<phx::InputSystem>();
   ViewerSystem* viewer_system = engine->CreateSystem<ViewerSystem>();
 
-  input_system->AddKeyPressCallback([&engine, &viewer_system](char key) {
-    if (key == 'q') engine->Stop();
-    if (key == 'f')
-      viewer_system->SetShowFramerate(!viewer_system->GetShowFramerate());
-  });
+  phx::Keyboard* keyboard =
+      engine->GetSystem<phx::DeviceSystem>()->GetDevices<phx::Keyboard>()[0];
+  keyboard->RegisterKeySignal(
+      [&engine, &viewer_system](char key, phx::Keyboard::KeyEvent key_event,
+                                int) {
+        if (key_event != phx::Keyboard::KEY_PRESSED) return;
+        if (key == 'q') engine->Stop();
+        if (key == 'f')
+          viewer_system->SetShowFramerate(!viewer_system->GetShowFramerate());
+      });
 
   auto handle = std::async([&scene, rendering_system, splash]() {
     phx::SceneLoader::InsertModelIntoScene("models/bunny.obj", scene.get());
diff --git a/library/phx/display/display_system_window.cpp b/library/phx/display/display_system_window.cpp
index 190c2fe08b595e1b073a287a949629ba4b7c6b30..b5ca76366c272ee271027b2d4eeb73c9f7405472 100644
--- a/library/phx/display/display_system_window.cpp
+++ b/library/phx/display/display_system_window.cpp
@@ -31,9 +31,9 @@
 
 #include "SDL2/SDL_video.h"
 
-#include "phx/core/scene.hpp"
 #include "phx/core/logger.hpp"
 #include "phx/core/runtime_component.hpp"
+#include "phx/core/scene.hpp"
 #include "phx/rendering/rendering_system.hpp"
 
 #undef CreateWindow
@@ -74,4 +74,12 @@ void DisplaySystemWindow::CreateRenderTarget(Scene* scene, float field_of_view,
   camera->AddComponent<RenderTarget>(window_size);
 }
 
+boost::signals2::connection DisplaySystemWindow::AddQuitCallback(
+    const std::function<void()>& callback) {
+  return sdl_event_receiver.quit_signal_.connect(callback);
+}
+void DisplaySystemWindow::SDLEventReceiver::OnSDLEvent(const SDL_Event& event) {
+  if (event.type == SDL_QUIT) quit_signal_();
+}
+
 }  // namespace phx
diff --git a/library/phx/display/display_system_window.hpp b/library/phx/display/display_system_window.hpp
index cf8a3b559bbcc58796be3b1e4b34b76d01c36392..385f6d8d431bf3b42e23d300c585a1a2db9429d1 100644
--- a/library/phx/display/display_system_window.hpp
+++ b/library/phx/display/display_system_window.hpp
@@ -29,8 +29,9 @@
 #include "phx/core/scene.hpp"
 #include "phx/display/display_system.hpp"
 #include "phx/display/window.hpp"
-#include "phx/rendering/backend/render_target.hpp"
 #include "phx/export.hpp"
+#include "phx/input/sdl_device.hpp"
+#include "phx/rendering/backend/render_target.hpp"
 
 #undef CreateWindow
 
@@ -58,8 +59,18 @@ class PHOENIX_EXPORT DisplaySystemWindow : public DisplaySystem {
   void CreateRenderTarget(Scene* scene, float field_of_view, float near_plane,
                           float far_plane);
 
+  boost::signals2::connection AddQuitCallback(
+      const std::function<void()>& callback);
+
  protected:
   std::unique_ptr<Window> window_;
+
+  class SDLEventReceiver : public SDLDevice {
+   public:
+    void OnSDLEvent(const SDL_Event& event) override;
+    boost::signals2::signal<void()> quit_signal_;
+  };
+  SDLEventReceiver sdl_event_receiver;
 };
 
 template <typename... Arguments>
diff --git a/library/phx/input/input_system.cpp b/library/phx/input/input_system.cpp
deleted file mode 100644
index 5b68fd671b36d533dc6a57822b7b4afba9b7590d..0000000000000000000000000000000000000000
--- a/library/phx/input/input_system.cpp
+++ /dev/null
@@ -1,107 +0,0 @@
-//------------------------------------------------------------------------------
-// Project Phoenix
-//
-// Copyright (c) 2017-2018 RWTH Aachen University, Germany,
-// Virtual Reality & Immersive Visualization Group.
-//------------------------------------------------------------------------------
-//                                 License
-//
-// Licensed under the 3-Clause BSD License (the "License");
-// you may not use this file except in compliance with the License.
-// See the file LICENSE for the full text.
-// You may obtain a copy of the License at
-//
-//     https://opensource.org/licenses/BSD-3-Clause
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//------------------------------------------------------------------------------
-
-#include "phx/input/input_system.hpp"
-
-#include <string>
-
-#include "phx/core/logger.hpp"
-#include "phx/rendering/rendering_system.hpp"
-#include "phx/suppress_warnings.hpp"
-
-SUPPRESS_WARNINGS_BEGIN
-#include "SDL.h"
-SUPPRESS_WARNINGS_END
-
-namespace phx {
-InputSystem::InputSystem(Engine* engine) : System(engine) {
-  SDL_Init(SDL_INIT_EVENTS);
-}
-
-InputSystem::~InputSystem() { SDL_QuitSubSystem(SDL_INIT_EVENTS); }
-
-void InputSystem::Update(const FrameTimer::TimeInfo&) {
-  UpdateSDLEvents();
-}
-
-boost::signals2::connection InputSystem::AddQuitCallback(
-    const std::function<void()>& callback) {
-  return quit_signal_.connect(callback);
-}
-
-boost::signals2::connection InputSystem::AddKeyPressCallback(
-    const std::function<void(char)>& callback) {
-  return key_press_signal_.connect(callback);
-}
-
-boost::signals2::connection InputSystem::AddKeyReleaseCallback(
-    const std::function<void(char)>& callback) {
-  return key_release_signal_.connect(callback);
-}
-
-boost::signals2::connection InputSystem::AddMouseMoveCallback(
-    const std::function<void(int, int)>& callback) {
-  return mouse_move_signal_.connect(callback);
-}
-
-boost::signals2::connection InputSystem::AddMousePressCallback(
-    const std::function<void(unsigned)>& callback) {
-  return mouse_press_signal_.connect(callback);
-}
-
-boost::signals2::connection InputSystem::AddMouseReleaseCallback(
-    const std::function<void(unsigned)>& callback) {
-  return mouse_release_signal_.connect(callback);
-}
-
-std::string InputSystem::ToString() const { return "InputSystem"; }
-
-void InputSystem::UpdateSDLEvents() {
-  SDL_Event event;
-  while (SDL_PollEvent(&event) != 0) {
-    switch (event.type) {
-      case SDL_QUIT:
-        quit_signal_();
-        break;
-      case SDL_KEYDOWN:
-        key_press_signal_(static_cast<char>(event.key.keysym.sym));
-        break;
-      case SDL_KEYUP:
-        key_release_signal_(static_cast<char>(event.key.keysym.sym));
-        break;
-      case SDL_MOUSEMOTION:
-        mouse_move_signal_(static_cast<int>(event.motion.xrel),
-                           static_cast<int>(event.motion.yrel));
-        break;
-      case SDL_MOUSEBUTTONDOWN:
-        mouse_press_signal_(event.button.button);
-        break;
-      case SDL_MOUSEBUTTONUP:
-        mouse_release_signal_(event.button.button);
-        break;
-      default:
-        break;
-    }
-  }
-}
-
-}  // namespace phx
diff --git a/library/phx/input/input_system.hpp b/library/phx/input/input_system.hpp
deleted file mode 100644
index 9862ea78119438869c1e37162909df8c9faba169..0000000000000000000000000000000000000000
--- a/library/phx/input/input_system.hpp
+++ /dev/null
@@ -1,86 +0,0 @@
-//------------------------------------------------------------------------------
-// Project Phoenix
-//
-// Copyright (c) 2017-2018 RWTH Aachen University, Germany,
-// Virtual Reality & Immersive Visualization Group.
-//------------------------------------------------------------------------------
-//                                 License
-//
-// Licensed under the 3-Clause BSD License (the "License");
-// you may not use this file except in compliance with the License.
-// See the file LICENSE for the full text.
-// You may obtain a copy of the License at
-//
-//     https://opensource.org/licenses/BSD-3-Clause
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//------------------------------------------------------------------------------
-
-#ifndef LIBRARY_PHX_INPUT_INPUT_SYSTEM_HPP_
-#define LIBRARY_PHX_INPUT_INPUT_SYSTEM_HPP_
-
-#include <functional>
-#include <string>
-
-#include "phx/suppress_warnings.hpp"
-
-SUPPRESS_WARNINGS_BEGIN
-#define BOOST_BIND_NO_PLACEHOLDERS
-#include "boost/signals2/connection.hpp"
-#include "boost/signals2/signal.hpp"
-SUPPRESS_WARNINGS_END
-
-#include "phx/core/engine.hpp"
-#include "phx/core/system.hpp"
-#include "phx/export.hpp"
-
-namespace phx {
-
-class PHOENIX_EXPORT InputSystem : public System {
- public:
-  InputSystem() = delete;
-  InputSystem(const InputSystem&) = delete;
-  InputSystem(InputSystem&&) = default;
-  ~InputSystem() override;
-
-  void Update(const FrameTimer::TimeInfo& time_info) override;
-
-  InputSystem& operator=(const InputSystem&) = delete;
-  InputSystem& operator=(InputSystem&&) = default;
-
-  boost::signals2::connection AddQuitCallback(
-      const std::function<void()>& callback);
-  boost::signals2::connection AddKeyPressCallback(
-      const std::function<void(char)>& callback);
-  boost::signals2::connection AddKeyReleaseCallback(
-      const std::function<void(char)>& callback);
-  boost::signals2::connection AddMouseMoveCallback(
-      const std::function<void(int, int)>& callback);
-  boost::signals2::connection AddMousePressCallback(
-      const std::function<void(unsigned int)>& callback);
-  boost::signals2::connection AddMouseReleaseCallback(
-      const std::function<void(unsigned int)>& callback);
-
-  std::string ToString() const override;
-
- private:
-  friend InputSystem* Engine::CreateSystem<InputSystem>();
-  explicit InputSystem(Engine* engine);
-
-  void UpdateSDLEvents();
-
-  boost::signals2::signal<void()> quit_signal_;
-  boost::signals2::signal<void(char)> key_press_signal_;
-  boost::signals2::signal<void(char)> key_release_signal_;
-  boost::signals2::signal<void(int, int)> mouse_move_signal_;
-  boost::signals2::signal<void(unsigned int)> mouse_press_signal_;
-  boost::signals2::signal<void(unsigned int)> mouse_release_signal_;
-};
-
-}  // namespace phx
-
-#endif  // LIBRARY_PHX_INPUT_INPUT_SYSTEM_HPP_
diff --git a/library/phx/input/keyboard.cpp b/library/phx/input/keyboard.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..2bf5757c33c5119fea4a6a8cd97192bfa30e46f8
--- /dev/null
+++ b/library/phx/input/keyboard.cpp
@@ -0,0 +1,46 @@
+//------------------------------------------------------------------------------
+// Project Phoenix
+//
+// Copyright (c) 2017-2018 RWTH Aachen University, Germany,
+// Virtual Reality & Immersive Visualization Group.
+//------------------------------------------------------------------------------
+//                                 License
+//
+// Licensed under the 3-Clause BSD License (the "License");
+// you may not use this file except in compliance with the License.
+// See the file LICENSE for the full text.
+// You may obtain a copy of the License at
+//
+//     https://opensource.org/licenses/BSD-3-Clause
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//------------------------------------------------------------------------------
+
+#include "phx/input/keyboard.hpp"
+
+#include <string>
+#include <vector>
+
+#include "phx/core/logger.hpp"
+
+namespace phx {
+void Keyboard::Update() { SDLDevice::Update(); }
+
+void Keyboard::OnSDLEvent(const SDL_Event& event) {
+  if (event.type == SDL_KEYDOWN)
+    key_signal_(static_cast<char>(event.key.keysym.sym), KEY_PRESSED,
+                static_cast<int>(event.key.keysym.mod));
+  else if (event.type == SDL_KEYUP)
+    key_signal_(static_cast<char>(event.key.keysym.sym), KEY_RELEASED,
+                static_cast<int>(event.key.keysym.mod));
+}
+
+boost::signals2::connection Keyboard::RegisterKeySignal(
+    const std::function<void(char, KeyEvent, int)>& callback) {
+  return key_signal_.connect(callback);
+}
+}  // namespace phx
diff --git a/library/phx/input/keyboard.hpp b/library/phx/input/keyboard.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..80ca1b3a5d1e93de1f926dd47a94911f3b428abb
--- /dev/null
+++ b/library/phx/input/keyboard.hpp
@@ -0,0 +1,58 @@
+//------------------------------------------------------------------------------
+// Project Phoenix
+//
+// Copyright (c) 2017-2018 RWTH Aachen University, Germany,
+// Virtual Reality & Immersive Visualization Group.
+//------------------------------------------------------------------------------
+//                                 License
+//
+// Licensed under the 3-Clause BSD License (the "License");
+// you may not use this file except in compliance with the License.
+// See the file LICENSE for the full text.
+// You may obtain a copy of the License at
+//
+//     https://opensource.org/licenses/BSD-3-Clause
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//------------------------------------------------------------------------------
+
+#ifndef LIBRARY_PHX_INPUT_KEYBOARD_HPP_
+#define LIBRARY_PHX_INPUT_KEYBOARD_HPP_
+
+#include <memory>
+
+#define BOOST_BIND_NO_PLACEHOLDERS
+// otherwise boosts _ placeholders conflict with trompeloeil ones
+#include "boost/signals2/connection.hpp"
+#include "boost/signals2/signal.hpp"
+
+#include "phx/input/sdl_device.hpp"
+
+#include "phx/export.hpp"
+
+namespace phx {
+
+class PHOENIX_EXPORT Keyboard : public SDLDevice {
+ public:
+  enum KeyEvent { KEY_PRESSED, KEY_RELEASED };
+
+  virtual ~Keyboard() = default;
+
+  void Update() override;
+  void OnSDLEvent(const SDL_Event& event) override;
+
+  // the 3rd paramter are the modifiers, see SDL_Keymod
+  boost::signals2::connection RegisterKeySignal(
+      const std::function<void(char, KeyEvent, int)>& callback);
+
+ private:
+  boost::signals2::signal<void(char, KeyEvent, int)> key_signal_;
+};
+
+}  // namespace phx
+
+#endif  // LIBRARY_PHX_INPUT_KEYBOARD_HPP_
diff --git a/library/phx/input/mouse.cpp b/library/phx/input/mouse.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..f575e1b3dcd5568b19ef7b75a8a1d4e8fd42cee7
--- /dev/null
+++ b/library/phx/input/mouse.cpp
@@ -0,0 +1,78 @@
+//------------------------------------------------------------------------------
+// Project Phoenix
+//
+// Copyright (c) 2017-2018 RWTH Aachen University, Germany,
+// Virtual Reality & Immersive Visualization Group.
+//------------------------------------------------------------------------------
+//                                 License
+//
+// Licensed under the 3-Clause BSD License (the "License");
+// you may not use this file except in compliance with the License.
+// See the file LICENSE for the full text.
+// You may obtain a copy of the License at
+//
+//     https://opensource.org/licenses/BSD-3-Clause
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//------------------------------------------------------------------------------
+
+#include "phx/input/mouse.hpp"
+
+#include <string>
+#include <vector>
+
+#include "phx/core/logger.hpp"
+
+namespace phx {
+
+void Mouse::Update() { SDLDevice::Update(); }
+
+void Mouse::OnSDLEvent(const SDL_Event& event) {
+  ButtonEvent event_type = BUTTON_PRESSED;
+  if (event.type == SDL_MOUSEMOTION) {
+    move_signal_(static_cast<int>(event.motion.xrel),
+                 static_cast<int>(event.motion.yrel));
+  } else if (event.type == SDL_MOUSEBUTTONDOWN) {
+    event_type = BUTTON_PRESSED;
+  } else if (event.type == SDL_MOUSEBUTTONUP) {
+    event_type = BUTTON_RELEASED;
+  } else {
+    return;
+  }
+
+  switch (event.button.button) {
+    case LEFT_BUTTON:
+      button_signal_(LEFT_BUTTON, event_type);
+      break;
+    case MIDDLE_BUTTON:
+      button_signal_(MIDDLE_BUTTON, event_type);
+      break;
+    case RIGHT_BUTTON:
+      button_signal_(RIGHT_BUTTON, event_type);
+      break;
+    case X1_BUTTON:
+      button_signal_(X1_BUTTON, event_type);
+      break;
+    case X2_BUTTON:
+      button_signal_(X2_BUTTON, event_type);
+      break;
+    default:
+      break;
+  }
+}
+
+boost::signals2::connection Mouse::RegisterButtonSignal(
+    const std::function<void(ButtonId, ButtonEvent)>& callback) {
+  return button_signal_.connect(callback);
+}
+
+boost::signals2::connection Mouse::RegisterMoveSignal(
+    const std::function<void(int, int)>& callback) {
+  return move_signal_.connect(callback);
+}
+
+}  // namespace phx
diff --git a/library/phx/input/mouse.hpp b/library/phx/input/mouse.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..500ffd35ec021075409c2d0fb0acadbdbe95a9bd
--- /dev/null
+++ b/library/phx/input/mouse.hpp
@@ -0,0 +1,68 @@
+//------------------------------------------------------------------------------
+// Project Phoenix
+//
+// Copyright (c) 2017-2018 RWTH Aachen University, Germany,
+// Virtual Reality & Immersive Visualization Group.
+//------------------------------------------------------------------------------
+//                                 License
+//
+// Licensed under the 3-Clause BSD License (the "License");
+// you may not use this file except in compliance with the License.
+// See the file LICENSE for the full text.
+// You may obtain a copy of the License at
+//
+//     https://opensource.org/licenses/BSD-3-Clause
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//------------------------------------------------------------------------------
+
+#ifndef LIBRARY_PHX_INPUT_MOUSE_HPP_
+#define LIBRARY_PHX_INPUT_MOUSE_HPP_
+
+#include <memory>
+
+#define BOOST_BIND_NO_PLACEHOLDERS
+// otherwise boosts _ placeholders conflict with trompeloeil ones
+#include "boost/signals2/connection.hpp"
+#include "boost/signals2/signal.hpp"
+
+#include "phx/input/sdl_device.hpp"
+#include "phx/suppress_warnings.hpp"
+
+#include "phx/export.hpp"
+
+namespace phx {
+
+class PHOENIX_EXPORT Mouse : public SDLDevice {
+ public:
+  enum ButtonEvent { BUTTON_PRESSED, BUTTON_RELEASED };
+  enum ButtonId {
+    LEFT_BUTTON = SDL_BUTTON_LEFT,
+    MIDDLE_BUTTON = SDL_BUTTON_MIDDLE,
+    RIGHT_BUTTON = SDL_BUTTON_RIGHT,
+    X1_BUTTON = SDL_BUTTON_X1,
+    X2_BUTTON = SDL_BUTTON_X2
+  };
+
+  virtual ~Mouse() = default;
+
+  void Update() override;
+  void OnSDLEvent(const SDL_Event& event) override;
+
+  boost::signals2::connection RegisterButtonSignal(
+      const std::function<void(ButtonId, ButtonEvent)>& callback);
+  boost::signals2::connection RegisterMoveSignal(
+      const std::function<void(int, int)>& callback);
+
+ private:
+  boost::signals2::signal<void(ButtonId, ButtonEvent)> button_signal_;
+  boost::signals2::signal<void(int, int)> move_signal_;
+};
+
+}  // namespace phx
+
+#endif  // LIBRARY_PHX_INPUT_MOUSE_HPP_
diff --git a/library/phx/setup.cpp b/library/phx/setup.cpp
index 4169253e75d4abb6206ed974f45774c7ffe325a6..e15b52e2d225a041f34fb855349c4048e10ea4dc 100644
--- a/library/phx/setup.cpp
+++ b/library/phx/setup.cpp
@@ -37,7 +37,8 @@
 #include "phx/display/display_system_openvr.hpp"
 #include "phx/display/display_system_window.hpp"
 #include "phx/display/hmd.hpp"
-#include "phx/input/input_system.hpp"
+#include "phx/input/keyboard.hpp"
+#include "phx/input/mouse.hpp"
 #include "phx/input/openvr_controller_model_system.hpp"
 #include "phx/rendering/backend/render_target.hpp"
 #include "phx/rendering/render_passes/blit_pass.hpp"
@@ -56,12 +57,14 @@ std::unique_ptr<Engine> Setup::CreateDefaultEngine(bool use_hmd_if_available) {
   engine->SetScene(std::make_shared<Scene>());
 
   auto behavior_system = engine->CreateSystem<BehaviorSystem>();
-  engine->CreateSystem<InputSystem>()->AddQuitCallback(
-      [engine_ptr]() { engine_ptr->Stop(); });
 
   auto device_system = engine->CreateSystem<DeviceSystem>();
+  device_system->AddDevice<Mouse>();
+  device_system->AddDevice<Keyboard>();
 
   auto displaysys_window = engine->CreateSystem<DisplaySystemWindow>();
+  displaysys_window->AddQuitCallback([engine_ptr]() { engine_ptr->Stop(); });
+
   DisplaySystemOpenVR* displaysys_openVR = nullptr;
   bool using_hmd = false;
 
diff --git a/tests/src/test_keyboard.cpp b/tests/src/test_keyboard.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..bfb5f6e79411b0cb6f3245dcc5674d5aae954705
--- /dev/null
+++ b/tests/src/test_keyboard.cpp
@@ -0,0 +1,77 @@
+//------------------------------------------------------------------------------
+// Project Phoenix
+//
+// Copyright (c) 2017-2018 RWTH Aachen University, Germany,
+// Virtual Reality & Immersive Visualization Group.
+//------------------------------------------------------------------------------
+//                                 License
+//
+// Licensed under the 3-Clause BSD License (the "License");
+// you may not use this file except in compliance with the License.
+// See the file LICENSE for the full text.
+// You may obtain a copy of the License at
+//
+//     https://opensource.org/licenses/BSD-3-Clause
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//------------------------------------------------------------------------------
+
+#include <memory>
+
+#include "catch/catch.hpp"
+
+#include "phx/input/keyboard.hpp"
+
+#include "trompeloeil.hpp"
+
+#include "mocks/sdl_mock.hpp"
+
+using trompeloeil::_;
+using trompeloeil::ne;
+
+extern template struct trompeloeil::reporter<trompeloeil::specialized>;
+
+SCENARIO("Keyboard signals received events.", "[phx][phx::Keyboard]") {
+  SDL_MOCK_ALLOW_ANY_CALL
+  GIVEN("A keyboard device.") {
+    phx::Keyboard keyboard;
+
+    WHEN("a key stroke signal is registered") {
+      char received_character = ' ';
+      int received_modifiers = 0;
+      keyboard.RegisterKeySignal(
+          [&received_character, &received_modifiers](
+              char character, phx::Keyboard::KeyEvent event, int modifiers) {
+            if (event == phx::Keyboard::KEY_PRESSED) {
+              received_character = character;
+              received_modifiers = modifiers;
+            }
+          });
+
+      WHEN("A keyboard event is fired by SDL.") {
+        SDL_Event key_event;
+        key_event.type = SDL_KEYDOWN;
+        key_event.key.keysym.sym = 'a';
+        key_event.key.keysym.mod = (KMOD_LSHIFT | KMOD_LCTRL);
+        auto first_call = std::make_shared<bool>(true);
+        ALLOW_CALL(sdl_mock.Get(), SDL_PollEvent(_))
+            .SIDE_EFFECT(*_1 = key_event)
+            .SIDE_EFFECT(*first_call = false)
+            .WITH(*first_call == true)
+            .RETURN(true);
+        ALLOW_CALL(sdl_mock.Get(), SDL_PollEvent(_))
+            .WITH(*first_call == false)
+            .RETURN(false);
+        THEN("I should receive a signal.") {
+          keyboard.Update();
+          REQUIRE(received_character == 'a');
+          REQUIRE(received_modifiers == (KMOD_LSHIFT | KMOD_LCTRL));
+        }
+      }
+    }
+  }
+}
diff --git a/tests/src/test_mouse.cpp b/tests/src/test_mouse.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..b395803e58d310b301f9bb33e93d054e370543e8
--- /dev/null
+++ b/tests/src/test_mouse.cpp
@@ -0,0 +1,101 @@
+//------------------------------------------------------------------------------
+// Project Phoenix
+//
+// Copyright (c) 2017-2018 RWTH Aachen University, Germany,
+// Virtual Reality & Immersive Visualization Group.
+//------------------------------------------------------------------------------
+//                                 License
+//
+// Licensed under the 3-Clause BSD License (the "License");
+// you may not use this file except in compliance with the License.
+// See the file LICENSE for the full text.
+// You may obtain a copy of the License at
+//
+//     https://opensource.org/licenses/BSD-3-Clause
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//------------------------------------------------------------------------------
+
+#include <memory>
+
+#include "catch/catch.hpp"
+
+#include "phx/input/mouse.hpp"
+
+#include "trompeloeil.hpp"
+
+#include "mocks/sdl_mock.hpp"
+
+using trompeloeil::_;
+using trompeloeil::ne;
+
+extern template struct trompeloeil::reporter<trompeloeil::specialized>;
+
+SCENARIO("Mouse signals received events.", "[phx][phx::Mouse]") {
+  SDL_MOCK_ALLOW_ANY_CALL
+  GIVEN("A mouse device.") {
+    phx::Mouse mouse;
+
+    WHEN("a mouse button signal is registered") {
+      bool success = false;
+      mouse.RegisterButtonSignal(
+          [&success](phx::Mouse::ButtonId id, phx::Mouse::ButtonEvent event) {
+            if (id == phx::Mouse::LEFT_BUTTON &&
+                event == phx::Mouse::BUTTON_PRESSED)
+              success = true;
+          });
+
+      WHEN("A mouse button event is fired by SDL.") {
+        SDL_Event mouse_event;
+        mouse_event.type = SDL_MOUSEBUTTONDOWN;
+        mouse_event.button.button = SDL_BUTTON_LEFT;
+        auto first_call = std::make_shared<bool>(true);
+        ALLOW_CALL(sdl_mock.Get(), SDL_PollEvent(_))
+            .SIDE_EFFECT(*_1 = mouse_event)
+            .SIDE_EFFECT(*first_call = false)
+            .WITH(*first_call == true)
+            .RETURN(true);
+        ALLOW_CALL(sdl_mock.Get(), SDL_PollEvent(_))
+            .WITH(*first_call == false)
+            .RETURN(false);
+        THEN("I should receive a signal.") {
+          mouse.Update();
+          REQUIRE(success);
+        }
+      }
+    }
+
+    WHEN("a mouse move signal is registered") {
+      int x_received = 0;
+      int y_received = 0;
+      mouse.RegisterMoveSignal([&x_received, &y_received](int x, int y) {
+        x_received = x;
+        y_received = y;
+      });
+      WHEN("A mouse move event is fired by SDL.") {
+        SDL_Event mouse_event;
+        mouse_event.type = SDL_MOUSEMOTION;
+        mouse_event.motion.xrel = 100;
+        mouse_event.motion.yrel = 200;
+        auto first_call = std::make_shared<bool>(true);
+        ALLOW_CALL(sdl_mock.Get(), SDL_PollEvent(_))
+            .SIDE_EFFECT(*_1 = mouse_event)
+            .SIDE_EFFECT(*first_call = false)
+            .WITH(*first_call == true)
+            .RETURN(true);
+        ALLOW_CALL(sdl_mock.Get(), SDL_PollEvent(_))
+            .WITH(*first_call == false)
+            .RETURN(false);
+        THEN("I should receive a signal.") {
+          mouse.Update();
+          REQUIRE(x_received == 100);
+          REQUIRE(y_received == 200);
+        }
+      }
+    }
+  }
+}